├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json └── src ├── AbstractTransport.php ├── Exception ├── InvalidResponseCodeException.php └── UnexpectedResponseException.php ├── Http.php ├── HttpFactory.php ├── Response.php ├── Transport ├── Curl.php ├── Socket.php └── Stream.php └── TransportInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The HTTP Package [![Build Status](https://ci.joomla.org/api/badges/joomla-framework/http/status.svg?ref=refs/heads/3.x-dev)](https://ci.joomla.org/joomla-framework/http) 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/joomla/http/v/stable)](https://packagist.org/packages/joomla/http) 4 | [![Total Downloads](https://poser.pugx.org/joomla/http/downloads)](https://packagist.org/packages/joomla/http) 5 | [![Latest Unstable Version](https://poser.pugx.org/joomla/http/v/unstable)](https://packagist.org/packages/joomla/http) 6 | [![License](https://poser.pugx.org/joomla/http/license)](https://packagist.org/packages/joomla/http) 7 | 8 | The HTTP package includes a [PSR-18](http://www.php-fig.org/psr/psr-18/) compatible HTTP client to facilitate RESTful HTTP requests 9 | over a variety of transport protocols. 10 | 11 | ## Requirements 12 | 13 | * PHP 8.1 or later 14 | 15 | ## Installation via Composer 16 | 17 | Add `"joomla/http": "~3.0"` to the require block in your composer.json and then run `composer install`. 18 | 19 | ```json 20 | { 21 | "require": { 22 | "joomla/http": "~3.0" 23 | } 24 | } 25 | ``` 26 | 27 | Alternatively, you can simply run the following from the command line: 28 | 29 | ```sh 30 | composer require joomla/http "~3.0" 31 | ``` 32 | 33 | If you want to include the test sources and docs, use 34 | 35 | ```sh 36 | composer require --prefer-source joomla/http "~3.0" 37 | ``` 38 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | These versions are currently being supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.x.x | :white_check_mark: | 10 | | 2.0.x | :white_check_mark: | 11 | | 1.3.x | :x: | 12 | | < 1.3 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a security issue in the core Joomla! CMS or Framework, or with a joomla.org website, please submit 17 | [the form on our portal](https://developer.joomla.org/security/contact-the-team.html) containing as much detail 18 | as possible about the issue. Additional information about our security team and their processes may be found on 19 | our [Security page](https://developer.joomla.org/security.html). 20 | 21 | To report an issue in a Joomla! extension, please submit it to the [Vulnerable Extensions List](https://vel.joomla.org/submit-vel). 22 | 23 | For support with a site which has been attacked, please visit the [Joomla! Forum](https://forum.joomla.org/viewforum.php?f=714). 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joomla/http", 3 | "type": "joomla-package", 4 | "description": "Joomla HTTP Package", 5 | "keywords": ["joomla", "framework", "http"], 6 | "homepage": "https://github.com/joomla-framework/http", 7 | "license": "GPL-2.0-or-later", 8 | "require": { 9 | "php": "^8.1.0", 10 | "psr/http-client": "^1.0", 11 | "psr/http-message": "^1.0", 12 | "joomla/uri": "^3.0", 13 | "composer/ca-bundle": "^1.3.5", 14 | "laminas/laminas-diactoros": "^2.24.0" 15 | }, 16 | "require-dev": { 17 | "joomla/test": "^3.0", 18 | "phpunit/phpunit": "^9.5.28", 19 | "squizlabs/php_codesniffer": "^3.7.2", 20 | "phpstan/phpstan": "^2.0", 21 | "phpstan/phpstan-deprecation-rules": "^2.0", 22 | "phan/phan": "^5.4.2" 23 | }, 24 | "suggest": { 25 | "ext-curl": "To use cURL for HTTP connections" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Joomla\\Http\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Joomla\\Http\\Tests\\": "Tests/" 35 | } 36 | }, 37 | "minimum-stability": "stable", 38 | "extra": { 39 | "branch-alias": { 40 | "dev-2.0-dev": "2.0-dev", 41 | "dev-3.x-dev": "3.0-dev" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/AbstractTransport.php: -------------------------------------------------------------------------------- 1 | options = $options; 48 | } 49 | 50 | /** 51 | * Get an option from the HTTP transport. 52 | * 53 | * @param string $key The name of the option to get. 54 | * @param mixed $default The default value if the option is not set. 55 | * 56 | * @return mixed The option value. 57 | * 58 | * @since 2.0.0 59 | */ 60 | protected function getOption(string $key, $default = null) 61 | { 62 | return $this->options[$key] ?? $default; 63 | } 64 | 65 | /** 66 | * Processes the headers from a transport's response data. 67 | * 68 | * @param array $headers The headers to process. 69 | * 70 | * @return array 71 | * 72 | * @since 2.0.0 73 | */ 74 | protected function processHeaders(array $headers): array 75 | { 76 | $verifiedHeaders = []; 77 | 78 | // Add the response headers to the response object. 79 | foreach ($headers as $header) { 80 | $pos = strpos($header, ':'); 81 | $keyName = trim(substr($header, 0, $pos)); 82 | 83 | if (!isset($verifiedHeaders[$keyName])) { 84 | $verifiedHeaders[$keyName] = []; 85 | } 86 | 87 | $verifiedHeaders[$keyName][] = trim(substr($header, ($pos + 1))); 88 | } 89 | 90 | return $verifiedHeaders; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Exception/InvalidResponseCodeException.php: -------------------------------------------------------------------------------- 1 | response = $response; 45 | } 46 | 47 | /** 48 | * Get the Response object. 49 | * 50 | * @return Response 51 | * 52 | * @since 1.2.0 53 | */ 54 | public function getResponse() 55 | { 56 | return $this->response; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | options = $options; 60 | 61 | if (!$transport) { 62 | $transport = (new HttpFactory())->getAvailableDriver($this->options); 63 | 64 | // Ensure the transport is a TransportInterface instance or bail out 65 | if (!($transport instanceof TransportInterface)) { 66 | throw new \InvalidArgumentException(sprintf('A valid %s object was not set.', TransportInterface::class)); 67 | } 68 | } 69 | 70 | $this->transport = $transport; 71 | } 72 | 73 | /** 74 | * Get an option from the HTTP client. 75 | * 76 | * @param string $key The name of the option to get. 77 | * @param mixed $default The default value if the option is not set. 78 | * 79 | * @return mixed The option value. 80 | * 81 | * @since 1.0 82 | */ 83 | public function getOption($key, $default = null) 84 | { 85 | return $this->options[$key] ?? $default; 86 | } 87 | 88 | /** 89 | * Set an option for the HTTP client. 90 | * 91 | * @param string $key The name of the option to set. 92 | * @param mixed $value The option value to set. 93 | * 94 | * @return $this 95 | * 96 | * @since 1.0 97 | */ 98 | public function setOption($key, $value) 99 | { 100 | $this->options[$key] = $value; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Method to send the OPTIONS command to the server. 107 | * 108 | * @param string|UriInterface $url The URI to the resource to request. 109 | * @param array $headers An array of request headers to send with the request. 110 | * @param integer $timeout Read timeout in seconds. 111 | * 112 | * @return Response 113 | * 114 | * @since 1.0 115 | */ 116 | public function options($url, array $headers = [], $timeout = null) 117 | { 118 | return $this->makeTransportRequest('OPTIONS', $url, null, $headers, $timeout); 119 | } 120 | 121 | /** 122 | * Method to send the HEAD command to the server. 123 | * 124 | * @param string|UriInterface $url The URI to the resource to request. 125 | * @param array $headers An array of request headers to send with the request. 126 | * @param integer $timeout Read timeout in seconds. 127 | * 128 | * @return Response 129 | * 130 | * @since 1.0 131 | */ 132 | public function head($url, array $headers = [], $timeout = null) 133 | { 134 | return $this->makeTransportRequest('HEAD', $url, null, $headers, $timeout); 135 | } 136 | 137 | /** 138 | * Method to send the GET command to the server. 139 | * 140 | * @param string|UriInterface $url The URI to the resource to request. 141 | * @param array $headers An array of request headers to send with the request. 142 | * @param integer $timeout Read timeout in seconds. 143 | * 144 | * @return Response 145 | * 146 | * @since 1.0 147 | */ 148 | public function get($url, array $headers = [], $timeout = null) 149 | { 150 | return $this->makeTransportRequest('GET', $url, null, $headers, $timeout); 151 | } 152 | 153 | /** 154 | * Method to send the POST command to the server. 155 | * 156 | * @param string|UriInterface $url The URI to the resource to request. 157 | * @param mixed $data Either an associative array or a string to be sent with the request. 158 | * @param array $headers An array of request headers to send with the request. 159 | * @param integer $timeout Read timeout in seconds. 160 | * 161 | * @return Response 162 | * 163 | * @since 1.0 164 | */ 165 | public function post($url, $data, array $headers = [], $timeout = null) 166 | { 167 | return $this->makeTransportRequest('POST', $url, $data, $headers, $timeout); 168 | } 169 | 170 | /** 171 | * Method to send the PUT command to the server. 172 | * 173 | * @param string|UriInterface $url The URI to the resource to request. 174 | * @param mixed $data Either an associative array or a string to be sent with the request. 175 | * @param array $headers An array of request headers to send with the request. 176 | * @param integer $timeout Read timeout in seconds. 177 | * 178 | * @return Response 179 | * 180 | * @since 1.0 181 | */ 182 | public function put($url, $data, array $headers = [], $timeout = null) 183 | { 184 | return $this->makeTransportRequest('PUT', $url, $data, $headers, $timeout); 185 | } 186 | 187 | /** 188 | * Method to send the DELETE command to the server. 189 | * 190 | * @param string|UriInterface $url The URI to the resource to request. 191 | * @param array $headers An array of request headers to send with the request. 192 | * @param integer $timeout Read timeout in seconds. 193 | * @param mixed $data Either an associative array or a string to be sent with the request. 194 | * 195 | * @return Response 196 | * 197 | * @since 1.0 198 | */ 199 | public function delete($url, array $headers = [], $timeout = null, $data = null) 200 | { 201 | return $this->makeTransportRequest('DELETE', $url, $data, $headers, $timeout); 202 | } 203 | 204 | /** 205 | * Method to send the TRACE command to the server. 206 | * 207 | * @param string|UriInterface $url The URI to the resource to request. 208 | * @param array $headers An array of request headers to send with the request. 209 | * @param integer $timeout Read timeout in seconds. 210 | * 211 | * @return Response 212 | * 213 | * @since 1.0 214 | */ 215 | public function trace($url, array $headers = [], $timeout = null) 216 | { 217 | return $this->makeTransportRequest('TRACE', $url, null, $headers, $timeout); 218 | } 219 | 220 | /** 221 | * Method to send the PATCH command to the server. 222 | * 223 | * @param string|UriInterface $url The URI to the resource to request. 224 | * @param mixed $data Either an associative array or a string to be sent with the request. 225 | * @param array $headers An array of request headers to send with the request. 226 | * @param integer $timeout Read timeout in seconds. 227 | * 228 | * @return Response 229 | * 230 | * @since 1.0 231 | */ 232 | public function patch($url, $data, array $headers = [], $timeout = null) 233 | { 234 | return $this->makeTransportRequest('PATCH', $url, $data, $headers, $timeout); 235 | } 236 | 237 | /** 238 | * Sends a PSR-7 request and returns a PSR-7 response. 239 | * 240 | * @param RequestInterface $request The PSR-7 request object. 241 | * 242 | * @return ResponseInterface|Response 243 | * 244 | * @since 2.0.0 245 | */ 246 | public function sendRequest(RequestInterface $request): ResponseInterface 247 | { 248 | $data = $request->getBody()->getContents(); 249 | 250 | return $this->makeTransportRequest( 251 | $request->getMethod(), 252 | new Uri((string) $request->getUri()), 253 | empty($data) ? null : $data, 254 | $request->getHeaders() 255 | ); 256 | } 257 | 258 | /** 259 | * Send a request to the server and return a Response object with the response. 260 | * 261 | * @param string $method The HTTP method for sending the request. 262 | * @param string|UriInterface $url The URI to the resource to request. 263 | * @param mixed $data Either an associative array or a string to be sent with the request. 264 | * @param array $headers An array of request headers to send with the request. 265 | * @param integer $timeout Read timeout in seconds. 266 | * 267 | * @return Response 268 | * 269 | * @since 1.0 270 | * @throws \InvalidArgumentException 271 | */ 272 | protected function makeTransportRequest($method, $url, $data = null, array $headers = [], $timeout = null) 273 | { 274 | // Look for headers set in the options. 275 | if (isset($this->options['headers'])) { 276 | $temp = (array) $this->options['headers']; 277 | 278 | foreach ($temp as $key => $val) { 279 | if (!isset($headers[$key])) { 280 | $headers[$key] = $val; 281 | } 282 | } 283 | } 284 | 285 | // Look for timeout set in the options. 286 | if ($timeout === null && isset($this->options['timeout'])) { 287 | $timeout = $this->options['timeout']; 288 | } 289 | 290 | $userAgent = isset($this->options['userAgent']) ? $this->options['userAgent'] : null; 291 | 292 | // Convert to a Uri object if we were given a string 293 | if (\is_string($url)) { 294 | $url = new Uri($url); 295 | } elseif (!($url instanceof UriInterface)) { 296 | throw new \InvalidArgumentException( 297 | sprintf( 298 | 'A string or %s object must be provided, a "%s" was provided.', 299 | UriInterface::class, 300 | \gettype($url) 301 | ) 302 | ); 303 | } 304 | 305 | return $this->transport->request($method, $url, $data, $headers, $timeout, $userAgent); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/HttpFactory.php: -------------------------------------------------------------------------------- 1 | getAvailableDriver($options, $adapters)) { 40 | throw new \RuntimeException('No transport driver available.'); 41 | } 42 | 43 | return new Http($options, $driver); 44 | } 45 | 46 | /** 47 | * Finds an available TransportInterface object for communication 48 | * 49 | * @param array|\ArrayAccess $options Options for creating TransportInterface object 50 | * @param array|string $default Adapter (string) or queue of adapters (array) to use 51 | * 52 | * @return TransportInterface|boolean Interface sub-class or boolean false if no adapters are available 53 | * 54 | * @since 1.0 55 | * @throws \InvalidArgumentException 56 | */ 57 | public function getAvailableDriver($options = [], $default = null) 58 | { 59 | if (!\is_array($options) && !($options instanceof \ArrayAccess)) { 60 | throw new \InvalidArgumentException( 61 | 'The options param must be an array or implement the ArrayAccess interface.' 62 | ); 63 | } 64 | 65 | if ($default === null) { 66 | $availableAdapters = $this->getHttpTransports(); 67 | } else { 68 | settype($default, 'array'); 69 | $availableAdapters = $default; 70 | } 71 | 72 | // Check if there is at least one available http transport adapter 73 | if (!\count($availableAdapters)) { 74 | return false; 75 | } 76 | 77 | foreach ($availableAdapters as $adapter) { 78 | $class = __NAMESPACE__ . '\\Transport\\' . ucfirst($adapter); 79 | 80 | if (class_exists($class)) { 81 | if ($class::isSupported()) { 82 | return new $class($options); 83 | } 84 | } 85 | } 86 | 87 | return false; 88 | } 89 | 90 | /** 91 | * Get the HTTP transport handlers 92 | * 93 | * @return string[] An array of available transport handler types 94 | * 95 | * @since 1.0 96 | */ 97 | public function getHttpTransports() 98 | { 99 | $names = []; 100 | $iterator = new \DirectoryIterator(__DIR__ . '/Transport'); 101 | 102 | /** @var \DirectoryIterator $file */ 103 | foreach ($iterator as $file) { 104 | $fileName = $file->getFilename(); 105 | 106 | // Only load for php files. 107 | if ($file->isFile() && $file->getExtension() == 'php') { 108 | $names[] = substr($fileName, 0, strrpos($fileName, '.')); 109 | } 110 | } 111 | 112 | // Keep alphabetical order across all environments 113 | sort($names); 114 | 115 | // If curl is available set it to the first position 116 | $key = array_search('Curl', $names); 117 | 118 | if ($key) { 119 | unset($names[$key]); 120 | array_unshift($names, 'Curl'); 121 | } 122 | 123 | return $names; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | getBody(); 40 | 41 | case 'code': 42 | return $this->getStatusCode(); 43 | 44 | case 'headers': 45 | return $this->getHeaders(); 46 | 47 | default: 48 | $trace = debug_backtrace(); 49 | 50 | trigger_error( 51 | sprintf( 52 | 'Undefined property via __get(): %s in %s on line %s', 53 | $name, 54 | $trace[0]['file'], 55 | $trace[0]['line'] 56 | ), 57 | E_USER_NOTICE 58 | ); 59 | 60 | break; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Transport/Curl.php: -------------------------------------------------------------------------------- 1 | setCAOptionAndValue($ch); 48 | 49 | $options = []; 50 | 51 | // Set the request method. 52 | switch (strtoupper($method)) { 53 | case 'GET': 54 | $options[\CURLOPT_HTTPGET] = true; 55 | 56 | break; 57 | 58 | case 'POST': 59 | $options[\CURLOPT_POST] = true; 60 | 61 | break; 62 | 63 | default: 64 | $options[\CURLOPT_CUSTOMREQUEST] = strtoupper($method); 65 | 66 | break; 67 | } 68 | 69 | // Don't wait for body when $method is HEAD 70 | $options[\CURLOPT_NOBODY] = ($method === 'HEAD'); 71 | 72 | // Initialize the certificate store 73 | $options[CURLOPT_CAINFO] = $this->getOption('curl.certpath', CaBundle::getSystemCaRootBundlePath()); 74 | 75 | // If data exists let's encode it and make sure our Content-type header is set. 76 | if (isset($data)) { 77 | // If the data is a scalar value simply add it to the cURL post fields. 78 | if (is_scalar($data) || (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart/form-data') === 0)) { 79 | $options[\CURLOPT_POSTFIELDS] = $data; 80 | } else { 81 | // Otherwise we need to encode the value first. 82 | $options[\CURLOPT_POSTFIELDS] = http_build_query($data); 83 | } 84 | 85 | if (!isset($headers['Content-Type'])) { 86 | $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; 87 | } 88 | 89 | // Add the relevant headers. 90 | if (is_scalar($options[\CURLOPT_POSTFIELDS])) { 91 | $headers['Content-Length'] = \strlen($options[\CURLOPT_POSTFIELDS]); 92 | } 93 | } 94 | 95 | // Build the headers string for the request. 96 | $headerArray = []; 97 | 98 | if (!empty($headers)) { 99 | foreach ($headers as $key => $value) { 100 | if (\is_array($value)) { 101 | foreach ($value as $header) { 102 | $headerArray[] = "$key: $header"; 103 | } 104 | } else { 105 | $headerArray[] = "$key: $value"; 106 | } 107 | } 108 | 109 | // Add the headers string into the stream context options array. 110 | $options[\CURLOPT_HTTPHEADER] = $headerArray; 111 | } 112 | 113 | // Curl needs the accepted encoding header as option 114 | if (isset($headers['Accept-Encoding'])) { 115 | $options[\CURLOPT_ENCODING] = $headers['Accept-Encoding']; 116 | } 117 | 118 | // If an explicit timeout is given use it. 119 | if (isset($timeout)) { 120 | $options[\CURLOPT_TIMEOUT] = (int) $timeout; 121 | $options[\CURLOPT_CONNECTTIMEOUT] = (int) $timeout; 122 | } 123 | 124 | // If an explicit user agent is given use it. 125 | if (isset($userAgent)) { 126 | $options[\CURLOPT_USERAGENT] = $userAgent; 127 | } 128 | 129 | // Set the request URL. 130 | $options[\CURLOPT_URL] = (string) $uri; 131 | 132 | // We want our headers. :-) 133 | $options[\CURLOPT_HEADER] = true; 134 | 135 | // Return it... echoing it would be tacky. 136 | $options[\CURLOPT_RETURNTRANSFER] = true; 137 | 138 | // Override the Expect header to prevent cURL from confusing itself in its own stupidity. 139 | // Link: http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/ 140 | $options[\CURLOPT_HTTPHEADER][] = 'Expect:'; 141 | 142 | // Follow redirects if server config allows 143 | if ($this->redirectsAllowed()) { 144 | $options[\CURLOPT_FOLLOWLOCATION] = (bool) $this->getOption('follow_location', true); 145 | } 146 | 147 | // Authentication, if needed 148 | if ($this->getOption('userauth') && $this->getOption('passwordauth')) { 149 | $options[\CURLOPT_USERPWD] = $this->getOption('userauth') . ':' . $this->getOption('passwordauth'); 150 | $options[\CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; 151 | } 152 | 153 | // Configure protocol version 154 | if ($protocolVersion = $this->getOption('protocolVersion')) { 155 | $options[\CURLOPT_HTTP_VERSION] = $this->mapProtocolVersion($protocolVersion); 156 | } 157 | 158 | // Set any custom transport options 159 | foreach ($this->getOption('transport.curl', []) as $key => $value) { 160 | $options[$key] = $value; 161 | } 162 | 163 | // Set the cURL options. 164 | curl_setopt_array($ch, $options); 165 | 166 | // Execute the request and close the connection. 167 | $content = curl_exec($ch); 168 | 169 | // Check if the content is a string. If it is not, it must be an error. 170 | if (!\is_string($content)) { 171 | $message = curl_error($ch); 172 | 173 | if (empty($message)) { 174 | // Error but nothing from cURL? Create our own 175 | $message = 'No HTTP response received'; 176 | } 177 | 178 | throw new \RuntimeException($message); 179 | } 180 | 181 | // Get the request information. 182 | $info = curl_getinfo($ch); 183 | 184 | // Close the connection. 185 | curl_close($ch); 186 | 187 | return $this->getResponse($content, $info); 188 | } 189 | 190 | /** 191 | * Configure the cURL resources with the appropriate root certificates. 192 | * 193 | * @param \CurlHandle $ch The cURL resource you want to configure the certificates on. 194 | * 195 | * @return void 196 | * 197 | * @since 1.3.2 198 | */ 199 | protected function setCAOptionAndValue($ch) 200 | { 201 | if ($certpath = $this->getOption('curl.certpath')) { 202 | // Option is passed to a .PEM file. 203 | curl_setopt($ch, \CURLOPT_CAINFO, $certpath); 204 | 205 | return; 206 | } 207 | 208 | $caPathOrFile = CaBundle::getSystemCaRootBundlePath(); 209 | 210 | if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) { 211 | curl_setopt($ch, \CURLOPT_CAPATH, $caPathOrFile); 212 | 213 | return; 214 | } 215 | 216 | curl_setopt($ch, \CURLOPT_CAINFO, $caPathOrFile); 217 | } 218 | 219 | /** 220 | * Method to get a response object from a server response. 221 | * 222 | * @param string $content The complete server response, including headers 223 | * as a string if the response has no errors. 224 | * @param array $info The cURL request information. 225 | * 226 | * @return Response 227 | * 228 | * @since 1.0 229 | * @throws InvalidResponseCodeException 230 | */ 231 | protected function getResponse($content, $info) 232 | { 233 | // Try to get header size 234 | if (isset($info['header_size'])) { 235 | $headerString = trim(substr($content, 0, $info['header_size'])); 236 | $headerArray = explode("\r\n\r\n", $headerString); 237 | 238 | // Get the last set of response headers as an array. 239 | $headers = explode("\r\n", array_pop($headerArray)); 240 | 241 | // Set the body for the response. 242 | $body = substr($content, $info['header_size']); 243 | } else { 244 | // Fallback and try to guess header count by redirect count 245 | // Get the number of redirects that occurred. 246 | $redirects = $info['redirect_count'] ?? 0; 247 | 248 | /* 249 | * Split the response into headers and body. If cURL encountered redirects, the headers for the redirected requests will 250 | * also be included. So we split the response into header + body + the number of redirects and only use the last two 251 | * sections which should be the last set of headers and the actual body. 252 | */ 253 | $response = explode("\r\n\r\n", $content, 2 + $redirects); 254 | 255 | // Set the body for the response. 256 | $body = array_pop($response); 257 | 258 | // Get the last set of response headers as an array. 259 | $headers = explode("\r\n", array_pop($response)); 260 | } 261 | 262 | // Get the response code from the first offset of the response headers. 263 | preg_match('/[0-9]{3}/', array_shift($headers), $matches); 264 | 265 | $code = \count($matches) ? $matches[0] : null; 266 | 267 | if (!is_numeric($code)) { 268 | // No valid response code was detected. 269 | throw new InvalidResponseCodeException('No HTTP response code found.'); 270 | } 271 | 272 | $statusCode = (int) $code; 273 | $verifiedHeaders = $this->processHeaders($headers); 274 | 275 | $streamInterface = new StreamResponse('php://memory', 'rw'); 276 | $streamInterface->write($body); 277 | 278 | return new Response($streamInterface, $statusCode, $verifiedHeaders); 279 | } 280 | 281 | /** 282 | * Method to check if HTTP transport cURL is available for use 283 | * 284 | * @return boolean True if available, else false 285 | * 286 | * @since 1.0 287 | */ 288 | public static function isSupported() 289 | { 290 | return \function_exists('curl_version') && curl_version(); 291 | } 292 | 293 | /** 294 | * Get the cURL constant for a HTTP protocol version 295 | * 296 | * @param string $version The HTTP protocol version to use 297 | * 298 | * @return integer 299 | * 300 | * @since 1.3.1 301 | */ 302 | private function mapProtocolVersion(string $version): int 303 | { 304 | switch ($version) { 305 | case '1.0': 306 | return \CURL_HTTP_VERSION_1_0; 307 | 308 | case '1.1': 309 | return \CURL_HTTP_VERSION_1_1; 310 | 311 | case '2.0': 312 | case '2': 313 | if (\defined('CURL_HTTP_VERSION_2')) { 314 | return \CURL_HTTP_VERSION_2; 315 | } 316 | } 317 | 318 | return \CURL_HTTP_VERSION_NONE; 319 | } 320 | 321 | /** 322 | * Check if redirects are allowed 323 | * 324 | * @return boolean 325 | * 326 | * @since 1.2.1 327 | */ 328 | private function redirectsAllowed(): bool 329 | { 330 | return true; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/Transport/Socket.php: -------------------------------------------------------------------------------- 1 | connect($uri, $timeout); 52 | 53 | // Make sure the connection is alive and valid. 54 | if (!\is_resource($connection)) { 55 | throw new \RuntimeException('Not connected to server.'); 56 | } 57 | 58 | // Make sure the connection has not timed out. 59 | $meta = stream_get_meta_data($connection); 60 | 61 | if ($meta['timed_out']) { 62 | throw new \RuntimeException('Server connection timed out.'); 63 | } 64 | 65 | // Get the request path from the URI object. 66 | $path = $uri->toString(['path', 'query']); 67 | 68 | // If we have data to send make sure our request is setup for it. 69 | if (!empty($data)) { 70 | // If the data is not a scalar value encode it to be sent with the request. 71 | if (!is_scalar($data)) { 72 | $data = http_build_query($data); 73 | } 74 | 75 | if (!isset($headers['Content-Type'])) { 76 | $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; 77 | } 78 | 79 | // Add the relevant headers. 80 | $headers['Content-Length'] = \strlen($data); 81 | } 82 | 83 | // Configure protocol version, use transport's default if not set otherwise. 84 | $protocolVersion = $this->getOption('protocolVersion', '1.0'); 85 | 86 | // HTTP/1.1 streams using the socket wrapper require a Connection: close header 87 | if ($protocolVersion == '1.1' && !isset($headers['Connection'])) { 88 | $headers['Connection'] = 'close'; 89 | } 90 | 91 | // Build the request payload. 92 | $request = []; 93 | $request[] = strtoupper($method) . ' ' . ((empty($path)) ? '/' : $path) . ' HTTP/' . $protocolVersion; 94 | 95 | if (!isset($headers['Host'])) { 96 | $request[] = 'Host: ' . $uri->getHost(); 97 | } 98 | 99 | // If an explicit user agent is given use it. 100 | if (isset($userAgent)) { 101 | $headers['User-Agent'] = $userAgent; 102 | } 103 | 104 | // If we have a username then we include basic authentication credentials. 105 | if ($uri->getUser()) { 106 | $authString = $uri->getUser() . ':' . $uri->getPass(); 107 | $headers['Authorization'] = 'Basic ' . base64_encode($authString); 108 | } 109 | 110 | // If there are custom headers to send add them to the request payload. 111 | if (!empty($headers)) { 112 | foreach ($headers as $key => $value) { 113 | if (\is_array($value)) { 114 | foreach ($value as $header) { 115 | $request[] = "$key: $header"; 116 | } 117 | } else { 118 | $request[] = "$key: $value"; 119 | } 120 | } 121 | } 122 | 123 | // Authentication, if needed 124 | if ($this->getOption('userauth') && $this->getOption('passwordauth')) { 125 | $request[] = 'Authorization: Basic ' . base64_encode($this->getOption('userauth') . ':' . $this->getOption('passwordauth')); 126 | } 127 | 128 | // Set any custom transport options 129 | foreach ($this->getOption('transport.socket', []) as $value) { 130 | $request[] = $value; 131 | } 132 | 133 | // If we have data to send add it to the request payload. 134 | if (!empty($data)) { 135 | $request[] = null; 136 | $request[] = $data; 137 | } 138 | 139 | // Send the request to the server. 140 | fwrite($connection, implode("\r\n", $request) . "\r\n\r\n"); 141 | 142 | // Get the response data from the server. 143 | $content = ''; 144 | 145 | while (!feof($connection)) { 146 | $content .= fgets($connection, 4096); 147 | } 148 | 149 | $content = $this->getResponse($content); 150 | 151 | // Follow Http redirects 152 | if ($content->getStatusCode() >= 301 && $content->getStatusCode() < 400 && $content->hasHeader('Location')) { 153 | return $this->request($method, new Uri($content->getHeaderLine('Location')), $data, $headers, $timeout, $userAgent); 154 | } 155 | 156 | return $content; 157 | } 158 | 159 | /** 160 | * Method to get a response object from a server response. 161 | * 162 | * @param string $content The complete server response, including headers. 163 | * 164 | * @return Response 165 | * 166 | * @since 1.0 167 | * @throws \UnexpectedValueException 168 | * @throws InvalidResponseCodeException 169 | */ 170 | protected function getResponse($content) 171 | { 172 | if (empty($content)) { 173 | throw new \UnexpectedValueException('No content in response.'); 174 | } 175 | 176 | // Split the response into headers and body. 177 | $response = explode("\r\n\r\n", $content, 2); 178 | 179 | // Get the response headers as an array. 180 | $headers = explode("\r\n", $response[0]); 181 | 182 | // Set the body for the response. 183 | $body = empty($response[1]) ? '' : $response[1]; 184 | 185 | // Get the response code from the first offset of the response headers. 186 | preg_match('/[0-9]{3}/', array_shift($headers), $matches); 187 | $code = $matches[0]; 188 | 189 | if (!is_numeric($code)) { 190 | // No valid response code was detected. 191 | throw new InvalidResponseCodeException('No HTTP response code found.'); 192 | } 193 | 194 | $statusCode = (int) $code; 195 | $verifiedHeaders = $this->processHeaders($headers); 196 | 197 | $streamInterface = new StreamResponse('php://memory', 'rw'); 198 | $streamInterface->write($body); 199 | 200 | return new Response($streamInterface, $statusCode, $verifiedHeaders); 201 | } 202 | 203 | /** 204 | * Method to connect to a server and get the resource. 205 | * 206 | * @param UriInterface $uri The URI to connect with. 207 | * @param integer $timeout Read timeout in seconds. 208 | * 209 | * @return resource Socket connection resource. 210 | * 211 | * @since 1.0 212 | * @throws \RuntimeException 213 | */ 214 | protected function connect(UriInterface $uri, $timeout = null) 215 | { 216 | $errno = null; 217 | $err = null; 218 | 219 | // Get the host from the uri. 220 | $host = ($uri->isSsl()) ? 'ssl://' . $uri->getHost() : $uri->getHost(); 221 | 222 | // If the port is not explicitly set in the URI detect it. 223 | if (!$uri->getPort()) { 224 | $port = ($uri->getScheme() == 'https') ? 443 : 80; 225 | } else { 226 | // Use the set port. 227 | $port = $uri->getPort(); 228 | } 229 | 230 | // Build the connection key for resource memory caching. 231 | $key = md5($host . $port); 232 | 233 | // If the connection already exists, use it. 234 | if (!empty($this->connections[$key]) && \is_resource($this->connections[$key])) { 235 | // Connection reached EOF, cannot be used anymore 236 | $meta = stream_get_meta_data($this->connections[$key]); 237 | 238 | if ($meta['eof']) { 239 | if (!fclose($this->connections[$key])) { 240 | throw new \RuntimeException('Cannot close connection'); 241 | } 242 | } elseif (!$meta['timed_out']) { 243 | // Make sure the connection has not timed out. 244 | return $this->connections[$key]; 245 | } 246 | } 247 | 248 | if (!is_numeric($timeout)) { 249 | $timeout = ini_get('default_socket_timeout'); 250 | } 251 | 252 | // Capture PHP errors 253 | error_clear_last(); 254 | 255 | // PHP sends a warning if the uri does not exists; we silence it and throw an exception instead. 256 | // Attempt to connect to the server 257 | $connection = @fsockopen($host, $port, $errno, $err, $timeout); 258 | 259 | if (!$connection) { 260 | $error = error_get_last(); 261 | 262 | if ($error === null || $error['message'] === '') { 263 | // Error but nothing from php? Create our own 264 | $error = [ 265 | 'message' => sprintf('Could not connect to resource %s: %s (%d)', $uri, $err, $errno), 266 | ]; 267 | } 268 | 269 | throw new \RuntimeException($error['message']); 270 | } 271 | 272 | // Since the connection was successful let's store it in case we need to use it later. 273 | $this->connections[$key] = $connection; 274 | 275 | stream_set_timeout($this->connections[$key], (int) $timeout); 276 | 277 | return $this->connections[$key]; 278 | } 279 | 280 | /** 281 | * Method to check if http transport socket available for use 282 | * 283 | * @return boolean True if available else false 284 | * 285 | * @since 1.0 286 | */ 287 | public static function isSupported() 288 | { 289 | return \function_exists('fsockopen') && \is_callable('fsockopen'); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Transport/Stream.php: -------------------------------------------------------------------------------- 1 | strtoupper($method)]; 54 | 55 | // If data exists let's encode it and make sure our Content-Type header is set. 56 | if (isset($data)) { 57 | // If the data is a scalar value simply add it to the stream context options. 58 | if (is_scalar($data)) { 59 | $options['content'] = $data; 60 | } else { 61 | // Otherwise we need to encode the value first. 62 | $options['content'] = http_build_query($data); 63 | } 64 | 65 | if (!isset($headers['Content-Type'])) { 66 | $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; 67 | } 68 | 69 | // Add the relevant headers. 70 | $headers['Content-Length'] = \strlen($options['content']); 71 | } 72 | 73 | // If an explicit timeout is given user it. 74 | if (isset($timeout)) { 75 | $options['timeout'] = (int)$timeout; 76 | } 77 | 78 | // If an explicit user agent is given use it. 79 | if (isset($userAgent)) { 80 | $options['user_agent'] = $userAgent; 81 | } 82 | 83 | // Ignore HTTP errors so that we can capture them. 84 | $options['ignore_errors'] = 1; 85 | 86 | // Follow redirects. 87 | $options['follow_location'] = (int)$this->getOption('follow_location', 1); 88 | 89 | // Configure protocol version, use transport's default if not set otherwise. 90 | $options['protocol_version'] = $this->getOption('protocolVersion', '1.0'); 91 | 92 | // HTTP/1.1 streams using the PHP stream wrapper require a Connection: close header 93 | if ($options['protocol_version'] == '1.1' && !isset($headers['Connection'])) { 94 | $headers['Connection'] = 'close'; 95 | } 96 | 97 | // Add the proxy configuration if enabled 98 | if ($this->getOption('proxy.enabled', false)) { 99 | $options['request_fulluri'] = true; 100 | 101 | if ($this->getOption('proxy.host') && $this->getOption('proxy.port')) { 102 | $options['proxy'] = $this->getOption('proxy.host') . ':' . (int)$this->getOption('proxy.port'); 103 | } 104 | 105 | // If authentication details are provided, add those as well 106 | if ($this->getOption('proxy.user') && $this->getOption('proxy.password')) { 107 | $headers['Proxy-Authorization'] = 'Basic ' . base64_encode( 108 | $this->getOption('proxy.user') . ':' . $this->getOption('proxy.password') 109 | ); 110 | } 111 | } 112 | 113 | // Build the headers string for the request. 114 | if (!empty($headers)) { 115 | $headerString = ''; 116 | 117 | foreach ($headers as $key => $value) { 118 | if (\is_array($value)) { 119 | foreach ($value as $header) { 120 | $headerString .= "$key: $header\r\n"; 121 | } 122 | } else { 123 | $headerString .= "$key: $value\r\n"; 124 | } 125 | } 126 | 127 | // Add the headers string into the stream context options array. 128 | $options['header'] = trim($headerString, "\r\n"); 129 | } 130 | 131 | // Authentication, if needed 132 | if ($uri instanceof Uri && $this->getOption('userauth') && $this->getOption('passwordauth')) { 133 | $uri->setUser($this->getOption('userauth')); 134 | $uri->setPass($this->getOption('passwordauth')); 135 | } 136 | 137 | // Set any custom transport options 138 | foreach ($this->getOption('transport.stream', []) as $key => $value) { 139 | $options[$key] = $value; 140 | } 141 | 142 | // Get the current context options. 143 | $contextOptions = stream_context_get_options(stream_context_get_default()); 144 | 145 | // Add our options to the currently defined options, if any. 146 | $contextOptions['http'] = isset($contextOptions['http']) ? array_merge( 147 | $contextOptions['http'], 148 | $options 149 | ) : $options; 150 | 151 | // Create the stream context for the request. 152 | $streamOptions = [ 153 | 'http' => $options, 154 | 'ssl' => [ 155 | 'verify_peer' => true, 156 | 'verify_depth' => 5, 157 | 'verify_peer_name' => true, 158 | ], 159 | ]; 160 | 161 | // The cacert may be a file or path 162 | $certpath = $this->getOption('stream.certpath', CaBundle::getSystemCaRootBundlePath()); 163 | 164 | if (is_dir($certpath)) { 165 | $streamOptions['ssl']['capath'] = $certpath; 166 | } else { 167 | $streamOptions['ssl']['cafile'] = $certpath; 168 | } 169 | 170 | $context = stream_context_create($streamOptions); 171 | 172 | // Capture PHP errors 173 | error_clear_last(); 174 | 175 | // Open the stream for reading. 176 | $stream = @fopen((string)$uri, 'r', false, $context); 177 | 178 | if (!$stream) { 179 | $error = error_get_last(); 180 | 181 | if ($error === null || $error['message'] === '') { 182 | // Error but nothing from php? Create our own 183 | $error = [ 184 | 'message' => sprintf('Could not connect to resource %s', $uri), 185 | ]; 186 | } 187 | 188 | throw new \RuntimeException($error['message']); 189 | } 190 | 191 | /** 192 | * Add stream_set_blocking option to support non-blocking mode 193 | * Default set to true to keep previous behaviour. 194 | * Set to false to enable non-blocking mode 195 | * 196 | * @see https://www.php.net/manual/en/function.stream-set-blocking.php 197 | */ 198 | $enableBlockingMode = isset($options['set_blocking']) ? $options['set_blocking'] : true; 199 | stream_set_blocking($stream, $enableBlockingMode); 200 | 201 | // Get the metadata for the stream, including response headers. 202 | $metadata = stream_get_meta_data($stream); 203 | 204 | // Get the contents from the stream. 205 | $content = stream_get_contents($stream); 206 | 207 | // Close the stream. 208 | fclose($stream); 209 | 210 | $headers = []; 211 | 212 | if (isset($metadata['wrapper_data']['headers'])) { 213 | $headers = $metadata['wrapper_data']['headers']; 214 | } elseif (isset($metadata['wrapper_data'])) { 215 | $headers = $metadata['wrapper_data']; 216 | } 217 | 218 | return $this->getResponse($headers, $content); 219 | } 220 | 221 | /** 222 | * Method to get a response object from a server response. 223 | * 224 | * @param array $headers The response headers as an array. 225 | * @param string $body The response body as a string. 226 | * 227 | * @return Response 228 | * 229 | * @throws InvalidResponseCodeException 230 | * @since 1.0 231 | */ 232 | protected function getResponse(array $headers, $body) 233 | { 234 | // Get the response code from the first offset of the response headers. 235 | preg_match('/[0-9]{3}/', array_shift($headers), $matches); 236 | $code = $matches[0]; 237 | 238 | if (!is_numeric($code)) { 239 | // No valid response code was detected. 240 | throw new InvalidResponseCodeException('No HTTP response code found.'); 241 | } 242 | 243 | $statusCode = (int)$code; 244 | $verifiedHeaders = $this->processHeaders($headers); 245 | 246 | $streamInterface = new StreamResponse('php://memory', 'rw'); 247 | $streamInterface->write($body); 248 | 249 | return new Response($streamInterface, $statusCode, $verifiedHeaders); 250 | } 251 | 252 | /** 253 | * Method to check if http transport stream available for use 254 | * 255 | * @return boolean True if available else false 256 | * 257 | * @since 1.0 258 | */ 259 | public static function isSupported() 260 | { 261 | return \function_exists('fopen') && \is_callable('fopen') && ini_get('allow_url_fopen'); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/TransportInterface.php: -------------------------------------------------------------------------------- 1 |