├── .gitignore ├── .mailmap ├── .rspec ├── .rubocop ├── .rubocop-disables.yml ├── .rubocop.yml ├── .travis.yml ├── AUTHORS ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── restclient ├── history.md ├── lib ├── rest-client.rb ├── rest_client.rb ├── restclient.rb └── restclient │ ├── abstract_response.rb │ ├── exceptions.rb │ ├── params_array.rb │ ├── payload.rb │ ├── platform.rb │ ├── raw_response.rb │ ├── request.rb │ ├── resource.rb │ ├── response.rb │ ├── utils.rb │ ├── version.rb │ ├── windows.rb │ └── windows │ └── root_certs.rb ├── rest-client.gemspec ├── rest-client.windows.gemspec └── spec ├── ISS.jpg ├── helpers.rb ├── integration ├── _lib.rb ├── capath_digicert │ ├── 3513523f.0 │ ├── 399e7759.0 │ ├── README │ └── digicert.crt ├── capath_verisign │ ├── 415660c1.0 │ ├── 7651b327.0 │ ├── README │ └── verisign.crt ├── certs │ ├── digicert.crt │ └── verisign.crt ├── httpbin_spec.rb ├── integration_spec.rb └── request_spec.rb ├── spec_helper.rb └── unit ├── _lib.rb ├── abstract_response_spec.rb ├── exceptions_spec.rb ├── params_array_spec.rb ├── payload_spec.rb ├── raw_response_spec.rb ├── request2_spec.rb ├── request_spec.rb ├── resource_spec.rb ├── response_spec.rb ├── restclient_spec.rb ├── utils_spec.rb └── windows └── root_certs_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /Gemfile.lock 3 | /.bundle 4 | /vendor 5 | /doc 6 | /pkg 7 | /rdoc 8 | /.yardoc 9 | /tmp 10 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Blake Mizerany <blake.mizerany@gmail.com> 2 | Lawrence Leonard Gilbert <larry@l2g.to> 3 | <larry@l2g.to> <larry@L2G.to> 4 | Marc-André Cournoyer <macournoyer@gmail.com> 5 | Matthew Manning <matt.manning@gmail.com> 6 | Nicholas Wieland <nicholas.wieland@gmail.com> 7 | Rafael Ssouza <rafael.ssouza@gmail.com> 8 | Richard Schneeman <richard.schneeman@gmail.com> 9 | Rick Olson <technoweenie@gmail.com> 10 | T. Watanabe <wtnabe@wt-srv.watanabe> 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.rubocop: -------------------------------------------------------------------------------- 1 | --display-cop-names 2 | --fail-level=W 3 | -------------------------------------------------------------------------------- /.rubocop-disables.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by `rubocop --auto-gen-config` 2 | # on 2014-07-08 08:57:44 +0000 using RuboCop version 0.24.1. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the offenses are removed from the code base. 5 | # Note that changes in the inspected code, or installation of new 6 | # versions of RuboCop, may require this file to be generated again. 7 | 8 | # TODO 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | Lint/StringConversionInInterpolation: 12 | Enabled: false 13 | 14 | # Tests only 15 | # Offense count: 16 16 | # Cop supports --auto-correct. 17 | Lint/UnusedBlockArgument: 18 | Enabled: false 19 | 20 | Security/Eval: 21 | Exclude: 22 | - rest-client.windows.gemspec 23 | 24 | Lint/HandleExceptions: 25 | Exclude: 26 | - lib/restclient/utils.rb 27 | 28 | Lint/UselessAccessModifier: 29 | Exclude: 30 | - lib/restclient/windows/root_certs.rb 31 | 32 | # Offense count: 4 33 | # Cop supports --auto-correct. 34 | Style/Alias: 35 | Enabled: false 36 | 37 | # TODO 38 | # Offense count: 3 39 | # Cop supports --auto-correct. 40 | Style/AndOr: 41 | Enabled: false 42 | 43 | # TODO 44 | # Offense count: 3 45 | # Cop supports --auto-correct. 46 | Style/BlockDelimiters: 47 | Enabled: false 48 | 49 | # Offense count: 48 50 | # Cop supports --auto-correct. 51 | # Configuration parameters: EnforcedStyle, SupportedStyles. 52 | Style/BracesAroundHashParameters: 53 | Enabled: false 54 | 55 | # Offense count: 1 56 | Naming/ClassAndModuleCamelCase: 57 | Exclude: 58 | - lib/restclient/windows/root_certs.rb 59 | 60 | # Offense count: 2 61 | # Configuration parameters: EnforcedStyle, SupportedStyles. 62 | Style/ClassAndModuleChildren: 63 | Enabled: false 64 | 65 | # TODO? 66 | # Offense count: 14 67 | Metrics/AbcSize: 68 | Max: 75 69 | 70 | # TODO? 71 | Metrics/MethodLength: 72 | Max: 66 73 | 74 | # TODO? 75 | # Offense count: 4 76 | Metrics/PerceivedComplexity: 77 | Max: 24 78 | 79 | # Offense count: 1 80 | # Configuration parameters: CountComments. 81 | Metrics/ClassLength: 82 | Max: 411 83 | 84 | # TODO 85 | # Offense count: 5 86 | Style/ClassVars: 87 | Enabled: false 88 | 89 | # TODO 90 | # Offense count: 5 91 | # Cop supports --auto-correct. 92 | # Configuration parameters: PreferredMethods. 93 | Style/CollectionMethods: 94 | Enabled: false 95 | 96 | # TODO 97 | # Offense count: 4 98 | # Cop supports --auto-correct. 99 | Style/ColonMethodCall: 100 | Enabled: false 101 | 102 | Style/ConditionalAssignment: 103 | EnforcedStyle: assign_inside_condition 104 | 105 | # Offense count: 2 106 | Naming/ConstantName: 107 | Enabled: false 108 | 109 | # TODO: eh? 110 | # Offense count: 4 111 | Metrics/CyclomaticComplexity: 112 | Max: 22 113 | 114 | Style/PreferredHashMethods: 115 | EnforcedStyle: verbose 116 | 117 | # TODO: docs 118 | # Offense count: 17 119 | Style/Documentation: 120 | Enabled: false 121 | 122 | # Offense count: 9 123 | # Configuration parameters: EnforcedStyle, SupportedStyles. 124 | Layout/DotPosition: 125 | Enabled: false 126 | 127 | # Offense count: 1 128 | Style/DoubleNegation: 129 | Enabled: false 130 | 131 | # TODO 132 | # Offense count: 2 133 | Style/EachWithObject: 134 | Enabled: false 135 | 136 | # Offense count: 5 137 | # Cop supports --auto-correct. 138 | Layout/EmptyLines: 139 | Enabled: false 140 | 141 | # Offense count: 11 142 | # Cop supports --auto-correct. 143 | # Configuration parameters: EnforcedStyle, SupportedStyles. 144 | Layout/EmptyLinesAroundClassBody: 145 | Enabled: false 146 | 147 | # Offense count: 1 148 | # Cop supports --auto-correct. 149 | Layout/EmptyLinesAroundMethodBody: 150 | Enabled: false 151 | 152 | # Offense count: 9 153 | # Cop supports --auto-correct. 154 | # Configuration parameters: EnforcedStyle, SupportedStyles. 155 | Layout/EmptyLinesAroundModuleBody: 156 | Enabled: false 157 | 158 | Layout/EmptyLinesAroundExceptionHandlingKeywords: 159 | Enabled: false 160 | 161 | # Offense count: 31 162 | # Configuration parameters: EnforcedStyle, SupportedStyles. 163 | Style/Encoding: 164 | Enabled: false 165 | 166 | Naming/FileName: 167 | Exclude: 168 | - lib/rest-client.rb 169 | 170 | # Offense count: 3 171 | # Configuration parameters: EnforcedStyle, SupportedStyles. 172 | Style/FormatString: 173 | Enabled: false 174 | 175 | # TODO: enable 176 | # Cop supports --auto-correct. 177 | # Configuration parameters: SupportedStyles. 178 | Style/HashSyntax: 179 | Enabled: false 180 | 181 | # NOTABUG 182 | # Offense count: 8 183 | # Configuration parameters: MaxLineLength. 184 | Style/IfUnlessModifier: 185 | Enabled: false 186 | 187 | Layout/IndentFirstHashElement: 188 | Exclude: 189 | - 'spec/**/*.rb' 190 | 191 | # NOTABUG 192 | # Offense count: 19 193 | Style/Lambda: 194 | Enabled: false 195 | 196 | # TODO 197 | # Offense count: 14 198 | # Cop supports --auto-correct. 199 | Layout/LeadingCommentSpace: 200 | Enabled: false 201 | 202 | Metrics/LineLength: 203 | Exclude: 204 | - 'spec/**/*.rb' 205 | - 'Rakefile' 206 | 207 | # TODO 208 | # Offense count: 28 209 | # Cop supports --auto-correct. 210 | # Configuration parameters: EnforcedStyle, SupportedStyles. 211 | Style/MethodDefParentheses: 212 | Enabled: false 213 | 214 | # TODO 215 | # Offense count: 1 216 | Style/ModuleFunction: 217 | Enabled: false 218 | 219 | # Offense count: 4 220 | # Configuration parameters: EnforcedStyle, SupportedStyles. 221 | Style/Next: 222 | Enabled: false 223 | 224 | # Offense count: 1 225 | # Cop supports --auto-correct. 226 | # Configuration parameters: IncludeSemanticChanges. 227 | Style/NonNilCheck: 228 | Enabled: false 229 | 230 | # TODO: exclude 231 | # Offense count: 1 232 | # Cop supports --auto-correct. 233 | Style/Not: 234 | Enabled: false 235 | 236 | # Offense count: 2 237 | # Cop supports --auto-correct. 238 | Style/NumericLiterals: 239 | MinDigits: 11 240 | 241 | # TODO? 242 | # Offense count: 1 243 | # Cop supports --auto-correct. 244 | # Configuration parameters: AllowSafeAssignment. 245 | Style/ParenthesesAroundCondition: 246 | Enabled: false 247 | 248 | # Offense count: 8 249 | # Cop supports --auto-correct. 250 | # Configuration parameters: PreferredDelimiters. 251 | Style/PercentLiteralDelimiters: 252 | PreferredDelimiters: 253 | '%w': '{}' 254 | '%W': '{}' 255 | '%Q': '{}' 256 | Exclude: 257 | - 'bin/restclient' 258 | 259 | # Offense count: 3 260 | # Configuration parameters: NamePrefixBlacklist. 261 | Naming/PredicateName: 262 | Enabled: false 263 | 264 | # TODO: configure 265 | # Offense count: 3 266 | # Configuration parameters: EnforcedStyle, SupportedStyles. 267 | Style/RaiseArgs: 268 | Enabled: false 269 | 270 | # TODO 271 | # Offense count: 1 272 | # Cop supports --auto-correct. 273 | Style/RedundantBegin: 274 | Enabled: false 275 | 276 | # Offense count: 2 277 | # Cop supports --auto-correct. 278 | Style/RedundantSelf: 279 | Enabled: false 280 | 281 | # Offense count: 1 282 | Style/RescueModifier: 283 | Enabled: false 284 | Exclude: 285 | - 'bin/restclient' 286 | 287 | # TODO: configure 288 | # Offense count: 12 289 | # Cop supports --auto-correct. 290 | # Configuration parameters: EnforcedStyle, SupportedStyles. 291 | Style/SignalException: 292 | Enabled: false 293 | 294 | # TODO 295 | # Offense count: 2 296 | # Cop supports --auto-correct. 297 | Layout/SpaceAfterNot: 298 | Enabled: false 299 | 300 | # Offense count: 19 301 | # Cop supports --auto-correct. 302 | # Configuration parameters: EnforcedStyle, SupportedStyles. 303 | Layout/SpaceAroundEqualsInParameterDefault: 304 | Enabled: false 305 | 306 | # Offense count: 20 307 | # Cop supports --auto-correct. 308 | Layout/SpaceAroundOperators: 309 | Enabled: false 310 | 311 | # Offense count: 9 312 | # Cop supports --auto-correct. 313 | # Configuration parameters: EnforcedStyle, SupportedStyles. 314 | Layout/SpaceBeforeBlockBraces: 315 | Enabled: false 316 | 317 | Layout/EmptyLinesAroundBlockBody: 318 | Enabled: false 319 | 320 | # Offense count: 37 321 | # Cop supports --auto-correct. 322 | # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. 323 | Layout/SpaceInsideBlockBraces: 324 | Enabled: false 325 | 326 | # Offense count: 181 327 | # Cop supports --auto-correct. 328 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles. 329 | Layout/SpaceInsideHashLiteralBraces: 330 | Enabled: false 331 | 332 | # TODO 333 | # Offense count: 9 334 | # Cop supports --auto-correct. 335 | Layout/SpaceInsideParens: 336 | Enabled: false 337 | 338 | # Offense count: 414 339 | # Cop supports --auto-correct. 340 | # Configuration parameters: EnforcedStyle, SupportedStyles. 341 | Style/StringLiterals: 342 | Enabled: false 343 | 344 | Style/TrailingCommaInArrayLiteral: 345 | EnforcedStyleForMultiline: consistent_comma 346 | Style/TrailingCommaInHashLiteral: 347 | EnforcedStyleForMultiline: consistent_comma 348 | Style/TrailingCommaInArguments: 349 | Enabled: false 350 | 351 | # TODO: configure 352 | # Offense count: 1 353 | # Cop supports --auto-correct. 354 | # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, Whitelist. 355 | Style/TrivialAccessors: 356 | Enabled: false 357 | Exclude: ['lib/restclient/payload.rb'] 358 | 359 | # TODO? 360 | # Offense count: 3 361 | Style/UnlessElse: 362 | Enabled: false 363 | 364 | # TODO? 365 | # Offense count: 6 366 | # Cop supports --auto-correct. 367 | Style/UnneededPercentQ: 368 | Enabled: false 369 | 370 | # Offense count: 5 371 | # Cop supports --auto-correct. 372 | Style/WordArray: 373 | MinSize: 4 374 | 375 | # TODO? 376 | # Offense count: 5 377 | # Cop supports --auto-correct. 378 | # Configuration parameters: EnforcedStyle, SupportedStyles. 379 | Style/BarePercentLiterals: 380 | Enabled: false 381 | 382 | 383 | Style/RescueStandardError: 384 | Exclude: 385 | - 'bin/restclient' 386 | - 'lib/restclient/windows/root_certs.rb' 387 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: 3 | - .rubocop-disables.yml 4 | 5 | AllCops: 6 | Exclude: 7 | - 'tmp/*.rb' 8 | - 'vendor/**/*' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Available ruby versions: http://rubies.travis-ci.org/ 2 | 3 | language: ruby 4 | sudo: false 5 | 6 | os: 7 | - linux 8 | # - osx 9 | 10 | rvm: 11 | - "2.1" # latest 2.1.x 12 | - "2.2.10" 13 | - "2.3.8" 14 | - "2.4.6" 15 | - "2.5.5" 16 | - "2.6.3" 17 | - "ruby-head" 18 | - "jruby-9.1.9.0" 19 | - "jruby-head" 20 | 21 | cache: bundler 22 | 23 | script: 24 | - bundle exec rake test 25 | - bundle exec rake rubocop 26 | 27 | branches: 28 | except: 29 | - "readme-edits" 30 | 31 | before_install: 32 | # Install rubygems < 3.0 so that we can support ruby < 2.3 33 | # https://github.com/rubygems/rubygems/issues/2534 34 | - gem install rubygems-update -v '<3' && update_rubygems 35 | # bundler installation needed for jruby-head 36 | # https://github.com/travis-ci/travis-ci/issues/5861 37 | # stick to bundler 1.x in order to support ruby < 2.3 38 | - gem install bundler -v '~> 1.17' 39 | 40 | # Travis macOS support is pretty janky. These are some hacks to include tests 41 | # only on versions that actually work. We test on macOS because Apple monkey 42 | # patches OpenSSL to have different behavior, and we want to ensure that SSL 43 | # verification at least is broken in the expected ways on macOS. 44 | # (last tested: 2019-08) 45 | matrix: 46 | # exclude: {} 47 | include: 48 | # test only a few versions on mac 49 | - os: osx 50 | rvm: 2.6.3 51 | - os: osx 52 | rvm: ruby-head 53 | - os: osx 54 | rvm: jruby-9.1.9.0 55 | - os: osx 56 | rvm: jruby-head 57 | 58 | allow_failures: 59 | - rvm: 'ruby-head' 60 | 61 | # return results as soon as mandatory versions are done 62 | fast_finish: true 63 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The Ruby REST Client would not be what it is today without the help of 2 | the following kind souls: 3 | 4 | Adam Jacob 5 | Adam Wiggins 6 | Adrian Rangel 7 | Alex Tomlins 8 | Aman Gupta 9 | Andy Brody 10 | Avi Deitcher 11 | Blake Mizerany 12 | Brad Ediger 13 | Braintree 14 | Brian Donovan 15 | Caleb Land 16 | Chris Dinn 17 | Chris Frohoff 18 | Chris Green 19 | Coda Hale 20 | Crawford 21 | Cyril Rohr 22 | Dan Mayer 23 | Dario Hamidi 24 | Darren Coxall 25 | David Backeus 26 | David Perkowski 27 | Dmitri Dolguikh 28 | Dusty Doris 29 | Dylan Egan 30 | El Draper 31 | Evan Broder 32 | Evan Smith 33 | François Beausoleil 34 | Gabriele Cirulli 35 | Garry Shutler 36 | Giovanni Cappellotto 37 | Greg Borenstein 38 | Harm Aarts 39 | Hiro Asari 40 | Hugh McGowan 41 | Ian Warshak 42 | Igor Zubkov 43 | Ivan Makfinsky 44 | JH. Chabran 45 | James Edward Gray II 46 | Jari Bakken 47 | Jeff Pereira 48 | Jeff Remer 49 | Jeffrey Hardy 50 | Jeremy Kemper 51 | Joe Rafaniello 52 | John Barnette 53 | Jon Rowe 54 | Jordi Massaguer Pla 55 | Joshua J. Campoverde 56 | Juan Alvarez 57 | Julien Kirch 58 | Jun Aruga 59 | Justin Coyne 60 | Justin Lambert 61 | Keith Rarick 62 | Kenichi Kamiya 63 | Kevin Read 64 | Kosuke Asami 65 | Kyle Meyer 66 | Kyle VanderBeek 67 | Lars Gierth 68 | Lawrence Leonard Gilbert 69 | Lee Jarvis 70 | Lennon Day-Reynolds 71 | Lin Jen-Shin 72 | Magne Matre Gåsland 73 | Marc-André Cournoyer 74 | Marius Butuc 75 | Matthew Manning 76 | Michael Klett 77 | Michael Rykov 78 | Michael Westbom 79 | Mike Fletcher 80 | Nelson Elhage 81 | Nicholas Wieland 82 | Nick Hammond 83 | Nick Plante 84 | Niko Dittmann 85 | Nikolay Shebanov 86 | Oscar Del Ben 87 | Pablo Astigarraga 88 | Paul Dlug 89 | Pedro Belo 90 | Pedro Chambino 91 | Philip Corliss 92 | Pierre-Louis Gottfrois 93 | Rafael Ssouza 94 | Richard Schneeman 95 | Rick Olson 96 | Robert Eanes 97 | Rodrigo Panachi 98 | Sam Norbury 99 | Samuel Cochran 100 | Syl Turner 101 | T. Watanabe 102 | Tekin 103 | W. Andrew Loe III 104 | Waynn Lue 105 | Xavier Shay 106 | tpresa 107 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | if !!File::ALT_SEPARATOR 4 | gemspec :name => 'rest-client.windows' 5 | else 6 | gemspec :name => 'rest-client' 7 | end 8 | 9 | group :test do 10 | gem 'rake' 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2008-2014 Rest Client Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # load `rake build/install/release tasks' 2 | require 'bundler/setup' 3 | require_relative './lib/restclient/version' 4 | 5 | namespace :ruby do 6 | Bundler::GemHelper.install_tasks(:name => 'rest-client') 7 | end 8 | 9 | require "rspec/core/rake_task" 10 | 11 | desc "Run all specs" 12 | RSpec::Core::RakeTask.new('spec') 13 | 14 | desc "Run unit specs" 15 | RSpec::Core::RakeTask.new('spec:unit') do |t| 16 | t.pattern = 'spec/unit/*_spec.rb' 17 | end 18 | 19 | desc "Run integration specs" 20 | RSpec::Core::RakeTask.new('spec:integration') do |t| 21 | t.pattern = 'spec/integration/*_spec.rb' 22 | end 23 | 24 | desc "Print specdocs" 25 | RSpec::Core::RakeTask.new(:doc) do |t| 26 | t.rspec_opts = ["--format", "specdoc", "--dry-run"] 27 | t.pattern = 'spec/**/*_spec.rb' 28 | end 29 | 30 | desc "Run all examples with RCov" 31 | RSpec::Core::RakeTask.new('rcov') do |t| 32 | t.pattern = 'spec/*_spec.rb' 33 | t.rcov = true 34 | t.rcov_opts = ['--exclude', 'examples'] 35 | end 36 | 37 | desc 'Regenerate authors file' 38 | task :authors do 39 | Dir.chdir(File.dirname(__FILE__)) do 40 | File.open('AUTHORS', 'w') do |f| 41 | f.write <<-EOM 42 | The Ruby REST Client would not be what it is today without the help of 43 | the following kind souls: 44 | 45 | EOM 46 | end 47 | 48 | sh 'git shortlog -s | cut -f 2 >> AUTHORS' 49 | end 50 | end 51 | 52 | task :default do 53 | sh 'rake -T' 54 | end 55 | 56 | def alias_task(alias_task, original) 57 | desc "Alias for rake #{original}" 58 | task alias_task, Rake.application[original].arg_names => original 59 | end 60 | alias_task(:test, :spec) 61 | 62 | ############################ 63 | 64 | WindowsPlatforms = %w{x86-mingw32 x64-mingw32 x86-mswin32} 65 | 66 | namespace :all do 67 | 68 | desc "Build rest-client #{RestClient::VERSION} for all platforms" 69 | task :build => ['ruby:build'] + \ 70 | WindowsPlatforms.map {|p| "windows:#{p}:build"} 71 | 72 | desc "Create tag v#{RestClient::VERSION} and for all platforms build and " \ 73 | "push rest-client #{RestClient::VERSION} to Rubygems" 74 | task :release => ['build', 'ruby:release'] + \ 75 | WindowsPlatforms.map {|p| "windows:#{p}:push"} 76 | 77 | end 78 | 79 | namespace :windows do 80 | spec_path = File.join(File.dirname(__FILE__), 'rest-client.windows.gemspec') 81 | 82 | WindowsPlatforms.each do |platform| 83 | namespace platform do 84 | gem_filename = "rest-client-#{RestClient::VERSION}-#{platform}.gem" 85 | base = File.dirname(__FILE__) 86 | pkg_dir = File.join(base, 'pkg') 87 | gem_file_path = File.join(pkg_dir, gem_filename) 88 | 89 | desc "Build #{gem_filename} into the pkg directory" 90 | task 'build' do 91 | orig_platform = ENV['BUILD_PLATFORM'] 92 | begin 93 | ENV['BUILD_PLATFORM'] = platform 94 | 95 | sh("gem build -V #{spec_path}") do |ok, res| 96 | if ok 97 | FileUtils.mkdir_p(pkg_dir) 98 | FileUtils.mv(File.join(base, gem_filename), pkg_dir) 99 | Bundler.ui.confirm("rest-client #{RestClient::VERSION} " \ 100 | "built to pkg/#{gem_filename}") 101 | else 102 | abort "Command `gem build` failed: #{res}" 103 | end 104 | end 105 | 106 | ensure 107 | ENV['BUILD_PLATFORM'] = orig_platform 108 | end 109 | end 110 | 111 | desc "Push #{gem_filename} to Rubygems" 112 | task 'push' do 113 | sh("gem push #{gem_file_path}") 114 | end 115 | end 116 | end 117 | 118 | end 119 | 120 | ############################ 121 | 122 | require 'rdoc/task' 123 | 124 | Rake::RDocTask.new do |t| 125 | t.rdoc_dir = 'rdoc' 126 | t.title = "rest-client, fetch RESTful resources effortlessly" 127 | t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' 128 | t.options << '--charset' << 'utf-8' 129 | t.rdoc_files.include('README.md') 130 | t.rdoc_files.include('lib/*.rb') 131 | end 132 | 133 | ############################ 134 | 135 | require 'rubocop/rake_task' 136 | 137 | RuboCop::RakeTask.new(:rubocop) do |t| 138 | t.options = ['--display-cop-names'] 139 | end 140 | alias_task(:lint, :rubocop) 141 | -------------------------------------------------------------------------------- /bin/restclient: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib" 4 | 5 | require 'rubygems' 6 | require 'restclient' 7 | require 'yaml' 8 | 9 | def usage(why = nil) 10 | puts "failed for reason: #{why}" if why 11 | puts "usage: restclient [get|put|post|delete] url|name [username] [password]" 12 | puts " The verb is optional, if you leave it off you'll get an interactive shell." 13 | puts " put and post both take the input body on stdin." 14 | exit(1) 15 | end 16 | 17 | POSSIBLE_VERBS = ['get', 'put', 'post', 'delete'] 18 | 19 | if POSSIBLE_VERBS.include? ARGV.first 20 | @verb = ARGV.shift 21 | else 22 | @verb = nil 23 | end 24 | 25 | @url = ARGV.shift || 'http://localhost:4567' 26 | 27 | config = YAML.load(File.read(ENV['HOME'] + "/.restclient")) rescue {} 28 | 29 | if (c = config[@url]) 30 | @url, @username, @password = [c['url'], c['username'], c['password']] 31 | else 32 | @url, @username, @password = [@url, * ARGV] 33 | end 34 | 35 | usage("invalid url '#{@url}") unless @url =~ /^https?/ 36 | usage("too few args") unless ARGV.size < 3 37 | 38 | def r 39 | @r ||= RestClient::Resource.new(@url, @username, @password) 40 | end 41 | 42 | r # force rc to load 43 | 44 | if @verb 45 | begin 46 | if %w( put post ).include? @verb 47 | puts r.send(@verb, STDIN.read) 48 | else 49 | puts r.send(@verb) 50 | end 51 | exit 0 52 | rescue RestClient::Exception => e 53 | puts e.response.body if e.respond_to?(:response) && e.response 54 | raise 55 | end 56 | end 57 | 58 | POSSIBLE_VERBS.each do |m| 59 | define_method(m.to_sym) do |path, *args, &b| 60 | r[path].public_send(m.to_sym, *args, &b) 61 | end 62 | end 63 | 64 | def method_missing(s, * args, & b) 65 | if POSSIBLE_VERBS.include? s 66 | begin 67 | r.send(s, *args, & b) 68 | rescue RestClient::RequestFailed => e 69 | print STDERR, e.response.body 70 | raise e 71 | end 72 | else 73 | super 74 | end 75 | end 76 | 77 | require 'irb' 78 | require 'irb/completion' 79 | 80 | if File.exist? ".irbrc" 81 | ENV['IRBRC'] = ".irbrc" 82 | end 83 | 84 | rcfile = File.expand_path("~/.restclientrc") 85 | if File.exist?(rcfile) 86 | load(rcfile) 87 | end 88 | 89 | ARGV.clear 90 | 91 | IRB.start 92 | exit! 93 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | # 2.1.0 2 | 3 | - Add a dependency on http-accept for parsing Content-Type charset headers. 4 | This works around a bad memory leak introduced in MRI Ruby 2.4.0 and fixed in 5 | Ruby 2.4.2. (#615) 6 | - Use mime/types/columnar from mime-types 2.6.1+, which is leaner in memory 7 | usage than the older storage model of mime-types. (#393) 8 | - Add `:log` option to individual requests. This allows users to set a log on a 9 | per-request / per-resource basis instead of the kludgy global log. (#538) 10 | - Log request duration by tracking request start and end times. Make 11 | `log_response` a method on the Response object, and ensure the `size` method 12 | works on RawResponse objects. (#126) 13 | - `# => 200 OK | text/html 1270 bytes, 0.08s` 14 | - Also add a new `:stream_log_percent` parameter, which is applicable only 15 | when `:raw_response => true` is set. This causes progress logs to be 16 | emitted only on every N% (default 10%) of the total download size rather 17 | than on every chunk. 18 | - Drop custom handling of compression and use built-in Net::HTTP support for 19 | supported Content-Encodings like gzip and deflate. Don't set any explicit 20 | `Accept-Encoding` header, rely instead on Net::HTTP defaults. (#597) 21 | - Note: this changes behavior for compressed responses when using 22 | `:raw_response => true`. Previously the raw response would not have been 23 | uncompressed by rest-client, but now Net::HTTP will uncompress it. 24 | - The previous fix to avoid having Netrc username/password override an 25 | Authorization header was case-sensitive and incomplete. Fix this by 26 | respecting existing Authorization headers, regardless of letter case. (#550) 27 | - Handle ParamsArray payloads. Previously, rest-client would silently drop a 28 | ParamsArray passed as the payload. Instead, automatically use 29 | Payload::Multipart if the ParamsArray contains a file handle, or use 30 | Payload::UrlEncoded if it doesn't. (#508) 31 | - Gracefully handle Payload objects (Payload::Base or subclasses) that are 32 | passed as a payload argument. Previously, `Payload.generate` would wrap a 33 | Payload object in Payload::Streamed, creating a pointlessly nested payload. 34 | Also add a `closed?` method to Payload objects, and don't error in 35 | `short_inspect` if `size` returns nil. (#603) 36 | - Test with an image in the public domain to avoid licensing complexity. (#607) 37 | 38 | # 2.0.2 39 | 40 | - Suppress the header override warning introduced in 2.0.1 if the value is the 41 | same. There's no conflict if the value is unchanged. (#578) 42 | 43 | # 2.0.1 44 | 45 | - Warn if auto-generated headers from the payload, such as Content-Type, 46 | override headers set by the user. This is usually not what the user wants to 47 | happen, and can be surprising. (#554) 48 | - Drop the old check for weak default TLS ciphers, and use the built-in Ruby 49 | defaults. Ruby versions from Oct. 2014 onward use sane defaults, so this is 50 | no longer needed. (#573) 51 | 52 | # 2.0.0 53 | 54 | This release is largely API compatible, but makes several breaking changes. 55 | 56 | - Drop support for Ruby 1.9 57 | - Allow mime-types as new as 3.x (requires ruby 2.0) 58 | - Respect Content-Type charset header provided by server. Previously, 59 | rest-client would not override the string encoding chosen by Net::HTTP. Now 60 | responses that specify a charset will yield a body string in that encoding. 61 | For example, `Content-Type: text/plain; charset=EUC-JP` will return a String 62 | encoded with `Encoding::EUC_JP`. (#361) 63 | - Change exceptions raised on request timeout. Instead of 64 | `RestClient::RequestTimeout` (which is still used for HTTP 408), network 65 | timeouts will now raise either `RestClient::Exceptions::ReadTimeout` or 66 | `RestClient::Exceptions::OpenTimeout`, both of which inherit from 67 | `RestClient::Exceptions::Timeout`. For backwards compatibility, this still 68 | inherits from `RestClient::RequestTimeout` so existing uses will still work. 69 | This may change in a future major release. These new timeout classes also 70 | make the original wrapped exception available as `#original_exception`. 71 | - Unify request exceptions under `RestClient::RequestFailed`, which still 72 | inherits from `ExceptionWithResponse`. Previously, HTTP 304, 401, and 404 73 | inherited directly from `ExceptionWithResponse` rather than from 74 | `RequestFailed`. Now _all_ HTTP status code exceptions inherit from both. 75 | - Rename the `:timeout` request option to `:read_timeout`. When `:timeout` is 76 | passed, now set both `:read_timeout` and `:open_timeout`. 77 | - Change default HTTP Accept header to `*/*` 78 | - Use a more descriptive User-Agent header by default 79 | - Drop RC4-MD5 from default cipher list 80 | - Only prepend http:// to URIs without a scheme 81 | - Fix some support for using IPv6 addresses in URLs (still affected by Ruby 82 | 2.0+ bug https://bugs.ruby-lang.org/issues/9129, with the fix expected to be 83 | backported to 2.0 and 2.1) 84 | - `Response` objects are now a subclass of `String` rather than a `String` that 85 | mixes in the response functionality. Most of the methods remain unchanged, 86 | but this makes it much easier to understand what is happening when you look 87 | at a RestClient response object. There are a few additional changes: 88 | - Response objects now implement `.inspect` to make this distinction clearer. 89 | - `Response#to_i` will now behave like `String#to_i` instead of returning the 90 | HTTP response code, which was very surprising behavior. 91 | - `Response#body` and `#to_s` will now return a true `String` object rather 92 | than self. Previously there was no easy way to get the true `String` 93 | response instead of the Frankenstein response string object with 94 | AbstractResponse mixed in. 95 | - Response objects no longer accept an extra request args hash, but instead 96 | access request args directly from the request object, which reduces 97 | confusion and duplication. 98 | - Handle multiple HTTP response headers with the same name (except for 99 | Set-Cookie, which is special) by joining the values with a comma space, 100 | compliant with RFC 7230 101 | - Rewrite cookie support to be much smarter and to use cookie jars consistently 102 | for requests, responses, and redirection in order to resolve long-standing 103 | complaints about the previously broken behavior: (#498) 104 | - The `:cookies` option may now be a Hash of Strings, an Array of 105 | HTTP::Cookie objects, or a full HTTP::CookieJar. 106 | - Add `RestClient::Request#cookie_jar` and reimplement `Request#cookies` to 107 | be a wrapper around the cookie jar. 108 | - Still support passing the `:cookies` option in the headers hash, but now 109 | raise ArgumentError if that option is also passed to `Request#initialize`. 110 | - Warn if both `:cookies` and a `Cookie` header are supplied. 111 | - Use the `Request#cookie_jar` as the basis for `Response#cookie_jar`, 112 | creating a copy of the jar and adding any newly received cookies. 113 | - When following redirection, also use this same strategy so that cookies 114 | from the original request are carried through in a standards-compliant way 115 | by the cookie jar. 116 | - Don't set basic auth header if explicit `Authorization` header is specified 117 | - Add `:proxy` option to requests, which can be used for thread-safe 118 | per-request proxy configuration, overriding `RestClient.proxy` 119 | - Allow overriding `ENV['http_proxy']` to disable proxies by setting 120 | `RestClient.proxy` to a falsey value. Previously there was no way in Ruby 2.x 121 | to turn off a proxy specified in the environment without changing `ENV`. 122 | - Add actual support for streaming request payloads. Previously rest-client 123 | would call `.to_s` even on RestClient::Payload::Streamed objects. Instead, 124 | treat any object that responds to `.read` as a streaming payload and pass it 125 | through to `.body_stream=` on the Net:HTTP object. This massively reduces the 126 | memory required for large file uploads. 127 | - Changes to redirection behavior: (#381, #484) 128 | - Remove `RestClient::MaxRedirectsReached` in favor of the normal 129 | `ExceptionWithResponse` subclasses. This makes the response accessible on 130 | the exception object as `.response`, making it possible for callers to tell 131 | what has actually happened when the redirect limit is reached. 132 | - When following HTTP redirection, store a list of each previous response on 133 | the response object as `.history`. This makes it possible to access the 134 | original response headers and body before the redirection was followed. 135 | - Follow redirection consistently, regardless of whether the HTTP method was 136 | passed as a symbol or string. Under the hood rest-client now normalizes the 137 | HTTP request method to a lowercase string. 138 | - Add `:before_execution_proc` option to `RestClient::Request`. This makes it 139 | possible to add procs like `RestClient.add_before_execution_proc` to a single 140 | request without global state. 141 | - Run tests on Travis's beta OS X support. 142 | - Make `Request#transmit` a private method, along with a few others. 143 | - Refactor URI parsing to happen earlier, in Request initialization. 144 | - Improve consistency and functionality of complex URL parameter handling: 145 | - When adding URL params, handle URLs that already contain params. 146 | - Add new convention for handling URL params containing deeply nested arrays 147 | and hashes, unify handling of null/empty values, and use the same code for 148 | GET and POST params. (#437) 149 | - Add the RestClient::ParamsArray class, a simple array-like container that 150 | can be used to pass multiple keys with same name or keys where the ordering 151 | is significant. 152 | - Add a few more exception classes for obscure HTTP status codes. 153 | - Multipart: use a much more robust multipart boundary with greater entropy. 154 | - Make `RestClient::Payload::Base#inspect` stop pretending to be a String. 155 | - Add `Request#redacted_uri` and `Request#redacted_url` to display the URI 156 | with any password redacted. 157 | 158 | # 2.0.0.rc1 159 | 160 | Changes in the release candidate that did not persist through the final 2.0.0 161 | release: 162 | - RestClient::Exceptions::Timeout was originally going to be a direct subclass 163 | of RestClient::Exception in the release candidate. This exception tree was 164 | made a subclass of RestClient::RequestTimeout prior to the final release. 165 | 166 | # 1.8.0 167 | 168 | - Security: implement standards compliant cookie handling by adding a 169 | dependency on http-cookie. This breaks compatibility, but was necessary to 170 | address a session fixation / cookie disclosure vulnerability. 171 | (#369 / CVE-2015-1820) 172 | 173 | Previously, any Set-Cookie headers found in an HTTP 30x response would be 174 | sent to the redirection target, regardless of domain. Responses now expose a 175 | cookie jar and respect standards compliant domain / path flags in Set-Cookie 176 | headers. 177 | 178 | # 1.7.3 179 | 180 | - Security: redact password in URI from logs (#349 / OSVDB-117461) 181 | - Drop monkey patch on MIME::Types (added `type_for_extension` method, use 182 | the public interface instead. 183 | 184 | # 1.7.2 185 | 186 | - Ignore duplicate certificates in CA store on Windows 187 | 188 | # 1.7.1 189 | 190 | - Relax mime-types dependency to continue supporting mime-types 1.x series. 191 | There seem to be a large number of popular gems that have depended on 192 | mime-types '~> 1.16' until very recently. 193 | - Improve urlencode performance 194 | - Clean up a number of style points 195 | 196 | # 1.7.0 197 | 198 | - This release drops support for Ruby 1.8.7 and breaks compatibility in a few 199 | other relatively minor ways 200 | - Upgrade to mime-types ~> 2.0 201 | - Don't CGI.unescape cookie values sent to the server (issue #89) 202 | - Add support for reading credentials from netrc 203 | - Lots of SSL changes and enhancements: (#268) 204 | - Enable peer verification by default (setting `VERIFY_PEER` with OpenSSL) 205 | - By default, use the system default certificate store for SSL verification, 206 | even on Windows (this uses a separate Windows build that pulls in ffi) 207 | - Add support for SSL `ca_path` 208 | - Add support for SSL `cert_store` 209 | - Add support for SSL `verify_callback` (with some caveats for jruby, OS X, #277) 210 | - Add support for SSL ciphers, and choose secure ones by default 211 | - Run tests under travis 212 | - Several other bugfixes and test improvements 213 | - Convert Errno::ETIMEDOUT to RestClient::RequestTimeout 214 | - Handle more HTTP response codes from recent standards 215 | - Save raw responses to binary mode tempfile (#110) 216 | - Disable timeouts with :timeout => nil rather than :timeout => -1 217 | - Drop all Net::HTTP monkey patches 218 | 219 | # 1.6.14 220 | 221 | - This release is unchanged from 1.6.9. It was published in order to supersede 222 | the malicious 1.6.10-13 versions, even for users who are still pinning to the 223 | legacy 1.6.x series. All users are encouraged to upgrade to rest-client 2.x. 224 | 225 | # 1.6.10, 1.6.11, 1.6.12, 1.6.13 (CVE-2019-15224) 226 | 227 | - These versions were pushed by a malicious actor and included a backdoor permitting 228 | remote code execution in Rails environments. (#713) 229 | - They were live for about five days before being yanked. 230 | 231 | # 1.6.9 232 | 233 | - Move rdoc to a development dependency 234 | 235 | # 1.6.8 236 | 237 | - The 1.6.x series will be the last to support Ruby 1.8.7 238 | - Pin mime-types to < 2.0 to maintain Ruby 1.8.7 support 239 | - Add Gemfile, AUTHORS, add license to gemspec 240 | - Point homepage at https://github.com/rest-client/rest-client 241 | - Clean up and fix various tests and ruby warnings 242 | - Backport `ssl_verify_callback` functionality from 1.7.0 243 | 244 | # 1.6.7 245 | 246 | - rebuild with 1.8.7 to avoid https://github.com/rubygems/rubygems/pull/57 247 | 248 | # 1.6.6 249 | 250 | - 1.6.5 was yanked 251 | 252 | # 1.6.5 253 | 254 | - RFC6265 requires single SP after ';' for separating parameters pairs in the 'Cookie:' header (patch provided by Hiroshi Nakamura) 255 | - enable url parameters for all actions 256 | - detect file parameters in arrays 257 | - allow disabling the timeouts by passing -1 (patch provided by Sven Böhm) 258 | 259 | # 1.6.4 260 | 261 | - fix restclient script compatibility with 1.9.2 262 | - fix unlinking temp file (patch provided by Evan Smith) 263 | - monkeypatching ruby for http patch method (patch provided by Syl Turner) 264 | 265 | # 1.6.3 266 | 267 | - 1.6.2 was yanked 268 | 269 | # 1.6.2 270 | 271 | - add support for HEAD in resources (patch provided by tpresa) 272 | - fix shell for 1.9.2 273 | - workaround when some gem monkeypatch net/http (patch provided by Ian Warshak) 274 | - DELETE requests should process parameters just like GET and HEAD 275 | - adding :block_response parameter for manual processing 276 | - limit number of redirections (patch provided by Chris Dinn) 277 | - close and unlink the temp file created by playload (patch provided by Chris Green) 278 | - make gemspec Rubygems 1.8 compatible (patch provided by David Backeus) 279 | - added RestClient.reset_before_execution_procs (patch provided by Cloudify) 280 | - added PATCH method (patch provided by Jeff Remer) 281 | - hack for HTTP servers that use raw DEFLATE compression, see http://www.ruby-forum.com/topic/136825 (path provided by James Reeves) 282 | 283 | # 1.6.1 284 | 285 | - add response body in Exception#inspect 286 | - add support for RestClient.options 287 | - fix tests for 1.9.2 (patch provided by Niko Dittmann) 288 | - block passing in Resource#[] (patch provided by Niko Dittmann) 289 | - cookies set in a response should be kept in a redirect 290 | - HEAD requests should process parameters just like GET (patch provided by Rob Eanes) 291 | - exception message should never be nil (patch provided by Michael Klett) 292 | 293 | # 1.6.0 294 | 295 | - forgot to include rest-client.rb in the gem 296 | - user, password and user-defined headers should survive a redirect 297 | - added all missing status codes 298 | - added parameter passing for get request using the :param key in header 299 | - the warning about the logger when using a string was a bad idea 300 | - multipart parameters names should not be escaped 301 | - remove the cookie escaping introduced by migrating to CGI cookie parsing in 1.5.1 302 | - add a streamed payload type (patch provided by Caleb Land) 303 | - Exception#http_body works even when no response 304 | 305 | # 1.5.1 306 | 307 | - only converts headers keys which are Symbols 308 | - use CGI for cookie parsing instead of custom code 309 | - unescape user and password before using them (patch provided by Lars Gierth) 310 | - expand ~ in ~/.restclientrc (patch provided by Mike Fletcher) 311 | - ssl verification raise an exception when the ca certificate is incorrect (patch provided by Braintree) 312 | 313 | # 1.5.0 314 | 315 | - the response is now a String with the Response module a.k.a. the change in 1.4.0 was a mistake (Response.body is returning self for compatability) 316 | - added AbstractResponse.to_i to improve semantic 317 | - multipart Payloads ignores the name attribute if it's not set (patch provided by Tekin Suleyman) 318 | - correctly takes into account user headers whose keys are strings (path provided by Cyril Rohr) 319 | - use binary mode for payload temp file 320 | - concatenate cookies with ';' 321 | - fixed deeper parameter handling 322 | - do not quote the boundary in the Content-Type header (patch provided by W. Andrew Loe III) 323 | 324 | # 1.4.2 325 | 326 | - fixed RestClient.add_before_execution_proc (patch provided by Nicholas Wieland) 327 | - fixed error when an exception is raised without a response (patch provided by Caleb Land) 328 | 329 | # 1.4.1 330 | 331 | - fixed parameters managment when using hash 332 | 333 | # 1.4.0 334 | 335 | - Response is no more a String, and the mixin is replaced by an abstract_response, existing calls are redirected to response body with a warning. 336 | - enable repeated parameters RestClient.post 'http://example.com/resource', :param1 => ['one', 'two', 'three'], => :param2 => 'foo' (patch provided by Rodrigo Panachi) 337 | - fixed the redirect code concerning relative path and query string combination (patch provided by Kevin Read) 338 | - redirection code moved to Response so redirection can be customized using the block syntax 339 | - only get and head redirections are now followed by default, as stated in the specification 340 | - added RestClient.add_before_execution_proc to hack the http request, like for oauth 341 | 342 | The response change may be breaking in rare cases. 343 | 344 | # 1.3.1 345 | 346 | - added compatibility to enable responses in exception to act like Net::HTTPResponse 347 | 348 | # 1.3.0 349 | 350 | - a block can be used to process a request's result, this enable to handle custom error codes or paththrought (design by Cyril Rohr) 351 | - cleaner log API, add a warning for some cases but should be compatible 352 | - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) 353 | - remove "Content-Length" and "Content-Type" headers when following a redirection (patch provided by haarts) 354 | - all http error codes have now a corresponding exception class and all of them contain the Reponse -> this means that the raised exception can be different 355 | - changed "Content-Disposition: multipart/form-data" to "Content-Disposition: form-data" per RFC 2388 (patch provided by Kyle Crawford) 356 | 357 | The only breaking change should be the exception classes, but as the new classes inherits from the existing ones, the breaking cases should be rare. 358 | 359 | # 1.2.0 360 | 361 | - formatting changed from tabs to spaces 362 | - logged requests now include generated headers 363 | - accept and content-type headers can now be specified using extentions: RestClient.post "http://example.com/resource", { 'x' => 1 }.to_json, :content_type => :json, :accept => :json 364 | - should be 1.1.1 but renamed to 1.2.0 because 1.1.X versions has already been packaged on Debian 365 | 366 | # 1.1.0 367 | 368 | - new maintainer: Archiloque, the working repo is now at http://github.com/archiloque/rest-client 369 | - a mailing list has been created at rest.client@librelist.com and an freenode irc channel #rest-client 370 | - François Beausoleil' multipart code from http://github.com/francois/rest-client has been merged 371 | - ability to use hash in hash as payload 372 | - the mime-type code now rely on the mime-types gem http://mime-types.rubyforge.org/ instead of an internal partial list 373 | - 204 response returns a Response instead of nil (patch provided by Elliott Draper) 374 | 375 | All changes exept the last one should be fully compatible with the previous version. 376 | 377 | NOTE: due to a dependency problem and to the last change, heroku users should update their heroku gem to >= 1.5.3 to be able to use this version. 378 | -------------------------------------------------------------------------------- /lib/rest-client.rb: -------------------------------------------------------------------------------- 1 | # More logical way to require 'rest-client' 2 | require File.dirname(__FILE__) + '/restclient' 3 | -------------------------------------------------------------------------------- /lib/rest_client.rb: -------------------------------------------------------------------------------- 1 | # This file exists for backward compatbility with require 'rest_client' 2 | require File.dirname(__FILE__) + '/restclient' 3 | -------------------------------------------------------------------------------- /lib/restclient.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'openssl' 3 | require 'stringio' 4 | require 'uri' 5 | 6 | require File.dirname(__FILE__) + '/restclient/version' 7 | require File.dirname(__FILE__) + '/restclient/platform' 8 | require File.dirname(__FILE__) + '/restclient/exceptions' 9 | require File.dirname(__FILE__) + '/restclient/utils' 10 | require File.dirname(__FILE__) + '/restclient/request' 11 | require File.dirname(__FILE__) + '/restclient/abstract_response' 12 | require File.dirname(__FILE__) + '/restclient/response' 13 | require File.dirname(__FILE__) + '/restclient/raw_response' 14 | require File.dirname(__FILE__) + '/restclient/resource' 15 | require File.dirname(__FILE__) + '/restclient/params_array' 16 | require File.dirname(__FILE__) + '/restclient/payload' 17 | require File.dirname(__FILE__) + '/restclient/windows' 18 | 19 | # This module's static methods are the entry point for using the REST client. 20 | # 21 | # # GET 22 | # xml = RestClient.get 'http://example.com/resource' 23 | # jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg' 24 | # 25 | # # authentication and SSL 26 | # RestClient.get 'https://user:password@example.com/private/resource' 27 | # 28 | # # POST or PUT with a hash sends parameters as a urlencoded form body 29 | # RestClient.post 'http://example.com/resource', :param1 => 'one' 30 | # 31 | # # nest hash parameters 32 | # RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' } 33 | # 34 | # # POST and PUT with raw payloads 35 | # RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain' 36 | # RestClient.post 'http://example.com/resource.xml', xml_doc 37 | # RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf' 38 | # 39 | # # DELETE 40 | # RestClient.delete 'http://example.com/resource' 41 | # 42 | # # retrieve the response http code and headers 43 | # res = RestClient.get 'http://example.com/some.jpg' 44 | # res.code # => 200 45 | # res.headers[:content_type] # => 'image/jpg' 46 | # 47 | # # HEAD 48 | # RestClient.head('http://example.com').headers 49 | # 50 | # To use with a proxy, just set RestClient.proxy to the proper http proxy: 51 | # 52 | # RestClient.proxy = "http://proxy.example.com/" 53 | # 54 | # Or inherit the proxy from the environment: 55 | # 56 | # RestClient.proxy = ENV['http_proxy'] 57 | # 58 | # For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call: 59 | # 60 | # >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz' 61 | # => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}" 62 | # 63 | module RestClient 64 | 65 | def self.get(url, headers={}, &block) 66 | Request.execute(:method => :get, :url => url, :headers => headers, &block) 67 | end 68 | 69 | def self.post(url, payload, headers={}, &block) 70 | Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers, &block) 71 | end 72 | 73 | def self.patch(url, payload, headers={}, &block) 74 | Request.execute(:method => :patch, :url => url, :payload => payload, :headers => headers, &block) 75 | end 76 | 77 | def self.put(url, payload, headers={}, &block) 78 | Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers, &block) 79 | end 80 | 81 | def self.delete(url, headers={}, &block) 82 | Request.execute(:method => :delete, :url => url, :headers => headers, &block) 83 | end 84 | 85 | def self.head(url, headers={}, &block) 86 | Request.execute(:method => :head, :url => url, :headers => headers, &block) 87 | end 88 | 89 | def self.options(url, headers={}, &block) 90 | Request.execute(:method => :options, :url => url, :headers => headers, &block) 91 | end 92 | 93 | # A global proxy URL to use for all requests. This can be overridden on a 94 | # per-request basis by passing `:proxy` to RestClient::Request. 95 | def self.proxy 96 | @proxy ||= nil 97 | end 98 | 99 | def self.proxy=(value) 100 | @proxy = value 101 | @proxy_set = true 102 | end 103 | 104 | # Return whether RestClient.proxy was set explicitly. We use this to 105 | # differentiate between no value being set and a value explicitly set to nil. 106 | # 107 | # @return [Boolean] 108 | # 109 | def self.proxy_set? 110 | @proxy_set ||= false 111 | end 112 | 113 | # Setup the log for RestClient calls. 114 | # Value should be a logger but can can be stdout, stderr, or a filename. 115 | # You can also configure logging by the environment variable RESTCLIENT_LOG. 116 | def self.log= log 117 | @@log = create_log log 118 | end 119 | 120 | # Create a log that respond to << like a logger 121 | # param can be 'stdout', 'stderr', a string (then we will log to that file) or a logger (then we return it) 122 | def self.create_log param 123 | if param 124 | if param.is_a? String 125 | if param == 'stdout' 126 | stdout_logger = Class.new do 127 | def << obj 128 | STDOUT.puts obj 129 | end 130 | end 131 | stdout_logger.new 132 | elsif param == 'stderr' 133 | stderr_logger = Class.new do 134 | def << obj 135 | STDERR.puts obj 136 | end 137 | end 138 | stderr_logger.new 139 | else 140 | file_logger = Class.new do 141 | attr_writer :target_file 142 | 143 | def << obj 144 | File.open(@target_file, 'a') { |f| f.puts obj } 145 | end 146 | end 147 | logger = file_logger.new 148 | logger.target_file = param 149 | logger 150 | end 151 | else 152 | param 153 | end 154 | end 155 | end 156 | 157 | @@env_log = create_log ENV['RESTCLIENT_LOG'] 158 | 159 | @@log = nil 160 | 161 | def self.log # :nodoc: 162 | @@env_log || @@log 163 | end 164 | 165 | @@before_execution_procs = [] 166 | 167 | # Add a Proc to be called before each request in executed. 168 | # The proc parameters will be the http request and the request params. 169 | def self.add_before_execution_proc &proc 170 | raise ArgumentError.new('block is required') unless proc 171 | @@before_execution_procs << proc 172 | end 173 | 174 | # Reset the procs to be called before each request is executed. 175 | def self.reset_before_execution_procs 176 | @@before_execution_procs = [] 177 | end 178 | 179 | def self.before_execution_procs # :nodoc: 180 | @@before_execution_procs 181 | end 182 | 183 | end 184 | -------------------------------------------------------------------------------- /lib/restclient/abstract_response.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'http-cookie' 3 | 4 | module RestClient 5 | 6 | module AbstractResponse 7 | 8 | attr_reader :net_http_res, :request, :start_time, :end_time, :duration 9 | 10 | def inspect 11 | raise NotImplementedError.new('must override in subclass') 12 | end 13 | 14 | # Logger from the request, potentially nil. 15 | def log 16 | request.log 17 | end 18 | 19 | def log_response 20 | return unless log 21 | 22 | code = net_http_res.code 23 | res_name = net_http_res.class.to_s.gsub(/\ANet::HTTP/, '') 24 | content_type = (net_http_res['Content-type'] || '').gsub(/;.*\z/, '') 25 | 26 | log << "# => #{code} #{res_name} | #{content_type} #{size} bytes, #{sprintf('%.2f', duration)}s\n" 27 | end 28 | 29 | # HTTP status code 30 | def code 31 | @code ||= @net_http_res.code.to_i 32 | end 33 | 34 | def history 35 | @history ||= request.redirection_history || [] 36 | end 37 | 38 | # A hash of the headers, beautified with symbols and underscores. 39 | # e.g. "Content-type" will become :content_type. 40 | def headers 41 | @headers ||= AbstractResponse.beautify_headers(@net_http_res.to_hash) 42 | end 43 | 44 | # The raw headers. 45 | def raw_headers 46 | @raw_headers ||= @net_http_res.to_hash 47 | end 48 | 49 | # @param [Net::HTTPResponse] net_http_res 50 | # @param [RestClient::Request] request 51 | # @param [Time] start_time 52 | def response_set_vars(net_http_res, request, start_time) 53 | @net_http_res = net_http_res 54 | @request = request 55 | @start_time = start_time 56 | @end_time = Time.now 57 | 58 | if @start_time 59 | @duration = @end_time - @start_time 60 | else 61 | @duration = nil 62 | end 63 | 64 | # prime redirection history 65 | history 66 | end 67 | 68 | # Hash of cookies extracted from response headers. 69 | # 70 | # NB: This will return only cookies whose domain matches this request, and 71 | # may not even return all of those cookies if there are duplicate names. 72 | # Use the full cookie_jar for more nuanced access. 73 | # 74 | # @see #cookie_jar 75 | # 76 | # @return [Hash] 77 | # 78 | def cookies 79 | hash = {} 80 | 81 | cookie_jar.cookies(@request.uri).each do |cookie| 82 | hash[cookie.name] = cookie.value 83 | end 84 | 85 | hash 86 | end 87 | 88 | # Cookie jar extracted from response headers. 89 | # 90 | # @return [HTTP::CookieJar] 91 | # 92 | def cookie_jar 93 | return @cookie_jar if defined?(@cookie_jar) && @cookie_jar 94 | 95 | jar = @request.cookie_jar.dup 96 | headers.fetch(:set_cookie, []).each do |cookie| 97 | jar.parse(cookie, @request.uri) 98 | end 99 | 100 | @cookie_jar = jar 101 | end 102 | 103 | # Return the default behavior corresponding to the response code: 104 | # 105 | # For 20x status codes: return the response itself 106 | # 107 | # For 30x status codes: 108 | # 301, 302, 307: redirect GET / HEAD if there is a Location header 109 | # 303: redirect, changing method to GET, if there is a Location header 110 | # 111 | # For all other responses, raise a response exception 112 | # 113 | def return!(&block) 114 | case code 115 | when 200..207 116 | self 117 | when 301, 302, 307 118 | case request.method 119 | when 'get', 'head' 120 | check_max_redirects 121 | follow_redirection(&block) 122 | else 123 | raise exception_with_response 124 | end 125 | when 303 126 | check_max_redirects 127 | follow_get_redirection(&block) 128 | else 129 | raise exception_with_response 130 | end 131 | end 132 | 133 | def to_i 134 | warn('warning: calling Response#to_i is not recommended') 135 | super 136 | end 137 | 138 | def description 139 | "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n" 140 | end 141 | 142 | # Follow a redirection response by making a new HTTP request to the 143 | # redirection target. 144 | def follow_redirection(&block) 145 | _follow_redirection(request.args.dup, &block) 146 | end 147 | 148 | # Follow a redirection response, but change the HTTP method to GET and drop 149 | # the payload from the original request. 150 | def follow_get_redirection(&block) 151 | new_args = request.args.dup 152 | new_args[:method] = :get 153 | new_args.delete(:payload) 154 | 155 | _follow_redirection(new_args, &block) 156 | end 157 | 158 | # Convert headers hash into canonical form. 159 | # 160 | # Header names will be converted to lowercase symbols with underscores 161 | # instead of hyphens. 162 | # 163 | # Headers specified multiple times will be joined by comma and space, 164 | # except for Set-Cookie, which will always be an array. 165 | # 166 | # Per RFC 2616, if a server sends multiple headers with the same key, they 167 | # MUST be able to be joined into a single header by a comma. However, 168 | # Set-Cookie (RFC 6265) cannot because commas are valid within cookie 169 | # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be 170 | # handled as a special case. 171 | # 172 | # http://tools.ietf.org/html/rfc2616#section-4.2 173 | # http://tools.ietf.org/html/rfc7230#section-3.2.2 174 | # http://tools.ietf.org/html/rfc6265 175 | # 176 | # @param headers [Hash] 177 | # @return [Hash] 178 | # 179 | def self.beautify_headers(headers) 180 | headers.inject({}) do |out, (key, value)| 181 | key_sym = key.tr('-', '_').downcase.to_sym 182 | 183 | # Handle Set-Cookie specially since it cannot be joined by comma. 184 | if key.downcase == 'set-cookie' 185 | out[key_sym] = value 186 | else 187 | out[key_sym] = value.join(', ') 188 | end 189 | 190 | out 191 | end 192 | end 193 | 194 | private 195 | 196 | # Follow a redirection 197 | # 198 | # @param new_args [Hash] Start with this hash of arguments for the 199 | # redirection request. The hash will be mutated, so be sure to dup any 200 | # existing hash that should not be modified. 201 | # 202 | def _follow_redirection(new_args, &block) 203 | 204 | # parse location header and merge into existing URL 205 | url = headers[:location] 206 | 207 | # cannot follow redirection if there is no location header 208 | unless url 209 | raise exception_with_response 210 | end 211 | 212 | # handle relative redirects 213 | unless url.start_with?('http') 214 | url = URI.parse(request.url).merge(url).to_s 215 | end 216 | new_args[:url] = url 217 | 218 | new_args[:password] = request.password 219 | new_args[:user] = request.user 220 | new_args[:headers] = request.headers 221 | new_args[:max_redirects] = request.max_redirects - 1 222 | 223 | # pass through our new cookie jar 224 | new_args[:cookies] = cookie_jar 225 | 226 | # prepare new request 227 | new_req = Request.new(new_args) 228 | 229 | # append self to redirection history 230 | new_req.redirection_history = history + [self] 231 | 232 | # execute redirected request 233 | new_req.execute(&block) 234 | end 235 | 236 | def check_max_redirects 237 | if request.max_redirects <= 0 238 | raise exception_with_response 239 | end 240 | end 241 | 242 | def exception_with_response 243 | begin 244 | klass = Exceptions::EXCEPTIONS_MAP.fetch(code) 245 | rescue KeyError 246 | raise RequestFailed.new(self, code) 247 | end 248 | 249 | raise klass.new(self, code) 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /lib/restclient/exceptions.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | 3 | # Hash of HTTP status code => message. 4 | # 5 | # 1xx: Informational - Request received, continuing process 6 | # 2xx: Success - The action was successfully received, understood, and 7 | # accepted 8 | # 3xx: Redirection - Further action must be taken in order to complete the 9 | # request 10 | # 4xx: Client Error - The request contains bad syntax or cannot be fulfilled 11 | # 5xx: Server Error - The server failed to fulfill an apparently valid 12 | # request 13 | # 14 | # @see 15 | # http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 16 | # 17 | STATUSES = {100 => 'Continue', 18 | 101 => 'Switching Protocols', 19 | 102 => 'Processing', #WebDAV 20 | 21 | 200 => 'OK', 22 | 201 => 'Created', 23 | 202 => 'Accepted', 24 | 203 => 'Non-Authoritative Information', # http/1.1 25 | 204 => 'No Content', 26 | 205 => 'Reset Content', 27 | 206 => 'Partial Content', 28 | 207 => 'Multi-Status', #WebDAV 29 | 208 => 'Already Reported', # RFC5842 30 | 226 => 'IM Used', # RFC3229 31 | 32 | 300 => 'Multiple Choices', 33 | 301 => 'Moved Permanently', 34 | 302 => 'Found', 35 | 303 => 'See Other', # http/1.1 36 | 304 => 'Not Modified', 37 | 305 => 'Use Proxy', # http/1.1 38 | 306 => 'Switch Proxy', # no longer used 39 | 307 => 'Temporary Redirect', # http/1.1 40 | 308 => 'Permanent Redirect', # RFC7538 41 | 42 | 400 => 'Bad Request', 43 | 401 => 'Unauthorized', 44 | 402 => 'Payment Required', 45 | 403 => 'Forbidden', 46 | 404 => 'Not Found', 47 | 405 => 'Method Not Allowed', 48 | 406 => 'Not Acceptable', 49 | 407 => 'Proxy Authentication Required', 50 | 408 => 'Request Timeout', 51 | 409 => 'Conflict', 52 | 410 => 'Gone', 53 | 411 => 'Length Required', 54 | 412 => 'Precondition Failed', 55 | 413 => 'Payload Too Large', # RFC7231 (renamed, see below) 56 | 414 => 'URI Too Long', # RFC7231 (renamed, see below) 57 | 415 => 'Unsupported Media Type', 58 | 416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below) 59 | 417 => 'Expectation Failed', 60 | 418 => 'I\'m A Teapot', #RFC2324 61 | 421 => 'Too Many Connections From This IP', 62 | 422 => 'Unprocessable Entity', #WebDAV 63 | 423 => 'Locked', #WebDAV 64 | 424 => 'Failed Dependency', #WebDAV 65 | 425 => 'Unordered Collection', #WebDAV 66 | 426 => 'Upgrade Required', 67 | 428 => 'Precondition Required', #RFC6585 68 | 429 => 'Too Many Requests', #RFC6585 69 | 431 => 'Request Header Fields Too Large', #RFC6585 70 | 449 => 'Retry With', #Microsoft 71 | 450 => 'Blocked By Windows Parental Controls', #Microsoft 72 | 73 | 500 => 'Internal Server Error', 74 | 501 => 'Not Implemented', 75 | 502 => 'Bad Gateway', 76 | 503 => 'Service Unavailable', 77 | 504 => 'Gateway Timeout', 78 | 505 => 'HTTP Version Not Supported', 79 | 506 => 'Variant Also Negotiates', 80 | 507 => 'Insufficient Storage', #WebDAV 81 | 508 => 'Loop Detected', # RFC5842 82 | 509 => 'Bandwidth Limit Exceeded', #Apache 83 | 510 => 'Not Extended', 84 | 511 => 'Network Authentication Required', # RFC6585 85 | } 86 | 87 | STATUSES_COMPATIBILITY = { 88 | # The RFCs all specify "Not Found", but "Resource Not Found" was used in 89 | # earlier RestClient releases. 90 | 404 => ['ResourceNotFound'], 91 | 92 | # HTTP 413 was renamed to "Payload Too Large" in RFC7231. 93 | 413 => ['RequestEntityTooLarge'], 94 | 95 | # HTTP 414 was renamed to "URI Too Long" in RFC7231. 96 | 414 => ['RequestURITooLong'], 97 | 98 | # HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233. 99 | 416 => ['RequestedRangeNotSatisfiable'], 100 | } 101 | 102 | 103 | # This is the base RestClient exception class. Rescue it if you want to 104 | # catch any exception that your request might raise 105 | # You can get the status code by e.http_code, or see anything about the 106 | # response via e.response. 107 | # For example, the entire result body (which is 108 | # probably an HTML error page) is e.response. 109 | class Exception < RuntimeError 110 | attr_accessor :response 111 | attr_accessor :original_exception 112 | attr_writer :message 113 | 114 | def initialize response = nil, initial_response_code = nil 115 | @response = response 116 | @message = nil 117 | @initial_response_code = initial_response_code 118 | end 119 | 120 | def http_code 121 | # return integer for compatibility 122 | if @response 123 | @response.code.to_i 124 | else 125 | @initial_response_code 126 | end 127 | end 128 | 129 | def http_headers 130 | @response.headers if @response 131 | end 132 | 133 | def http_body 134 | @response.body if @response 135 | end 136 | 137 | def to_s 138 | message 139 | end 140 | 141 | def message 142 | @message || default_message 143 | end 144 | 145 | def default_message 146 | self.class.name 147 | end 148 | end 149 | 150 | # Compatibility 151 | class ExceptionWithResponse < RestClient::Exception 152 | end 153 | 154 | # The request failed with an error code not managed by the code 155 | class RequestFailed < ExceptionWithResponse 156 | 157 | def default_message 158 | "HTTP status code #{http_code}" 159 | end 160 | 161 | def to_s 162 | message 163 | end 164 | end 165 | 166 | # RestClient exception classes. TODO: move all exceptions into this module. 167 | # 168 | # We will a create an exception for each status code, see 169 | # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 170 | # 171 | module Exceptions 172 | # Map http status codes to the corresponding exception class 173 | EXCEPTIONS_MAP = {} 174 | end 175 | 176 | # Create HTTP status exception classes 177 | STATUSES.each_pair do |code, message| 178 | klass = Class.new(RequestFailed) do 179 | send(:define_method, :default_message) {"#{http_code ? "#{http_code} " : ''}#{message}"} 180 | end 181 | klass_constant = const_set(message.delete(' \-\''), klass) 182 | Exceptions::EXCEPTIONS_MAP[code] = klass_constant 183 | end 184 | 185 | # Create HTTP status exception classes used for backwards compatibility 186 | STATUSES_COMPATIBILITY.each_pair do |code, compat_list| 187 | klass = Exceptions::EXCEPTIONS_MAP.fetch(code) 188 | compat_list.each do |old_name| 189 | const_set(old_name, klass) 190 | end 191 | end 192 | 193 | module Exceptions 194 | # We have to split the Exceptions module like we do here because the 195 | # EXCEPTIONS_MAP is under Exceptions, but we depend on 196 | # RestClient::RequestTimeout below. 197 | 198 | # Base class for request timeouts. 199 | # 200 | # NB: Previous releases of rest-client would raise RequestTimeout both for 201 | # HTTP 408 responses and for actual connection timeouts. 202 | class Timeout < RestClient::RequestTimeout 203 | def initialize(message=nil, original_exception=nil) 204 | super(nil, nil) 205 | self.message = message if message 206 | self.original_exception = original_exception if original_exception 207 | end 208 | end 209 | 210 | # Timeout when connecting to a server. Typically wraps Net::OpenTimeout (in 211 | # ruby 2.0 or greater). 212 | class OpenTimeout < Timeout 213 | def default_message 214 | 'Timed out connecting to server' 215 | end 216 | end 217 | 218 | # Timeout when reading from a server. Typically wraps Net::ReadTimeout (in 219 | # ruby 2.0 or greater). 220 | class ReadTimeout < Timeout 221 | def default_message 222 | 'Timed out reading data from server' 223 | end 224 | end 225 | end 226 | 227 | 228 | # The server broke the connection prior to the request completing. Usually 229 | # this means it crashed, or sometimes that your network connection was 230 | # severed before it could complete. 231 | class ServerBrokeConnection < RestClient::Exception 232 | def initialize(message = 'Server broke connection') 233 | super nil, nil 234 | self.message = message 235 | end 236 | end 237 | 238 | class SSLCertificateNotVerified < RestClient::Exception 239 | def initialize(message = 'SSL certificate not verified') 240 | super nil, nil 241 | self.message = message 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/restclient/params_array.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | 3 | # The ParamsArray class is used to represent an ordered list of [key, value] 4 | # pairs. Use this when you need to include a key multiple times or want 5 | # explicit control over parameter ordering. 6 | # 7 | # Most of the request payload & parameter functions normally accept a Hash of 8 | # keys => values, which does not allow for duplicated keys. 9 | # 10 | # @see RestClient::Utils.encode_query_string 11 | # @see RestClient::Utils.flatten_params 12 | # 13 | class ParamsArray 14 | include Enumerable 15 | 16 | # @param array [Array<Array>] An array of parameter key,value pairs. These 17 | # pairs may be 2 element arrays [key, value] or single element hashes 18 | # {key => value}. They may also be single element arrays to represent a 19 | # key with no value. 20 | # 21 | # @example 22 | # >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]]) 23 | # This will be encoded as "foo=123&foo=456&bar=789" 24 | # 25 | # @example 26 | # >> ParamsArray.new({foo: 123, bar: 456}) 27 | # This is valid, but there's no reason not to just use the Hash directly 28 | # instead of a ParamsArray. 29 | # 30 | # 31 | def initialize(array) 32 | @array = process_input(array) 33 | end 34 | 35 | def each(*args, &blk) 36 | @array.each(*args, &blk) 37 | end 38 | 39 | def empty? 40 | @array.empty? 41 | end 42 | 43 | private 44 | 45 | def process_input(array) 46 | array.map {|v| process_pair(v) } 47 | end 48 | 49 | # A pair may be: 50 | # - A single element hash, e.g. {foo: 'bar'} 51 | # - A two element array, e.g. ['foo', 'bar'] 52 | # - A one element array, e.g. ['foo'] 53 | # 54 | def process_pair(pair) 55 | case pair 56 | when Hash 57 | if pair.length != 1 58 | raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}") 59 | end 60 | pair.to_a.fetch(0) 61 | when Array 62 | if pair.length > 2 63 | raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}") 64 | end 65 | [pair.fetch(0), pair[1]] 66 | else 67 | # recurse, converting any non-array to an array 68 | process_pair(pair.to_a) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/restclient/payload.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'securerandom' 3 | require 'stringio' 4 | 5 | begin 6 | # Use mime/types/columnar if available, for reduced memory usage 7 | require 'mime/types/columnar' 8 | rescue LoadError 9 | require 'mime/types' 10 | end 11 | 12 | module RestClient 13 | module Payload 14 | extend self 15 | 16 | def generate(params) 17 | if params.is_a?(RestClient::Payload::Base) 18 | # pass through Payload objects unchanged 19 | params 20 | elsif params.is_a?(String) 21 | Base.new(params) 22 | elsif params.is_a?(Hash) 23 | if params.delete(:multipart) == true || has_file?(params) 24 | Multipart.new(params) 25 | else 26 | UrlEncoded.new(params) 27 | end 28 | elsif params.is_a?(ParamsArray) 29 | if _has_file?(params) 30 | Multipart.new(params) 31 | else 32 | UrlEncoded.new(params) 33 | end 34 | elsif params.respond_to?(:read) 35 | Streamed.new(params) 36 | else 37 | nil 38 | end 39 | end 40 | 41 | def has_file?(params) 42 | unless params.is_a?(Hash) 43 | raise ArgumentError.new("Must pass Hash, not #{params.inspect}") 44 | end 45 | _has_file?(params) 46 | end 47 | 48 | def _has_file?(obj) 49 | case obj 50 | when Hash, ParamsArray 51 | obj.any? {|_, v| _has_file?(v) } 52 | when Array 53 | obj.any? {|v| _has_file?(v) } 54 | else 55 | obj.respond_to?(:path) && obj.respond_to?(:read) 56 | end 57 | end 58 | 59 | class Base 60 | def initialize(params) 61 | build_stream(params) 62 | end 63 | 64 | def build_stream(params) 65 | @stream = StringIO.new(params) 66 | @stream.seek(0) 67 | end 68 | 69 | def read(*args) 70 | @stream.read(*args) 71 | end 72 | 73 | def to_s 74 | result = read 75 | @stream.seek(0) 76 | result 77 | end 78 | 79 | def headers 80 | {'Content-Length' => size.to_s} 81 | end 82 | 83 | def size 84 | @stream.size 85 | end 86 | 87 | alias :length :size 88 | 89 | def close 90 | @stream.close unless @stream.closed? 91 | end 92 | 93 | def closed? 94 | @stream.closed? 95 | end 96 | 97 | def to_s_inspect 98 | to_s.inspect 99 | end 100 | 101 | def short_inspect 102 | if size && size > 500 103 | "#{size} byte(s) length" 104 | else 105 | to_s_inspect 106 | end 107 | end 108 | 109 | end 110 | 111 | class Streamed < Base 112 | def build_stream(params = nil) 113 | @stream = params 114 | end 115 | 116 | def size 117 | if @stream.respond_to?(:size) 118 | @stream.size 119 | elsif @stream.is_a?(IO) 120 | @stream.stat.size 121 | end 122 | end 123 | 124 | # TODO (breaks compatibility): ought to use mime_for() to autodetect the 125 | # Content-Type for stream objects that have a filename. 126 | 127 | alias :length :size 128 | end 129 | 130 | class UrlEncoded < Base 131 | def build_stream(params = nil) 132 | @stream = StringIO.new(Utils.encode_query_string(params)) 133 | @stream.seek(0) 134 | end 135 | 136 | def headers 137 | super.merge({'Content-Type' => 'application/x-www-form-urlencoded'}) 138 | end 139 | end 140 | 141 | class Multipart < Base 142 | EOL = "\r\n" 143 | 144 | def build_stream(params) 145 | b = '--' + boundary 146 | 147 | @stream = Tempfile.new('rest-client.multipart.') 148 | @stream.binmode 149 | @stream.write(b + EOL) 150 | 151 | case params 152 | when Hash, ParamsArray 153 | x = Utils.flatten_params(params) 154 | else 155 | x = params 156 | end 157 | 158 | last_index = x.length - 1 159 | x.each_with_index do |a, index| 160 | k, v = * a 161 | if v.respond_to?(:read) && v.respond_to?(:path) 162 | create_file_field(@stream, k, v) 163 | else 164 | create_regular_field(@stream, k, v) 165 | end 166 | @stream.write(EOL + b) 167 | @stream.write(EOL) unless last_index == index 168 | end 169 | @stream.write('--') 170 | @stream.write(EOL) 171 | @stream.seek(0) 172 | end 173 | 174 | def create_regular_field(s, k, v) 175 | s.write("Content-Disposition: form-data; name=\"#{k}\"") 176 | s.write(EOL) 177 | s.write(EOL) 178 | s.write(v) 179 | end 180 | 181 | def create_file_field(s, k, v) 182 | begin 183 | s.write("Content-Disposition: form-data;") 184 | s.write(" name=\"#{k}\";") unless (k.nil? || k=='') 185 | s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}") 186 | s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}") 187 | s.write(EOL) 188 | while (data = v.read(8124)) 189 | s.write(data) 190 | end 191 | ensure 192 | v.close if v.respond_to?(:close) 193 | end 194 | end 195 | 196 | def mime_for(path) 197 | mime = MIME::Types.type_for path 198 | mime.empty? ? 'text/plain' : mime[0].content_type 199 | end 200 | 201 | def boundary 202 | return @boundary if defined?(@boundary) && @boundary 203 | 204 | # Use the same algorithm used by WebKit: generate 16 random 205 | # alphanumeric characters, replacing `+` `/` with `A` `B` (included in 206 | # the list twice) to round out the set of 64. 207 | s = SecureRandom.base64(12) 208 | s.tr!('+/', 'AB') 209 | 210 | @boundary = '----RubyFormBoundary' + s 211 | end 212 | 213 | # for Multipart do not escape the keys 214 | # 215 | # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in 216 | # practice no major browser that I'm aware of uses percent encoding. 217 | # 218 | # Further discussion of multipart encoding: 219 | # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930 220 | # 221 | def handle_key key 222 | key 223 | end 224 | 225 | def headers 226 | super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}}) 227 | end 228 | 229 | def close 230 | @stream.close! 231 | end 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /lib/restclient/platform.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | module RestClient 4 | module Platform 5 | # Return true if we are running on a darwin-based Ruby platform. This will 6 | # be false for jruby even on OS X. 7 | # 8 | # @return [Boolean] 9 | def self.mac_mri? 10 | RUBY_PLATFORM.include?('darwin') 11 | end 12 | 13 | # Return true if we are running on Windows. 14 | # 15 | # @return [Boolean] 16 | # 17 | def self.windows? 18 | # Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard 19 | # library uses that to test what platform it's on. 20 | !!File::ALT_SEPARATOR 21 | end 22 | 23 | # Return true if we are running on jruby. 24 | # 25 | # @return [Boolean] 26 | # 27 | def self.jruby? 28 | # defined on mri >= 1.9 29 | RUBY_ENGINE == 'jruby' 30 | end 31 | 32 | def self.architecture 33 | "#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}" 34 | end 35 | 36 | def self.ruby_agent_version 37 | case RUBY_ENGINE 38 | when 'jruby' 39 | "jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})" 40 | else 41 | "#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}" 42 | end 43 | end 44 | 45 | def self.default_user_agent 46 | "rest-client/#{VERSION} (#{architecture}) #{ruby_agent_version}" 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/restclient/raw_response.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | # The response from RestClient on a raw request looks like a string, but is 3 | # actually one of these. 99% of the time you're making a rest call all you 4 | # care about is the body, but on the occasion you want to fetch the 5 | # headers you can: 6 | # 7 | # RestClient.get('http://example.com').headers[:content_type] 8 | # 9 | # In addition, if you do not use the response as a string, you can access 10 | # a Tempfile object at res.file, which contains the path to the raw 11 | # downloaded request body. 12 | class RawResponse 13 | 14 | include AbstractResponse 15 | 16 | attr_reader :file, :request, :start_time, :end_time 17 | 18 | def inspect 19 | "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>" 20 | end 21 | 22 | # @param [Tempfile] tempfile The temporary file containing the body 23 | # @param [Net::HTTPResponse] net_http_res 24 | # @param [RestClient::Request] request 25 | # @param [Time] start_time 26 | def initialize(tempfile, net_http_res, request, start_time=nil) 27 | @file = tempfile 28 | 29 | # reopen the tempfile so we can read it 30 | @file.open 31 | 32 | response_set_vars(net_http_res, request, start_time) 33 | end 34 | 35 | def to_s 36 | body 37 | end 38 | 39 | def body 40 | @file.rewind 41 | @file.read 42 | end 43 | 44 | def size 45 | file.size 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/restclient/request.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'cgi' 3 | require 'netrc' 4 | require 'set' 5 | 6 | begin 7 | # Use mime/types/columnar if available, for reduced memory usage 8 | require 'mime/types/columnar' 9 | rescue LoadError 10 | require 'mime/types' 11 | end 12 | 13 | module RestClient 14 | # This class is used internally by RestClient to send the request, but you can also 15 | # call it directly if you'd like to use a method not supported by the 16 | # main API. For example: 17 | # 18 | # RestClient::Request.execute(:method => :head, :url => 'http://example.com') 19 | # 20 | # Mandatory parameters: 21 | # * :method 22 | # * :url 23 | # Optional parameters (have a look at ssl and/or uri for some explanations): 24 | # * :headers a hash containing the request headers 25 | # * :cookies may be a Hash{String/Symbol => String} of cookie values, an 26 | # Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These 27 | # will be added to a cookie jar before the request is sent. 28 | # * :user and :password for basic auth, will be replaced by a user/password available in the :url 29 | # * :block_response call the provided block with the HTTPResponse as parameter 30 | # * :raw_response return a low-level RawResponse instead of a Response 31 | # * :log Set the log for this request only, overriding RestClient.log, if 32 | # any. 33 | # * :stream_log_percent (Only relevant with :raw_response => true) Customize 34 | # the interval at which download progress is logged. Defaults to every 35 | # 10% complete. 36 | # * :max_redirects maximum number of redirections (default to 10) 37 | # * :proxy An HTTP proxy URI to use for this request. Any value here 38 | # (including nil) will override RestClient.proxy. 39 | # * :verify_ssl enable ssl verification, possible values are constants from 40 | # OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER 41 | # * :read_timeout and :open_timeout are how long to wait for a response and 42 | # to open a connection, in seconds. Pass nil to disable the timeout. 43 | # * :timeout can be used to set both timeouts 44 | # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path, 45 | # :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings 46 | # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection 47 | # * :ssl_ciphers sets SSL ciphers for the connection. See 48 | # OpenSSL::SSL::SSLContext#ciphers= 49 | # * :before_execution_proc a Proc to call before executing the request. This 50 | # proc, like procs from RestClient.before_execution_procs, will be 51 | # called with the HTTP request and request params. 52 | class Request 53 | 54 | attr_reader :method, :uri, :url, :headers, :payload, :proxy, 55 | :user, :password, :read_timeout, :max_redirects, 56 | :open_timeout, :raw_response, :processed_headers, :args, 57 | :ssl_opts 58 | 59 | # An array of previous redirection responses 60 | attr_accessor :redirection_history 61 | 62 | def self.execute(args, & block) 63 | new(args).execute(& block) 64 | end 65 | 66 | SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store 67 | version ciphers verify_callback verify_callback_warnings} 68 | 69 | def inspect 70 | "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>" 71 | end 72 | 73 | def initialize args 74 | @method = normalize_method(args[:method]) 75 | @headers = (args[:headers] || {}).dup 76 | if args[:url] 77 | @url = process_url_params(normalize_url(args[:url]), headers) 78 | else 79 | raise ArgumentError, "must pass :url" 80 | end 81 | 82 | @user = @password = nil 83 | parse_url_with_auth!(url) 84 | 85 | # process cookie arguments found in headers or args 86 | @cookie_jar = process_cookie_args!(@uri, @headers, args) 87 | 88 | @payload = Payload.generate(args[:payload]) 89 | 90 | @user = args[:user] if args.include?(:user) 91 | @password = args[:password] if args.include?(:password) 92 | 93 | if args.include?(:timeout) 94 | @read_timeout = args[:timeout] 95 | @open_timeout = args[:timeout] 96 | end 97 | if args.include?(:read_timeout) 98 | @read_timeout = args[:read_timeout] 99 | end 100 | if args.include?(:open_timeout) 101 | @open_timeout = args[:open_timeout] 102 | end 103 | @block_response = args[:block_response] 104 | @raw_response = args[:raw_response] || false 105 | 106 | @stream_log_percent = args[:stream_log_percent] || 10 107 | if @stream_log_percent <= 0 || @stream_log_percent > 100 108 | raise ArgumentError.new( 109 | "Invalid :stream_log_percent #{@stream_log_percent.inspect}") 110 | end 111 | 112 | @proxy = args.fetch(:proxy) if args.include?(:proxy) 113 | 114 | @ssl_opts = {} 115 | 116 | if args.include?(:verify_ssl) 117 | v_ssl = args.fetch(:verify_ssl) 118 | if v_ssl 119 | if v_ssl == true 120 | # interpret :verify_ssl => true as VERIFY_PEER 121 | @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER 122 | else 123 | # otherwise pass through any truthy values 124 | @ssl_opts[:verify_ssl] = v_ssl 125 | end 126 | else 127 | # interpret all falsy :verify_ssl values as VERIFY_NONE 128 | @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE 129 | end 130 | else 131 | # if :verify_ssl was not passed, default to VERIFY_PEER 132 | @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER 133 | end 134 | 135 | SSLOptionList.each do |key| 136 | source_key = ('ssl_' + key).to_sym 137 | if args.has_key?(source_key) 138 | @ssl_opts[key.to_sym] = args.fetch(source_key) 139 | end 140 | end 141 | 142 | # Set some other default SSL options, but only if we have an HTTPS URI. 143 | if use_ssl? 144 | 145 | # If there's no CA file, CA path, or cert store provided, use default 146 | if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store) 147 | @ssl_opts[:cert_store] = self.class.default_ssl_cert_store 148 | end 149 | end 150 | 151 | @log = args[:log] 152 | @max_redirects = args[:max_redirects] || 10 153 | @processed_headers = make_headers headers 154 | @processed_headers_lowercase = Hash[@processed_headers.map {|k, v| [k.downcase, v]}] 155 | @args = args 156 | 157 | @before_execution_proc = args[:before_execution_proc] 158 | end 159 | 160 | def execute & block 161 | # With 2.0.0+, net/http accepts URI objects in requests and handles wrapping 162 | # IPv6 addresses in [] for use in the Host request header. 163 | transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block 164 | ensure 165 | payload.close if payload 166 | end 167 | 168 | # SSL-related options 169 | def verify_ssl 170 | @ssl_opts.fetch(:verify_ssl) 171 | end 172 | SSLOptionList.each do |key| 173 | define_method('ssl_' + key) do 174 | @ssl_opts[key.to_sym] 175 | end 176 | end 177 | 178 | # Return true if the request URI will use HTTPS. 179 | # 180 | # @return [Boolean] 181 | # 182 | def use_ssl? 183 | uri.is_a?(URI::HTTPS) 184 | end 185 | 186 | # Extract the query parameters and append them to the url 187 | # 188 | # Look through the headers hash for a :params option (case-insensitive, 189 | # may be string or symbol). If present and the value is a Hash or 190 | # RestClient::ParamsArray, *delete* the key/value pair from the headers 191 | # hash and encode the value into a query string. Append this query string 192 | # to the URL and return the resulting URL. 193 | # 194 | # @param [String] url 195 | # @param [Hash] headers An options/headers hash to process. Mutation 196 | # warning: the params key may be removed if present! 197 | # 198 | # @return [String] resulting url with query string 199 | # 200 | def process_url_params(url, headers) 201 | url_params = nil 202 | 203 | # find and extract/remove "params" key if the value is a Hash/ParamsArray 204 | headers.delete_if do |key, value| 205 | if key.to_s.downcase == 'params' && 206 | (value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray)) 207 | if url_params 208 | raise ArgumentError.new("Multiple 'params' options passed") 209 | end 210 | url_params = value 211 | true 212 | else 213 | false 214 | end 215 | end 216 | 217 | # build resulting URL with query string 218 | if url_params && !url_params.empty? 219 | query_string = RestClient::Utils.encode_query_string(url_params) 220 | 221 | if url.include?('?') 222 | url + '&' + query_string 223 | else 224 | url + '?' + query_string 225 | end 226 | else 227 | url 228 | end 229 | end 230 | 231 | # Render a hash of key => value pairs for cookies in the Request#cookie_jar 232 | # that are valid for the Request#uri. This will not necessarily include all 233 | # cookies if there are duplicate keys. It's safer to use the cookie_jar 234 | # directly if that's a concern. 235 | # 236 | # @see Request#cookie_jar 237 | # 238 | # @return [Hash] 239 | # 240 | def cookies 241 | hash = {} 242 | 243 | @cookie_jar.cookies(uri).each do |c| 244 | hash[c.name] = c.value 245 | end 246 | 247 | hash 248 | end 249 | 250 | # @return [HTTP::CookieJar] 251 | def cookie_jar 252 | @cookie_jar 253 | end 254 | 255 | # Render a Cookie HTTP request header from the contents of the @cookie_jar, 256 | # or nil if the jar is empty. 257 | # 258 | # @see Request#cookie_jar 259 | # 260 | # @return [String, nil] 261 | # 262 | def make_cookie_header 263 | return nil if cookie_jar.nil? 264 | 265 | arr = cookie_jar.cookies(url) 266 | return nil if arr.empty? 267 | 268 | return HTTP::Cookie.cookie_value(arr) 269 | end 270 | 271 | # Process cookies passed as hash or as HTTP::CookieJar. For backwards 272 | # compatibility, these may be passed as a :cookies option masquerading 273 | # inside the headers hash. To avoid confusion, if :cookies is passed in 274 | # both headers and Request#initialize, raise an error. 275 | # 276 | # :cookies may be a: 277 | # - Hash{String/Symbol => String} 278 | # - Array<HTTP::Cookie> 279 | # - HTTP::CookieJar 280 | # 281 | # Passing as a hash: 282 | # Keys may be symbols or strings. Values must be strings. 283 | # Infer the domain name from the request URI and allow subdomains (as 284 | # though '.example.com' had been set in a Set-Cookie header). Assume a 285 | # path of '/'. 286 | # 287 | # RestClient::Request.new(url: 'http://example.com', method: :get, 288 | # :cookies => {:foo => 'Value', 'bar' => '123'} 289 | # ) 290 | # 291 | # results in cookies as though set from the server by: 292 | # Set-Cookie: foo=Value; Domain=.example.com; Path=/ 293 | # Set-Cookie: bar=123; Domain=.example.com; Path=/ 294 | # 295 | # which yields a client cookie header of: 296 | # Cookie: foo=Value; bar=123 297 | # 298 | # Passing as HTTP::CookieJar, which will be passed through directly: 299 | # 300 | # jar = HTTP::CookieJar.new 301 | # jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com', 302 | # path: '/', for_domain: false)) 303 | # 304 | # RestClient::Request.new(..., :cookies => jar) 305 | # 306 | # @param [URI::HTTP] uri The URI for the request. This will be used to 307 | # infer the domain name for cookies passed as strings in a hash. To avoid 308 | # this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash 309 | # values. 310 | # @param [Hash] headers The headers hash from which to pull the :cookies 311 | # option. MUTATION NOTE: This key will be deleted from the hash if 312 | # present. 313 | # @param [Hash] args The options passed to Request#initialize. This hash 314 | # will be used as another potential source for the :cookies key. 315 | # These args will not be mutated. 316 | # 317 | # @return [HTTP::CookieJar] A cookie jar containing the parsed cookies. 318 | # 319 | def process_cookie_args!(uri, headers, args) 320 | 321 | # Avoid ambiguity in whether options from headers or options from 322 | # Request#initialize should take precedence by raising ArgumentError when 323 | # both are present. Prior versions of rest-client claimed to give 324 | # precedence to init options, but actually gave precedence to headers. 325 | # Avoid that mess by erroring out instead. 326 | if headers[:cookies] && args[:cookies] 327 | raise ArgumentError.new( 328 | "Cannot pass :cookies in Request.new() and in headers hash") 329 | end 330 | 331 | cookies_data = headers.delete(:cookies) || args[:cookies] 332 | 333 | # return copy of cookie jar as is 334 | if cookies_data.is_a?(HTTP::CookieJar) 335 | return cookies_data.dup 336 | end 337 | 338 | # convert cookies hash into a CookieJar 339 | jar = HTTP::CookieJar.new 340 | 341 | (cookies_data || []).each do |key, val| 342 | 343 | # Support for Array<HTTP::Cookie> mode: 344 | # If key is a cookie object, add it to the jar directly and assert that 345 | # there is no separate val. 346 | if key.is_a?(HTTP::Cookie) 347 | if val 348 | raise ArgumentError.new("extra cookie val: #{val.inspect}") 349 | end 350 | 351 | jar.add(key) 352 | next 353 | end 354 | 355 | if key.is_a?(Symbol) 356 | key = key.to_s 357 | end 358 | 359 | # assume implicit domain from the request URI, and set for_domain to 360 | # permit subdomains 361 | jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase, 362 | path: '/', for_domain: true)) 363 | end 364 | 365 | jar 366 | end 367 | 368 | # Generate headers for use by a request. Header keys will be stringified 369 | # using `#stringify_headers` to normalize them as capitalized strings. 370 | # 371 | # The final headers consist of: 372 | # - default headers from #default_headers 373 | # - user_headers provided here 374 | # - headers from the payload object (e.g. Content-Type, Content-Lenth) 375 | # - cookie headers from #make_cookie_header 376 | # 377 | # BUG: stringify_headers does not alter the capitalization of headers that 378 | # are passed as strings, it only normalizes those passed as symbols. This 379 | # behavior will probably remain for a while for compatibility, but it means 380 | # that the warnings that attempt to detect accidental header overrides may 381 | # not always work. 382 | # https://github.com/rest-client/rest-client/issues/599 383 | # 384 | # @param [Hash] user_headers User-provided headers to include 385 | # 386 | # @return [Hash<String, String>] A hash of HTTP headers => values 387 | # 388 | def make_headers(user_headers) 389 | headers = stringify_headers(default_headers).merge(stringify_headers(user_headers)) 390 | 391 | # override headers from the payload (e.g. Content-Type, Content-Length) 392 | if @payload 393 | payload_headers = @payload.headers 394 | 395 | # Warn the user if we override any headers that were previously 396 | # present. This usually indicates that rest-client was passed 397 | # conflicting information, e.g. if it was asked to render a payload as 398 | # x-www-form-urlencoded but a Content-Type application/json was 399 | # also supplied by the user. 400 | payload_headers.each_pair do |key, val| 401 | if headers.include?(key) && headers[key] != val 402 | warn("warning: Overriding #{key.inspect} header " + 403 | "#{headers.fetch(key).inspect} with #{val.inspect} " + 404 | "due to payload") 405 | end 406 | end 407 | 408 | headers.merge!(payload_headers) 409 | end 410 | 411 | # merge in cookies 412 | cookies = make_cookie_header 413 | if cookies && !cookies.empty? 414 | if headers['Cookie'] 415 | warn('warning: overriding "Cookie" header with :cookies option') 416 | end 417 | headers['Cookie'] = cookies 418 | end 419 | 420 | headers 421 | end 422 | 423 | # The proxy URI for this request. If `:proxy` was provided on this request, 424 | # use it over `RestClient.proxy`. 425 | # 426 | # Return false if a proxy was explicitly set and is falsy. 427 | # 428 | # @return [URI, false, nil] 429 | # 430 | def proxy_uri 431 | if defined?(@proxy) 432 | if @proxy 433 | URI.parse(@proxy) 434 | else 435 | false 436 | end 437 | elsif RestClient.proxy_set? 438 | if RestClient.proxy 439 | URI.parse(RestClient.proxy) 440 | else 441 | false 442 | end 443 | else 444 | nil 445 | end 446 | end 447 | 448 | def net_http_object(hostname, port) 449 | p_uri = proxy_uri 450 | 451 | if p_uri.nil? 452 | # no proxy set 453 | Net::HTTP.new(hostname, port) 454 | elsif !p_uri 455 | # proxy explicitly set to none 456 | Net::HTTP.new(hostname, port, nil, nil, nil, nil) 457 | else 458 | Net::HTTP.new(hostname, port, 459 | p_uri.hostname, p_uri.port, p_uri.user, p_uri.password) 460 | 461 | end 462 | end 463 | 464 | def net_http_request_class(method) 465 | Net::HTTP.const_get(method.capitalize, false) 466 | end 467 | 468 | def net_http_do_request(http, req, body=nil, &block) 469 | if body && body.respond_to?(:read) 470 | req.body_stream = body 471 | return http.request(req, nil, &block) 472 | else 473 | return http.request(req, body, &block) 474 | end 475 | end 476 | 477 | # Normalize a URL by adding a protocol if none is present. 478 | # 479 | # If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a 480 | # scheme of 'http' will be added. This mimics the behavior of browsers and 481 | # user agents like cURL. 482 | # 483 | # @param [String] url A URL string. 484 | # 485 | # @return [String] 486 | # 487 | def normalize_url(url) 488 | url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i) 489 | url 490 | end 491 | 492 | # Return a certificate store that can be used to validate certificates with 493 | # the system certificate authorities. This will probably not do anything on 494 | # OS X, which monkey patches OpenSSL in terrible ways to insert its own 495 | # validation. On most *nix platforms, this will add the system certifcates 496 | # using OpenSSL::X509::Store#set_default_paths. On Windows, this will use 497 | # RestClient::Windows::RootCerts to look up the CAs trusted by the system. 498 | # 499 | # @return [OpenSSL::X509::Store] 500 | # 501 | def self.default_ssl_cert_store 502 | cert_store = OpenSSL::X509::Store.new 503 | cert_store.set_default_paths 504 | 505 | # set_default_paths() doesn't do anything on Windows, so look up 506 | # certificates using the win32 API. 507 | if RestClient::Platform.windows? 508 | RestClient::Windows::RootCerts.instance.to_a.uniq.each do |cert| 509 | begin 510 | cert_store.add_cert(cert) 511 | rescue OpenSSL::X509::StoreError => err 512 | # ignore duplicate certs 513 | raise unless err.message == 'cert already in hash table' 514 | end 515 | end 516 | end 517 | 518 | cert_store 519 | end 520 | 521 | def redacted_uri 522 | if uri.password 523 | sanitized_uri = uri.dup 524 | sanitized_uri.password = 'REDACTED' 525 | sanitized_uri 526 | else 527 | uri 528 | end 529 | end 530 | 531 | def redacted_url 532 | redacted_uri.to_s 533 | end 534 | 535 | # Default to the global logger if there's not a request-specific one 536 | def log 537 | @log || RestClient.log 538 | end 539 | 540 | def log_request 541 | return unless log 542 | 543 | out = [] 544 | 545 | out << "RestClient.#{method} #{redacted_url.inspect}" 546 | out << payload.short_inspect if payload 547 | out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ") 548 | log << out.join(', ') + "\n" 549 | end 550 | 551 | # Return a hash of headers whose keys are capitalized strings 552 | # 553 | # BUG: stringify_headers does not fix the capitalization of headers that 554 | # are already Strings. Leaving this behavior as is for now for 555 | # backwards compatibility. 556 | # https://github.com/rest-client/rest-client/issues/599 557 | # 558 | def stringify_headers headers 559 | headers.inject({}) do |result, (key, value)| 560 | if key.is_a? Symbol 561 | key = key.to_s.split(/_/).map(&:capitalize).join('-') 562 | end 563 | if 'CONTENT-TYPE' == key.upcase 564 | result[key] = maybe_convert_extension(value.to_s) 565 | elsif 'ACCEPT' == key.upcase 566 | # Accept can be composed of several comma-separated values 567 | if value.is_a? Array 568 | target_values = value 569 | else 570 | target_values = value.to_s.split ',' 571 | end 572 | result[key] = target_values.map { |ext| 573 | maybe_convert_extension(ext.to_s.strip) 574 | }.join(', ') 575 | else 576 | result[key] = value.to_s 577 | end 578 | result 579 | end 580 | end 581 | 582 | # Default headers set by RestClient. In addition to these headers, servers 583 | # will receive headers set by Net::HTTP, such as Accept-Encoding and Host. 584 | # 585 | # @return [Hash<Symbol, String>] 586 | def default_headers 587 | { 588 | :accept => '*/*', 589 | :user_agent => RestClient::Platform.default_user_agent, 590 | } 591 | end 592 | 593 | private 594 | 595 | # Parse the `@url` string into a URI object and save it as 596 | # `@uri`. Also save any basic auth user or password as @user and @password. 597 | # If no auth info was passed, check for credentials in a Netrc file. 598 | # 599 | # @param [String] url A URL string. 600 | # 601 | # @return [URI] 602 | # 603 | # @raise URI::InvalidURIError on invalid URIs 604 | # 605 | def parse_url_with_auth!(url) 606 | uri = URI.parse(url) 607 | 608 | if uri.hostname.nil? 609 | raise URI::InvalidURIError.new("bad URI(no host provided): #{url}") 610 | end 611 | 612 | @user = CGI.unescape(uri.user) if uri.user 613 | @password = CGI.unescape(uri.password) if uri.password 614 | if !@user && !@password 615 | @user, @password = Netrc.read[uri.hostname] 616 | end 617 | 618 | @uri = uri 619 | end 620 | 621 | def print_verify_callback_warnings 622 | warned = false 623 | if RestClient::Platform.mac_mri? 624 | warn('warning: ssl_verify_callback return code is ignored on OS X') 625 | warned = true 626 | end 627 | if RestClient::Platform.jruby? 628 | warn('warning: SSL verify_callback may not work correctly in jruby') 629 | warn('see https://github.com/jruby/jruby/issues/597') 630 | warned = true 631 | end 632 | warned 633 | end 634 | 635 | # Parse a method and return a normalized string version. 636 | # 637 | # Raise ArgumentError if the method is falsy, but otherwise do no 638 | # validation. 639 | # 640 | # @param method [String, Symbol] 641 | # 642 | # @return [String] 643 | # 644 | # @see net_http_request_class 645 | # 646 | def normalize_method(method) 647 | raise ArgumentError.new('must pass :method') unless method 648 | method.to_s.downcase 649 | end 650 | 651 | def transmit uri, req, payload, & block 652 | 653 | # We set this to true in the net/http block so that we can distinguish 654 | # read_timeout from open_timeout. Now that we only support Ruby 2.0+, 655 | # this is only needed for Timeout exceptions thrown outside of Net::HTTP. 656 | established_connection = false 657 | 658 | setup_credentials req 659 | 660 | net = net_http_object(uri.hostname, uri.port) 661 | net.use_ssl = uri.is_a?(URI::HTTPS) 662 | net.ssl_version = ssl_version if ssl_version 663 | net.ciphers = ssl_ciphers if ssl_ciphers 664 | 665 | net.verify_mode = verify_ssl 666 | 667 | net.cert = ssl_client_cert if ssl_client_cert 668 | net.key = ssl_client_key if ssl_client_key 669 | net.ca_file = ssl_ca_file if ssl_ca_file 670 | net.ca_path = ssl_ca_path if ssl_ca_path 671 | net.cert_store = ssl_cert_store if ssl_cert_store 672 | 673 | # We no longer rely on net.verify_callback for the main SSL verification 674 | # because it's not well supported on all platforms (see comments below). 675 | # But do allow users to set one if they want. 676 | if ssl_verify_callback 677 | net.verify_callback = ssl_verify_callback 678 | 679 | # Hilariously, jruby only calls the callback when cert_store is set to 680 | # something, so make sure to set one. 681 | # https://github.com/jruby/jruby/issues/597 682 | if RestClient::Platform.jruby? 683 | net.cert_store ||= OpenSSL::X509::Store.new 684 | end 685 | 686 | if ssl_verify_callback_warnings != false 687 | if print_verify_callback_warnings 688 | warn('pass :ssl_verify_callback_warnings => false to silence this') 689 | end 690 | end 691 | end 692 | 693 | if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE 694 | warn('WARNING: OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE') 695 | warn('This dangerous monkey patch leaves you open to MITM attacks!') 696 | warn('Try passing :verify_ssl => false instead.') 697 | end 698 | 699 | if defined? @read_timeout 700 | if @read_timeout == -1 701 | warn 'Deprecated: to disable timeouts, please use nil instead of -1' 702 | @read_timeout = nil 703 | end 704 | net.read_timeout = @read_timeout 705 | end 706 | if defined? @open_timeout 707 | if @open_timeout == -1 708 | warn 'Deprecated: to disable timeouts, please use nil instead of -1' 709 | @open_timeout = nil 710 | end 711 | net.open_timeout = @open_timeout 712 | end 713 | 714 | RestClient.before_execution_procs.each do |before_proc| 715 | before_proc.call(req, args) 716 | end 717 | 718 | if @before_execution_proc 719 | @before_execution_proc.call(req, args) 720 | end 721 | 722 | log_request 723 | 724 | start_time = Time.now 725 | tempfile = nil 726 | 727 | net.start do |http| 728 | established_connection = true 729 | 730 | if @block_response 731 | net_http_do_request(http, req, payload, &@block_response) 732 | else 733 | res = net_http_do_request(http, req, payload) { |http_response| 734 | if @raw_response 735 | # fetch body into tempfile 736 | tempfile = fetch_body_to_tempfile(http_response) 737 | else 738 | # fetch body 739 | http_response.read_body 740 | end 741 | http_response 742 | } 743 | process_result(res, start_time, tempfile, &block) 744 | end 745 | end 746 | rescue EOFError 747 | raise RestClient::ServerBrokeConnection 748 | rescue Net::OpenTimeout => err 749 | raise RestClient::Exceptions::OpenTimeout.new(nil, err) 750 | rescue Net::ReadTimeout => err 751 | raise RestClient::Exceptions::ReadTimeout.new(nil, err) 752 | rescue Timeout::Error, Errno::ETIMEDOUT => err 753 | # handling for non-Net::HTTP timeouts 754 | if established_connection 755 | raise RestClient::Exceptions::ReadTimeout.new(nil, err) 756 | else 757 | raise RestClient::Exceptions::OpenTimeout.new(nil, err) 758 | end 759 | 760 | rescue OpenSSL::SSL::SSLError => error 761 | # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just 762 | # pass through OpenSSL::SSL::SSLError directly. 763 | # 764 | # Exceptions in verify_callback are ignored [1], and jruby doesn't support 765 | # it at all [2]. RestClient has to catch OpenSSL::SSL::SSLError and either 766 | # re-throw it as is, or throw SSLCertificateNotVerified based on the 767 | # contents of the message field of the original exception. 768 | # 769 | # The client has to handle OpenSSL::SSL::SSLError exceptions anyway, so 770 | # we shouldn't make them handle both OpenSSL and RestClient exceptions. 771 | # 772 | # [1] https://github.com/ruby/ruby/blob/89e70fe8e7/ext/openssl/ossl.c#L238 773 | # [2] https://github.com/jruby/jruby/issues/597 774 | 775 | if error.message.include?("certificate verify failed") 776 | raise SSLCertificateNotVerified.new(error.message) 777 | else 778 | raise error 779 | end 780 | end 781 | 782 | def setup_credentials(req) 783 | if user && !@processed_headers_lowercase.include?('authorization') 784 | req.basic_auth(user, password) 785 | end 786 | end 787 | 788 | def fetch_body_to_tempfile(http_response) 789 | # Taken from Chef, which as in turn... 790 | # Stolen from http://www.ruby-forum.com/topic/166423 791 | # Kudos to _why! 792 | tf = Tempfile.new('rest-client.') 793 | tf.binmode 794 | 795 | size = 0 796 | total = http_response['Content-Length'].to_i 797 | stream_log_bucket = nil 798 | 799 | http_response.read_body do |chunk| 800 | tf.write chunk 801 | size += chunk.size 802 | if log 803 | if total == 0 804 | log << "streaming %s %s (%d of unknown) [0 Content-Length]\n" % [@method.upcase, @url, size] 805 | else 806 | percent = (size * 100) / total 807 | current_log_bucket, _ = percent.divmod(@stream_log_percent) 808 | if current_log_bucket != stream_log_bucket 809 | stream_log_bucket = current_log_bucket 810 | log << "streaming %s %s %d%% done (%d of %d)\n" % [@method.upcase, @url, (size * 100) / total, size, total] 811 | end 812 | end 813 | end 814 | end 815 | tf.close 816 | tf 817 | end 818 | 819 | # @param res The Net::HTTP response object 820 | # @param start_time [Time] Time of request start 821 | def process_result(res, start_time, tempfile=nil, &block) 822 | if @raw_response 823 | unless tempfile 824 | raise ArgumentError.new('tempfile is required') 825 | end 826 | response = RawResponse.new(tempfile, res, self, start_time) 827 | else 828 | response = Response.create(res.body, res, self, start_time) 829 | end 830 | 831 | response.log_response 832 | 833 | if block_given? 834 | block.call(response, self, res, & block) 835 | else 836 | response.return!(&block) 837 | end 838 | 839 | end 840 | 841 | def parser 842 | URI.const_defined?(:Parser) ? URI::Parser.new : URI 843 | end 844 | 845 | # Given a MIME type or file extension, return either a MIME type or, if 846 | # none is found, the input unchanged. 847 | # 848 | # >> maybe_convert_extension('json') 849 | # => 'application/json' 850 | # 851 | # >> maybe_convert_extension('unknown') 852 | # => 'unknown' 853 | # 854 | # >> maybe_convert_extension('application/xml') 855 | # => 'application/xml' 856 | # 857 | # @param ext [String] 858 | # 859 | # @return [String] 860 | # 861 | def maybe_convert_extension(ext) 862 | unless ext =~ /\A[a-zA-Z0-9_@-]+\z/ 863 | # Don't look up strings unless they look like they could be a file 864 | # extension known to mime-types. 865 | # 866 | # There currently isn't any API public way to look up extensions 867 | # directly out of MIME::Types, but the type_for() method only strips 868 | # off after a period anyway. 869 | return ext 870 | end 871 | 872 | types = MIME::Types.type_for(ext) 873 | if types.empty? 874 | ext 875 | else 876 | types.first.content_type 877 | end 878 | end 879 | end 880 | end 881 | -------------------------------------------------------------------------------- /lib/restclient/resource.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | # A class that can be instantiated for access to a RESTful resource, 3 | # including authentication. 4 | # 5 | # Example: 6 | # 7 | # resource = RestClient::Resource.new('http://some/resource') 8 | # jpg = resource.get(:accept => 'image/jpg') 9 | # 10 | # With HTTP basic authentication: 11 | # 12 | # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password') 13 | # resource.delete 14 | # 15 | # With a timeout (seconds): 16 | # 17 | # RestClient::Resource.new('http://slow', :read_timeout => 10) 18 | # 19 | # With an open timeout (seconds): 20 | # 21 | # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10) 22 | # 23 | # You can also use resources to share common headers. For headers keys, 24 | # symbols are converted to strings. Example: 25 | # 26 | # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 }) 27 | # 28 | # This header will be transported as X-Client-Version (notice the X prefix, 29 | # capitalization and hyphens) 30 | # 31 | # Use the [] syntax to allocate subresources: 32 | # 33 | # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd') 34 | # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain' 35 | # 36 | class Resource 37 | attr_reader :url, :options, :block 38 | 39 | def initialize(url, options={}, backwards_compatibility=nil, &block) 40 | @url = url 41 | @block = block 42 | if options.class == Hash 43 | @options = options 44 | else # compatibility with previous versions 45 | @options = { :user => options, :password => backwards_compatibility } 46 | end 47 | end 48 | 49 | def get(additional_headers={}, &block) 50 | headers = (options[:headers] || {}).merge(additional_headers) 51 | Request.execute(options.merge( 52 | :method => :get, 53 | :url => url, 54 | :headers => headers, 55 | :log => log), &(block || @block)) 56 | end 57 | 58 | def head(additional_headers={}, &block) 59 | headers = (options[:headers] || {}).merge(additional_headers) 60 | Request.execute(options.merge( 61 | :method => :head, 62 | :url => url, 63 | :headers => headers, 64 | :log => log), &(block || @block)) 65 | end 66 | 67 | def post(payload, additional_headers={}, &block) 68 | headers = (options[:headers] || {}).merge(additional_headers) 69 | Request.execute(options.merge( 70 | :method => :post, 71 | :url => url, 72 | :payload => payload, 73 | :headers => headers, 74 | :log => log), &(block || @block)) 75 | end 76 | 77 | def put(payload, additional_headers={}, &block) 78 | headers = (options[:headers] || {}).merge(additional_headers) 79 | Request.execute(options.merge( 80 | :method => :put, 81 | :url => url, 82 | :payload => payload, 83 | :headers => headers, 84 | :log => log), &(block || @block)) 85 | end 86 | 87 | def patch(payload, additional_headers={}, &block) 88 | headers = (options[:headers] || {}).merge(additional_headers) 89 | Request.execute(options.merge( 90 | :method => :patch, 91 | :url => url, 92 | :payload => payload, 93 | :headers => headers, 94 | :log => log), &(block || @block)) 95 | end 96 | 97 | def delete(additional_headers={}, &block) 98 | headers = (options[:headers] || {}).merge(additional_headers) 99 | Request.execute(options.merge( 100 | :method => :delete, 101 | :url => url, 102 | :headers => headers, 103 | :log => log), &(block || @block)) 104 | end 105 | 106 | def to_s 107 | url 108 | end 109 | 110 | def user 111 | options[:user] 112 | end 113 | 114 | def password 115 | options[:password] 116 | end 117 | 118 | def headers 119 | options[:headers] || {} 120 | end 121 | 122 | def read_timeout 123 | options[:read_timeout] 124 | end 125 | 126 | def open_timeout 127 | options[:open_timeout] 128 | end 129 | 130 | def log 131 | options[:log] || RestClient.log 132 | end 133 | 134 | # Construct a subresource, preserving authentication. 135 | # 136 | # Example: 137 | # 138 | # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd') 139 | # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain' 140 | # 141 | # This is especially useful if you wish to define your site in one place and 142 | # call it in multiple locations: 143 | # 144 | # def orders 145 | # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd') 146 | # end 147 | # 148 | # orders.get # GET http://example.com/orders 149 | # orders['1'].get # GET http://example.com/orders/1 150 | # orders['1/items'].delete # DELETE http://example.com/orders/1/items 151 | # 152 | # Nest resources as far as you want: 153 | # 154 | # site = RestClient::Resource.new('http://example.com') 155 | # posts = site['posts'] 156 | # first_post = posts['1'] 157 | # comments = first_post['comments'] 158 | # comments.post 'Hello', :content_type => 'text/plain' 159 | # 160 | def [](suburl, &new_block) 161 | case 162 | when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block) 163 | when block then self.class.new(concat_urls(url, suburl), options, &block) 164 | else self.class.new(concat_urls(url, suburl), options) 165 | end 166 | end 167 | 168 | def concat_urls(url, suburl) # :nodoc: 169 | url = url.to_s 170 | suburl = suburl.to_s 171 | if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/' 172 | url + suburl 173 | else 174 | "#{url}/#{suburl}" 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/restclient/response.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | 3 | # A Response from RestClient, you can access the response body, the code or the headers. 4 | # 5 | class Response < String 6 | 7 | include AbstractResponse 8 | 9 | # Return the HTTP response body. 10 | # 11 | # Future versions of RestClient will deprecate treating response objects 12 | # directly as strings, so it will be necessary to call `.body`. 13 | # 14 | # @return [String] 15 | # 16 | def body 17 | # Benchmarking suggests that "#{self}" is fastest, and that caching the 18 | # body string in an instance variable doesn't make it enough faster to be 19 | # worth the extra memory storage. 20 | String.new(self) 21 | end 22 | 23 | # Convert the HTTP response body to a pure String object. 24 | # 25 | # @return [String] 26 | def to_s 27 | body 28 | end 29 | 30 | # Convert the HTTP response body to a pure String object. 31 | # 32 | # @return [String] 33 | def to_str 34 | body 35 | end 36 | 37 | def inspect 38 | "<RestClient::Response #{code.inspect} #{body_truncated(10).inspect}>" 39 | end 40 | 41 | # Initialize a Response object. Because RestClient::Response is 42 | # (unfortunately) a subclass of String for historical reasons, 43 | # Response.create is the preferred initializer. 44 | # 45 | # @param [String, nil] body The response body from the Net::HTTPResponse 46 | # @param [Net::HTTPResponse] net_http_res 47 | # @param [RestClient::Request] request 48 | # @param [Time] start_time 49 | def self.create(body, net_http_res, request, start_time=nil) 50 | result = self.new(body || '') 51 | 52 | result.response_set_vars(net_http_res, request, start_time) 53 | fix_encoding(result) 54 | 55 | result 56 | end 57 | 58 | # Set the String encoding according to the 'Content-Type: charset' header, 59 | # if possible. 60 | def self.fix_encoding(response) 61 | charset = RestClient::Utils.get_encoding_from_headers(response.headers) 62 | encoding = nil 63 | 64 | begin 65 | encoding = Encoding.find(charset) if charset 66 | rescue ArgumentError 67 | if response.log 68 | response.log << "No such encoding: #{charset.inspect}" 69 | end 70 | end 71 | 72 | return unless encoding 73 | 74 | response.force_encoding(encoding) 75 | 76 | response 77 | end 78 | 79 | private 80 | 81 | def body_truncated(length) 82 | b = body 83 | if b.length > length 84 | b[0..length] + '...' 85 | else 86 | b 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/restclient/utils.rb: -------------------------------------------------------------------------------- 1 | require 'http/accept' 2 | 3 | module RestClient 4 | # Various utility methods 5 | module Utils 6 | 7 | # Return encoding from an HTTP header hash. 8 | # 9 | # We use the RFC 7231 specification and do not impose a default encoding on 10 | # text. This differs from the older RFC 2616 behavior, which specifies 11 | # using ISO-8859-1 for text/* content types without a charset. 12 | # 13 | # Strings will use the default encoding when this method returns nil. This 14 | # default is likely to be UTF-8 for Ruby >= 2.0 15 | # 16 | # @param headers [Hash<Symbol,String>] 17 | # 18 | # @return [String, nil] Return the string encoding or nil if no header is 19 | # found. 20 | # 21 | # @example 22 | # >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'}) 23 | # => "UTF-8" 24 | # 25 | def self.get_encoding_from_headers(headers) 26 | type_header = headers[:content_type] 27 | return nil unless type_header 28 | 29 | # TODO: remove this hack once we drop support for Ruby 2.0 30 | if RUBY_VERSION.start_with?('2.0') 31 | _content_type, params = deprecated_cgi_parse_header(type_header) 32 | 33 | if params.include?('charset') 34 | return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '') 35 | end 36 | 37 | else 38 | 39 | begin 40 | _content_type, params = cgi_parse_header(type_header) 41 | rescue HTTP::Accept::ParseError 42 | return nil 43 | else 44 | params['charset'] 45 | end 46 | end 47 | end 48 | 49 | # Parse a Content-Type like header. 50 | # 51 | # Return the main content-type and a hash of params. 52 | # 53 | # @param [String] line 54 | # @return [Array(String, Hash)] 55 | # 56 | def self.cgi_parse_header(line) 57 | types = HTTP::Accept::MediaTypes.parse(line) 58 | 59 | if types.empty? 60 | raise HTTP::Accept::ParseError.new("Found no types in header line") 61 | end 62 | 63 | [types.first.mime_type, types.first.parameters] 64 | end 65 | 66 | # Parse semi-colon separated, potentially quoted header string iteratively. 67 | # 68 | # @private 69 | # 70 | # @deprecated This method is deprecated and only exists to support Ruby 71 | # 2.0, which is not supported by HTTP::Accept. 72 | # 73 | # @todo remove this method when dropping support for Ruby 2.0 74 | # 75 | def self._cgi_parseparam(s) 76 | return enum_for(__method__, s) unless block_given? 77 | 78 | while s[0] == ';' 79 | s = s[1..-1] 80 | ends = s.index(';') 81 | while ends && ends > 0 \ 82 | && (s[0...ends].count('"') - 83 | s[0...ends].scan('\"').count) % 2 != 0 84 | ends = s.index(';', ends + 1) 85 | end 86 | if ends.nil? 87 | ends = s.length 88 | end 89 | f = s[0...ends] 90 | yield f.strip 91 | s = s[ends..-1] 92 | end 93 | nil 94 | end 95 | 96 | # Parse a Content-Type like header. 97 | # 98 | # Return the main content-type and a hash of options. 99 | # 100 | # This method was ported directly from Python's cgi.parse_header(). It 101 | # probably doesn't read or perform particularly well in ruby. 102 | # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331 103 | # 104 | # @param [String] line 105 | # @return [Array(String, Hash)] 106 | # 107 | # @deprecated This method is deprecated and only exists to support Ruby 108 | # 2.0, which is not supported by HTTP::Accept. 109 | # 110 | # @todo remove this method when dropping support for Ruby 2.0 111 | # 112 | def self.deprecated_cgi_parse_header(line) 113 | parts = _cgi_parseparam(';' + line) 114 | key = parts.next 115 | pdict = {} 116 | 117 | begin 118 | while (p = parts.next) 119 | i = p.index('=') 120 | if i 121 | name = p[0...i].strip.downcase 122 | value = p[i+1..-1].strip 123 | if value.length >= 2 && value[0] == '"' && value[-1] == '"' 124 | value = value[1...-1] 125 | value = value.gsub('\\\\', '\\').gsub('\\"', '"') 126 | end 127 | pdict[name] = value 128 | end 129 | end 130 | rescue StopIteration 131 | end 132 | 133 | [key, pdict] 134 | end 135 | 136 | # Serialize a ruby object into HTTP query string parameters. 137 | # 138 | # There is no standard for doing this, so we choose our own slightly 139 | # idiosyncratic format. The output closely matches the format understood by 140 | # Rails, Rack, and PHP. 141 | # 142 | # If you don't want handling of complex objects and only want to handle 143 | # simple flat hashes, you may want to use `URI.encode_www_form` instead, 144 | # which implements HTML5-compliant URL encoded form data. 145 | # 146 | # @param [Hash,ParamsArray] object The object to serialize 147 | # 148 | # @return [String] A string appropriate for use as an HTTP query string 149 | # 150 | # @see {flatten_params} 151 | # 152 | # @see URI.encode_www_form 153 | # 154 | # @see See also Object#to_query in ActiveSupport 155 | # @see http://php.net/manual/en/function.http-build-query.php 156 | # http_build_query in PHP 157 | # @see See also Rack::Utils.build_nested_query in Rack 158 | # 159 | # Notable differences from the ActiveSupport implementation: 160 | # 161 | # - Empty hash and empty array are treated the same as nil instead of being 162 | # omitted entirely from the output. Rather than disappearing, they will 163 | # appear to be nil instead. 164 | # 165 | # It's most common to pass a Hash as the object to serialize, but you can 166 | # also use a ParamsArray if you want to be able to pass the same key with 167 | # multiple values and not use the rack/rails array convention. 168 | # 169 | # @since 2.0.0 170 | # 171 | # @example Simple hashes 172 | # >> encode_query_string({foo: 123, bar: 456}) 173 | # => 'foo=123&bar=456' 174 | # 175 | # @example Simple arrays 176 | # >> encode_query_string({foo: [1,2,3]}) 177 | # => 'foo[]=1&foo[]=2&foo[]=3' 178 | # 179 | # @example Nested hashes 180 | # >> encode_query_string({outer: {foo: 123, bar: 456}}) 181 | # => 'outer[foo]=123&outer[bar]=456' 182 | # 183 | # @example Deeply nesting 184 | # >> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]}) 185 | # => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3' 186 | # 187 | # @example Null and empty values 188 | # >> encode_query_string({string: '', empty: nil, list: [], hash: {}}) 189 | # => 'string=&empty&list&hash' 190 | # 191 | # @example Nested nulls 192 | # >> encode_query_string({foo: {string: '', empty: nil}}) 193 | # => 'foo[string]=&foo[empty]' 194 | # 195 | # @example Multiple fields with the same name using ParamsArray 196 | # >> encode_query_string(RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]])) 197 | # => 'foo=1&foo=2&foo=3' 198 | # 199 | # @example Nested ParamsArray 200 | # >> encode_query_string({foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])}) 201 | # => 'foo[a]=1&foo[a]=2' 202 | # 203 | # >> encode_query_string(RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]])) 204 | # => 'foo[a]=1&foo[a]=2' 205 | # 206 | def self.encode_query_string(object) 207 | flatten_params(object, true).map {|k, v| v.nil? ? k : "#{k}=#{v}" }.join('&') 208 | end 209 | 210 | # Transform deeply nested param containers into a flat array of [key, 211 | # value] pairs. 212 | # 213 | # @example 214 | # >> flatten_params({key1: {key2: 123}}) 215 | # => [["key1[key2]", 123]] 216 | # 217 | # @example 218 | # >> flatten_params({key1: {key2: 123, arr: [1,2,3]}}) 219 | # => [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]] 220 | # 221 | # @param object [Hash, ParamsArray] The container to flatten 222 | # @param uri_escape [Boolean] Whether to URI escape keys and values 223 | # @param parent_key [String] Should not be passed (used for recursion) 224 | # 225 | def self.flatten_params(object, uri_escape=false, parent_key=nil) 226 | unless object.is_a?(Hash) || object.is_a?(ParamsArray) || 227 | (parent_key && object.is_a?(Array)) 228 | raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect) 229 | end 230 | 231 | # transform empty collections into nil, where possible 232 | if object.empty? && parent_key 233 | return [[parent_key, nil]] 234 | end 235 | 236 | # This is essentially .map(), but we need to do += for nested containers 237 | object.reduce([]) { |result, item| 238 | if object.is_a?(Array) 239 | # item is already the value 240 | k = nil 241 | v = item 242 | else 243 | # item is a key, value pair 244 | k, v = item 245 | k = escape(k.to_s) if uri_escape 246 | end 247 | 248 | processed_key = parent_key ? "#{parent_key}[#{k}]" : k 249 | 250 | case v 251 | when Array, Hash, ParamsArray 252 | result.concat flatten_params(v, uri_escape, processed_key) 253 | else 254 | v = escape(v.to_s) if uri_escape && v 255 | result << [processed_key, v] 256 | end 257 | } 258 | end 259 | 260 | # Encode string for safe transport by URI or form encoding. This uses a CGI 261 | # style escape, which transforms ` ` into `+` and various special 262 | # characters into percent encoded forms. 263 | # 264 | # This calls URI.encode_www_form_component for the implementation. The only 265 | # difference between this and CGI.escape is that it does not escape `*`. 266 | # http://stackoverflow.com/questions/25085992/ 267 | # 268 | # @see URI.encode_www_form_component 269 | # 270 | def self.escape(string) 271 | URI.encode_www_form_component(string) 272 | end 273 | end 274 | end 275 | -------------------------------------------------------------------------------- /lib/restclient/version.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | VERSION_INFO = [2, 1, 0].freeze 3 | VERSION = VERSION_INFO.map(&:to_s).join('.').freeze 4 | 5 | def self.version 6 | VERSION 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/restclient/windows.rb: -------------------------------------------------------------------------------- 1 | module RestClient 2 | module Windows 3 | end 4 | end 5 | 6 | if RestClient::Platform.windows? 7 | require_relative './windows/root_certs' 8 | end 9 | -------------------------------------------------------------------------------- /lib/restclient/windows/root_certs.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'ffi' 3 | 4 | # Adapted from Puppet, Copyright (c) Puppet Labs Inc, 5 | # licensed under the Apache License, Version 2.0. 6 | # 7 | # https://github.com/puppetlabs/puppet/blob/bbe30e0a/lib/puppet/util/windows/root_certs.rb 8 | 9 | # Represents a collection of trusted root certificates. 10 | # 11 | # @api public 12 | class RestClient::Windows::RootCerts 13 | include Enumerable 14 | extend FFI::Library 15 | 16 | typedef :ulong, :dword 17 | typedef :uintptr_t, :handle 18 | 19 | def initialize(roots) 20 | @roots = roots 21 | end 22 | 23 | # Enumerates each root certificate. 24 | # @yieldparam cert [OpenSSL::X509::Certificate] each root certificate 25 | # @api public 26 | def each 27 | @roots.each {|cert| yield cert} 28 | end 29 | 30 | # Returns a new instance. 31 | # @return [RestClient::Windows::RootCerts] object constructed from current root certificates 32 | def self.instance 33 | new(self.load_certs) 34 | end 35 | 36 | # Returns an array of root certificates. 37 | # 38 | # @return [Array<[OpenSSL::X509::Certificate]>] an array of root certificates 39 | # @api private 40 | def self.load_certs 41 | certs = [] 42 | 43 | # This is based on a patch submitted to openssl: 44 | # http://www.mail-archive.com/openssl-dev@openssl.org/msg26958.html 45 | ptr = FFI::Pointer::NULL 46 | store = CertOpenSystemStoreA(nil, "ROOT") 47 | begin 48 | while (ptr = CertEnumCertificatesInStore(store, ptr)) and not ptr.null? 49 | context = CERT_CONTEXT.new(ptr) 50 | cert_buf = context[:pbCertEncoded].read_bytes(context[:cbCertEncoded]) 51 | begin 52 | certs << OpenSSL::X509::Certificate.new(cert_buf) 53 | rescue => detail 54 | warn("Failed to import root certificate: #{detail.inspect}") 55 | end 56 | end 57 | ensure 58 | CertCloseStore(store, 0) 59 | end 60 | 61 | certs 62 | end 63 | 64 | private 65 | 66 | # typedef ULONG_PTR HCRYPTPROV_LEGACY; 67 | # typedef void *HCERTSTORE; 68 | 69 | class CERT_CONTEXT < FFI::Struct 70 | layout( 71 | :dwCertEncodingType, :dword, 72 | :pbCertEncoded, :pointer, 73 | :cbCertEncoded, :dword, 74 | :pCertInfo, :pointer, 75 | :hCertStore, :handle 76 | ) 77 | end 78 | 79 | # HCERTSTORE 80 | # WINAPI 81 | # CertOpenSystemStoreA( 82 | # __in_opt HCRYPTPROV_LEGACY hProv, 83 | # __in LPCSTR szSubsystemProtocol 84 | # ); 85 | ffi_lib :crypt32 86 | attach_function :CertOpenSystemStoreA, [:pointer, :string], :handle 87 | 88 | # PCCERT_CONTEXT 89 | # WINAPI 90 | # CertEnumCertificatesInStore( 91 | # __in HCERTSTORE hCertStore, 92 | # __in_opt PCCERT_CONTEXT pPrevCertContext 93 | # ); 94 | ffi_lib :crypt32 95 | attach_function :CertEnumCertificatesInStore, [:handle, :pointer], :pointer 96 | 97 | # BOOL 98 | # WINAPI 99 | # CertCloseStore( 100 | # __in_opt HCERTSTORE hCertStore, 101 | # __in DWORD dwFlags 102 | # ); 103 | ffi_lib :crypt32 104 | attach_function :CertCloseStore, [:handle, :dword], :bool 105 | end 106 | -------------------------------------------------------------------------------- /rest-client.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path('../lib/restclient/version', __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'rest-client' 7 | s.version = RestClient::VERSION 8 | s.authors = ['REST Client Team'] 9 | s.description = 'A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.' 10 | s.license = 'MIT' 11 | s.email = 'discuss@rest-client.groups.io' 12 | s.executables = ['restclient'] 13 | s.extra_rdoc_files = ['README.md', 'history.md'] 14 | s.files = `git ls-files -z`.split("\0") 15 | s.test_files = `git ls-files -z spec/`.split("\0") 16 | s.homepage = 'https://github.com/rest-client/rest-client' 17 | s.summary = 'Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.' 18 | 19 | s.add_development_dependency('webmock', '~> 2.0') 20 | s.add_development_dependency('rspec', '~> 3.0') 21 | s.add_development_dependency('pry', '~> 0') 22 | s.add_development_dependency('pry-doc', '~> 0') 23 | s.add_development_dependency('rdoc', '>= 2.4.2', '< 6.0') 24 | s.add_development_dependency('rubocop', '~> 0.49') 25 | 26 | s.add_dependency('http-accept', '>= 1.7.0', '< 2.0') 27 | s.add_dependency('http-cookie', '>= 1.0.2', '< 2.0') 28 | s.add_dependency('mime-types', '>= 1.16', '< 4.0') 29 | s.add_dependency('netrc', '~> 0.8') 30 | 31 | s.required_ruby_version = '>= 2.0.0' 32 | end 33 | -------------------------------------------------------------------------------- /rest-client.windows.gemspec: -------------------------------------------------------------------------------- 1 | # 2 | # Gemspec for Windows platforms. We can't put these in the main gemspec because 3 | # it results in bundler platform hell when trying to build the gem. 4 | # 5 | # Set $BUILD_PLATFORM when calling gem build with this gemspec to build for 6 | # Windows platforms like x86-mingw32. 7 | # 8 | s = eval(File.read(File.join(File.dirname(__FILE__), 'rest-client.gemspec'))) 9 | 10 | platform = ENV['BUILD_PLATFORM'] || RUBY_PLATFORM 11 | 12 | case platform 13 | when /(mingw32|mswin32)/ 14 | # ffi is needed for RestClient::Windows::RootCerts 15 | s.add_dependency('ffi', '~> 1.9') 16 | s.platform = platform 17 | end 18 | 19 | s 20 | -------------------------------------------------------------------------------- /spec/ISS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rest-client/rest-client/2c72a2e77e2e87d25ff38feba0cf048d51bd5eca/spec/ISS.jpg -------------------------------------------------------------------------------- /spec/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Helpers 4 | 5 | # @param [Hash] opts A hash of methods, passed directly to the double 6 | # definition. Use this to stub other required methods. 7 | # 8 | # @return double for Net::HTTPResponse 9 | def res_double(opts={}) 10 | instance_double('Net::HTTPResponse', {to_hash: {}, body: 'response body'}.merge(opts)) 11 | end 12 | 13 | # Given a Net::HTTPResponse or double and a Request or double, create a 14 | # RestClient::Response object. 15 | # 16 | # @param net_http_res_double an rspec double for Net::HTTPResponse 17 | # @param request A RestClient::Request or rspec double 18 | # 19 | # @return [RestClient::Response] 20 | # 21 | def response_from_res_double(net_http_res_double, request=nil, duration: 1) 22 | request ||= request_double() 23 | start_time = Time.now - duration 24 | 25 | response = RestClient::Response.create(net_http_res_double.body, net_http_res_double, request, start_time) 26 | 27 | # mock duration to ensure it gets the value we expect 28 | allow(response).to receive(:duration).and_return(duration) 29 | 30 | response 31 | end 32 | 33 | # Redirect stderr to a string for the duration of the passed block. 34 | def fake_stderr 35 | original_stderr = $stderr 36 | $stderr = StringIO.new 37 | yield 38 | $stderr.string 39 | ensure 40 | $stderr = original_stderr 41 | end 42 | 43 | # Create a double for RestClient::Request 44 | def request_double(url: 'http://example.com', method: 'get') 45 | instance_double('RestClient::Request', 46 | url: url, uri: URI.parse(url), method: method, user: nil, password: nil, 47 | cookie_jar: HTTP::CookieJar.new, redirection_history: nil, 48 | args: {url: url, method: method}) 49 | end 50 | 51 | def test_image_path 52 | File.dirname(__FILE__) + "/ISS.jpg" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/integration/_lib.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | -------------------------------------------------------------------------------- /spec/integration/capath_digicert/3513523f.0: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 5 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 6 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 7 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 9 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 10 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 11 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 12 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 13 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 14 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 15 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 16 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 17 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 18 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 19 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 20 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 21 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/integration/capath_digicert/399e7759.0: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 5 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 6 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 7 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 9 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 10 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 11 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 12 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 13 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 14 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 15 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 16 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 17 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 18 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 19 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 20 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 21 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/integration/capath_digicert/README: -------------------------------------------------------------------------------- 1 | The CA path symlinks can be created by c_rehash(1ssl). 2 | 3 | But in order for the tests to work on Windows, they have to be regular files. 4 | You can turn them all into regular files by running this on a GNU system: 5 | 6 | for file in $(find . -type l); do 7 | cp -iv --remove-destination $(readlink -e $file) $file 8 | done 9 | -------------------------------------------------------------------------------- /spec/integration/capath_digicert/digicert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 5 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 6 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 7 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 9 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 10 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 11 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 12 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 13 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 14 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 15 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 16 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 17 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 18 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 19 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 20 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 21 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/integration/capath_verisign/415660c1.0: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG 3 | A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz 4 | cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 5 | MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV 6 | BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt 7 | YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN 8 | ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE 9 | BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is 10 | I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G 11 | CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do 12 | lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc 13 | AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /spec/integration/capath_verisign/7651b327.0: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG 3 | A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz 4 | cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 5 | MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV 6 | BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt 7 | YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN 8 | ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE 9 | BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is 10 | I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G 11 | CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do 12 | lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc 13 | AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /spec/integration/capath_verisign/README: -------------------------------------------------------------------------------- 1 | The CA path symlinks can be created by c_rehash(1ssl). 2 | 3 | But in order for the tests to work on Windows, they have to be regular files. 4 | You can turn them all into regular files by running this on a GNU system: 5 | 6 | for file in $(find . -type l); do 7 | cp -iv --remove-destination $(readlink -e $file) $file 8 | done 9 | -------------------------------------------------------------------------------- /spec/integration/capath_verisign/verisign.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG 3 | A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz 4 | cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 5 | MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV 6 | BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt 7 | YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN 8 | ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE 9 | BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is 10 | I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G 11 | CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do 12 | lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc 13 | AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /spec/integration/certs/digicert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 5 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 6 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 7 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 9 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 10 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 11 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 12 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 13 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 14 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 15 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 16 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 17 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 18 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 19 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 20 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 21 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/integration/certs/verisign.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG 3 | A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz 4 | cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 5 | MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV 6 | BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt 7 | YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN 8 | ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE 9 | BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is 10 | I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G 11 | CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do 12 | lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc 13 | AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /spec/integration/httpbin_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | require 'json' 3 | 4 | require 'zlib' 5 | 6 | describe RestClient::Request do 7 | before(:all) do 8 | WebMock.disable! 9 | end 10 | 11 | after(:all) do 12 | WebMock.enable! 13 | end 14 | 15 | def default_httpbin_url 16 | # add a hack to work around java/jruby bug 17 | # java.lang.RuntimeException: Could not generate DH keypair with backtrace 18 | # Also (2017-04-09) Travis Jruby versions have a broken CA keystore 19 | if ENV['TRAVIS_RUBY_VERSION'] =~ /\Ajruby-/ 20 | 'http://httpbin.org/' 21 | else 22 | 'https://httpbin.org/' 23 | end 24 | end 25 | 26 | def httpbin(suffix='') 27 | url = ENV.fetch('HTTPBIN_URL', default_httpbin_url) 28 | unless url.end_with?('/') 29 | url += '/' 30 | end 31 | 32 | url + suffix 33 | end 34 | 35 | def execute_httpbin(suffix, opts={}) 36 | opts = {url: httpbin(suffix)}.merge(opts) 37 | RestClient::Request.execute(opts) 38 | end 39 | 40 | def execute_httpbin_json(suffix, opts={}) 41 | JSON.parse(execute_httpbin(suffix, opts)) 42 | end 43 | 44 | describe '.execute' do 45 | it 'sends a user agent' do 46 | data = execute_httpbin_json('user-agent', method: :get) 47 | expect(data['user-agent']).to match(/rest-client/) 48 | end 49 | 50 | it 'receives cookies on 302' do 51 | expect { 52 | execute_httpbin('cookies/set?foo=bar', method: :get, max_redirects: 0) 53 | }.to raise_error(RestClient::Found) { |ex| 54 | expect(ex.http_code).to eq 302 55 | expect(ex.response.cookies['foo']).to eq 'bar' 56 | } 57 | end 58 | 59 | it 'passes along cookies through 302' do 60 | data = execute_httpbin_json('cookies/set?foo=bar', method: :get) 61 | expect(data).to have_key('cookies') 62 | expect(data['cookies']['foo']).to eq 'bar' 63 | end 64 | 65 | it 'handles quote wrapped cookies' do 66 | expect { 67 | execute_httpbin('cookies/set?foo=' + CGI.escape('"bar:baz"'), 68 | method: :get, max_redirects: 0) 69 | }.to raise_error(RestClient::Found) { |ex| 70 | expect(ex.http_code).to eq 302 71 | expect(ex.response.cookies['foo']).to eq '"bar:baz"' 72 | } 73 | end 74 | 75 | it 'sends basic auth' do 76 | user = 'user' 77 | pass = 'pass' 78 | 79 | data = execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: pass) 80 | expect(data).to eq({'authenticated' => true, 'user' => user}) 81 | 82 | expect { 83 | execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: 'badpass') 84 | }.to raise_error(RestClient::Unauthorized) { |ex| 85 | expect(ex.http_code).to eq 401 86 | } 87 | end 88 | 89 | it 'handles gzipped/deflated responses' do 90 | [['gzip', 'gzipped'], ['deflate', 'deflated']].each do |encoding, var| 91 | raw = execute_httpbin(encoding, method: :get) 92 | 93 | begin 94 | data = JSON.parse(raw) 95 | rescue StandardError 96 | puts "Failed to parse: " + raw.inspect 97 | raise 98 | end 99 | 100 | expect(data['method']).to eq 'GET' 101 | expect(data.fetch(var)).to be true 102 | end 103 | end 104 | 105 | it 'does not uncompress response when accept-encoding is set' do 106 | # == gzip == 107 | raw = execute_httpbin('gzip', method: :get, headers: {accept_encoding: 'gzip, deflate'}) 108 | 109 | # check for gzip magic number 110 | expect(raw.body).to start_with("\x1F\x8B".b) 111 | 112 | decoded = Zlib::GzipReader.new(StringIO.new(raw.body)).read 113 | parsed = JSON.parse(decoded) 114 | 115 | expect(parsed['method']).to eq 'GET' 116 | expect(parsed.fetch('gzipped')).to be true 117 | 118 | # == delate == 119 | raw = execute_httpbin('deflate', method: :get, headers: {accept_encoding: 'gzip, deflate'}) 120 | 121 | decoded = Zlib::Inflate.new.inflate(raw.body) 122 | parsed = JSON.parse(decoded) 123 | 124 | expect(parsed['method']).to eq 'GET' 125 | expect(parsed.fetch('deflated')).to be true 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/integration/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require_relative '_lib' 3 | require 'base64' 4 | 5 | describe RestClient do 6 | 7 | it "a simple request" do 8 | body = 'abc' 9 | stub_request(:get, "www.example.com").to_return(:body => body, :status => 200) 10 | response = RestClient.get "www.example.com" 11 | expect(response.code).to eq 200 12 | expect(response.body).to eq body 13 | end 14 | 15 | it "a 404" do 16 | body = "Ho hai ! I'm not here !" 17 | stub_request(:get, "www.example.com").to_return(:body => body, :status => 404) 18 | begin 19 | RestClient.get "www.example.com" 20 | raise 21 | rescue RestClient::ResourceNotFound => e 22 | expect(e.http_code).to eq 404 23 | expect(e.response.code).to eq 404 24 | expect(e.response.body).to eq body 25 | expect(e.http_body).to eq body 26 | end 27 | end 28 | 29 | describe 'charset parsing' do 30 | it 'handles utf-8' do 31 | body = "λ".force_encoding('ASCII-8BIT') 32 | stub_request(:get, "www.example.com").to_return( 33 | :body => body, :status => 200, :headers => { 34 | 'Content-Type' => 'text/plain; charset=UTF-8' 35 | }) 36 | response = RestClient.get "www.example.com" 37 | expect(response.encoding).to eq Encoding::UTF_8 38 | expect(response.valid_encoding?).to eq true 39 | end 40 | 41 | it 'handles windows-1252' do 42 | body = "\xff".force_encoding('ASCII-8BIT') 43 | stub_request(:get, "www.example.com").to_return( 44 | :body => body, :status => 200, :headers => { 45 | 'Content-Type' => 'text/plain; charset=windows-1252' 46 | }) 47 | response = RestClient.get "www.example.com" 48 | expect(response.encoding).to eq Encoding::WINDOWS_1252 49 | expect(response.encode('utf-8')).to eq "ÿ" 50 | expect(response.valid_encoding?).to eq true 51 | end 52 | 53 | it 'handles binary' do 54 | body = "\xfe".force_encoding('ASCII-8BIT') 55 | stub_request(:get, "www.example.com").to_return( 56 | :body => body, :status => 200, :headers => { 57 | 'Content-Type' => 'application/octet-stream; charset=binary' 58 | }) 59 | response = RestClient.get "www.example.com" 60 | expect(response.encoding).to eq Encoding::BINARY 61 | expect { 62 | response.encode('utf-8') 63 | }.to raise_error(Encoding::UndefinedConversionError) 64 | expect(response.valid_encoding?).to eq true 65 | end 66 | 67 | it 'handles euc-jp' do 68 | body = "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA". 69 | force_encoding(Encoding::BINARY) 70 | body_utf8 = 'あいうえお' 71 | expect(body_utf8.encoding).to eq Encoding::UTF_8 72 | 73 | stub_request(:get, 'www.example.com').to_return( 74 | :body => body, :status => 200, :headers => { 75 | 'Content-Type' => 'text/plain; charset=EUC-JP' 76 | }) 77 | response = RestClient.get 'www.example.com' 78 | expect(response.encoding).to eq Encoding::EUC_JP 79 | expect(response.valid_encoding?).to eq true 80 | expect(response.length).to eq 5 81 | expect(response.encode('utf-8')).to eq body_utf8 82 | end 83 | 84 | it 'defaults to the default encoding' do 85 | stub_request(:get, 'www.example.com').to_return( 86 | body: 'abc', status: 200, headers: { 87 | 'Content-Type' => 'text/plain' 88 | }) 89 | 90 | response = RestClient.get 'www.example.com' 91 | # expect(response.encoding).to eq Encoding.default_external 92 | expect(response.encoding).to eq Encoding::UTF_8 93 | end 94 | 95 | it 'handles invalid encoding' do 96 | stub_request(:get, 'www.example.com').to_return( 97 | body: 'abc', status: 200, headers: { 98 | 'Content-Type' => 'text; charset=plain' 99 | }) 100 | 101 | response = RestClient.get 'www.example.com' 102 | # expect(response.encoding).to eq Encoding.default_external 103 | expect(response.encoding).to eq Encoding::UTF_8 104 | end 105 | 106 | it 'leaves images as binary' do 107 | gif = Base64.strict_decode64('R0lGODlhAQABAAAAADs=') 108 | 109 | stub_request(:get, 'www.example.com').to_return( 110 | body: gif, status: 200, headers: { 111 | 'Content-Type' => 'image/gif' 112 | }) 113 | 114 | response = RestClient.get 'www.example.com' 115 | expect(response.encoding).to eq Encoding::BINARY 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/integration/request_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Request do 4 | before(:all) do 5 | WebMock.disable! 6 | end 7 | 8 | after(:all) do 9 | WebMock.enable! 10 | end 11 | 12 | describe "ssl verification" do 13 | it "is successful with the correct ca_file" do 14 | request = RestClient::Request.new( 15 | :method => :get, 16 | :url => 'https://www.mozilla.org', 17 | :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "digicert.crt") 18 | ) 19 | expect { request.execute }.to_not raise_error 20 | end 21 | 22 | it "is successful with the correct ca_path" do 23 | request = RestClient::Request.new( 24 | :method => :get, 25 | :url => 'https://www.mozilla.org', 26 | :ssl_ca_path => File.join(File.dirname(__FILE__), "capath_digicert") 27 | ) 28 | expect { request.execute }.to_not raise_error 29 | end 30 | 31 | # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just 32 | # pass through OpenSSL::SSL::SSLError directly. See note in 33 | # lib/restclient/request.rb. 34 | # 35 | # On OS X, this test fails since Apple has patched OpenSSL to always fall 36 | # back on the system CA store. 37 | it "is unsuccessful with an incorrect ca_file", :unless => RestClient::Platform.mac_mri? do 38 | request = RestClient::Request.new( 39 | :method => :get, 40 | :url => 'https://www.mozilla.org', 41 | :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "verisign.crt") 42 | ) 43 | expect { request.execute }.to raise_error(RestClient::SSLCertificateNotVerified) 44 | end 45 | 46 | # On OS X, this test fails since Apple has patched OpenSSL to always fall 47 | # back on the system CA store. 48 | it "is unsuccessful with an incorrect ca_path", :unless => RestClient::Platform.mac_mri? do 49 | request = RestClient::Request.new( 50 | :method => :get, 51 | :url => 'https://www.mozilla.org', 52 | :ssl_ca_path => File.join(File.dirname(__FILE__), "capath_verisign") 53 | ) 54 | expect { request.execute }.to raise_error(RestClient::SSLCertificateNotVerified) 55 | end 56 | 57 | it "is successful using the default system cert store" do 58 | request = RestClient::Request.new( 59 | :method => :get, 60 | :url => 'https://www.mozilla.org', 61 | :verify_ssl => true, 62 | ) 63 | expect {request.execute }.to_not raise_error 64 | end 65 | 66 | it "executes the verify_callback" do 67 | ran_callback = false 68 | request = RestClient::Request.new( 69 | :method => :get, 70 | :url => 'https://www.mozilla.org', 71 | :verify_ssl => true, 72 | :ssl_verify_callback => lambda { |preverify_ok, store_ctx| 73 | ran_callback = true 74 | preverify_ok 75 | }, 76 | ) 77 | expect {request.execute }.to_not raise_error 78 | expect(ran_callback).to eq(true) 79 | end 80 | 81 | it "fails verification when the callback returns false", 82 | :unless => RestClient::Platform.mac_mri? do 83 | request = RestClient::Request.new( 84 | :method => :get, 85 | :url => 'https://www.mozilla.org', 86 | :verify_ssl => true, 87 | :ssl_verify_callback => lambda { |preverify_ok, store_ctx| false }, 88 | ) 89 | expect { request.execute }.to raise_error(RestClient::SSLCertificateNotVerified) 90 | end 91 | 92 | it "succeeds verification when the callback returns true", 93 | :unless => RestClient::Platform.mac_mri? do 94 | request = RestClient::Request.new( 95 | :method => :get, 96 | :url => 'https://www.mozilla.org', 97 | :verify_ssl => true, 98 | :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "verisign.crt"), 99 | :ssl_verify_callback => lambda { |preverify_ok, store_ctx| true }, 100 | ) 101 | expect { request.execute }.to_not raise_error 102 | end 103 | end 104 | 105 | describe "timeouts" do 106 | it "raises OpenTimeout when it hits an open timeout" do 107 | request = RestClient::Request.new( 108 | :method => :get, 109 | :url => 'http://www.mozilla.org', 110 | :open_timeout => 1e-10, 111 | ) 112 | expect { request.execute }.to( 113 | raise_error(RestClient::Exceptions::OpenTimeout)) 114 | end 115 | 116 | it "raises ReadTimeout when it hits a read timeout via :read_timeout" do 117 | request = RestClient::Request.new( 118 | :method => :get, 119 | :url => 'https://www.mozilla.org', 120 | :read_timeout => 1e-10, 121 | ) 122 | expect { request.execute }.to( 123 | raise_error(RestClient::Exceptions::ReadTimeout)) 124 | end 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'webmock/rspec' 2 | require 'rest-client' 3 | 4 | require_relative './helpers' 5 | 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | RSpec.configure do |config| 8 | config.raise_errors_for_deprecations! 9 | 10 | # Run specs in random order to surface order dependencies. If you find an 11 | # order dependency and want to debug it, you can fix the order by providing 12 | # the seed, which is printed after each run. 13 | # --seed 1234 14 | config.order = 'random' 15 | 16 | # always run with ruby warnings enabled 17 | # TODO: figure out why this is so obscenely noisy (rspec bug?) 18 | # config.warnings = true 19 | 20 | # add helpers 21 | config.include Helpers, :include_helpers 22 | 23 | config.mock_with :rspec do |mocks| 24 | mocks.yield_receiver_to_any_instance_implementation_blocks = true 25 | end 26 | end 27 | 28 | # always run with ruby warnings enabled (see above) 29 | $VERBOSE = true 30 | -------------------------------------------------------------------------------- /spec/unit/_lib.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | -------------------------------------------------------------------------------- /spec/unit/abstract_response_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::AbstractResponse, :include_helpers do 4 | 5 | # Sample class implementing AbstractResponse used for testing. 6 | class MyAbstractResponse 7 | 8 | include RestClient::AbstractResponse 9 | 10 | attr_accessor :size 11 | 12 | def initialize(net_http_res, request) 13 | response_set_vars(net_http_res, request, Time.now - 1) 14 | end 15 | 16 | end 17 | 18 | before do 19 | @net_http_res = res_double() 20 | @request = request_double(url: 'http://example.com', method: 'get') 21 | @response = MyAbstractResponse.new(@net_http_res, @request) 22 | end 23 | 24 | it "fetches the numeric response code" do 25 | expect(@net_http_res).to receive(:code).and_return('200') 26 | expect(@response.code).to eq 200 27 | end 28 | 29 | it "has a nice description" do 30 | expect(@net_http_res).to receive(:to_hash).and_return({'Content-Type' => ['application/pdf']}) 31 | expect(@net_http_res).to receive(:code).and_return('200') 32 | expect(@response.description).to eq "200 OK | application/pdf bytes\n" 33 | end 34 | 35 | describe '.beautify_headers' do 36 | it "beautifies the headers by turning the keys to symbols" do 37 | h = RestClient::AbstractResponse.beautify_headers('content-type' => [ 'x' ]) 38 | expect(h.keys.first).to eq :content_type 39 | end 40 | 41 | it "beautifies the headers by turning the values to strings instead of one-element arrays" do 42 | h = RestClient::AbstractResponse.beautify_headers('x' => [ 'text/html' ] ) 43 | expect(h.values.first).to eq 'text/html' 44 | end 45 | 46 | it 'joins multiple header values by comma' do 47 | expect(RestClient::AbstractResponse.beautify_headers( 48 | {'My-Header' => ['one', 'two']} 49 | )).to eq({:my_header => 'one, two'}) 50 | end 51 | 52 | it 'leaves set-cookie headers as array' do 53 | expect(RestClient::AbstractResponse.beautify_headers( 54 | {'Set-Cookie' => ['cookie1=foo', 'cookie2=bar']} 55 | )).to eq({:set_cookie => ['cookie1=foo', 'cookie2=bar']}) 56 | end 57 | end 58 | 59 | it "fetches the headers" do 60 | expect(@net_http_res).to receive(:to_hash).and_return('content-type' => [ 'text/html' ]) 61 | expect(@response.headers).to eq({ :content_type => 'text/html' }) 62 | end 63 | 64 | it "extracts cookies from response headers" do 65 | expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) 66 | expect(@response.cookies).to eq({ 'session_id' => '1' }) 67 | end 68 | 69 | it "extract strange cookies" do 70 | expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']) 71 | expect(@response.headers).to eq({:set_cookie => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']}) 72 | expect(@response.cookies).to eq({ 'session_id' => 'ZJ/HQVH6YE+rVkTpn0zvTQ==' }) 73 | end 74 | 75 | it "doesn't escape cookies" do 76 | expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca; path=/']) 77 | expect(@response.cookies).to eq({ 'session_id' => 'BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca' }) 78 | end 79 | 80 | describe '.cookie_jar' do 81 | it 'extracts cookies into cookie jar' do 82 | expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) 83 | expect(@response.cookie_jar).to be_a HTTP::CookieJar 84 | 85 | cookie = @response.cookie_jar.cookies.first 86 | expect(cookie.domain).to eq 'example.com' 87 | expect(cookie.name).to eq 'session_id' 88 | expect(cookie.value).to eq '1' 89 | expect(cookie.path).to eq '/' 90 | end 91 | 92 | it 'handles cookies when URI scheme is implicit' do 93 | net_http_res = double('net http response') 94 | expect(net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) 95 | request = double('request', url: 'example.com', uri: URI.parse('http://example.com'), 96 | method: 'get', cookie_jar: HTTP::CookieJar.new, redirection_history: nil) 97 | response = MyAbstractResponse.new(net_http_res, request) 98 | expect(response.cookie_jar).to be_a HTTP::CookieJar 99 | 100 | cookie = response.cookie_jar.cookies.first 101 | expect(cookie.domain).to eq 'example.com' 102 | expect(cookie.name).to eq 'session_id' 103 | expect(cookie.value).to eq '1' 104 | expect(cookie.path).to eq '/' 105 | end 106 | end 107 | 108 | it "can access the net http result directly" do 109 | expect(@response.net_http_res).to eq @net_http_res 110 | end 111 | 112 | describe "#return!" do 113 | it "should return the response itself on 200-codes" do 114 | expect(@net_http_res).to receive(:code).and_return('200') 115 | expect(@response.return!).to be_equal(@response) 116 | end 117 | 118 | it "should raise RequestFailed on unknown codes" do 119 | expect(@net_http_res).to receive(:code).and_return('1000') 120 | expect { @response.return! }.to raise_error RestClient::RequestFailed 121 | end 122 | 123 | it "should raise an error on a redirection after non-GET/HEAD requests" do 124 | expect(@net_http_res).to receive(:code).and_return('301') 125 | expect(@request).to receive(:method).and_return('put') 126 | expect(@response).not_to receive(:follow_redirection) 127 | expect { @response.return! }.to raise_error RestClient::RequestFailed 128 | end 129 | 130 | it "should follow 302 redirect" do 131 | expect(@net_http_res).to receive(:code).and_return('302') 132 | expect(@response).to receive(:check_max_redirects).and_return('fake-check') 133 | expect(@response).to receive(:follow_redirection).and_return('fake-redirection') 134 | expect(@response.return!).to eq 'fake-redirection' 135 | end 136 | 137 | it "should gracefully handle 302 redirect with no location header" do 138 | @net_http_res = res_double(code: 302) 139 | @request = request_double() 140 | @response = MyAbstractResponse.new(@net_http_res, @request) 141 | expect(@response).to receive(:check_max_redirects).and_return('fake-check') 142 | expect { @response.return! }.to raise_error RestClient::Found 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/unit/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Exception do 4 | it "returns a 'message' equal to the class name if the message is not set, because 'message' should not be nil" do 5 | e = RestClient::Exception.new 6 | expect(e.message).to eq "RestClient::Exception" 7 | end 8 | 9 | it "returns the 'message' that was set" do 10 | e = RestClient::Exception.new 11 | message = "An explicitly set message" 12 | e.message = message 13 | expect(e.message).to eq message 14 | end 15 | 16 | it "sets the exception message to ErrorMessage" do 17 | expect(RestClient::ResourceNotFound.new.message).to eq 'Not Found' 18 | end 19 | 20 | it "contains exceptions in RestClient" do 21 | expect(RestClient::Unauthorized.new).to be_a_kind_of(RestClient::Exception) 22 | expect(RestClient::ServerBrokeConnection.new).to be_a_kind_of(RestClient::Exception) 23 | end 24 | end 25 | 26 | describe RestClient::ServerBrokeConnection do 27 | it "should have a default message of 'Server broke connection'" do 28 | e = RestClient::ServerBrokeConnection.new 29 | expect(e.message).to eq 'Server broke connection' 30 | end 31 | end 32 | 33 | describe RestClient::RequestFailed do 34 | before do 35 | @response = double('HTTP Response', :code => '502') 36 | end 37 | 38 | it "stores the http response on the exception" do 39 | response = "response" 40 | begin 41 | raise RestClient::RequestFailed, response 42 | rescue RestClient::RequestFailed => e 43 | expect(e.response).to eq response 44 | end 45 | end 46 | 47 | it "http_code convenience method for fetching the code as an integer" do 48 | expect(RestClient::RequestFailed.new(@response).http_code).to eq 502 49 | end 50 | 51 | it "http_body convenience method for fetching the body (decoding when necessary)" do 52 | expect(RestClient::RequestFailed.new(@response).http_code).to eq 502 53 | expect(RestClient::RequestFailed.new(@response).message).to eq 'HTTP status code 502' 54 | end 55 | 56 | it "shows the status code in the message" do 57 | expect(RestClient::RequestFailed.new(@response).to_s).to match(/502/) 58 | end 59 | end 60 | 61 | describe RestClient::ResourceNotFound do 62 | it "also has the http response attached" do 63 | response = "response" 64 | begin 65 | raise RestClient::ResourceNotFound, response 66 | rescue RestClient::ResourceNotFound => e 67 | expect(e.response).to eq response 68 | end 69 | end 70 | 71 | it 'stores the body on the response of the exception' do 72 | body = "body" 73 | stub_request(:get, "www.example.com").to_return(:body => body, :status => 404) 74 | begin 75 | RestClient.get "www.example.com" 76 | raise 77 | rescue RestClient::ResourceNotFound => e 78 | expect(e.response.body).to eq body 79 | end 80 | end 81 | end 82 | 83 | describe "backwards compatibility" do 84 | it 'aliases RestClient::NotFound as ResourceNotFound' do 85 | expect(RestClient::ResourceNotFound).to eq RestClient::NotFound 86 | end 87 | 88 | it 'aliases old names for HTTP 413, 414, 416' do 89 | expect(RestClient::RequestEntityTooLarge).to eq RestClient::PayloadTooLarge 90 | expect(RestClient::RequestURITooLong).to eq RestClient::URITooLong 91 | expect(RestClient::RequestedRangeNotSatisfiable).to eq RestClient::RangeNotSatisfiable 92 | end 93 | 94 | it 'subclasses NotFound from RequestFailed, ExceptionWithResponse' do 95 | expect(RestClient::NotFound).to be < RestClient::RequestFailed 96 | expect(RestClient::NotFound).to be < RestClient::ExceptionWithResponse 97 | end 98 | 99 | it 'subclasses timeout from RestClient::RequestTimeout, RequestFailed, EWR' do 100 | expect(RestClient::Exceptions::OpenTimeout).to be < RestClient::Exceptions::Timeout 101 | expect(RestClient::Exceptions::ReadTimeout).to be < RestClient::Exceptions::Timeout 102 | 103 | expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestTimeout 104 | expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestFailed 105 | expect(RestClient::Exceptions::Timeout).to be < RestClient::ExceptionWithResponse 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /spec/unit/params_array_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::ParamsArray do 4 | 5 | describe '.new' do 6 | it 'accepts various types of containers' do 7 | as_array = [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]] 8 | [ 9 | [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]], 10 | [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}], 11 | [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}], 12 | [{foo: 123}, [:foo, 456], {bar: 789}, {empty: nil}], 13 | [{foo: 123}, [:foo, 456], {bar: 789}, [:empty]], 14 | ].each do |input| 15 | expect(RestClient::ParamsArray.new(input).to_a).to eq as_array 16 | end 17 | 18 | expect(RestClient::ParamsArray.new([]).to_a).to eq [] 19 | expect(RestClient::ParamsArray.new([]).empty?).to eq true 20 | end 21 | 22 | it 'rejects various invalid input' do 23 | expect { 24 | RestClient::ParamsArray.new([[]]) 25 | }.to raise_error(IndexError) 26 | 27 | expect { 28 | RestClient::ParamsArray.new([[1,2,3]]) 29 | }.to raise_error(ArgumentError) 30 | 31 | expect { 32 | RestClient::ParamsArray.new([1,2,3]) 33 | }.to raise_error(NoMethodError) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/payload_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | 3 | require_relative '_lib' 4 | 5 | describe RestClient::Payload, :include_helpers do 6 | context "Base Payload" do 7 | it "should reset stream after to_s" do 8 | payload = RestClient::Payload::Base.new('foobar') 9 | expect(payload.to_s).to eq 'foobar' 10 | expect(payload.to_s).to eq 'foobar' 11 | end 12 | end 13 | 14 | context "A regular Payload" do 15 | it "should use standard enctype as default content-type" do 16 | expect(RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']). 17 | to eq 'application/x-www-form-urlencoded' 18 | end 19 | 20 | it "should form properly encoded params" do 21 | expect(RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s). 22 | to eq "foo=bar" 23 | expect(["foo=bar&baz=qux", "baz=qux&foo=bar"]).to include( 24 | RestClient::Payload::UrlEncoded.new({:foo => 'bar', :baz => 'qux'}).to_s) 25 | end 26 | 27 | it "should escape parameters" do 28 | expect(RestClient::Payload::UrlEncoded.new({'foo + bar' => 'baz'}).to_s). 29 | to eq "foo+%2B+bar=baz" 30 | end 31 | 32 | it "should properly handle hashes as parameter" do 33 | expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz'}}).to_s). 34 | to eq "foo[bar]=baz" 35 | expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => {:baz => 'qux'}}}).to_s). 36 | to eq "foo[bar][baz]=qux" 37 | end 38 | 39 | it "should handle many attributes inside a hash" do 40 | parameters = RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz', :baz => 'qux'}}).to_s 41 | expect(parameters).to eq 'foo[bar]=baz&foo[baz]=qux' 42 | end 43 | 44 | it "should handle attributes inside an array inside an hash" do 45 | parameters = RestClient::Payload::UrlEncoded.new({"foo" => [{"bar" => 'baz'}, {"bar" => 'qux'}]}).to_s 46 | expect(parameters).to eq 'foo[][bar]=baz&foo[][bar]=qux' 47 | end 48 | 49 | it "should handle arrays inside a hash inside a hash" do 50 | parameters = RestClient::Payload::UrlEncoded.new({"foo" => {'even' => [0, 2], 'odd' => [1, 3]}}).to_s 51 | expect(parameters).to eq 'foo[even][]=0&foo[even][]=2&foo[odd][]=1&foo[odd][]=3' 52 | end 53 | 54 | it "should form properly use symbols as parameters" do 55 | expect(RestClient::Payload::UrlEncoded.new({:foo => :bar}).to_s). 56 | to eq "foo=bar" 57 | expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => :baz}}).to_s). 58 | to eq "foo[bar]=baz" 59 | end 60 | 61 | it "should properly handle arrays as repeated parameters" do 62 | expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s). 63 | to eq "foo[]=bar" 64 | expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s). 65 | to eq "foo[]=bar&foo[]=baz" 66 | end 67 | 68 | it 'should not close if stream already closed' do 69 | p = RestClient::Payload::UrlEncoded.new({'foo ' => 'bar'}) 70 | 3.times {p.close} 71 | end 72 | 73 | end 74 | 75 | context "A multipart Payload" do 76 | it "should use standard enctype as default content-type" do 77 | m = RestClient::Payload::Multipart.new({}) 78 | allow(m).to receive(:boundary).and_return(123) 79 | expect(m.headers['Content-Type']).to eq 'multipart/form-data; boundary=123' 80 | end 81 | 82 | it 'should not error on close if stream already closed' do 83 | m = RestClient::Payload::Multipart.new(:file => File.new(test_image_path)) 84 | 3.times {m.close} 85 | end 86 | 87 | it "should form properly separated multipart data" do 88 | m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]]) 89 | expect(m.to_s).to eq <<-EOS 90 | --#{m.boundary}\r 91 | Content-Disposition: form-data; name="bar"\r 92 | \r 93 | baz\r 94 | --#{m.boundary}\r 95 | Content-Disposition: form-data; name="foo"\r 96 | \r 97 | bar\r 98 | --#{m.boundary}--\r 99 | EOS 100 | end 101 | 102 | it "should not escape parameters names" do 103 | m = RestClient::Payload::Multipart.new([["bar ", "baz"]]) 104 | expect(m.to_s).to eq <<-EOS 105 | --#{m.boundary}\r 106 | Content-Disposition: form-data; name="bar "\r 107 | \r 108 | baz\r 109 | --#{m.boundary}--\r 110 | EOS 111 | end 112 | 113 | it "should form properly separated multipart data" do 114 | f = File.new(test_image_path) 115 | m = RestClient::Payload::Multipart.new({:foo => f}) 116 | expect(m.to_s).to eq <<-EOS 117 | --#{m.boundary}\r 118 | Content-Disposition: form-data; name="foo"; filename="ISS.jpg"\r 119 | Content-Type: image/jpeg\r 120 | \r 121 | #{File.open(f.path, 'rb'){|bin| bin.read}}\r 122 | --#{m.boundary}--\r 123 | EOS 124 | end 125 | 126 | it "should ignore the name attribute when it's not set" do 127 | f = File.new(test_image_path) 128 | m = RestClient::Payload::Multipart.new({nil => f}) 129 | expect(m.to_s).to eq <<-EOS 130 | --#{m.boundary}\r 131 | Content-Disposition: form-data; filename="ISS.jpg"\r 132 | Content-Type: image/jpeg\r 133 | \r 134 | #{File.open(f.path, 'rb'){|bin| bin.read}}\r 135 | --#{m.boundary}--\r 136 | EOS 137 | end 138 | 139 | it "should detect optional (original) content type and filename" do 140 | f = File.new(test_image_path) 141 | expect(f).to receive(:content_type).and_return('text/plain') 142 | expect(f).to receive(:original_filename).and_return('foo.txt') 143 | m = RestClient::Payload::Multipart.new({:foo => f}) 144 | expect(m.to_s).to eq <<-EOS 145 | --#{m.boundary}\r 146 | Content-Disposition: form-data; name="foo"; filename="foo.txt"\r 147 | Content-Type: text/plain\r 148 | \r 149 | #{File.open(f.path, 'rb'){|bin| bin.read}}\r 150 | --#{m.boundary}--\r 151 | EOS 152 | end 153 | 154 | it "should handle hash in hash parameters" do 155 | m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}}) 156 | expect(m.to_s).to eq <<-EOS 157 | --#{m.boundary}\r 158 | Content-Disposition: form-data; name="bar[baz]"\r 159 | \r 160 | foo\r 161 | --#{m.boundary}--\r 162 | EOS 163 | 164 | f = File.new(test_image_path) 165 | f.instance_eval "def content_type; 'text/plain'; end" 166 | f.instance_eval "def original_filename; 'foo.txt'; end" 167 | m = RestClient::Payload::Multipart.new({:foo => {:bar => f}}) 168 | expect(m.to_s).to eq <<-EOS 169 | --#{m.boundary}\r 170 | Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r 171 | Content-Type: text/plain\r 172 | \r 173 | #{File.open(f.path, 'rb'){|bin| bin.read}}\r 174 | --#{m.boundary}--\r 175 | EOS 176 | end 177 | 178 | it 'should correctly format hex boundary' do 179 | allow(SecureRandom).to receive(:base64).with(12).and_return('TGs89+ttw/xna6TV') 180 | f = File.new(test_image_path) 181 | m = RestClient::Payload::Multipart.new({:foo => f}) 182 | expect(m.boundary).to eq('-' * 4 + 'RubyFormBoundary' + 'TGs89AttwBxna6TV') 183 | end 184 | 185 | end 186 | 187 | context "streamed payloads" do 188 | it "should properly determine the size of file payloads" do 189 | f = File.new(test_image_path) 190 | payload = RestClient::Payload.generate(f) 191 | expect(payload.size).to eq 72_463 192 | expect(payload.length).to eq 72_463 193 | end 194 | 195 | it "should properly determine the size of other kinds of streaming payloads" do 196 | s = StringIO.new 'foo' 197 | payload = RestClient::Payload.generate(s) 198 | expect(payload.size).to eq 3 199 | expect(payload.length).to eq 3 200 | 201 | begin 202 | f = Tempfile.new "rest-client" 203 | f.write 'foo bar' 204 | 205 | payload = RestClient::Payload.generate(f) 206 | expect(payload.size).to eq 7 207 | expect(payload.length).to eq 7 208 | ensure 209 | f.close 210 | end 211 | end 212 | 213 | it "should have a closed? method" do 214 | f = File.new(test_image_path) 215 | payload = RestClient::Payload.generate(f) 216 | expect(payload.closed?).to be_falsey 217 | payload.close 218 | expect(payload.closed?).to be_truthy 219 | end 220 | end 221 | 222 | context "Payload generation" do 223 | it "should recognize standard urlencoded params" do 224 | expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded) 225 | end 226 | 227 | it "should recognize multipart params" do 228 | f = File.new(test_image_path) 229 | expect(RestClient::Payload.generate({"foo" => f})).to be_kind_of(RestClient::Payload::Multipart) 230 | end 231 | 232 | it "should be multipart if forced" do 233 | expect(RestClient::Payload.generate({"foo" => "bar", :multipart => true})).to be_kind_of(RestClient::Payload::Multipart) 234 | end 235 | 236 | it "should handle deeply nested multipart" do 237 | f = File.new(test_image_path) 238 | params = {foo: RestClient::ParamsArray.new({nested: f})} 239 | expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart) 240 | end 241 | 242 | 243 | it "should return data if no of the above" do 244 | expect(RestClient::Payload.generate("data")).to be_kind_of(RestClient::Payload::Base) 245 | end 246 | 247 | it "should recognize nested multipart payloads in hashes" do 248 | f = File.new(test_image_path) 249 | expect(RestClient::Payload.generate({"foo" => {"file" => f}})).to be_kind_of(RestClient::Payload::Multipart) 250 | end 251 | 252 | it "should recognize nested multipart payloads in arrays" do 253 | f = File.new(test_image_path) 254 | expect(RestClient::Payload.generate({"foo" => [f]})).to be_kind_of(RestClient::Payload::Multipart) 255 | end 256 | 257 | it "should recognize file payloads that can be streamed" do 258 | f = File.new(test_image_path) 259 | expect(RestClient::Payload.generate(f)).to be_kind_of(RestClient::Payload::Streamed) 260 | end 261 | 262 | it "should recognize other payloads that can be streamed" do 263 | expect(RestClient::Payload.generate(StringIO.new('foo'))).to be_kind_of(RestClient::Payload::Streamed) 264 | end 265 | 266 | # hashery gem introduces Hash#read convenience method. Existence of #read method used to determine of content is streameable :/ 267 | it "shouldn't treat hashes as streameable" do 268 | expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded) 269 | end 270 | 271 | it "should recognize multipart payload wrapped in ParamsArray" do 272 | f = File.new(test_image_path) 273 | params = RestClient::ParamsArray.new([[:image, f]]) 274 | expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart) 275 | end 276 | 277 | it "should handle non-multipart payload wrapped in ParamsArray" do 278 | params = RestClient::ParamsArray.new([[:arg, 'value1'], [:arg, 'value2']]) 279 | expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::UrlEncoded) 280 | end 281 | 282 | it "should pass through Payload::Base and subclasses unchanged" do 283 | payloads = [ 284 | RestClient::Payload::Base.new('foobar'), 285 | RestClient::Payload::UrlEncoded.new({:foo => 'bar'}), 286 | RestClient::Payload::Streamed.new(File.new(test_image_path)), 287 | RestClient::Payload::Multipart.new({myfile: File.new(test_image_path)}), 288 | ] 289 | 290 | payloads.each do |payload| 291 | expect(RestClient::Payload.generate(payload)).to equal(payload) 292 | end 293 | end 294 | end 295 | end 296 | -------------------------------------------------------------------------------- /spec/unit/raw_response_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::RawResponse do 4 | before do 5 | @tf = double("Tempfile", :read => "the answer is 42", :open => true, :rewind => true) 6 | @net_http_res = double('net http response') 7 | @request = double('restclient request', :redirection_history => nil) 8 | @response = RestClient::RawResponse.new(@tf, @net_http_res, @request) 9 | end 10 | 11 | it "behaves like string" do 12 | expect(@response.to_s).to eq 'the answer is 42' 13 | end 14 | 15 | it "exposes a Tempfile" do 16 | expect(@response.file).to eq @tf 17 | end 18 | 19 | it "includes AbstractResponse" do 20 | expect(RestClient::RawResponse.ancestors).to include(RestClient::AbstractResponse) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/request2_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Request, :include_helpers do 4 | 5 | context 'params for GET requests' do 6 | it "manage params for get requests" do 7 | stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200) 8 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}).body).to eq 'foo' 9 | 10 | stub_request(:get, 'http://some/resource').with(:headers => {'Accept'=>'*/*', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200) 11 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => :a}).body).to eq 'foo' 12 | end 13 | 14 | it 'adds GET params when params are present in URL' do 15 | stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200) 16 | expect(RestClient::Request.execute(:url => 'http://some/resource?a=b', :method => :get, :headers => {:foo => :bar, :params => {:c => 'd'}}).body).to eq 'foo' 17 | end 18 | 19 | it 'encodes nested GET params' do 20 | stub_request(:get, 'http://some/resource?a[foo][]=1&a[foo][]=2&a[bar]&b=foo+bar&math=2+%2B+2+%3D%3D+4').with(:headers => {'Accept'=>'*/*',}).to_return(:body => 'foo', :status => 200) 21 | expect(RestClient::Request.execute(url: 'http://some/resource', method: :get, headers: { 22 | params: { 23 | a: { 24 | foo: [1,2], 25 | bar: nil, 26 | }, 27 | b: 'foo bar', 28 | math: '2 + 2 == 4', 29 | } 30 | }).body).to eq 'foo' 31 | end 32 | 33 | end 34 | 35 | it "can use a block to process response" do 36 | response_value = nil 37 | block = proc do |http_response| 38 | response_value = http_response.body 39 | end 40 | stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200) 41 | RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}, :block_response => block) 42 | expect(response_value).to eq "foo" 43 | end 44 | 45 | it 'closes payload if not nil' do 46 | test_file = File.new(test_image_path) 47 | 48 | stub_request(:post, 'http://some/resource').with(:headers => {'Accept'=>'*/*'}).to_return(:body => 'foo', :status => 200) 49 | RestClient::Request.execute(:url => 'http://some/resource', :method => :post, :payload => {:file => test_file}) 50 | 51 | expect(test_file.closed?).to be true 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /spec/unit/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Resource do 4 | before do 5 | @resource = RestClient::Resource.new('http://some/resource', :user => 'jane', :password => 'mypass', :headers => {'X-Something' => '1'}) 6 | end 7 | 8 | context "Resource delegation" do 9 | it "GET" do 10 | expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 11 | @resource.get 12 | end 13 | 14 | it "HEAD" do 15 | expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 16 | @resource.head 17 | end 18 | 19 | it "POST" do 20 | expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 21 | @resource.post 'abc', :content_type => 'image/jpg' 22 | end 23 | 24 | it "PUT" do 25 | expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 26 | @resource.put 'abc', :content_type => 'image/jpg' 27 | end 28 | 29 | it "PATCH" do 30 | expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 31 | @resource.patch 'abc', :content_type => 'image/jpg' 32 | end 33 | 34 | it "DELETE" do 35 | expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass', :log => nil) 36 | @resource.delete 37 | end 38 | 39 | it "overrides resource headers" do 40 | expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '2'}, :user => 'jane', :password => 'mypass', :log => nil) 41 | @resource.get 'X-Something' => '2' 42 | end 43 | end 44 | 45 | it "can instantiate with no user/password" do 46 | @resource = RestClient::Resource.new('http://some/resource') 47 | end 48 | 49 | it "is backwards compatible with previous constructor" do 50 | @resource = RestClient::Resource.new('http://some/resource', 'user', 'pass') 51 | expect(@resource.user).to eq 'user' 52 | expect(@resource.password).to eq 'pass' 53 | end 54 | 55 | it "concatenates urls, inserting a slash when it needs one" do 56 | expect(@resource.concat_urls('http://example.com', 'resource')).to eq 'http://example.com/resource' 57 | end 58 | 59 | it "concatenates urls, using no slash if the first url ends with a slash" do 60 | expect(@resource.concat_urls('http://example.com/', 'resource')).to eq 'http://example.com/resource' 61 | end 62 | 63 | it "concatenates urls, using no slash if the second url starts with a slash" do 64 | expect(@resource.concat_urls('http://example.com', '/resource')).to eq 'http://example.com/resource' 65 | end 66 | 67 | it "concatenates even non-string urls, :posts + 1 => 'posts/1'" do 68 | expect(@resource.concat_urls(:posts, 1)).to eq 'posts/1' 69 | end 70 | 71 | it "offers subresources via []" do 72 | parent = RestClient::Resource.new('http://example.com') 73 | expect(parent['posts'].url).to eq 'http://example.com/posts' 74 | end 75 | 76 | it "transports options to subresources" do 77 | parent = RestClient::Resource.new('http://example.com', :user => 'user', :password => 'password') 78 | expect(parent['posts'].user).to eq 'user' 79 | expect(parent['posts'].password).to eq 'password' 80 | end 81 | 82 | it "passes a given block to subresources" do 83 | block = proc {|r| r} 84 | parent = RestClient::Resource.new('http://example.com', &block) 85 | expect(parent['posts'].block).to eq block 86 | end 87 | 88 | it "the block should be overrideable" do 89 | block1 = proc {|r| r} 90 | block2 = proc {|r| } 91 | parent = RestClient::Resource.new('http://example.com', &block1) 92 | # parent['posts', &block2].block.should eq block2 # ruby 1.9 syntax 93 | expect(parent.send(:[], 'posts', &block2).block).to eq block2 94 | expect(parent.send(:[], 'posts', &block2).block).not_to eq block1 95 | end 96 | 97 | # Test fails on jruby 9.1.[0-5].* due to 98 | # https://github.com/jruby/jruby/issues/4217 99 | it "the block should be overrideable in ruby 1.9 syntax", 100 | :unless => (RUBY_ENGINE == 'jruby' && JRUBY_VERSION =~ /\A9\.1\.[0-5]\./) \ 101 | do 102 | block1 = proc {|r| r} 103 | block2 = ->(r) {} 104 | 105 | parent = RestClient::Resource.new('http://example.com', &block1) 106 | expect(parent['posts', &block2].block).to eq block2 107 | expect(parent['posts', &block2].block).not_to eq block1 108 | end 109 | 110 | it "prints its url with to_s" do 111 | expect(RestClient::Resource.new('x').to_s).to eq 'x' 112 | end 113 | 114 | describe 'block' do 115 | it 'can use block when creating the resource' do 116 | stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) 117 | resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' } 118 | expect(resource.get).to eq 'foo' 119 | end 120 | 121 | it 'can use block when executing the resource' do 122 | stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) 123 | resource = RestClient::Resource.new('www.example.com') 124 | expect(resource.get { |response, request| 'foo' }).to eq 'foo' 125 | end 126 | 127 | it 'execution block override resource block' do 128 | stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) 129 | resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' } 130 | expect(resource.get { |response, request| 'bar' }).to eq 'bar' 131 | end 132 | 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/unit/response_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Response, :include_helpers do 4 | before do 5 | @net_http_res = res_double(to_hash: {'Status' => ['200 OK']}, code: '200', body: 'abc') 6 | @example_url = 'http://example.com' 7 | @request = request_double(url: @example_url, method: 'get') 8 | @response = response_from_res_double(@net_http_res, @request, duration: 1) 9 | end 10 | 11 | it "behaves like string" do 12 | expect(@response.to_s).to eq 'abc' 13 | expect(@response.to_str).to eq 'abc' 14 | 15 | expect(@response).to receive(:warn) 16 | expect(@response.to_i).to eq 0 17 | end 18 | 19 | it "accepts nil strings and sets it to empty for the case of HEAD" do 20 | # TODO 21 | expect(RestClient::Response.create(nil, @net_http_res, @request).to_s).to eq "" 22 | end 23 | 24 | describe 'header processing' do 25 | it "test headers and raw headers" do 26 | expect(@response.raw_headers["Status"][0]).to eq "200 OK" 27 | expect(@response.headers[:status]).to eq "200 OK" 28 | end 29 | 30 | it 'handles multiple headers by joining with comma' do 31 | net_http_res = res_double(to_hash: {'My-Header' => ['foo', 'bar']}, code: '200', body: nil) 32 | example_url = 'http://example.com' 33 | request = request_double(url: example_url, method: 'get') 34 | response = response_from_res_double(net_http_res, request) 35 | 36 | expect(response.raw_headers['My-Header']).to eq ['foo', 'bar'] 37 | expect(response.headers[:my_header]).to eq 'foo, bar' 38 | end 39 | end 40 | 41 | describe "cookie processing" do 42 | it "should correctly deal with one Set-Cookie header with one cookie inside" do 43 | header_val = "main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT".freeze 44 | 45 | net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => [header_val]}) 46 | response = RestClient::Response.create('abc', net_http_res, @request) 47 | expect(response.headers[:set_cookie]).to eq [header_val] 48 | expect(response.cookies).to eq({ "main_page" => "main_page_no_rewrite" }) 49 | end 50 | 51 | it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do 52 | net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]}) 53 | response = RestClient::Response.create('abc', net_http_res, @request) 54 | expect(response.headers[:set_cookie]).to eq ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"] 55 | expect(response.cookies).to eq({ 56 | "main_page" => "main_page_no_rewrite", 57 | "remember_me" => "", 58 | "user" => "somebody" 59 | }) 60 | end 61 | 62 | it "should correctly deal with multiple cookies [one Set-Cookie header with multiple cookies]" do 63 | net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT, remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT, user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]}) 64 | response = RestClient::Response.create('abc', net_http_res, @request) 65 | expect(response.cookies).to eq({ 66 | "main_page" => "main_page_no_rewrite", 67 | "remember_me" => "", 68 | "user" => "somebody" 69 | }) 70 | end 71 | end 72 | 73 | describe "exceptions processing" do 74 | it "should return itself for normal codes" do 75 | (200..206).each do |code| 76 | net_http_res = res_double(:code => '200') 77 | resp = RestClient::Response.create('abc', net_http_res, @request) 78 | resp.return! 79 | end 80 | end 81 | 82 | it "should throw an exception for other codes" do 83 | RestClient::Exceptions::EXCEPTIONS_MAP.each_pair do |code, exc| 84 | unless (200..207).include? code 85 | net_http_res = res_double(:code => code.to_i) 86 | resp = RestClient::Response.create('abc', net_http_res, @request) 87 | allow(@request).to receive(:max_redirects).and_return(5) 88 | expect { resp.return! }.to raise_error(exc) 89 | end 90 | end 91 | end 92 | 93 | end 94 | 95 | describe "redirection" do 96 | 97 | it "follows a redirection when the request is a get" do 98 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'}) 99 | stub_request(:get, 'http://new/resource').to_return(:body => 'Foo') 100 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo' 101 | end 102 | 103 | it "keeps redirection history" do 104 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'}) 105 | stub_request(:get, 'http://new/resource').to_return(:body => 'Foo') 106 | r = RestClient::Request.execute(url: 'http://some/resource', method: :get) 107 | expect(r.body).to eq 'Foo' 108 | expect(r.history.length).to eq 1 109 | expect(r.history.fetch(0)).to be_a(RestClient::Response) 110 | expect(r.history.fetch(0).code).to be 301 111 | end 112 | 113 | it "follows a redirection and keep the parameters" do 114 | stub_request(:get, 'http://some/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'}) 115 | stub_request(:get, 'http://new/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => 'Foo') 116 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :user => 'foo', :password => 'bar', :headers => {:accept => :json}).body).to eq 'Foo' 117 | end 118 | 119 | it "follows a redirection and keep the cookies" do 120 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://some/new_resource', }) 121 | stub_request(:get, 'http://some/new_resource').with(:headers => {'Cookie' => 'Foo=Bar'}).to_return(:body => 'Qux') 122 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Qux' 123 | end 124 | 125 | it 'respects cookie domains on redirect' do 126 | stub_request(:get, 'http://some.example.com/').to_return(:body => '', :status => 301, 127 | :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://new.example.com/', }) 128 | stub_request(:get, 'http://new.example.com/').with( 129 | :headers => {'Cookie' => 'passedthrough=1'}).to_return(:body => 'Qux') 130 | 131 | expect(RestClient::Request.execute(:url => 'http://some.example.com/', :method => :get, cookies: [HTTP::Cookie.new('passedthrough', '1', domain: 'new.example.com', path: '/')]).body).to eq 'Qux' 132 | end 133 | 134 | it "doesn't follow a 301 when the request is a post" do 135 | net_http_res = res_double(:code => 301) 136 | response = response_from_res_double(net_http_res, request_double(method: 'post')) 137 | 138 | expect { 139 | response.return! 140 | }.to raise_error(RestClient::MovedPermanently) 141 | end 142 | 143 | it "doesn't follow a 302 when the request is a post" do 144 | net_http_res = res_double(:code => 302) 145 | response = response_from_res_double(net_http_res, request_double(method: 'post')) 146 | 147 | expect { 148 | response.return! 149 | }.to raise_error(RestClient::Found) 150 | end 151 | 152 | it "doesn't follow a 307 when the request is a post" do 153 | net_http_res = res_double(:code => 307) 154 | response = response_from_res_double(net_http_res, request_double(method: 'post')) 155 | 156 | expect(response).not_to receive(:follow_redirection) 157 | expect { 158 | response.return! 159 | }.to raise_error(RestClient::TemporaryRedirect) 160 | end 161 | 162 | it "doesn't follow a redirection when the request is a put" do 163 | net_http_res = res_double(:code => 301) 164 | response = response_from_res_double(net_http_res, request_double(method: 'put')) 165 | expect { 166 | response.return! 167 | }.to raise_error(RestClient::MovedPermanently) 168 | end 169 | 170 | it "follows a redirection when the request is a post and result is a 303" do 171 | stub_request(:put, 'http://some/resource').to_return(:body => '', :status => 303, :headers => {'Location' => 'http://new/resource'}) 172 | stub_request(:get, 'http://new/resource').to_return(:body => 'Foo') 173 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body).to eq 'Foo' 174 | end 175 | 176 | it "follows a redirection when the request is a head" do 177 | stub_request(:head, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'}) 178 | stub_request(:head, 'http://new/resource').to_return(:body => 'Foo') 179 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body).to eq 'Foo' 180 | end 181 | 182 | it "handles redirects with relative paths" do 183 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index'}) 184 | stub_request(:get, 'http://some/index').to_return(:body => 'Foo') 185 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo' 186 | end 187 | 188 | it "handles redirects with relative path and query string" do 189 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index?q=1'}) 190 | stub_request(:get, 'http://some/index?q=1').to_return(:body => 'Foo') 191 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo' 192 | end 193 | 194 | it "follow a redirection when the request is a get and the response is in the 30x range" do 195 | stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'}) 196 | stub_request(:get, 'http://new/resource').to_return(:body => 'Foo') 197 | expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo' 198 | end 199 | 200 | it "follows no more than 10 redirections before raising error" do 201 | stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'}) 202 | stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'}) 203 | expect { 204 | RestClient::Request.execute(url: 'http://some/redirect-1', method: :get) 205 | }.to raise_error(RestClient::MovedPermanently) { |ex| 206 | ex.response.history.each {|r| expect(r).to be_a(RestClient::Response) } 207 | expect(ex.response.history.length).to eq 10 208 | } 209 | expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(10) 210 | end 211 | 212 | it "follows no more than max_redirects redirections, if specified" do 213 | stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'}) 214 | stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'}) 215 | expect { 216 | RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 5) 217 | }.to raise_error(RestClient::MovedPermanently) { |ex| 218 | expect(ex.response.history.length).to eq 5 219 | } 220 | expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(5) 221 | end 222 | 223 | it "allows for manual following of redirects" do 224 | stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/resource'}) 225 | stub_request(:get, 'http://some/resource').to_return(:body => 'Qux', :status => 200) 226 | 227 | begin 228 | RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 0) 229 | rescue RestClient::MovedPermanently => err 230 | resp = err.response.follow_redirection 231 | else 232 | raise 'notreached' 233 | end 234 | 235 | expect(resp.code).to eq 200 236 | expect(resp.body).to eq 'Qux' 237 | end 238 | end 239 | 240 | describe "logging" do 241 | it "uses the request's logger" do 242 | stub_request(:get, 'http://some/resource').to_return(body: 'potato', status: 200) 243 | 244 | logger = double('logger', '<<' => true) 245 | request = RestClient::Request.new(url: 'http://some/resource', method: :get, log: logger) 246 | 247 | expect(logger).to receive(:<<) 248 | 249 | request.execute 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /spec/unit/restclient_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient do 4 | describe "API" do 5 | it "GET" do 6 | expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) 7 | RestClient.get('http://some/resource') 8 | end 9 | 10 | it "POST" do 11 | expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {}) 12 | RestClient.post('http://some/resource', 'payload') 13 | end 14 | 15 | it "PUT" do 16 | expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {}) 17 | RestClient.put('http://some/resource', 'payload') 18 | end 19 | 20 | it "PATCH" do 21 | expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'payload', :headers => {}) 22 | RestClient.patch('http://some/resource', 'payload') 23 | end 24 | 25 | it "DELETE" do 26 | expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}) 27 | RestClient.delete('http://some/resource') 28 | end 29 | 30 | it "HEAD" do 31 | expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {}) 32 | RestClient.head('http://some/resource') 33 | end 34 | 35 | it "OPTIONS" do 36 | expect(RestClient::Request).to receive(:execute).with(:method => :options, :url => 'http://some/resource', :headers => {}) 37 | RestClient.options('http://some/resource') 38 | end 39 | end 40 | 41 | describe "logging" do 42 | after do 43 | RestClient.log = nil 44 | end 45 | 46 | it "uses << if the log is not a string" do 47 | log = RestClient.log = [] 48 | expect(log).to receive(:<<).with('xyz') 49 | RestClient.log << 'xyz' 50 | end 51 | 52 | it "displays the log to stdout" do 53 | RestClient.log = 'stdout' 54 | expect(STDOUT).to receive(:puts).with('xyz') 55 | RestClient.log << 'xyz' 56 | end 57 | 58 | it "displays the log to stderr" do 59 | RestClient.log = 'stderr' 60 | expect(STDERR).to receive(:puts).with('xyz') 61 | RestClient.log << 'xyz' 62 | end 63 | 64 | it "append the log to the requested filename" do 65 | RestClient.log = '/tmp/restclient.log' 66 | f = double('file handle') 67 | expect(File).to receive(:open).with('/tmp/restclient.log', 'a').and_yield(f) 68 | expect(f).to receive(:puts).with('xyz') 69 | RestClient.log << 'xyz' 70 | end 71 | end 72 | 73 | describe 'version' do 74 | # test that there is a sane version number to avoid accidental 0.0.0 again 75 | it 'has a version > 2.0.0.alpha, < 3.0' do 76 | ver = Gem::Version.new(RestClient.version) 77 | expect(Gem::Requirement.new('> 2.0.0.alpha', '< 3.0')).to be_satisfied_by(ver) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/unit/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '_lib' 2 | 3 | describe RestClient::Utils do 4 | describe '.get_encoding_from_headers' do 5 | it 'assumes no encoding by default for text' do 6 | headers = {:content_type => 'text/plain'} 7 | expect(RestClient::Utils.get_encoding_from_headers(headers)). 8 | to eq nil 9 | end 10 | 11 | it 'returns nil on failures' do 12 | expect(RestClient::Utils.get_encoding_from_headers( 13 | {:content_type => 'blah'})).to eq nil 14 | expect(RestClient::Utils.get_encoding_from_headers( 15 | {})).to eq nil 16 | expect(RestClient::Utils.get_encoding_from_headers( 17 | {:content_type => 'foo; bar=baz'})).to eq nil 18 | end 19 | 20 | it 'handles various charsets' do 21 | expect(RestClient::Utils.get_encoding_from_headers( 22 | {:content_type => 'text/plain; charset=UTF-8'})).to eq 'UTF-8' 23 | expect(RestClient::Utils.get_encoding_from_headers( 24 | {:content_type => 'application/json; charset=ISO-8859-1'})). 25 | to eq 'ISO-8859-1' 26 | expect(RestClient::Utils.get_encoding_from_headers( 27 | {:content_type => 'text/html; charset=windows-1251'})). 28 | to eq 'windows-1251' 29 | 30 | expect(RestClient::Utils.get_encoding_from_headers( 31 | {:content_type => 'text/html; charset="UTF-16"'})). 32 | to eq 'UTF-16' 33 | end 34 | end 35 | 36 | describe '.cgi_parse_header' do 37 | it 'parses headers', :unless => RUBY_VERSION.start_with?('2.0') do 38 | expect(RestClient::Utils.cgi_parse_header('text/plain')). 39 | to eq ['text/plain', {}] 40 | 41 | expect(RestClient::Utils.cgi_parse_header('text/vnd.just.made.this.up')). 42 | to eq ['text/vnd.just.made.this.up', {}] 43 | 44 | expect(RestClient::Utils.cgi_parse_header('text/plain;charset=us-ascii')). 45 | to eq ['text/plain', {'charset' => 'us-ascii'}] 46 | 47 | expect(RestClient::Utils.cgi_parse_header('text/plain ; charset="us-ascii"')). 48 | to eq ['text/plain', {'charset' => 'us-ascii'}] 49 | 50 | expect(RestClient::Utils.cgi_parse_header( 51 | 'text/plain ; charset="us-ascii"; another=opt')). 52 | to eq ['text/plain', {'charset' => 'us-ascii', 'another' => 'opt'}] 53 | 54 | expect(RestClient::Utils.cgi_parse_header( 55 | 'foo/bar; filename="silly.txt"')). 56 | to eq ['foo/bar', {'filename' => 'silly.txt'}] 57 | 58 | expect(RestClient::Utils.cgi_parse_header( 59 | 'foo/bar; filename="strange;name"')). 60 | to eq ['foo/bar', {'filename' => 'strange;name'}] 61 | 62 | expect(RestClient::Utils.cgi_parse_header( 63 | 'foo/bar; filename="strange;name";size=123')).to eq \ 64 | ['foo/bar', {'filename' => 'strange;name', 'size' => '123'}] 65 | 66 | expect(RestClient::Utils.cgi_parse_header( 67 | 'foo/bar; name="files"; filename="fo\\"o;bar"')).to eq \ 68 | ['foo/bar', {'name' => 'files', 'filename' => 'fo"o;bar'}] 69 | end 70 | end 71 | 72 | describe '.encode_query_string' do 73 | it 'handles simple hashes' do 74 | { 75 | {foo: 123, bar: 456} => 'foo=123&bar=456', 76 | {'foo' => 123, 'bar' => 456} => 'foo=123&bar=456', 77 | {foo: 'abc', bar: 'one two'} => 'foo=abc&bar=one+two', 78 | {escaped: '1+2=3'} => 'escaped=1%2B2%3D3', 79 | {'escaped + key' => 'foo'} => 'escaped+%2B+key=foo', 80 | }.each_pair do |input, expected| 81 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 82 | end 83 | end 84 | 85 | it 'handles simple arrays' do 86 | { 87 | {foo: [1, 2, 3]} => 'foo[]=1&foo[]=2&foo[]=3', 88 | {foo: %w{a b c}, bar: [1, 2, 3]} => 'foo[]=a&foo[]=b&foo[]=c&bar[]=1&bar[]=2&bar[]=3', 89 | {foo: ['one two', 3]} => 'foo[]=one+two&foo[]=3', 90 | {'a+b' => [1,2,3]} => 'a%2Bb[]=1&a%2Bb[]=2&a%2Bb[]=3', 91 | }.each_pair do |input, expected| 92 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 93 | end 94 | end 95 | 96 | it 'handles nested hashes' do 97 | { 98 | {outer: {foo: 123, bar: 456}} => 'outer[foo]=123&outer[bar]=456', 99 | {outer: {foo: [1, 2, 3], bar: 'baz'}} => 'outer[foo][]=1&outer[foo][]=2&outer[foo][]=3&outer[bar]=baz', 100 | }.each_pair do |input, expected| 101 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 102 | end 103 | end 104 | 105 | it 'handles null and empty values' do 106 | { 107 | {string: '', empty: nil, list: [], hash: {}, falsey: false } => 108 | 'string=&empty&list&hash&falsey=false', 109 | }.each_pair do |input, expected| 110 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 111 | end 112 | end 113 | 114 | it 'handles nested nulls' do 115 | { 116 | {foo: {string: '', empty: nil}} => 'foo[string]=&foo[empty]', 117 | }.each_pair do |input, expected| 118 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 119 | end 120 | end 121 | 122 | it 'handles deep nesting' do 123 | { 124 | {coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]} => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3', 125 | }.each_pair do |input, expected| 126 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 127 | end 128 | end 129 | 130 | it 'handles multiple fields with the same name using ParamsArray' do 131 | { 132 | RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]) => 'foo=1&foo=2&foo=3', 133 | }.each_pair do |input, expected| 134 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 135 | end 136 | end 137 | 138 | it 'handles nested ParamsArrays' do 139 | { 140 | {foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])} => 'foo[a]=1&foo[a]=2', 141 | RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]) => 'foo[a]=1&foo[a]=2', 142 | }.each_pair do |input, expected| 143 | expect(RestClient::Utils.encode_query_string(input)).to eq expected 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/unit/windows/root_certs_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../_lib' 2 | 3 | describe 'RestClient::Windows::RootCerts', 4 | :if => RestClient::Platform.windows? do 5 | let(:x509_store) { RestClient::Windows::RootCerts.instance.to_a } 6 | 7 | it 'should return at least one X509 certificate' do 8 | expect(x509_store.to_a.size).to be >= 1 9 | end 10 | 11 | it 'should return an X509 certificate with a subject' do 12 | x509 = x509_store.first 13 | 14 | expect(x509.subject.to_s).to match(/CN=.*/) 15 | end 16 | 17 | it 'should return X509 certificate objects' do 18 | x509_store.each do |cert| 19 | expect(cert).to be_a(OpenSSL::X509::Certificate) 20 | end 21 | end 22 | end 23 | --------------------------------------------------------------------------------