├── .github └── workflows │ └── linux.yml ├── .gitignore ├── Changes ├── LICENSE ├── META.json ├── Makefile.PL ├── README.md ├── cpanfile ├── dist.ini ├── lib └── HTTP │ ├── Tinyish.pm │ └── Tinyish │ ├── Base.pm │ ├── Curl.pm │ ├── HTTPTiny.pm │ ├── LWP.pm │ └── Wget.pm └── t └── tinyish.t /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | perl: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | perl-version: 13 | - '5.24' 14 | - '5.26' 15 | - '5.28' 16 | - '5.30' 17 | - '5.32' 18 | - '5.34' 19 | - '5.36' 20 | - '5.38' 21 | container: 22 | image: perl:${{ matrix.perl-version }} 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Install Dependencies 26 | run: | 27 | curl -sL https://cpanmin.us/ | perl - -nq --with-develop --installdeps . 28 | - name: Run Tests 29 | run: prove -lr t 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /HTTP-Tinyish-* 2 | /.build 3 | /_build* 4 | /Build 5 | MYMETA.* 6 | !META.json 7 | /.prove 8 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for HTTP-Tinyish 2 | 3 | {{$NEXT}} 4 | 5 | 0.19 2024-03-08 14:30:05 PST 6 | - Specify :raw for IPC::Run3 to force binary on Windows #24 7 | 8 | 0.18 2022-06-20 16:44:43 PDT 9 | - Support max_redirect: 0 (skaji) #23 10 | 11 | 0.17 2020-07-07 16:55:12 PDT 12 | - Support saving error output from curl in internal errors (skaji) 13 | 14 | 0.16 2020-05-13 10:11:24 PDT 15 | - Support patch() method #20 16 | 17 | 0.15 2018-09-22 13:40:30 PDT 18 | - in LWP adapter, check if Mozilla::CA is installed as well #15 19 | 20 | 0.14 2018-04-20 10:48:38 CEST 21 | - Make CPAN testers look better 22 | 23 | 0.13 2018-04-19 11:50:57 CEST 24 | - Handle errors from Curl's mirror method (skaji) 25 | - Convert LWP's 'internal response' to an internal error 26 | 27 | 0.12 2017-02-11 18:07:14 PST 28 | - Relax minimum perl version to 5.8.1 #10 29 | 30 | 0.11 2017-01-29 16:02:35 PST 31 | - Add protocol to HTTP::Tinyish->request (skaji) #7 32 | - Use HTTP::Tiny->can_ssl to check if SSL is supported (skaji) #8 33 | 34 | 0.10 2017-01-02 14:02:54 PST 35 | - remove tempfiles created by curl backend (skaji) #6 36 | 37 | 0.09 2016-11-18 20:45:53 PST 38 | - Fix HTTP/2 where it appears in redirects (skaji) #5 39 | 40 | 0.08 2016-11-18 09:09:04 PST 41 | - Accept HTTP/2 when curl is compiled with http2 support (skaji) #4 42 | 43 | 0.07 2016-01-22 19:33:04 PST 44 | - 304 response is considered a success #3 45 | 46 | 0.06 2015-09-26 10:22:09 PDT 47 | - Fixed parsing of HTTP Status reason (shoichikaji) #2 48 | 49 | 0.05 2015-05-08 14:31:30 PDT 50 | - Test with wget 1.12 and up. Also tried to support 1.11 but its exit code and weird 51 | server response options made it difficult, so the configure process makes sure that 52 | wget is 1.12 or higher. 53 | 54 | 0.04 2015-05-07 19:30:05 PDT 55 | - call ssl_opts attribute only when LWP supports it 56 | 57 | 0.03 2015-05-07 19:23:10 PDT 58 | - Make LWP backend compatible back to 5.823 really 59 | 60 | 0.02 2015-05-07 18:43:37 PDT 61 | - updated documentation 62 | 63 | 0.01 2015-05-07 18:31:22 PDT 64 | - Initial release 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is copyright (c) 2015- by Tatsuhiko Miyagawa. 2 | 3 | This is free software; you can redistribute it and/or modify it under 4 | the same terms as the Perl 5 programming language system itself. 5 | 6 | Terms of the Perl programming language system itself 7 | 8 | a) the GNU General Public License as published by the Free 9 | Software Foundation; either version 1, or (at your option) any 10 | later version, or 11 | b) the "Artistic License" 12 | 13 | --- The GNU General Public License, Version 1, February 1989 --- 14 | 15 | This software is Copyright (c) 2015- by Tatsuhiko Miyagawa. 16 | 17 | This is free software, licensed under: 18 | 19 | The GNU General Public License, Version 1, February 1989 20 | 21 | GNU GENERAL PUBLIC LICENSE 22 | Version 1, February 1989 23 | 24 | Copyright (C) 1989 Free Software Foundation, Inc. 25 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 26 | 27 | Everyone is permitted to copy and distribute verbatim copies 28 | of this license document, but changing it is not allowed. 29 | 30 | Preamble 31 | 32 | The license agreements of most software companies try to keep users 33 | at the mercy of those companies. By contrast, our General Public 34 | License is intended to guarantee your freedom to share and change free 35 | software--to make sure the software is free for all its users. The 36 | General Public License applies to the Free Software Foundation's 37 | software and to any other program whose authors commit to using it. 38 | You can use it for your programs, too. 39 | 40 | When we speak of free software, we are referring to freedom, not 41 | price. Specifically, the General Public License is designed to make 42 | sure that you have the freedom to give away or sell copies of free 43 | software, that you receive source code or can get it if you want it, 44 | that you can change the software or use pieces of it in new free 45 | programs; and that you know you can do these things. 46 | 47 | To protect your rights, we need to make restrictions that forbid 48 | anyone to deny you these rights or to ask you to surrender the rights. 49 | These restrictions translate to certain responsibilities for you if you 50 | distribute copies of the software, or if you modify it. 51 | 52 | For example, if you distribute copies of a such a program, whether 53 | gratis or for a fee, you must give the recipients all the rights that 54 | you have. You must make sure that they, too, receive or can get the 55 | source code. And you must tell them their rights. 56 | 57 | We protect your rights with two steps: (1) copyright the software, and 58 | (2) offer you this license which gives you legal permission to copy, 59 | distribute and/or modify the software. 60 | 61 | Also, for each author's protection and ours, we want to make certain 62 | that everyone understands that there is no warranty for this free 63 | software. If the software is modified by someone else and passed on, we 64 | want its recipients to know that what they have is not the original, so 65 | that any problems introduced by others will not reflect on the original 66 | authors' reputations. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | GNU GENERAL PUBLIC LICENSE 72 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 73 | 74 | 0. This License Agreement applies to any program or other work which 75 | contains a notice placed by the copyright holder saying it may be 76 | distributed under the terms of this General Public License. The 77 | "Program", below, refers to any such program or work, and a "work based 78 | on the Program" means either the Program or any work containing the 79 | Program or a portion of it, either verbatim or with modifications. Each 80 | licensee is addressed as "you". 81 | 82 | 1. You may copy and distribute verbatim copies of the Program's source 83 | code as you receive it, in any medium, provided that you conspicuously and 84 | appropriately publish on each copy an appropriate copyright notice and 85 | disclaimer of warranty; keep intact all the notices that refer to this 86 | General Public License and to the absence of any warranty; and give any 87 | other recipients of the Program a copy of this General Public License 88 | along with the Program. You may charge a fee for the physical act of 89 | transferring a copy. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion of 92 | it, and copy and distribute such modifications under the terms of Paragraph 93 | 1 above, provided that you also do the following: 94 | 95 | a) cause the modified files to carry prominent notices stating that 96 | you changed the files and the date of any change; and 97 | 98 | b) cause the whole of any work that you distribute or publish, that 99 | in whole or in part contains the Program or any part thereof, either 100 | with or without modifications, to be licensed at no charge to all 101 | third parties under the terms of this General Public License (except 102 | that you may choose to grant warranty protection to some or all 103 | third parties, at your option). 104 | 105 | c) If the modified program normally reads commands interactively when 106 | run, you must cause it, when started running for such interactive use 107 | in the simplest and most usual way, to print or display an 108 | announcement including an appropriate copyright notice and a notice 109 | that there is no warranty (or else, saying that you provide a 110 | warranty) and that users may redistribute the program under these 111 | conditions, and telling the user how to view a copy of this General 112 | Public License. 113 | 114 | d) You may charge a fee for the physical act of transferring a 115 | copy, and you may at your option offer warranty protection in 116 | exchange for a fee. 117 | 118 | Mere aggregation of another independent work with the Program (or its 119 | derivative) on a volume of a storage or distribution medium does not bring 120 | the other work under the scope of these terms. 121 | 122 | 3. You may copy and distribute the Program (or a portion or derivative of 123 | it, under Paragraph 2) in object code or executable form under the terms of 124 | Paragraphs 1 and 2 above provided that you also do one of the following: 125 | 126 | a) accompany it with the complete corresponding machine-readable 127 | source code, which must be distributed under the terms of 128 | Paragraphs 1 and 2 above; or, 129 | 130 | b) accompany it with a written offer, valid for at least three 131 | years, to give any third party free (except for a nominal charge 132 | for the cost of distribution) a complete machine-readable copy of the 133 | corresponding source code, to be distributed under the terms of 134 | Paragraphs 1 and 2 above; or, 135 | 136 | c) accompany it with the information you received as to where the 137 | corresponding source code may be obtained. (This alternative is 138 | allowed only for noncommercial distribution and only if you 139 | received the program in object code or executable form alone.) 140 | 141 | Source code for a work means the preferred form of the work for making 142 | modifications to it. For an executable file, complete source code means 143 | all the source code for all modules it contains; but, as a special 144 | exception, it need not include source code for modules which are standard 145 | libraries that accompany the operating system on which the executable 146 | file runs, or for standard header files or definitions files that 147 | accompany that operating system. 148 | 149 | 4. You may not copy, modify, sublicense, distribute or transfer the 150 | Program except as expressly provided under this General Public License. 151 | Any attempt otherwise to copy, modify, sublicense, distribute or transfer 152 | the Program is void, and will automatically terminate your rights to use 153 | the Program under this License. However, parties who have received 154 | copies, or rights to use copies, from you under this General Public 155 | License will not have their licenses terminated so long as such parties 156 | remain in full compliance. 157 | 158 | 5. By copying, distributing or modifying the Program (or any work based 159 | on the Program) you indicate your acceptance of this license to do so, 160 | and all its terms and conditions. 161 | 162 | 6. Each time you redistribute the Program (or any work based on the 163 | Program), the recipient automatically receives a license from the original 164 | licensor to copy, distribute or modify the Program subject to these 165 | terms and conditions. You may not impose any further restrictions on the 166 | recipients' exercise of the rights granted herein. 167 | 168 | 7. The Free Software Foundation may publish revised and/or new versions 169 | of the General Public License from time to time. Such new versions will 170 | be similar in spirit to the present version, but may differ in detail to 171 | address new problems or concerns. 172 | 173 | Each version is given a distinguishing version number. If the Program 174 | specifies a version number of the license which applies to it and "any 175 | later version", you have the option of following the terms and conditions 176 | either of that version or of any later version published by the Free 177 | Software Foundation. If the Program does not specify a version number of 178 | the license, you may choose any version ever published by the Free Software 179 | Foundation. 180 | 181 | 8. If you wish to incorporate parts of the Program into other free 182 | programs whose distribution conditions are different, write to the author 183 | to ask for permission. For software which is copyrighted by the Free 184 | Software Foundation, write to the Free Software Foundation; we sometimes 185 | make exceptions for this. Our decision will be guided by the two goals 186 | of preserving the free status of all derivatives of our free software and 187 | of promoting the sharing and reuse of software generally. 188 | 189 | NO WARRANTY 190 | 191 | 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 192 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 193 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 194 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 195 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 196 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 197 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 198 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 199 | REPAIR OR CORRECTION. 200 | 201 | 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 202 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 203 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 204 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 205 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 206 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 207 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 208 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 209 | POSSIBILITY OF SUCH DAMAGES. 210 | 211 | END OF TERMS AND CONDITIONS 212 | 213 | Appendix: How to Apply These Terms to Your New Programs 214 | 215 | If you develop a new program, and you want it to be of the greatest 216 | possible use to humanity, the best way to achieve this is to make it 217 | free software which everyone can redistribute and change under these 218 | terms. 219 | 220 | To do so, attach the following notices to the program. It is safest to 221 | attach them to the start of each source file to most effectively convey 222 | the exclusion of warranty; and each file should have at least the 223 | "copyright" line and a pointer to where the full notice is found. 224 | 225 | 226 | Copyright (C) 19yy 227 | 228 | This program is free software; you can redistribute it and/or modify 229 | it under the terms of the GNU General Public License as published by 230 | the Free Software Foundation; either version 1, or (at your option) 231 | any later version. 232 | 233 | This program is distributed in the hope that it will be useful, 234 | but WITHOUT ANY WARRANTY; without even the implied warranty of 235 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 236 | GNU General Public License for more details. 237 | 238 | You should have received a copy of the GNU General Public License 239 | along with this program; if not, write to the Free Software 240 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 241 | 242 | 243 | Also add information on how to contact you by electronic and paper mail. 244 | 245 | If the program is interactive, make it output a short notice like this 246 | when it starts in an interactive mode: 247 | 248 | Gnomovision version 69, Copyright (C) 19xx name of author 249 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 250 | This is free software, and you are welcome to redistribute it 251 | under certain conditions; type `show c' for details. 252 | 253 | The hypothetical commands `show w' and `show c' should show the 254 | appropriate parts of the General Public License. Of course, the 255 | commands you use may be called something other than `show w' and `show 256 | c'; they could even be mouse-clicks or menu items--whatever suits your 257 | program. 258 | 259 | You should also get your employer (if you work as a programmer) or your 260 | school, if any, to sign a "copyright disclaimer" for the program, if 261 | necessary. Here a sample; alter the names: 262 | 263 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 264 | program `Gnomovision' (a program to direct compilers to make passes 265 | at assemblers) written by James Hacker. 266 | 267 | , 1 April 1989 268 | Ty Coon, President of Vice 269 | 270 | That's all there is to it! 271 | 272 | 273 | --- The Artistic License 1.0 --- 274 | 275 | This software is Copyright (c) 2015- by Tatsuhiko Miyagawa. 276 | 277 | This is free software, licensed under: 278 | 279 | The Artistic License 1.0 280 | 281 | The Artistic License 282 | 283 | Preamble 284 | 285 | The intent of this document is to state the conditions under which a Package 286 | may be copied, such that the Copyright Holder maintains some semblance of 287 | artistic control over the development of the package, while giving the users of 288 | the package the right to use and distribute the Package in a more-or-less 289 | customary fashion, plus the right to make reasonable modifications. 290 | 291 | Definitions: 292 | 293 | - "Package" refers to the collection of files distributed by the Copyright 294 | Holder, and derivatives of that collection of files created through 295 | textual modification. 296 | - "Standard Version" refers to such a Package if it has not been modified, 297 | or has been modified in accordance with the wishes of the Copyright 298 | Holder. 299 | - "Copyright Holder" is whoever is named in the copyright or copyrights for 300 | the package. 301 | - "You" is you, if you're thinking about copying or distributing this Package. 302 | - "Reasonable copying fee" is whatever you can justify on the basis of media 303 | cost, duplication charges, time of people involved, and so on. (You will 304 | not be required to justify it to the Copyright Holder, but only to the 305 | computing community at large as a market that must bear the fee.) 306 | - "Freely Available" means that no fee is charged for the item itself, though 307 | there may be fees involved in handling the item. It also means that 308 | recipients of the item may redistribute it under the same conditions they 309 | received it. 310 | 311 | 1. You may make and give away verbatim copies of the source form of the 312 | Standard Version of this Package without restriction, provided that you 313 | duplicate all of the original copyright notices and associated disclaimers. 314 | 315 | 2. You may apply bug fixes, portability fixes and other modifications derived 316 | from the Public Domain or from the Copyright Holder. A Package modified in such 317 | a way shall still be considered the Standard Version. 318 | 319 | 3. You may otherwise modify your copy of this Package in any way, provided that 320 | you insert a prominent notice in each changed file stating how and when you 321 | changed that file, and provided that you do at least ONE of the following: 322 | 323 | a) place your modifications in the Public Domain or otherwise make them 324 | Freely Available, such as by posting said modifications to Usenet or an 325 | equivalent medium, or placing the modifications on a major archive site 326 | such as ftp.uu.net, or by allowing the Copyright Holder to include your 327 | modifications in the Standard Version of the Package. 328 | 329 | b) use the modified Package only within your corporation or organization. 330 | 331 | c) rename any non-standard executables so the names do not conflict with 332 | standard executables, which must also be provided, and provide a separate 333 | manual page for each non-standard executable that clearly documents how it 334 | differs from the Standard Version. 335 | 336 | d) make other distribution arrangements with the Copyright Holder. 337 | 338 | 4. You may distribute the programs of this Package in object code or executable 339 | form, provided that you do at least ONE of the following: 340 | 341 | a) distribute a Standard Version of the executables and library files, 342 | together with instructions (in the manual page or equivalent) on where to 343 | get the Standard Version. 344 | 345 | b) accompany the distribution with the machine-readable source of the Package 346 | with your modifications. 347 | 348 | c) accompany any non-standard executables with their corresponding Standard 349 | Version executables, giving the non-standard executables non-standard 350 | names, and clearly documenting the differences in manual pages (or 351 | equivalent), together with instructions on where to get the Standard 352 | Version. 353 | 354 | d) make other distribution arrangements with the Copyright Holder. 355 | 356 | 5. You may charge a reasonable copying fee for any distribution of this 357 | Package. You may charge any fee you choose for support of this Package. You 358 | may not charge a fee for this Package itself. However, you may distribute this 359 | Package in aggregate with other (possibly commercial) programs as part of a 360 | larger (possibly commercial) software distribution provided that you do not 361 | advertise this Package as a product of your own. 362 | 363 | 6. The scripts and library files supplied as input to or produced as output 364 | from the programs of this Package do not automatically fall under the copyright 365 | of this Package, but belong to whomever generated them, and may be sold 366 | commercially, and may be aggregated with this Package. 367 | 368 | 7. C or perl subroutines supplied by you and linked into this Package shall not 369 | be considered part of this Package. 370 | 371 | 8. The name of the Copyright Holder may not be used to endorse or promote 372 | products derived from this software without specific prior written permission. 373 | 374 | 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED 375 | WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF 376 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 377 | 378 | The End 379 | 380 | -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract" : "HTTP::Tiny compatible HTTP client wrappers", 3 | "author" : [ 4 | "Tatsuhiko Miyagawa" 5 | ], 6 | "dynamic_config" : 0, 7 | "generated_by" : "Dist::Milla version v1.0.22, Dist::Zilla version 6.025, CPAN::Meta::Converter version 2.150010", 8 | "license" : [ 9 | "perl_5" 10 | ], 11 | "meta-spec" : { 12 | "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", 13 | "version" : 2 14 | }, 15 | "name" : "HTTP-Tinyish", 16 | "no_index" : { 17 | "directory" : [ 18 | "eg", 19 | "examples", 20 | "inc", 21 | "share", 22 | "t", 23 | "xt" 24 | ] 25 | }, 26 | "prereqs" : { 27 | "configure" : { 28 | "requires" : { 29 | "ExtUtils::MakeMaker" : "0" 30 | }, 31 | "suggests" : { 32 | "JSON::PP" : "2.27300" 33 | } 34 | }, 35 | "develop" : { 36 | "requires" : { 37 | "Dist::Milla" : "v1.0.22", 38 | "IO::Socket::SSL" : "1.42", 39 | "LWP" : "6", 40 | "LWP::Protocol::https" : "6", 41 | "Mozilla::CA" : "0", 42 | "Net::SSLeay" : "1.49", 43 | "Test::Pod" : "1.41" 44 | } 45 | }, 46 | "runtime" : { 47 | "requires" : { 48 | "File::Which" : "0", 49 | "HTTP::Tiny" : "0.055", 50 | "IPC::Run3" : "0", 51 | "parent" : "0", 52 | "perl" : "5.008001" 53 | } 54 | }, 55 | "test" : { 56 | "requires" : { 57 | "JSON::PP" : "0", 58 | "Test::More" : "0.96" 59 | } 60 | } 61 | }, 62 | "release_status" : "stable", 63 | "resources" : { 64 | "bugtracker" : { 65 | "web" : "https://github.com/miyagawa/HTTP-Tinyish/issues" 66 | }, 67 | "homepage" : "https://github.com/miyagawa/HTTP-Tinyish", 68 | "repository" : { 69 | "type" : "git", 70 | "url" : "https://github.com/miyagawa/HTTP-Tinyish.git", 71 | "web" : "https://github.com/miyagawa/HTTP-Tinyish" 72 | } 73 | }, 74 | "version" : "0.19", 75 | "x_contributors" : [ 76 | "Nicolas R ", 77 | "Patrick B\u00f6ker ", 78 | "Shoichi Kaji ", 79 | "Stig Palmquist ", 80 | "Takahiro SHIMIZU ", 81 | "Tatsuhiko Miyagawa ", 82 | "Tatsuhiko Miyagawa " 83 | ], 84 | "x_generated_by_perl" : "v5.34.1", 85 | "x_serialization_backend" : "Cpanel::JSON::XS version 4.27", 86 | "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later", 87 | "x_static_install" : 1 88 | } 89 | 90 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.025. 2 | use strict; 3 | use warnings; 4 | 5 | use 5.008001; 6 | 7 | use ExtUtils::MakeMaker; 8 | 9 | my %WriteMakefileArgs = ( 10 | "ABSTRACT" => "HTTP::Tiny compatible HTTP client wrappers", 11 | "AUTHOR" => "Tatsuhiko Miyagawa", 12 | "CONFIGURE_REQUIRES" => { 13 | "ExtUtils::MakeMaker" => 0 14 | }, 15 | "DISTNAME" => "HTTP-Tinyish", 16 | "LICENSE" => "perl", 17 | "MIN_PERL_VERSION" => "5.008001", 18 | "NAME" => "HTTP::Tinyish", 19 | "PREREQ_PM" => { 20 | "File::Which" => 0, 21 | "HTTP::Tiny" => "0.055", 22 | "IPC::Run3" => 0, 23 | "parent" => 0 24 | }, 25 | "TEST_REQUIRES" => { 26 | "JSON::PP" => 0, 27 | "Test::More" => "0.96" 28 | }, 29 | "VERSION" => "0.19", 30 | "test" => { 31 | "TESTS" => "t/*.t" 32 | } 33 | ); 34 | 35 | 36 | my %FallbackPrereqs = ( 37 | "File::Which" => 0, 38 | "HTTP::Tiny" => "0.055", 39 | "IPC::Run3" => 0, 40 | "JSON::PP" => 0, 41 | "Test::More" => "0.96", 42 | "parent" => 0 43 | ); 44 | 45 | 46 | unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { 47 | delete $WriteMakefileArgs{TEST_REQUIRES}; 48 | delete $WriteMakefileArgs{BUILD_REQUIRES}; 49 | $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; 50 | } 51 | 52 | delete $WriteMakefileArgs{CONFIGURE_REQUIRES} 53 | unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; 54 | 55 | WriteMakefile(%WriteMakefileArgs); 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | HTTP::Tinyish - HTTP::Tiny compatible HTTP client wrappers 4 | 5 | # SYNOPSIS 6 | 7 | my $http = HTTP::Tinyish->new(agent => "Mozilla/4.0"); 8 | 9 | my $res = $http->get("http://www.cpan.org/"); 10 | warn $res->{status}; 11 | 12 | $http->post("http://example.com/post", { 13 | headers => { "Content-Type" => "application/x-www-form-urlencoded" }, 14 | content => "foo=bar&baz=quux", 15 | }); 16 | 17 | $http->mirror("http://www.cpan.org/modules/02packages.details.txt.gz", "./02packages.details.txt.gz"); 18 | 19 | # DESCRIPTION 20 | 21 | HTTP::Tinyish is a wrapper module for HTTP client modules 22 | [LWP](https://metacpan.org/pod/LWP), [HTTP::Tiny](https://metacpan.org/pod/HTTP%3A%3ATiny) and HTTP client software `curl` and `wget`. 23 | 24 | It provides an API compatible to HTTP::Tiny, and the implementation 25 | has been extracted out of [App::cpanminus](https://metacpan.org/pod/App%3A%3Acpanminus). This module can be useful 26 | in a restrictive environment where you need to be able to download 27 | CPAN modules without an HTTPS support in built-in HTTP library. 28 | 29 | # BACKEND SELECTION 30 | 31 | Backends are searched in the order of: [LWP](https://metacpan.org/pod/LWP), [HTTP::Tiny](https://metacpan.org/pod/HTTP%3A%3ATiny), `curl` 32 | and `wget`. HTTP::Tinyish will auto-detect if the backend also 33 | supports HTTPS, and use the appropriate backend based on the given 34 | URL to the request methods. 35 | 36 | For example, if you only have HTTP::Tiny but without SSL related 37 | modules, it is possible that: 38 | 39 | my $http = HTTP::Tinyish->new; 40 | 41 | $http->get("http://example.com"); # uses HTTP::Tiny 42 | $http->get("https://example.com"); # uses curl 43 | 44 | # COMPATIBILITIES 45 | 46 | All request related methods such as `get`, `post`, `put`, 47 | `delete`, `request`, `patch` and `mirror` are supported. 48 | 49 | ## LWP 50 | 51 | - [LWP](https://metacpan.org/pod/LWP) backend requires [LWP](https://metacpan.org/pod/LWP) 5.802 or over to be functional, and [LWP::Protocol::https](https://metacpan.org/pod/LWP%3A%3AProtocol%3A%3Ahttps) to send HTTPS requests. 52 | - `mirror` method doesn't consider third options hash into account (i.e. you can't override the HTTP headers). 53 | - proxy is automatically detected from environment variables. 54 | - `timeout`, `max_redirect`, `agent`, `default_headers` and `verify_SSL` are translated. 55 | 56 | ## HTTP::Tiny 57 | 58 | Because the actual HTTP::Tiny backend is used, all APIs are supported. 59 | 60 | ## Curl 61 | 62 | - This module has been tested with curl 7.22 and later. 63 | - HTTPS support is automatically detected by running `curl --version` and see its protocol output. 64 | - `timeout`, `max_redirect`, `agent`, `default_headers` and `verify_SSL` are supported. 65 | 66 | ## Wget 67 | 68 | - This module requires Wget 1.12 and later. 69 | - Wget prior to 1.15 doesn't support sending custom HTTP methods, so if you use `$http->put` for example, you'll get an internal error response (599). 70 | - HTTPS support is automatically detected. 71 | - `mirror()` method doesn't send `If-Modified-Since` header to the server, which will result in full-download every time because `wget` doesn't support `--timestamping` combined with `-O` option. 72 | - `timeout`, `max_redirect`, `agent`, `default_headers` and `verify_SSL` are supported. 73 | 74 | # SIMILAR MODULES 75 | 76 | - [File::Fetch](https://metacpan.org/pod/File%3A%3AFetch) - is core since 5.10. Has support for non-HTTP protocols such as ftp and git. Does not support HTTPS or basic authentication as of this writing. 77 | - [Plient](https://metacpan.org/pod/Plient) - provides more complete runtime API, but seems only compatible on Unix environments. Does not support mirror() method. 78 | 79 | # AUTHOR 80 | 81 | Tatsuhiko Miyagawa 82 | 83 | # COPYRIGHT 84 | 85 | Tatsuhiko Miyagawa, 2015- 86 | 87 | # LICENSE 88 | 89 | This module is licensed under the same terms as Perl itself. 90 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'perl', '5.008001'; 2 | 3 | requires 'parent'; 4 | requires 'File::Which'; 5 | requires 'IPC::Run3'; 6 | requires 'HTTP::Tiny', 0.055; 7 | 8 | on test => sub { 9 | requires 'Test::More', '0.96'; 10 | requires 'JSON::PP'; 11 | }; 12 | 13 | on develop => sub { 14 | requires 'LWP', 6; 15 | requires 'LWP::Protocol::https', 6; 16 | requires 'IO::Socket::SSL', 1.42; 17 | requires 'Net::SSLeay', 1.49; 18 | requires 'Mozilla::CA'; 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | [@Milla] 2 | installer = MakeMaker -------------------------------------------------------------------------------- /lib/HTTP/Tinyish.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish; 2 | use strict; 3 | use warnings; 4 | use Carp (); 5 | 6 | our $VERSION = '0.19'; 7 | 8 | our $PreferredBackend; # for tests 9 | our @Backends = map "HTTP::Tinyish::$_", qw( LWP HTTPTiny Curl Wget ); 10 | my %configured; 11 | 12 | sub new { 13 | my($class, %attr) = @_; 14 | bless \%attr, $class; 15 | } 16 | 17 | for my $method (qw/get head put post delete mirror patch/) { 18 | no strict 'refs'; 19 | eval <<"HERE"; 20 | sub $method { 21 | my \$self = shift; 22 | \$self->_backend_for(\$_[0])->$method(\@_); 23 | } 24 | HERE 25 | } 26 | 27 | sub request { 28 | my $self = shift; 29 | $self->_backend_for($_[1])->request(@_); 30 | } 31 | 32 | sub _backend_for { 33 | my($self, $url) = @_; 34 | 35 | my($scheme) = $url =~ m!^(https?):!; 36 | Carp::croak "URL Scheme '$url' not supported." unless $scheme; 37 | 38 | for my $backend ($self->backends) { 39 | $self->configure_backend($backend) or next; 40 | if ($backend->supports($scheme)) { 41 | return $backend->new(%$self); 42 | } 43 | } 44 | 45 | Carp::croak "No backend configured for scheme $scheme"; 46 | } 47 | 48 | sub backends { 49 | $PreferredBackend ? ($PreferredBackend) : @Backends; 50 | } 51 | 52 | sub configure_backend { 53 | my($self, $backend) = @_; 54 | unless (exists $configured{$backend}) { 55 | $configured{$backend} = 56 | eval { require_module($backend); $backend->configure }; 57 | } 58 | $configured{$backend}; 59 | } 60 | 61 | sub require_module { 62 | local $_ = shift; 63 | s!::!/!g; 64 | require "$_.pm"; 65 | } 66 | 67 | 1; 68 | 69 | __END__ 70 | 71 | =head1 NAME 72 | 73 | HTTP::Tinyish - HTTP::Tiny compatible HTTP client wrappers 74 | 75 | =head1 SYNOPSIS 76 | 77 | my $http = HTTP::Tinyish->new(agent => "Mozilla/4.0"); 78 | 79 | my $res = $http->get("http://www.cpan.org/"); 80 | warn $res->{status}; 81 | 82 | $http->post("http://example.com/post", { 83 | headers => { "Content-Type" => "application/x-www-form-urlencoded" }, 84 | content => "foo=bar&baz=quux", 85 | }); 86 | 87 | $http->mirror("http://www.cpan.org/modules/02packages.details.txt.gz", "./02packages.details.txt.gz"); 88 | 89 | =head1 DESCRIPTION 90 | 91 | HTTP::Tinyish is a wrapper module for HTTP client modules 92 | L, L and HTTP client software C and C. 93 | 94 | It provides an API compatible to HTTP::Tiny, and the implementation 95 | has been extracted out of L. This module can be useful 96 | in a restrictive environment where you need to be able to download 97 | CPAN modules without an HTTPS support in built-in HTTP library. 98 | 99 | =head1 BACKEND SELECTION 100 | 101 | Backends are searched in the order of: L, L, C 102 | and C. HTTP::Tinyish will auto-detect if the backend also 103 | supports HTTPS, and use the appropriate backend based on the given 104 | URL to the request methods. 105 | 106 | For example, if you only have HTTP::Tiny but without SSL related 107 | modules, it is possible that: 108 | 109 | my $http = HTTP::Tinyish->new; 110 | 111 | $http->get("http://example.com"); # uses HTTP::Tiny 112 | $http->get("https://example.com"); # uses curl 113 | 114 | =head1 COMPATIBILITIES 115 | 116 | All request related methods such as C, C, C, 117 | C, C, C and C are supported. 118 | 119 | =head2 LWP 120 | 121 | =over 4 122 | 123 | =item * 124 | 125 | L backend requires L 5.802 or over to be functional, and L to send HTTPS requests. 126 | 127 | =item * 128 | 129 | C method doesn't consider third options hash into account (i.e. you can't override the HTTP headers). 130 | 131 | =item * 132 | 133 | proxy is automatically detected from environment variables. 134 | 135 | =item * 136 | 137 | C, C, C, C and C are translated. 138 | 139 | =back 140 | 141 | =head2 HTTP::Tiny 142 | 143 | Because the actual HTTP::Tiny backend is used, all APIs are supported. 144 | 145 | =head2 Curl 146 | 147 | =over 148 | 149 | =item * 150 | 151 | This module has been tested with curl 7.22 and later. 152 | 153 | =item * 154 | 155 | HTTPS support is automatically detected by running C and see its protocol output. 156 | 157 | =item * 158 | 159 | C, C, C, C and C are supported. 160 | 161 | =back 162 | 163 | =head2 Wget 164 | 165 | =over 4 166 | 167 | =item * 168 | 169 | This module requires Wget 1.12 and later. 170 | 171 | =item * 172 | 173 | Wget prior to 1.15 doesn't support sending custom HTTP methods, so if you use C<< $http->put >> for example, you'll get an internal error response (599). 174 | 175 | =item * 176 | 177 | HTTPS support is automatically detected. 178 | 179 | =item * 180 | 181 | C method doesn't send C header to the server, which will result in full-download every time because C doesn't support C<--timestamping> combined with C<-O> option. 182 | 183 | =item * 184 | 185 | C, C, C, C and C are supported. 186 | 187 | =back 188 | 189 | =head1 SIMILAR MODULES 190 | 191 | =over 4 192 | 193 | =item * 194 | 195 | L - is core since 5.10. Has support for non-HTTP protocols such as ftp and git. Does not support HTTPS or basic authentication as of this writing. 196 | 197 | =item * 198 | 199 | L - provides more complete runtime API, but seems only compatible on Unix environments. Does not support mirror() method. 200 | 201 | =back 202 | 203 | =head1 AUTHOR 204 | 205 | Tatsuhiko Miyagawa 206 | 207 | =head1 COPYRIGHT 208 | 209 | Tatsuhiko Miyagawa, 2015- 210 | 211 | =head1 LICENSE 212 | 213 | This module is licensed under the same terms as Perl itself. 214 | 215 | =cut 216 | 217 | -------------------------------------------------------------------------------- /lib/HTTP/Tinyish/Base.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish::Base; 2 | use strict; 3 | use warnings; 4 | 5 | for my $sub_name ( qw/get head put post delete patch/ ) { 6 | my $req_method = uc $sub_name; 7 | eval <<"HERE"; 8 | sub $sub_name { 9 | my (\$self, \$url, \$args) = \@_; 10 | \@_ == 2 || (\@_ == 3 && ref \$args eq 'HASH') 11 | or Carp::croak(q/Usage: \$http->$sub_name(URL, [HASHREF])/ . "\n"); 12 | return \$self->request('$req_method', \$url, \$args || {}); 13 | } 14 | 15 | HERE 16 | } 17 | 18 | sub parse_http_header { 19 | my($self, $header, $res) = @_; 20 | 21 | # it might have multiple headers in it because of redirects 22 | $header =~ s/.*^(HTTP\/\d(?:\.\d)?)/$1/ms; 23 | 24 | # grab the first chunk until the line break 25 | if ($header =~ /^(.*?\x0d?\x0a\x0d?\x0a)/) { 26 | $header = $1; 27 | } 28 | 29 | # parse into lines 30 | my @header = split /\x0d?\x0a/,$header; 31 | my $status_line = shift @header; 32 | 33 | # join folded lines 34 | my @out; 35 | for (@header) { 36 | if(/^[ \t]+/) { 37 | return -1 unless @out; 38 | $out[-1] .= $_; 39 | } else { 40 | push @out, $_; 41 | } 42 | } 43 | 44 | my($proto, $status, $reason) = split / /, $status_line, 3; 45 | return unless $proto and $proto =~ /^HTTP\/(\d+)(\.(\d+))?$/i; 46 | 47 | $res->{status} = $status; 48 | $res->{reason} = $reason; 49 | $res->{success} = $status =~ /^(?:2|304)/; 50 | $res->{protocol} = $proto; 51 | 52 | # import headers 53 | my $token = qr/[^][\x00-\x1f\x7f()<>@,;:\\"\/?={} \t]+/; 54 | my $k; 55 | for my $header (@out) { 56 | if ( $header =~ s/^($token): ?// ) { 57 | $k = lc $1; 58 | } elsif ( $header =~ /^\s+/) { 59 | # multiline header 60 | } else { 61 | return -1; 62 | } 63 | 64 | if (exists $res->{headers}{$k}) { 65 | $res->{headers}{$k} = [$res->{headers}{$k}] 66 | unless ref $res->{headers}{$k}; 67 | push @{$res->{headers}{$k}}, $header; 68 | } else { 69 | $res->{headers}{$k} = $header; 70 | } 71 | } 72 | } 73 | 74 | sub internal_error { 75 | my($self, $url, $message) = @_; 76 | 77 | return { 78 | content => $message, 79 | headers => { "content-length" => length($message), "content-type" => "text/plain" }, 80 | reason => "Internal Exception", 81 | status => 599, 82 | success => "", 83 | url => $url, 84 | }; 85 | } 86 | 87 | 1; 88 | -------------------------------------------------------------------------------- /lib/HTTP/Tinyish/Curl.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish::Curl; 2 | use strict; 3 | use warnings; 4 | use parent qw(HTTP::Tinyish::Base); 5 | 6 | use IPC::Run3 qw(run3); 7 | use File::Which qw(which); 8 | use File::Temp (); 9 | 10 | my %supports; 11 | my $curl; 12 | 13 | sub _slurp { 14 | open my $fh, "<", shift or die $!; 15 | local $/; 16 | <$fh>; 17 | } 18 | 19 | sub configure { 20 | my $class = shift; 21 | 22 | my %meta; 23 | $curl = which('curl'); 24 | 25 | eval { 26 | run3([$curl, '--version'], \undef, \my $version, \my $error); 27 | if ($version =~ /^Protocols: (.*)/m) { 28 | my %protocols = map { $_ => 1 } split /\s/, $1; 29 | $supports{http} = 1 if $protocols{http}; 30 | $supports{https} = 1 if $protocols{https}; 31 | } 32 | 33 | $meta{$curl} = $version; 34 | }; 35 | 36 | \%meta; 37 | } 38 | 39 | sub supports { $supports{$_[1]} } 40 | 41 | sub new { 42 | my($class, %attr) = @_; 43 | bless \%attr, $class; 44 | } 45 | 46 | sub request { 47 | my($self, $method, $url, $opts) = @_; 48 | $opts ||= {}; 49 | 50 | my(undef, $temp) = File::Temp::tempfile(UNLINK => 1); 51 | 52 | my($output, $error); 53 | eval { 54 | run3 [ 55 | $curl, 56 | '-X', $method, 57 | ($method eq 'HEAD' ? ('--head') : ()), 58 | $self->build_options($url, $opts), 59 | '--dump-header', $temp, 60 | $url, 61 | ], \undef, \$output, \$error, 62 | { 63 | binmode_stdout => ":raw", 64 | binmode_stderr => ":raw", 65 | }; 66 | }; 67 | 68 | if ($@ or $?) { 69 | return $self->internal_error($url, $@ || $error); 70 | } 71 | 72 | my $res = { url => $url, content => $output }; 73 | $self->parse_http_header( _slurp($temp), $res ); 74 | $res; 75 | } 76 | 77 | sub mirror { 78 | my($self, $url, $file, $opts) = @_; 79 | $opts ||= {}; 80 | 81 | my(undef, $temp) = File::Temp::tempfile(UNLINK => 1); 82 | 83 | my($output, $error); 84 | eval { 85 | run3 [ 86 | $curl, 87 | $self->build_options($url, $opts), 88 | '-z', $file, 89 | '-o', $file, 90 | '--dump-header', $temp, 91 | '--remote-time', 92 | $url, 93 | ], \undef, \$output, \$error, 94 | { 95 | binmode_stdout => ":raw", 96 | binmode_stderr => ":raw", 97 | }; 98 | }; 99 | 100 | if ($@ or $?) { 101 | return $self->internal_error($url, $@ || $error); 102 | } 103 | 104 | my $res = { url => $url, content => $output }; 105 | $self->parse_http_header( _slurp($temp), $res ); 106 | $res; 107 | } 108 | 109 | sub build_options { 110 | my($self, $url, $opts) = @_; 111 | 112 | my @options = ( 113 | '--silent', 114 | '--show-error', 115 | '--max-time', ($self->{timeout} || 60), 116 | '--user-agent', ($self->{agent} || "HTTP-Tinyish/$HTTP::Tinyish::VERSION"), 117 | ); 118 | if (my $max_redirect = exists $self->{max_redirect} ? $self->{max_redirect} : 5) { 119 | push @options, '--location', '--max-redirs', $max_redirect; 120 | } 121 | 122 | my %headers; 123 | if ($self->{default_headers}) { 124 | %headers = %{$self->{default_headers}}; 125 | } 126 | if ($opts->{headers}) { 127 | %headers = (%headers, %{$opts->{headers}}); 128 | } 129 | $self->_translate_headers(\%headers, \@options); 130 | 131 | unless ($self->{verify_SSL}) { 132 | push @options, '--insecure'; 133 | } 134 | 135 | if ($opts->{content}) { 136 | my $content; 137 | if (ref $opts->{content} eq 'CODE') { 138 | while (my $chunk = $opts->{content}->()) { 139 | $content .= $chunk; 140 | } 141 | } else { 142 | $content = $opts->{content}; 143 | } 144 | push @options, '--data', $content; 145 | } 146 | 147 | @options; 148 | } 149 | 150 | sub _translate_headers { 151 | my($self, $headers, $options) = @_; 152 | 153 | for my $field (keys %$headers) { 154 | my $value = $headers->{$field}; 155 | if (ref $value eq 'ARRAY') { 156 | push @$options, map { ('-H', "$field:$_") } @$value; 157 | } else { 158 | push @$options, '-H', "$field:$value"; 159 | } 160 | } 161 | } 162 | 163 | 1; 164 | -------------------------------------------------------------------------------- /lib/HTTP/Tinyish/HTTPTiny.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish::HTTPTiny; 2 | use strict; 3 | use parent qw(HTTP::Tinyish::Base); 4 | use HTTP::Tiny; 5 | 6 | my %supports = (http => 1); 7 | 8 | sub configure { 9 | my %meta = ("HTTP::Tiny" => $HTTP::Tiny::VERSION); 10 | 11 | $supports{https} = HTTP::Tiny->can_ssl; 12 | 13 | \%meta; 14 | } 15 | 16 | sub supports { $supports{$_[1]} } 17 | 18 | sub new { 19 | my($class, %attrs) = @_; 20 | bless { 21 | tiny => HTTP::Tiny->new(%attrs), 22 | }, $class; 23 | } 24 | 25 | sub request { 26 | my $self = shift; 27 | $self->{tiny}->request(@_); 28 | } 29 | 30 | sub mirror { 31 | my $self = shift; 32 | $self->{tiny}->mirror(@_); 33 | } 34 | 35 | 1; 36 | 37 | -------------------------------------------------------------------------------- /lib/HTTP/Tinyish/LWP.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish::LWP; 2 | use strict; 3 | use parent qw(HTTP::Tinyish::Base); 4 | 5 | use LWP 5.802; 6 | use LWP::UserAgent; 7 | 8 | my %supports = (http => 1); 9 | 10 | sub configure { 11 | my %meta = ( 12 | LWP => $LWP::VERSION, 13 | ); 14 | 15 | if (eval { require LWP::Protocol::https; require Mozilla::CA; 1 }) { 16 | $supports{https} = 1; 17 | $meta{"LWP::Protocol::https"} = $LWP::Protocol::https::VERSION; 18 | } 19 | 20 | \%meta; 21 | } 22 | 23 | sub supports { 24 | $supports{$_[1]}; 25 | } 26 | 27 | sub new { 28 | my($class, %attr) = @_; 29 | 30 | my $ua = LWP::UserAgent->new; 31 | 32 | bless { 33 | ua => $class->translate_lwp($ua, %attr), 34 | }, $class; 35 | } 36 | 37 | sub _headers_to_hashref { 38 | my($self, $hdrs) = @_; 39 | 40 | my %headers; 41 | for my $field ($hdrs->header_field_names) { 42 | $headers{lc $field} = $hdrs->header($field); # could be an array ref 43 | } 44 | 45 | \%headers; 46 | } 47 | 48 | sub request { 49 | my($self, $method, $url, $opts) = @_; 50 | $opts ||= {}; 51 | 52 | my $req = HTTP::Request->new($method => $url); 53 | 54 | if ($opts->{headers}) { 55 | $req->header(%{$opts->{headers}}); 56 | } 57 | 58 | if ($opts->{content}) { 59 | $req->content($opts->{content}); 60 | } 61 | 62 | my $res = $self->{ua}->request($req); 63 | 64 | if ($self->is_internal_response($res)) { 65 | return $self->internal_error($url, $res->content); 66 | } 67 | 68 | return { 69 | url => $url, 70 | content => $res->decoded_content(charset => 'none'), 71 | success => $res->is_success, 72 | status => $res->code, 73 | reason => $res->message, 74 | headers => $self->_headers_to_hashref($res->headers), 75 | protocol => $res->protocol, 76 | }; 77 | } 78 | 79 | sub mirror { 80 | my($self, $url, $file) = @_; 81 | 82 | # TODO support optional headers 83 | my $res = $self->{ua}->mirror($url, $file); 84 | 85 | if ($self->is_internal_response($res)) { 86 | return $self->internal_error($url, $res->content); 87 | } 88 | 89 | return { 90 | url => $url, 91 | content => $res->decoded_content, 92 | success => $res->is_success || $res->code == 304, 93 | status => $res->code, 94 | reason => $res->message, 95 | headers => $self->_headers_to_hashref($res->headers), 96 | protocol => $res->protocol, 97 | }; 98 | } 99 | 100 | sub translate_lwp { 101 | my($class, $agent, %attr) = @_; 102 | 103 | $agent->parse_head(0); 104 | $agent->env_proxy; 105 | $agent->timeout(delete $attr{timeout} || 60); 106 | $agent->max_redirect(exists $attr{max_redirect} ? $attr{max_redirect} : 5); 107 | $agent->agent(delete $attr{agent} || "HTTP-Tinyish/$HTTP::Tinyish::VERSION"); 108 | 109 | # LWP default is to verify, HTTP::Tiny isn't 110 | unless ($attr{verify_SSL}) { 111 | if ($agent->can("ssl_opts")) { 112 | $agent->ssl_opts(verify_hostname => 0); 113 | } 114 | } 115 | 116 | if ($attr{default_headers}) { 117 | $agent->default_headers( HTTP::Headers->new(%{$attr{default_headers}}) ); 118 | } 119 | 120 | $agent; 121 | } 122 | 123 | sub is_internal_response { 124 | my($self, $res) = @_; 125 | 126 | $res->code == 500 && 127 | ( $res->header('Client-Warning') || '' ) eq 'Internal response'; 128 | } 129 | 130 | 1; 131 | -------------------------------------------------------------------------------- /lib/HTTP/Tinyish/Wget.pm: -------------------------------------------------------------------------------- 1 | package HTTP::Tinyish::Wget; 2 | use strict; 3 | use warnings; 4 | use parent qw(HTTP::Tinyish::Base); 5 | 6 | use IPC::Run3 qw(run3); 7 | use File::Which qw(which); 8 | 9 | my %supports; 10 | my $wget; 11 | my $method_supported; 12 | 13 | sub _run_wget { 14 | run3([$wget, @_], \undef, \my $out, \my $err); 15 | wantarray ? ($out, $err) : $out; 16 | } 17 | 18 | sub configure { 19 | my $class = shift; 20 | my %meta; 21 | 22 | $wget = which('wget'); 23 | 24 | eval { 25 | local $ENV{LC_ALL} = 'en_US'; 26 | 27 | $meta{$wget} = _run_wget('--version'); 28 | unless ($meta{$wget} =~ /GNU Wget 1\.(\d+)/ and $1 >= 12) { 29 | die "Wget version is too old. $meta{$wget}"; 30 | } 31 | 32 | my $config = $class->new(agent => __PACKAGE__); 33 | my @options = grep { $_ ne '--quiet' } $config->build_options("GET"); 34 | 35 | my(undef, $err) = _run_wget(@options, 'https://'); 36 | if ($err && $err =~ /HTTPS support not compiled/) { 37 | $supports{http} = 1; 38 | } elsif ($err && $err =~ /Invalid host/) { 39 | $supports{http} = $supports{https} = 1; 40 | } 41 | 42 | (undef, $err) = _run_wget('--method', 'GET', 'http://'); 43 | if ($err && $err =~ /Invalid host/) { 44 | $method_supported = $meta{method_supported} = 1; 45 | } 46 | 47 | }; 48 | 49 | \%meta; 50 | } 51 | 52 | sub supports { $supports{$_[1]} } 53 | 54 | sub new { 55 | my($class, %attr) = @_; 56 | bless \%attr, $class; 57 | } 58 | 59 | sub request { 60 | my($self, $method, $url, $opts) = @_; 61 | $opts ||= {}; 62 | 63 | my($stdout, $stderr); 64 | eval { 65 | run3 [ 66 | $wget, 67 | $self->build_options($method, $url, $opts), 68 | $url, 69 | '-O', '-', 70 | ], \undef, \$stdout, \$stderr; 71 | }; 72 | 73 | # wget exit codes: (man wget) 74 | # 4 Network failure. 75 | # 5 SSL verification failure. 76 | # 6 Username/password authentication failure. 77 | # 7 Protocol errors. 78 | # 8 Server issued an error response. 79 | if ($@ or $? && ($? >> 8) <= 5) { 80 | return $self->internal_error($url, $@ || $stderr); 81 | } 82 | 83 | my $header = ''; 84 | $stderr =~ s{^ (\S.*)$}{ $header .= $1."\n" }gem; 85 | 86 | my $res = { url => $url, content => $stdout }; 87 | $self->parse_http_header($header, $res); 88 | $res; 89 | } 90 | 91 | sub mirror { 92 | my($self, $url, $file, $opts) = @_; 93 | $opts ||= {}; 94 | 95 | # This doesn't send If-Modified-Since because -O and -N are mutually exclusive :( 96 | my($stdout, $stderr); 97 | eval { 98 | run3 [ 99 | $wget, 100 | $self->build_options("GET", $url, $opts), 101 | $url, 102 | '-O', $file, 103 | ], \undef, \$stdout, \$stderr, 104 | { 105 | binmode_stdout => ":raw", 106 | binmode_stderr => ":raw", 107 | }; 108 | }; 109 | 110 | if ($@ or $?) { 111 | return $self->internal_error($url, $@ || $stderr); 112 | } 113 | 114 | $stderr =~ s/^ //gm; 115 | 116 | my $res = { url => $url, content => $stdout }; 117 | $self->parse_http_header($stderr, $res); 118 | $res; 119 | } 120 | 121 | sub build_options { 122 | my($self, $method, $url, $opts) = @_; 123 | 124 | my @options = ( 125 | '--retry-connrefused', 126 | '--server-response', 127 | '--timeout', ($self->{timeout} || 60), 128 | '--tries', 1, 129 | '--max-redirect', (exists $self->{max_redirect} ? $self->{max_redirect} : 5), 130 | '--user-agent', ($self->{agent} || "HTTP-Tinyish/$HTTP::Tinyish::VERSION"), 131 | ); 132 | 133 | if ($method_supported) { 134 | push @options, "--method", $method; 135 | } else { 136 | if ($method eq 'GET' or $method eq 'POST') { 137 | # OK 138 | } elsif ($method eq 'HEAD') { 139 | push @options, '--spider'; 140 | } else { 141 | die "This version of wget doesn't support specifying HTTP method '$method'"; 142 | } 143 | } 144 | 145 | if ($self->{agent}) { 146 | push @options, '--user-agent', $self->{agent}; 147 | } 148 | 149 | my %headers; 150 | if ($self->{default_headers}) { 151 | %headers = %{$self->{default_headers}}; 152 | } 153 | if ($opts->{headers}) { 154 | %headers = (%headers, %{$opts->{headers}}); 155 | } 156 | $self->_translate_headers(\%headers, \@options); 157 | 158 | if ($supports{https} && !$self->{verify_SSL}) { 159 | push @options, '--no-check-certificate'; 160 | } 161 | 162 | if ($opts->{content}) { 163 | my $content; 164 | if (ref $opts->{content} eq 'CODE') { 165 | while (my $chunk = $opts->{content}->()) { 166 | $content .= $chunk; 167 | } 168 | } else { 169 | $content = $opts->{content}; 170 | } 171 | 172 | if ($method_supported) { 173 | push @options, '--body-data', $content; 174 | } else { 175 | push @options, '--post-data', $content; 176 | } 177 | } 178 | 179 | @options; 180 | } 181 | 182 | sub _translate_headers { 183 | my($self, $headers, $options) = @_; 184 | 185 | for my $field (keys %$headers) { 186 | my $value = $headers->{$field}; 187 | if (ref $value eq 'ARRAY') { 188 | # wget doesn't honor multiple header fields 189 | push @$options, '--header', "$field:" . join(",", @$value); 190 | } else { 191 | push @$options, '--header', "$field:$value"; 192 | } 193 | } 194 | } 195 | 196 | 1; 197 | -------------------------------------------------------------------------------- /t/tinyish.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use Test::More; 3 | use HTTP::Tinyish; 4 | use File::Temp qw(tempdir); 5 | use JSON::PP qw(decode_json); 6 | 7 | unless ($ENV{LIVE_TEST} or -e ".git") { 8 | pass "skip network testing"; 9 | done_testing; 10 | exit; 11 | } 12 | 13 | sub read_file { 14 | open my $fh, "<", shift; 15 | join "", <$fh>; 16 | } 17 | 18 | my @backends = $ENV{TEST_BACKEND} 19 | ? map "HTTP::Tinyish::$_", split(",", $ENV{TEST_BACKEND}) 20 | : @HTTP::Tinyish::Backends; 21 | 22 | for my $backend (@backends) { 23 | $HTTP::Tinyish::PreferredBackend = $backend; 24 | my $config = HTTP::Tinyish->configure_backend($backend); 25 | next unless $config && $backend->supports('http'); 26 | 27 | diag "Testing with $backend"; 28 | 29 | my $res = HTTP::Tinyish->new->get("http://www.cpan.org"); 30 | is $res->{status}, 200; 31 | like $res->{content}, qr/Comprehensive/; 32 | 33 | SKIP: { 34 | skip "HTTPS is not supported with $backend", 2 unless $backend->supports('https'); 35 | $res = HTTP::Tinyish->new(verify_SSL => 1)->get("https://github.com/"); 36 | is $res->{status}, 200; 37 | like $res->{content}, qr/github/i; 38 | 39 | $res = HTTP::Tinyish->new(verify_SSL => 0)->get("https://cpan.metacpan.org/"); 40 | is $res->{status}, 200; 41 | like $res->{content}, qr/Comprehensive/i; 42 | 43 | for (qw(expired untrusted-root wrong.host)) { 44 | $res = HTTP::Tinyish->new(verify_SSL => 1)->get("https://$_.badssl.com/"); 45 | is $res->{status}, 599, $_; 46 | ok !$res->{success}, $_; 47 | } 48 | } 49 | 50 | $res = HTTP::Tinyish->new->get("http://example.invalid"); 51 | is $res->{status}, 599; 52 | ok !$res->{success}; 53 | 54 | $res = HTTP::Tinyish->new->head("http://httpbin.org/headers"); 55 | is $res->{status}, 200; 56 | 57 | $res = HTTP::Tinyish->new->post("http://httpbin.org/post", { 58 | headers => { 'Content-Type' => 'application/x-www-form-urlencoded' }, 59 | content => "foo=1&bar=2", 60 | }); 61 | is $res->{status}, 200; 62 | is_deeply decode_json($res->{content})->{form}, { foo => "1", bar => "2" }; 63 | 64 | # patch 65 | $res = HTTP::Tinyish->new->patch("http://httpbin.org/patch", { 66 | headers => { 'Content-Type' => 'application/x-www-form-urlencoded' }, 67 | content => "foo=1&bar=2", 68 | }); 69 | is $res->{status}, 200; 70 | is_deeply decode_json($res->{content})->{form}, { foo => "1", bar => "2" }; 71 | 72 | SKIP: { 73 | skip "HTTP::Tiny's and LWP's chunked uploads are not supported by httpbin.", 1 if $backend =~ /HTTPTiny|LWP/; 74 | my @data = ("xyz\n", "xyz"); 75 | $res = HTTP::Tinyish->new(timeout => 1)->post("http://httpbin.org/post", { 76 | headers => { 'Content-Type' => 'application/octet-stream' }, 77 | content => sub { shift @data }, 78 | }); 79 | is $res->{status}, 200; 80 | is_deeply decode_json($res->{content})->{data}, "xyz\nxyz"; 81 | } 82 | 83 | SKIP: { 84 | skip "wget before 1.15 doesn't support custom HTTP methods", 2 85 | if $backend =~ /Wget/ && !$config->{method_supported}; 86 | $res = HTTP::Tinyish->new->put("http://httpbin.org/put", { 87 | headers => { 'Content-Type' => 'text/plain' }, 88 | content => "foobarbaz", 89 | }); 90 | is $res->{status}, 200; 91 | is_deeply decode_json($res->{content})->{data}, "foobarbaz"; 92 | } 93 | 94 | $res = HTTP::Tinyish->new(default_headers => { "Foo" => "Bar", Dnt => "1" }) 95 | ->get("http://httpbin.org/headers", { headers => { "Foo" => ["Bar", "Baz"] } }); 96 | SKIP: { 97 | skip "httpbin does not support multiple headers", 1; 98 | is decode_json($res->{content})->{headers}{Foo}, "Bar,Baz"; 99 | } 100 | is decode_json($res->{content})->{headers}{Dnt}, "1"; 101 | 102 | my $fn = tempdir(CLEANUP => 1) . "/index.html"; 103 | $res = HTTP::Tinyish->new->mirror("http://www.cpan.org", $fn); 104 | is $res->{status}, 200; 105 | like read_file($fn), qr/Comprehensive/; 106 | 107 | SKIP: { 108 | skip "Wget doesn't handle mirror", 1 if $backend =~ /Wget/; 109 | $res = HTTP::Tinyish->new->mirror("http://www.cpan.org", $fn); 110 | is $res->{status}, 304; 111 | ok $res->{success}; 112 | } 113 | 114 | my $fn = tempdir(CLEANUP => 1) . "/index.html"; 115 | $res = HTTP::Tinyish->new->mirror("http://example.invalid", $fn); 116 | is $res->{status}, 599; 117 | ok !$res->{success}; 118 | 119 | $res = HTTP::Tinyish->new(agent => "Menlo/1")->get("http://httpbin.org/user-agent"); 120 | is_deeply decode_json($res->{content}), { 'user-agent' => "Menlo/1" }; 121 | 122 | $res = HTTP::Tinyish->new->get("http://httpbin.org/status/404"); 123 | is $res->{status}, 404; 124 | is $res->{reason}, "NOT FOUND"; 125 | 126 | $res = HTTP::Tinyish->new->get("http://httpbin.org/response-headers?Foo=Bar+Baz"); 127 | is $res->{headers}{foo}, "Bar Baz"; 128 | 129 | $res = HTTP::Tinyish->new->get("http://httpbin.org/basic-auth/user/passwd"); 130 | is $res->{status}, 401; 131 | 132 | $res = HTTP::Tinyish->new->get("http://user:passwd\@httpbin.org/basic-auth/user/passwd"); 133 | is $res->{status}, 200; 134 | is_deeply decode_json($res->{content}), { authenticated => JSON::PP::true(), user => "user" }; 135 | 136 | if (0) { 137 | # for some reason it's broken 138 | $res = HTTP::Tinyish->new->get("http://httpbin.org/redirect/1"); 139 | is $res->{status}, 200; 140 | } 141 | 142 | $res = HTTP::Tinyish->new(max_redirect => 0)->get("http://httpbin.org/redirect/1"); 143 | is $res->{status}, 302; 144 | 145 | $res = HTTP::Tinyish->new(max_redirect => 2)->get("http://httpbin.org/redirect/3"); 146 | isnt $res->{status}, 200; # either 302 or 599 147 | 148 | $res = HTTP::Tinyish->new(timeout => 1)->get("http://httpbin.org/delay/2"); 149 | is substr($res->{status}, 0, 1), '5'; 150 | 151 | $res = HTTP::Tinyish->new->get("http://httpbin.org/encoding/utf8"); 152 | like $res->{content}, qr/コンニチハ/; 153 | } 154 | 155 | done_testing; 156 | --------------------------------------------------------------------------------