em-http-request-1.1.2/0000755000004100000410000000000012261227270014610 5ustar www-datawww-dataem-http-request-1.1.2/Rakefile0000644000004100000410000000027612261227270016262 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' desc "Run all RSpec tests" RSpec::Core::RakeTask.new(:spec) task :default => :spec task :test => [:spec] em-http-request-1.1.2/.gemtest0000644000004100000410000000000012261227270016247 0ustar www-datawww-dataem-http-request-1.1.2/Gemfile0000644000004100000410000000031612261227270016103 0ustar www-datawww-datasource 'http://rubygems.org' gemspec group :benchmark do gem 'curb' gem 'excon' gem 'httparty' gem 'rest-client' gem 'sinatra' gem 'streamly_ffi' gem 'tach', '>= 0.0.8' gem 'typhoeus' end em-http-request-1.1.2/examples/0000755000004100000410000000000012261227270016426 5ustar www-datawww-dataem-http-request-1.1.2/examples/digest_auth/0000755000004100000410000000000012261227270020726 5ustar www-datawww-dataem-http-request-1.1.2/examples/digest_auth/client.rb0000644000004100000410000000110412261227270022525 0ustar www-datawww-data$: << 'lib' << '../../lib' require 'em-http' require 'em-http/middleware/digest_auth' digest_config = { :username => 'digest_username', :password => 'digest_password' } EM.run do conn_handshake = EM::HttpRequest.new('http://localhost:3000') http_handshake = conn_handshake.get http_handshake.callback do conn = EM::HttpRequest.new('http://localhost:3000') conn.use EM::Middleware::DigestAuth, http_handshake.response_header['WWW_AUTHENTICATE'], digest_config http = conn.get http.callback do puts http.response EM.stop end end end em-http-request-1.1.2/examples/digest_auth/server.rb0000644000004100000410000000126012261227270022560 0ustar www-datawww-datarequire 'webrick' include WEBrick config = { :Realm => 'DigestAuth_REALM' } htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' htdigest.set_passwd config[:Realm], 'digest_username', 'digest_password' htdigest.flush config[:UserDB] = htdigest digest_auth = WEBrick::HTTPAuth::DigestAuth.new config class TestServlet < HTTPServlet::AbstractServlet def do_GET(req, res) @options[0][:authenticator].authenticate req, res res.body = "You are authenticated to see the super secret data\n" end end s = HTTPServer.new(:Port => 3000) s.mount('/', TestServlet, {:authenticator => digest_auth}) trap("INT") do File.delete('my_password_file') s.shutdown end s.start em-http-request-1.1.2/examples/socks5.rb0000644000004100000410000000106112261227270020160 0ustar www-datawww-datarequire 'rubygems' require 'eventmachine' require '../lib/em-http' EM.run do # Establish a SOCKS5 tunnel via SSH # ssh -D 8000 some_remote_machine connection_options = {:proxy => {:host => '127.0.0.1', :port => 8000, :type => :socks5}} http = EM::HttpRequest.new('http://igvita.com/', connection_options).get :redirects => 2 http.callback { puts "#{http.response_header.status} - #{http.response.length} bytes\n" puts http.response EM.stop } http.errback { puts "Error: " + http.error puts http.inspect EM.stop } end em-http-request-1.1.2/examples/multi.rb0000644000004100000410000000073612261227270020113 0ustar www-datawww-data$: << '../lib' << 'lib' require 'eventmachine' require 'em-http' EventMachine.run { multi = EventMachine::MultiRequest.new reqs = [ 'http://google.com/', 'http://google.ca:81/' ] reqs.each_with_index do |url, idx| http = EventMachine::HttpRequest.new(url, :connect_timeout => 1) req = http.get multi.add idx, req end multi.callback do p multi.responses[:callback].size p multi.responses[:errback].size EventMachine.stop end } em-http-request-1.1.2/examples/.gitignore0000644000004100000410000000002012261227270020406 0ustar www-datawww-datatwitter_auth.rb em-http-request-1.1.2/examples/fibered-http.rb0000644000004100000410000000214712261227270021334 0ustar www-datawww-data$: << 'lib' << '../lib' require 'eventmachine' require 'em-http' require 'fiber' # Using Fibers in Ruby 1.9 to simulate blocking IO / IO scheduling # while using the async EventMachine API's def async_fetch(url) f = Fiber.current http = EventMachine::HttpRequest.new(url, :connect_timeout => 10, :inactivity_timeout => 20).get http.callback { f.resume(http) } http.errback { f.resume(http) } Fiber.yield if http.error p [:HTTP_ERROR, http.error] end http end EventMachine.run do Fiber.new{ puts "Setting up HTTP request #1" data = async_fetch('http://0.0.0.0/') puts "Fetched page #1: #{data.response_header.status}" puts "Setting up HTTP request #2" data = async_fetch('http://www.yahoo.com/') puts "Fetched page #2: #{data.response_header.status}" puts "Setting up HTTP request #3" data = async_fetch('http://non-existing.domain/') puts "Fetched page #3: #{data.response_header.status}" EventMachine.stop }.resume end puts "Done" # Setting up HTTP request #1 # Fetched page #1: 302 # Setting up HTTP request #2 # Fetched page #2: 200 # Done em-http-request-1.1.2/examples/oauth-tweet.rb0000644000004100000410000000142012261227270021216 0ustar www-datawww-data$: << 'lib' << '../lib' require 'em-http' require 'em-http/middleware/oauth' require 'em-http/middleware/json_response' require 'pp' OAuthConfig = { :consumer_key => '', :consumer_secret => '', :access_token => '', :access_token_secret => '' } EM.run do # automatically parse the JSON response into a Ruby object EventMachine::HttpRequest.use EventMachine::Middleware::JSONResponse # sign the request with OAuth credentials conn = EventMachine::HttpRequest.new('http://api.twitter.com/1/statuses/home_timeline.json') conn.use EventMachine::Middleware::OAuth, OAuthConfig http = conn.get http.callback do pp http.response EM.stop end http.errback do puts "Failed retrieving user stream." pp http.response EM.stop end end em-http-request-1.1.2/examples/fetch.rb0000644000004100000410000000104212261227270020041 0ustar www-datawww-datarequire 'rubygems' require 'eventmachine' require '../lib/em-http' urls = ARGV if urls.size < 1 puts "Usage: #{$0} <...>" exit end pending = urls.size EM.run do urls.each do |url| http = EM::HttpRequest.new(url).get http.callback { puts "#{url}\n#{http.response_header.status} - #{http.response.length} bytes\n" puts http.response pending -= 1 EM.stop if pending < 1 } http.errback { puts "#{url}\n" + http.error pending -= 1 EM.stop if pending < 1 } end end em-http-request-1.1.2/.rspec0000644000004100000410000000000012261227270015713 0ustar www-datawww-dataem-http-request-1.1.2/Changelog.md0000644000004100000410000000426312261227270017026 0ustar www-datawww-data# Changelog ## master - User-Agent header is now removed if set to nil. ## 1.0.0.beta.1 / 2011-02-20 - The big rewrite - Switched parser from Ragel to http_parser.rb - Removed em_buffer C extension - Added support for HTTP keepalive - Added support for HTTP pipelining - ~60% performance improvement across the board: less GC time! - Refactored & split all tests - Basic 100-Continue handling on POST/PUT ## 0.3.0 / 2011-01-15 - IMPORTANT: default to non-persistent connections (timeout => 0 now requires :keepalive => true) - see: https://github.com/igrigorik/em-http-request/commit/1ca5b608e876c18fa6cfa318d0685dcf5b974e09 - added escape_utils dependency to fix slow encode on long string escapes - bugfix: proxy authorization headers - bugfix: default to Encoding.default_external on invalid encoding in response - bugfix: do not normalize URI's internally - bugfix: more robust Encoding detection ## 0.2.15 / 2010-11-18 - bugfix: follow redirects on missing content-length - bugfix: fixed undefined warnings when running in strict mode ## 0.2.14 / 2010-10-06 - bugfix: form-encode keys/values of ruby objects passed in as body ## 0.2.13 / 2010-09-25 - added SOCKS5 proxy support - bugfix: follow redirects on HEAD requests ## 0.2.12 / 2010-09-12 - added headers callback (http.headers {|h| p h}) - added .close method on client obj to terminate session (accepts message) - bugfix: report 0 for response status on 1.9 on timeouts - bugfix: handle bad Location host redirects - bugfix: reset host override on connect ## 0.2.11 / 2010-08-16 - all URIs are now normalized prior to dispatch (and on redirect) - default to direct proxy (instead of CONNECT handshake) - better performance - specify :proxy => {:tunnel => true} if you need to force CONNECT route - MultiRequest accepts block syntax for dispatching parallel requests (see specs) - MockHttpRequest accepts block syntax (see Mock wiki page) - bugfix: nullbyte frame for websockets - bugfix: set @uri on DNS resolve failure - bugfix: handle bad hosts in absolute redirects - bugfix: reset seen content on redirects (doh!) - bugfix: invalid multibyte escape in websocket regex (1.9.2+) - bugfix: report etag and last_modified headers correctly em-http-request-1.1.2/spec/0000755000004100000410000000000012261227270015542 5ustar www-datawww-dataem-http-request-1.1.2/spec/middleware/0000755000004100000410000000000012261227270017657 5ustar www-datawww-dataem-http-request-1.1.2/spec/middleware/oauth2_spec.rb0000644000004100000410000000140512261227270022420 0ustar www-datawww-datadescribe EventMachine::Middleware::OAuth2 do it "should add an access token to a URI with no query parameters" do middleware = EventMachine::Middleware::OAuth2.new(:access_token => "fedcba9876543210") uri = Addressable::URI.parse("https://graph.facebook.com/me") middleware.update_uri! uri uri.to_s.should == "https://graph.facebook.com/me?access_token=fedcba9876543210" end it "should add an access token to a URI with query parameters" do middleware = EventMachine::Middleware::OAuth2.new(:access_token => "fedcba9876543210") uri = Addressable::URI.parse("https://graph.facebook.com/me?fields=photo") middleware.update_uri! uri uri.to_s.should == "https://graph.facebook.com/me?fields=photo&access_token=fedcba9876543210" end end em-http-request-1.1.2/spec/ssl_spec.rb0000644000004100000410000000065712261227270017712 0ustar www-datawww-datarequire 'helper' requires_connection do describe EventMachine::HttpRequest do it "should initiate SSL/TLS on HTTPS connections" do EventMachine.run { http = EventMachine::HttpRequest.new('https://mail.google.com:443/mail/').get http.errback { failed(http) } http.callback { http.response_header.status.should == 302 EventMachine.stop } } end end end em-http-request-1.1.2/spec/helper.rb0000644000004100000410000000101112261227270017337 0ustar www-datawww-datarequire 'rubygems' require 'bundler/setup' require 'em-http' require 'em-http/middleware/oauth2' require 'multi_json' require 'stallion' require 'stub_server' def failed(http = nil) EventMachine.stop http ? fail(http.error) : fail end def requires_connection(&blk) blk.call if system('ping -t1 -c1 google.com 2>&1 > /dev/null') end def requires_port(port, &blk) port_open = true begin s = TCPSocket.new('localhost', port) s.close() rescue port_open = false end blk.call if port_open end em-http-request-1.1.2/spec/encoding_spec.rb0000644000004100000410000000326512261227270020675 0ustar www-datawww-data# -*- encoding: utf-8 -*- require 'helper' describe EventMachine::HttpEncoding do include EventMachine::HttpEncoding it "should transform a basic hash into HTTP POST Params" do form_encode_body({:a => "alpha", :b => "beta"}).should == "a=alpha&b=beta" end it "should transform a more complex hash into HTTP POST Params" do form_encode_body({:a => "a", :b => ["c", "d", "e"]}).should == "a=a&b[0]=c&b[1]=d&b[2]=e" end it "should transform a very complex hash into HTTP POST Params" do params = form_encode_body({:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]}) # 1.8.7 does not have ordered hashes. params.split(/&/).sort.join('&').should == "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f" end it "should escape values" do params = form_encode_body({:stuff => 'string&string'}) params.should == "stuff=string%26string" end it "should escape keys" do params = form_encode_body({'bad&str'=> {'key&key' => [:a, :b]}}) params.should == 'bad%26str[key%26key][0]=a&bad%26str[key%26key][1]=b' end it "should escape keys and values" do params = form_encode_body({'bad&str'=> {'key&key' => ['bad+&stuff', '[test]']}}) params.should == "bad%26str[key%26key][0]=bad%2B%26stuff&bad%26str[key%26key][1]=%5Btest%5D" end it "should not issue warnings on non-ASCII encodings" do # I don't know how to check for ruby warnings. params = escape('valö') params = escape('valö'.encode('ISO-8859-15')) end # xit "should be fast on long string escapes" do # s = Time.now # 5000.times { |n| form_encode_body({:a => "{a:'b', d:'f', g:['a','b']}"*50}) } # (Time.now - s).should satisfy { |t| t < 1.5 } # end end em-http-request-1.1.2/spec/redirect_spec.rb0000644000004100000410000002651212261227270020710 0ustar www-datawww-datarequire 'helper' class RedirectMiddleware attr_reader :call_count def initialize @call_count = 0 end def request(c, h, r) @call_count += 1 [h.merge({'EM-Middleware' => @call_count.to_s}), r] end end class PickyRedirectMiddleware < RedirectMiddleware def response(r) if r.redirect? && r.response_header['LOCATION'][-1].chr == '3' # set redirects to 0 to avoid further processing r.req.redirects = 0 end end end describe EventMachine::HttpRequest do it "should follow location redirects" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect').get :redirects => 1 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.redirects.should == 1 EM.stop } } end it "should not follow redirects on created" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/created').get :redirects => 1 http.errback { failed(http) } http.callback { http.response_header.status.should == 201 http.response.should match(/Hello/) EM.stop } } end it "should not forward cookies across domains with http redirect" do expires = (Date.today + 2).strftime('%a, %d %b %Y %T GMT') response =<<-HTTP.gsub(/^ +/, '') HTTP/1.1 301 MOVED PERMANENTLY Location: http://localhost:8071/ Set-Cookie: foo=bar; expires=#{expires}; path=/; HttpOnly HTTP EventMachine.run do @stub = StubServer.new(:host => '127.0.0.1', :port => 8070, :response => response) @echo = StubServer.new(:host => 'localhost', :port => 8071, :echo => true) http = EventMachine::HttpRequest.new('http://127.0.0.1:8070/').get :redirects => 1 http.errback { failed(http) } http.callback do http.response.should_not match(/Cookie/) @stub.stop @echo.stop EM.stop end end end it "should forward valid cookies across domains with http redirect" do expires = (Date.today + 2).strftime('%a, %d %b %Y %T GMT') response =<<-HTTP.gsub(/^ +/, '') HTTP/1.1 301 MOVED PERMANENTLY Location: http://127.0.0.1:8071/ Set-Cookie: foo=bar; expires=#{expires}; path=/; HttpOnly HTTP EventMachine.run do @stub = StubServer.new(:host => '127.0.0.1', :port => 8070, :response => response) @echo = StubServer.new(:host => '127.0.0.1', :port => 8071, :echo => true) http = EventMachine::HttpRequest.new('http://127.0.0.1:8070/').get :redirects => 1 http.errback { failed(http) } http.callback do http.response.should match(/Cookie/) @stub.stop @echo.stop EM.stop end end end it "should normalize path and forward valid cookies across domains" do expires = (Date.today + 2).strftime('%a, %d %b %Y %T GMT') response =<<-HTTP.gsub(/^ +/, '') HTTP/1.1 301 MOVED PERMANENTLY Location: http://127.0.0.1:8071?omg=ponies Set-Cookie: foo=bar; expires=#{expires}; path=/; HttpOnly HTTP EventMachine.run do @stub = StubServer.new(:host => '127.0.0.1', :port => 8070, :response => response) @echo = StubServer.new(:host => '127.0.0.1', :port => 8071, :echo => true) http = EventMachine::HttpRequest.new('http://127.0.0.1:8070/').get :redirects => 1 http.errback { failed(http) } http.callback do http.response.should match(/Cookie/) @stub.stop @echo.stop EM.stop end end end it "should redirect with missing content-length" do EventMachine.run { response = "HTTP/1.0 301 MOVED PERMANENTLY\r\nlocation: http://127.0.0.1:8090/redirect\r\n\r\n" @stub = StubServer.new(:host => '127.0.0.1', :port => 8070, :response => response) http = EventMachine::HttpRequest.new('http://127.0.0.1:8070/').get :redirects => 3 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.redirects.should == 3 @stub.stop EM.stop } } end it "should follow redirects on HEAD method" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/head').head :redirects => 1 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/' EM.stop } } end it "should report last_effective_url" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/' EM.stop } } end it "should default to 0 redirects" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect').get http.errback { failed(http) } http.callback { http.response_header.status.should == 301 http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/redirect' http.redirects.should == 0 EM.stop } } end it "should not invoke redirect logic on failed(http) connections" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8070/', :connect_timeout => 0.1).get :redirects => 5 http.callback { failed(http) } http.errback { http.redirects.should == 0 EM.stop } } end it "should normalize redirect urls" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/bad').get :redirects => 1 http.errback { failed(http) } http.callback { http.last_effective_url.to_s.should match('http://127.0.0.1:8090/') http.response.should match('Hello, World!') EM.stop } } end it "should fail gracefully on a missing host in absolute Location header" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/nohost').get :redirects => 1 http.callback { failed(http) } http.errback { http.error.should == 'Location header format error' EM.stop } } end it "should apply timeout settings on redirects" do EventMachine.run { t = Time.now.to_i EventMachine.heartbeat_interval = 0.1 conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/timeout', :inactivity_timeout => 0.1) http = conn.get :redirects => 1 http.callback { failed(http) } http.errback { (Time.now.to_i - t).should <= 1 EM.stop } } end it "should capture and pass cookies on redirect and pass_cookies by default" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/multiple-with-cookie').get :redirects => 2, :head => {'cookie' => 'id=2;'} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.redirects.should == 2 http.cookies.should include("id=2;") http.cookies.should include("another_id=1") EM.stop } } end it "should capture and not pass cookies on redirect if passing is disabled via pass_cookies" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/multiple-with-cookie').get :redirects => 2, :pass_cookies => false, :head => {'cookie' => 'id=2;'} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.redirects.should == 2 http.cookies.should include("id=2;") http.cookies.should_not include("another_id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;") EM.stop } } end it "should follow location redirects with path" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect').get :path => '/redirect', :redirects => 1 http.errback { failed(http) } http.callback { http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.response_header.status.should == 200 http.redirects.should == 1 EM.stop } } end it "should call middleware each time it redirects" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/middleware_redirects_1') conn.use RedirectMiddleware http = conn.get :redirects => 3 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header['EM_MIDDLEWARE'].to_i.should == 3 EM.stop } } end it "should call middleware which may reject a redirection" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/middleware_redirects_1') conn.use PickyRedirectMiddleware http = conn.get :redirects => 3 http.errback { failed(http) } http.callback { http.response_header.status.should == 301 http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/redirect/middleware_redirects_2' EM.stop } } end it "should not add default http port to redirect url that don't include it" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/http_no_port') http = conn.get :redirects => 1 http.errback { http.last_effective_url.to_s.should == 'http://host/' EM.stop } } end it "should not add default https port to redirect url that don't include it" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/https_no_port') http = conn.get :redirects => 1 http.errback { http.last_effective_url.to_s.should == 'https://host/' EM.stop } } end it "should keep default http port in redirect url that include it" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/http_with_port') http = conn.get :redirects => 1 http.errback { http.last_effective_url.to_s.should == 'http://host:80/' EM.stop } } end it "should keep default https port in redirect url that include it" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/https_with_port') http = conn.get :redirects => 1 http.errback { http.last_effective_url.to_s.should == 'https://host:443/' EM.stop } } end end em-http-request-1.1.2/spec/http_proxy_spec.rb0000644000004100000410000000575012261227270021330 0ustar www-datawww-datarequire 'helper' describe EventMachine::HttpRequest do context "connections via" do let(:proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083 }} } let(:authenticated_proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083, :authorization => ["user", "name"] } } } it "should use HTTP proxy" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header.should_not include("X_PROXY_AUTH") http.response.should match('test') EventMachine.stop } } end it "should use HTTP proxy with authentication" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/proxyauth?q=test', authenticated_proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header['X_PROXY_AUTH'].should == "Proxy-Authorization: Basic dXNlcjpuYW1l" http.response.should match('test') EventMachine.stop } } end it "should send absolute URIs to the proxy server" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 # The test proxy server gives the requested uri back in this header http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/?q=test' http.response_header['X_THE_REQUESTED_URI'].should_not == '/?q=test' http.response.should match('test') EventMachine.stop } } end it "should include query parameters specified in the options" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/', proxy).get :query => { 'q' => 'test' } http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match('test') EventMachine.stop } } end it "should use HTTP proxy while redirecting" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect', proxy).get :redirects => 1 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/gzip' http.response_header['X_THE_REQUESTED_URI'].should_not == '/redirect' http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip' http.redirects.should == 1 EventMachine.stop } } end end end em-http-request-1.1.2/spec/fixtures/0000755000004100000410000000000012261227270017413 5ustar www-datawww-dataem-http-request-1.1.2/spec/fixtures/google.ca0000644000004100000410000000077512261227270021205 0ustar www-datawww-dataHTTP/1.1 301 Moved Permanently Location: http://www.google.ca/ Content-Type: text/html; charset=UTF-8 Date: Sun, 09 Jan 2011 02:51:43 GMT Expires: Tue, 08 Feb 2011 02:51:43 GMT Cache-Control: public, max-age=2592000 Server: gws Content-Length: 218 X-XSS-Protection: 1; mode=block 301 Moved

301 Moved

The document has moved here. em-http-request-1.1.2/spec/fixtures/gzip-sample.gz0000644000004100000410000003773012261227270022217 0ustar www-datawww-dataYPfileE -o,[66R*V{wͱl_o;_1Xcνo}cwZW5NyGrٹg~/_F*W;U~}| ~wnyob'-~wԣ<ٻg>y1N\wZk+'o}h{vݿw.r_ͻӷ}_!?o=;y:ovb[~2[V 4\/1=K^ew[_UY˒v}wc3y3Y\v6Y\i>v_,A6|YʽDkܕ{gz.;ٍ,c4lenf6VcyU(6#~w97<ˆ5Yd }jc|ٯZ&bv\ٮֳ˛*b7VN]̻|_l8pc1r8PͲ|y]*[v&n1ѾFKf^S~?mFN65TlP|freb#cn+vhϛE?_K{Xk }bG)5E y&" ےw$dcWJ;DV:?7['X̱1 DY6@}۱H_-H亄qr*8Qa%:E997{i Xݼm/zDBN A-"g-W͂%곲/SE K\bA6."34+q\4z-E(G{9ea&KMH\C'q2 |H`,\N_r >^\Sg)b$FiF&t71 眰q7'-< G2w8Y̌S&Kx-XՅSWNH IPȊ}l3hr@%8 `W_tKN_V(⟱K=!zg fb96֞-в0X^sr8Y`)yܳÄ j,.$ rYLpJz:7b9ِ,@"^H? GMNnKh|D^2`IqA+1^mK| ݀1 DgY,ay9K 7}?fL>h!Ȳ&b yB1S9X ?9qG/gJ{/'7/'f]8 `mbp6|xv㙏(!Z>4HKWjU\+-2/ulN &qZyX r&[`EK̲qq;@J5.R Pd88 ǡ̭@ %>xÅdp߀u?^w18W~?3y{,wq<]n5A x)r L8K68X^)$ ~8](G5:V<3ΐ&%rwJ(rYp5q%r\?%5''.GġsfqYo~@šT:0q8 ߓŏ`q%Uk%O0NȲ~Sc %M'd;.&(dmw 8w7Yjrl{[s^7(͉#Ai$OIvYv6Sv{AVY^\?*'> !"gbuH A>ykh}E`)9p8ŇƯ?'c/W HsYV=8GON6ᖰ4"=a05.}q,&.wЯIp,w઀JT'P(~jvdD1m^ Ⳳ73&P.9IxąuXHOklC 8$Hq,QG#)qcXHm쉠l:e̅cm<'#G54H0k2d>PV+5H i(x/FL[64(&ʒ}" iR`~C&ebIAArفo"ʖŭ}Yu En{!"|zn*νC| qq)no4VAWL#d_2 j}f&s`oqq%l hၸo`8mz*fɃq 9C0{M~Fr/1؋@rҡ;ObO5#] E nhD'[&471ЀɅrWL.K٠ÁȠLL' [ َG䕳g+-r\b"]2IV"RNas3" c%Ғo;FDoh,glx {*YxHOQf5ޓ\Y&o2":s|%Ln$ˍ<:杠dyt-ZQ, * bTףarL#ߩx^YցXfH1A+CU1hV㛄6)!mx f, )idYy\VOIbU- 9T^0&-`_Li k2% 4APg9eo+.1>v7/ ) C fM?9X$>ǖcpY2YII:7\Яh:&X#g MPО$B&,UCd޻xX7{NL5X0B,͕Z`4;V'JŦ>Hc`%w`\~rD* (ǿx kflVcII8Oy>:͏lm+?K²E ;nH@+POxF9HC|0L4),Ѧȿ#`mŔ_K.q~Ϊꋬ(Vcl# AcGļ9OD= Z°8h_GV1O3H"[zD~Zj=qpYx. kZ#v'y)tQ1ZF_x*MUfc.]4i7s*EV "Q$ +ZD} R$l&aZXg)A 9djLE8+bqFv+ 'te%p#^LIrLCX[Gۤ87Aj-˳y&ogE>(: vn&Qy(4NWbNep(|yHPG?RWWr~E}LnȨ HX2Q`eцhV.xMZ{qd=M>8F->RQa!pnRĺذۿq16S6synTyr Ym,xa<4+ d{$H ]Ytӗ;>/.( i&:!@>2i /$f%+&.ԥ $z8c]{+[ 4!m"A[ERRp ePP>XOt=s`I>*q#WiS4$Nn!3IQ,3D$90w880tkJ]ʪ&6NiAlx,#eiԾNny-52GVza#=-v KcgXjf%sO0LDȋ~(?UhF +yת}r*c&8ʘ Q"l$̈a%SfET N<] dUt? #Y<˺G~L9ZOJЫ̱))Mx t(R3}]xEJ?\z0[bN !IG6W+ԧR#GΚM ;X0v!&r÷|?T5<7\@+uX{j}]dU# ]&a7zS"o9Z\X8 u&@",<$5<)bٳrQ/j&0X;|1~{r ٸ l'@6(y[Q uFVy 579ͩx1h4+I~&Y-!~\(&Q80],2LDVI:)œ}Yu?}%P2=c'9D 2 )Zn$_G_AQRjbhD'Q٤/LLd1{ FBlYGZ&/yiD4WE$PU=խ%}4d]wʉA9TnYe"Ǿ֮p փ,c"z szosM!~%JCвC| |ޤiQdnưACE|Dwƅ>^VPϱ&AP[ EYXhUe-4h OAa- De$RW/mg-8(%k@a~AgIgJp yg~'H6ق˸q(ԩ Ck -𕪿EuZc1/ԯhoL^bNle6\s'k![PJJiMc! b6pP"H`YFC9sȄ#.ł2DuZ<; ̮@)D &aFYb-uܳ@!YJko L(/r%٤Ɂ%i HΏT_ pHk5jv)#5cADV:jfuۈa1FŎ([ʎ常'TgS@9J1:<.yM:+ՠU ^<,RAԻ d1@oL'm(iXduRA ʱ8N(G J`T*j ,mUOꟇ%CyQ#ւaZ#2g+`H@jyRoڗ9(H}9rQT\$Il(zLq07ޟ` CfՒu w?T4VnR/$>-e}HmE ڴ Xd0*5а@YxiktCb`85.0ŋI7}3QJ;Kq\Wylit*n-AQ~s$)s!b4@c.Z$@P17 'h$kEy'>2a5&(VPnXf>' <vA$5 a ̫V~jЊ{ TfLzlo=T>DȺU>sO9wKX$_k]a&ګpLX  /nb RiJj8ZW'yB٨\4qR~%*yy<7*f1(աvg7'Ȁ6ڤA<<ݎ o8Ϧnu&7P`** @>G-/$WvfySbJz_S*\.~*gx3n-SN-Ad\[&Ͽ,YtAQi=d5YN(bUl˲YoJމ͒P 65L=!9KOzo ]Ϧk#Gs) q-1t)BziSksR=V 3icxswuȿ6)Zh I^ôF#,@4Hc!7h'NP{/ Nf*g&才7( , j?p/>J5n_46(*s?KII 9ԵT0 } lCK :&C­Z$C9"q8p:q*;8} O@8#Mfvb"NsP X#['%@h nRŕRn2W& b|DQ "5ʒkGKݎD<(C vz 0&"&R߁#D݊ bC)FlB@zIPqb7v]Zkh v ]"/HV4K%?oGbT "=`+a@E!JP%BF&h?Nq 6p[qt3 AY+~HY&Nհ("o'%1KbS *ҠEUh!RgAP!톟Uw#TleU#4btHcR)1!qʖOj@R=TS$yae0S4%Qnw5IQicaq?HN)A9JBFP\cɜ{{;Ihw=ם<6R8t*?]jTL7s3< U#tF4M`6bNIƤG_ *)EWTMS6 ӊ==˩U<,U *=IȝOjRUw}GˠR wtO^CwJY00W(g6qjUSL[I&yl3(mԃ({#{Er҈7MAX6y;g"18aʫ593]hȀЪq^xy3Qf$nXݝŕR`LQȺ0l"Lnie~֤-!$Y}E%gRq(u Ipe<.A%.qUjp,m=5) Unŝ4S_ +0U[&FjhnqJ˪qXACn~I~#ImU}U씇909}.nw0G~8@k[SJM5k[<4ӌ,/7L0UE*hD2FtJ㾂{PГ6CPEAt,[-ct4uVBh@^HUI3nMk"jyfdAI+t"X h6MlTEL@,f6XHp(XƠ%[*ngrT ;" κ%P\jӝt)t} 0L[/fl; փ+dYjIʵ[!Y@Oѫy!K7=vwh`W xwH f[kNNNif'COk5hsU; آë30 ONiIiiTIX"!&ZY~PC^ȞI%1H5Ynjesz  :3b &&*makmgU-ED >:zuTWs&at6װ))JT̢ؤ >i44(.'\}QYU0|l"Ď3+3_uT)gDԌ=kQueʓ2QE$5'Z4q>'`ӫ PWd:&]5iT@B՜Xl:l,\TM0 ;ɩE۲0gfR2Ȉa bP 9` !-2d{ ؅`G]km)ƥFɫhH-Z.ƹREКmNlJH` hpNÔUbCJ62z',Ӳ%L'\ BRjcf.~0 a-t ڶmk퐜|['vicwSO`eֳg*o,j2@N=#7H.B2u=T G W^eVϮggmAP=l=G6#9R}eLgd$Lyǩ-L-fX1EcEEBܿMHroOkW";^RZi; g{fSD}t1LdPjLnYzB،tL" sk.R ma?&MQz5VkhKtɯQsA__4Zl #ȑl d6(M*\wA[Ekn']0,D@H:%i5h]:߂)hٴ~fG(nCip>HNE^-*}j09F'H>jt>̑H1j<;e(nA 9AN5 } $4ĩ/2sd<:R;F8QפSI5׼f;R@/1p6/TG(]UO8pH8TuG@􄪇Rߛ¸xAh ,^jϷ ؤ<=$;"dNIRN n6 ۵YAhtIHd,RBxSÂxBpx$*:;pzl(bn ՘Wb, "í>,C`(/A>"{ #* ^I&:;E3лKt9=TT؈9rmtR ,sBQ[lC7FL÷0()uw 񑭨F̃ڎamB;#PH ,Y N"PK@ЎMO"W_r-u\z8ӯSG! Myl9Or 5q9'e {Q6&hڧepؠöR^o#47zN#!g=.jKM{=lrH)c m iHޖ]1Q9eP|t1ֲj`ᕛ$.L죕gHF^1E KZ[E, 6ej S} ai,d+ U$4mpԉ8 Ic~Fsf?+2 yP ܨw쨐AcLsd1;M(T@!ͻQun*Zj 9)Z&!AT3_Qn"ug62+e40KZvh_n an9`ˇ֟\lzf/dQ 4[X*]`/4!}w;5~3j%䂌i< ׺5z9h^ ̌Pɹ'av貞Ҙݔh $cX.5곸R8 0:bȗYMk1{fVGgrbImkT) oV `)'tEXs(p+ufu\Ne|.\ؓ9Kh3 00*ǰ]D&!g$%nḁc=z%&1پ0-T)8F_$BD+]yNA9 Lk猑tם-_I4BJշ"NY<)k`ȴ{.@L:A8xtQJ)k*,=uz\;⪯6oo!fNF#Rm(Nb<׹ #r|&*oL0R-~( J/iCavDo'BFYb;Ch#@I2@;,-tɄfl>Cr]LXҬWA?,~R 3&ud H*~7[l؆d|ܯ9@ b@&X T 1GDP'81+ո3MEɤe &ʢ1GurEt(TX:a )sޟC@CAdWd6:̀$LQj/IVy;ädo!ZEJyܻE>Htu;wY% 0\ӣbr048`?@QԳ׳rݲcpwDҗo9-vUH8MR|>t98o$4m 1Ov3CAͤw,%=ϾMnoE*Ir=5zmIΰA=n_uKTv% 1[]qːM:i.}qL4s`~  /-,*(pֵ#:E4=l~d65D @Lc(>C@1Y&+ 22 ȓOP!/~MP>TrBA^nM*gBUK 8iߠF+o˸Mc$wj E8|ʔV裧^K'T}Of(4ՓȰPY5ԅᮌOva^ nrK&|Olk`{faZXc?7 ]ZxVwb>ںL'11A|իh ALml%zRXkJ,`bgKY.~-YGC0KXM̕ҭJCU&nkcDp8[ѪE)wۭNPT2h#()N^T_F8:/44O?Al;~'ѐ%G:2 N1Br1&2̢9pJ,>G;>f!]?8oxjSM0o`~`:@E{p~JeW2Ec* T[g}B`{|(2ԑBw %5^i!=LtfwWD Y}3o cV׎*}:]QAv4!?ᗏ:|?mYWS#W<z@%Eg O !h$qݴ]Zb#s(3V~nwNM(N&@^.S̩(cv-tfu1N;>SZlQ)m |TydO6B s?!`T&%}8&E~s۴ԻQzLW=r8& 4U;Ӹ! [o} m9bm+Y*l7q ·CkՒ RVћE/K((,[ZUO6hf ;HlkifVޫ9IANrQ_jŢW YhtJ4fռÇ/j\Mn5~^ڹe8o uLIvV;Bslѣf5q}xUHb>RH5>"u~c|Ew-TڎS8!Cʦޤ93Cm N~[gw, &fKʽ3fvF,$\ .Ae@! \t@Ga`oMQhDwY"HpI[Yeem-http-request-1.1.2/spec/gzip_spec.rb0000644000004100000410000000300612261227270020051 0ustar www-datawww-datarequire 'helper' describe EventMachine::HttpDecoders::GZip do let(:compressed) { compressed = ["1f8b08089668a6500003686900cbc8e402007a7a6fed03000000"].pack("H*") } it "should extract the stream of a vanilla gzip" do header = EventMachine::HttpDecoders::GZipHeader.new stream = header.extract_stream(compressed) stream.unpack("H*")[0].should eq("cbc8e402007a7a6fed03000000") end it "should decompress a vanilla gzip" do decompressed = "" gz = EventMachine::HttpDecoders::GZip.new do |data| decompressed << data end gz << compressed gz.finalize! decompressed.should eq("hi\n") end it "should decompress a vanilla gzip file byte by byte" do decompressed = "" gz = EventMachine::HttpDecoders::GZip.new do |data| decompressed << data end compressed.each_char do |byte| gz << byte end gz.finalize! decompressed.should eq("hi\n") end it "should decompress a large file" do decompressed = "" gz = EventMachine::HttpDecoders::GZip.new do |data| decompressed << data end gz << File.read(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz") gz.finalize! decompressed.size.should eq(32907) end it "should fail with a DecoderError if not a gzip file" do not_a_gzip = ["1f8c08089668a650000"].pack("H*") header = EventMachine::HttpDecoders::GZipHeader.new lambda { header.extract_stream(not_a_gzip) }.should raise_exception(EventMachine::HttpDecoders::DecoderError) end end em-http-request-1.1.2/spec/socksify_proxy_spec.rb0000644000004100000410000000324312261227270022176 0ustar www-datawww-datarequire 'helper' requires_connection do requires_port(8080) do describe EventMachine::HttpRequest do # ssh -D 8080 igvita let(:proxy) { {:proxy => { :host => '127.0.0.1', :port => 8080, :type => :socks5 }} } it "should use SOCKS5 proxy" do EventMachine.run { http = EventMachine::HttpRequest.new('http://jsonip.com/', proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match('173.230.151.99') EventMachine.stop } } end end end requires_port(8081) do describe EventMachine::HttpRequest do # brew install tinyproxy let(:http_proxy) { {:proxy => { :host => '127.0.0.1', :port => 8081 }} } it "should use HTTP proxy by default" do EventMachine.run { http = EventMachine::HttpRequest.new('http://jsonip.com/', http_proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) EventMachine.stop } } end it "should auto CONNECT via HTTP proxy for HTTPS requests" do EventMachine.run { http = EventMachine::HttpRequest.new('https://ipjson.herokuapp.com/', http_proxy).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) EventMachine.stop } } end end end end em-http-request-1.1.2/spec/client_spec.rb0000644000004100000410000006315512261227270020371 0ustar www-datawww-datarequire 'helper' describe EventMachine::HttpRequest do def failed(http=nil) EventMachine.stop http ? fail(http.error) : fail end it "should perform successful GET" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/Hello/) EventMachine.stop } } end it "should perform successful GET with a URI passed as argument" do EventMachine.run { uri = URI.parse('http://127.0.0.1:8090/') http = EventMachine::HttpRequest.new(uri).get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/Hello/) EventMachine.stop } } end it "should succeed GET on missing path" do EventMachine.run { lambda { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090').get http.callback { http.response.should match(/Hello/) EventMachine.stop } }.should_not raise_error } end it "should raise error on invalid URL" do EventMachine.run { lambda { EventMachine::HttpRequest.new('random?text').get }.should raise_error EM.stop } end it "should perform successful HEAD with a URI passed as argument" do EventMachine.run { uri = URI.parse('http://127.0.0.1:8090/') http = EventMachine::HttpRequest.new(uri).head http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should == "" EventMachine.stop } } end it "should perform successful DELETE with a URI passed as argument" do EventMachine.run { uri = URI.parse('http://127.0.0.1:8090/') http = EventMachine::HttpRequest.new(uri).delete http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should == "" EventMachine.stop } } end it "should return 404 on invalid path" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail').get http.errback { failed(http) } http.callback { http.response_header.status.should == 404 EventMachine.stop } } end it "should return HTTP reason" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail').get http.errback { failed(http) } http.callback { http.response_header.status.should == 404 http.response_header.http_reason.should == 'Not Found' EventMachine.stop } } end it "should return HTTP reason 'unknown' on a non-standard status code" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail_with_nonstandard_response').get http.errback { failed(http) } http.callback { http.response_header.status.should == 420 http.response_header.http_reason.should == 'unknown' EventMachine.stop } } end it "should build query parameters from Hash" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => {:q => 'test'} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/test/) EventMachine.stop } } end it "should pass query parameters string" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => "q=test" http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/test/) EventMachine.stop } } end it "should encode an array of query parameters" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get :query => {:hash =>['value1','value2']} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/hash\[\]=value1&hash\[\]=value2/) EventMachine.stop } } end it "should perform successful PUT" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').put :body => "data" http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/data/) EventMachine.stop } } end it "should perform successful POST" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => "data" http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/data/) EventMachine.stop } } end it "should perform successful PATCH" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').patch :body => "data" http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/data/) EventMachine.stop } } end it "should escape body on POST" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {:stuff => 'string&string'} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should == "stuff=string%26string" EventMachine.stop } } end it "should perform successful POST with Ruby Hash/Array as params" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {"key1" => 1, "key2" => [2,3]} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match(/key1=1&key2\[0\]=2&key2\[1\]=3/) EventMachine.stop } } end it "should set content-length to 0 on posts with empty bodies" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_length_from_header').post http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.strip.split(':')[1].should == '0' EventMachine.stop } } end it "should perform successful POST with Ruby Hash/Array as params and with the correct content length" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_length').post :body => {"key1" => "data1"} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.to_i.should == 10 EventMachine.stop } } end it "should perform successful GET with custom header" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :head => {'if-none-match' => 'evar!'} http.errback { p http; failed(http) } http.callback { http.response_header.status.should == 304 EventMachine.stop } } end it "should perform basic auth" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/authtest').get :head => {'authorization' => ['user', 'pass']} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EventMachine.stop } } end it "should perform basic auth via the URL" do EventMachine.run { http = EventMachine::HttpRequest.new('http://user:pass@127.0.0.1:8090/authtest').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EventMachine.stop } } end it "should return peer's IP address" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/') conn.peer.should be_nil http = conn.get http.peer.should be_nil http.errback { failed(http) } http.callback { conn.peer.should == '127.0.0.1' http.peer.should == '127.0.0.1' EventMachine.stop } } end it "should remove all newlines from long basic auth header" do EventMachine.run { auth = {'authorization' => ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz']} http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => auth http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should == "Basic YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhOnp6enp6enp6enp6enp6enp6enp6enp6enp6enp6eg==" EventMachine.stop } } end it "should send proper OAuth auth header" do EventMachine.run { oauth_header = 'OAuth oauth_nonce="oqwgSYFUD87MHmJJDv7bQqOF2EPnVus7Wkqj5duNByU", b=c, d=e' http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => { 'authorization' => oauth_header } http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should == oauth_header EventMachine.stop } } end it "should return ETag and Last-Modified headers" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header.etag.should match('abcdefg') http.response_header.last_modified.should match('Fri, 13 Aug 2010 17:31:21 GMT') EventMachine.stop } } end it "should return raw headers in a hash" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_headers').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header.raw['Set-Cookie'].should match('test=yes') http.response_header.raw['X-Forward-Host'].should match('proxy.local') EventMachine.stop } } end it "should detect deflate encoding" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => {"accept-encoding" => "deflate"} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "deflate" http.response.should == "compressed" EventMachine.stop } } end it "should detect gzip encoding" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip').get :head => {"accept-encoding" => "gzip, compressed"} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == "compressed" EventMachine.stop } } end it "should stream gzip responses" do expected_response = Zlib::GzipReader.open(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz") { |f| f.read } actual_response = '' EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip-large').get :head => {"accept-encoding" => "gzip, compressed"} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == '' actual_response.should == expected_response EventMachine.stop } http.stream do |chunk| actual_response << chunk end } end it "should not decode the response when configured so" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip').get :head => { "accept-encoding" => "gzip, compressed" }, :decoding => false http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" raw = http.response Zlib::GzipReader.new(StringIO.new(raw)).read.should == "compressed" EventMachine.stop } } end it "should timeout after 0.1 seconds of inactivity" do EventMachine.run { t = Time.now.to_i EventMachine.heartbeat_interval = 0.1 http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/timeout', :inactivity_timeout => 0.1).get http.errback { http.error.should == Errno::ETIMEDOUT (Time.now.to_i - t).should <= 1 EventMachine.stop } http.callback { failed(http) } } end it "should complete a Location: with a relative path" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/relative-location').get http.errback { failed(http) } http.callback { http.response_header['LOCATION'].should == 'http://127.0.0.1:8090/forwarded' EventMachine.stop } } end context "body content-type encoding" do it "should not set content type on string in body" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => "data" http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should be_empty EventMachine.stop } } end it "should set content-type automatically when passed a ruby hash/array for body" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => {:a => :b} http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response.should match("application/x-www-form-urlencoded") EventMachine.stop } } end it "should not override content-type when passing in ruby hash/array for body" do EventMachine.run { ct = 'text; charset=utf-8' http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({ :body => {:a => :b}, :head => {'content-type' => ct}}) http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.content_charset.should == Encoding.find('utf-8') if defined? Encoding http.response_header["CONTENT_TYPE"].should == ct EventMachine.stop } } end it "should default to external encoding on invalid encoding" do EventMachine.run { ct = 'text/html; charset=utf-8lias' http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({ :body => {:a => :b}, :head => {'content-type' => ct}}) http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.content_charset.should == Encoding.find('utf-8') if defined? Encoding http.response_header["CONTENT_TYPE"].should == ct EventMachine.stop } } end it "should processed escaped content-type" do EventMachine.run { ct = "text/html; charset=\"ISO-8859-4\"" http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({ :body => {:a => :b}, :head => {'content-type' => ct}}) http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.content_charset.should == Encoding.find('ISO-8859-4') if defined? Encoding http.response_header["CONTENT_TYPE"].should == ct EventMachine.stop } } end end context "optional header callback" do it "should optionally pass the response headers" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get http.errback { failed(http) } http.headers { |hash| hash.should be_an_kind_of Hash hash.should include 'CONNECTION' hash.should include 'CONTENT_LENGTH' } http.callback { http.response_header.status.should == 200 http.response.should match(/Hello/) EventMachine.stop } } end it "should allow to terminate current connection from header callback" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get http.callback { failed(http) } http.headers { |hash| hash.should be_an_kind_of Hash hash.should include 'CONNECTION' hash.should include 'CONTENT_LENGTH' http.close('header callback terminated connection') } http.errback { |e| http.response_header.status.should == 200 http.error.should == 'header callback terminated connection' http.response.should == '' EventMachine.stop } } end end it "should optionally pass the response body progressively" do EventMachine.run { body = '' http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get http.errback { failed(http) } http.stream { |chunk| body += chunk } http.callback { http.response_header.status.should == 200 http.response.should == '' body.should match(/Hello/) EventMachine.stop } } end it "should optionally pass the deflate-encoded response body progressively" do EventMachine.run { body = '' http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => { "accept-encoding" => "deflate, compressed" } http.errback { failed(http) } http.stream { |chunk| body += chunk } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "deflate" http.response.should == '' body.should == "compressed" EventMachine.stop } } end it "should accept & return cookie header to user" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/set_cookie').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header.cookie.should == "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;" EventMachine.stop } } end it "should return array of cookies on multiple Set-Cookie headers" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/set_multiple_cookies').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header.cookie.size.should == 2 http.response_header.cookie.first.should == "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;" http.response_header.cookie.last.should == "id=2;" EventMachine.stop } } end it "should pass cookie header to server from string" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => 'id=2;'} http.errback { failed(http) } http.callback { http.response.should == "id=2;" EventMachine.stop } } end it "should pass cookie header to server from Hash" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => {'id' => 2}} http.errback { failed(http) } http.callback { http.response.should == "id=2;" EventMachine.stop } } end it "should get the body without Content-Length" do EventMachine.run { @s = StubServer.new("HTTP/1.1 200 OK\r\n\r\nFoo") http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.errback { failed(http) } http.callback { http.response.should match(/Foo/) http.response_header['CONTENT_LENGTH'].should be_nil @s.stop EventMachine.stop } } end context "when talking to a stub HTTP/1.0 server" do it "should get the body without Content-Length" do EventMachine.run { @s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo") http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.errback { failed(http) } http.callback { http.response.should match(/Foo/) http.response_header['CONTENT_LENGTH'].should be_nil @s.stop EventMachine.stop } } end it "should work with \\n instead of \\r\\n" do EventMachine.run { @s = StubServer.new("HTTP/1.0 200 OK\nContent-Type: text/plain\nContent-Length: 3\nConnection: close\n\nFoo") http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header['CONTENT_TYPE'].should == 'text/plain' http.response.should match(/Foo/) @s.stop EventMachine.stop } } end it "should handle invalid HTTP response" do EventMachine.run { @s = StubServer.new("") http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.callback { failed(http) } http.errback { http.error.should_not be_nil EM.stop } } end end it "should stream a file off disk" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :file => 'spec/fixtures/google.ca' http.errback { failed(http) } http.callback { http.response.should match('google') EventMachine.stop } } end it "should reconnect if connection was closed between requests" do EventMachine.run { conn = EM::HttpRequest.new('http://127.0.0.1:8090/') req = conn.get req.callback do conn.close('client closing connection') EM.next_tick do req = conn.get :path => "/gzip" req.callback do req.response_header.status.should == 200 req.response.should match('compressed') EventMachine.stop end end end } end it "should report error if connection was closed by server on client keepalive requests" do EventMachine.run { conn = EM::HttpRequest.new('http://127.0.0.1:8090/') req = conn.get :keepalive => true req.callback do req = conn.get req.callback { failed(http) } req.errback do req.error.should match('connection closed by server') EventMachine.stop end end } end it 'should handle malformed Content-Type header repetitions' do EventMachine.run { response =<<-HTTP.gsub(/^ +/, '').strip HTTP/1.0 200 OK Content-Type: text/plain; charset=iso-8859-1 Content-Type: text/plain; charset=utf-8 Content-Length: 5 Connection: close Hello HTTP @s = StubServer.new(response) http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.errback { failed(http) } http.callback { http.content_charset.should == Encoding::ISO_8859_1 if defined? Encoding EventMachine.stop } } end it "should allow indifferent access to headers" do EventMachine.run { response =<<-HTTP.gsub(/^ +/, '').strip HTTP/1.0 200 OK Content-Type: text/plain; charset=utf-8 X-Custom-Header: foo Content-Length: 5 Connection: close Hello HTTP @s = StubServer.new(response) http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get http.errback { failed(http) } http.callback { http.response_header["Content-Type"].should == "text/plain; charset=utf-8" http.response_header["CONTENT_TYPE"].should == "text/plain; charset=utf-8" http.response_header["Content-Length"].should == "5" http.response_header["CONTENT_LENGTH"].should == "5" http.response_header["X-Custom-Header"].should == "foo" http.response_header["X_CUSTOM_HEADER"].should == "foo" EventMachine.stop } } end context "User-Agent" do it 'should default to "EventMachine HttpClient"' do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get http.errback { failed(http) } http.callback { http.response.should == '"EventMachine HttpClient"' EventMachine.stop } } end it 'should keep header if given empty string' do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>'' }) http.errback { failed(http) } http.callback { http.response.should == '""' EventMachine.stop } } end it 'should ommit header if given nil' do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>nil }) http.errback { failed(http) } http.callback { http.response.should == 'nil' EventMachine.stop } } end end end em-http-request-1.1.2/spec/digest_auth_spec.rb0000644000004100000410000000355612261227270021412 0ustar www-datawww-datarequire 'helper' $: << 'lib' << '../lib' require 'em-http/middleware/digest_auth' describe 'Digest Auth Authentication header generation' do before :each do @reference_header = 'Digest username="digest_username", realm="DigestAuth_REALM", algorithm=MD5, uri="/", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", response="96829962ffc31fa2852f86dc7f9f609b", opaque="BzdNK3gsJ2ixTrBJ"' end it 'should generate the correct header' do www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5' params = { username: 'digest_username', password: 'digest_password' } middleware = EM::Middleware::DigestAuth.new(www_authenticate, params) middleware.build_auth_digest('GET', '/').should == @reference_header end it 'should not generate the same header for a different user' do www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5' params = { username: 'digest_username_2', password: 'digest_password' } middleware = EM::Middleware::DigestAuth.new(www_authenticate, params) middleware.build_auth_digest('GET', '/').should_not == @reference_header end it 'should not generate the same header if the nounce changes' do www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg6", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5' params = { username: 'digest_username_2', password: 'digest_password' } middleware = EM::Middleware::DigestAuth.new(www_authenticate, params) middleware.build_auth_digest('GET', '/').should_not == @reference_header end end em-http-request-1.1.2/spec/stub_server.rb0000644000004100000410000000160312261227270020432 0ustar www-datawww-dataclass StubServer module Server def receive_data(data) if echo? send_data("HTTP/1.0 200 OK\r\nContent-Length: #{data.bytesize}\r\nContent-Type: text/plain\r\n\r\n") send_data(data) else send_data @response end close_connection_after_writing end def echo= flag @echo = flag end def echo? !!@echo end def response=(response) @response = response end end def initialize options = {} options = {:response => options} if options.kind_of?(String) options = {:port => 8081, :host => '127.0.0.1'}.merge(options) host = options[:host] port = options[:port] @sig = EventMachine::start_server(host, port, Server) do |server| server.response = options[:response] server.echo = options[:echo] end end def stop EventMachine.stop_server @sig end end em-http-request-1.1.2/spec/pipelining_spec.rb0000644000004100000410000000311512261227270021237 0ustar www-datawww-datarequire 'helper' requires_connection do describe EventMachine::HttpRequest do it "should perform successful pipelined GETs" do EventMachine.run do # Mongrel doesn't support pipelined requests - bah! conn = EventMachine::HttpRequest.new('http://www.igvita.com/') pipe1 = conn.get :keepalive => true pipe2 = conn.get :path => '/archives/', :keepalive => true processed = 0 stop = proc { EM.stop if processed == 2} pipe1.errback { failed(conn) } pipe1.callback { processed += 1 pipe1.response_header.status.should == 200 stop.call } pipe2.errback { failed(conn) } pipe2.callback { processed += 1 pipe2.response_header.status.should == 200 pipe2.response.should match(/html/i) stop.call } end end it "should perform successful pipelined HEAD requests" do EventMachine.run do conn = EventMachine::HttpRequest.new('http://www.igvita.com/') pipe1 = conn.head :keepalive => true pipe2 = conn.head :path => '/archives/', :keepalive => true processed = 0 stop = proc { EM.stop if processed == 2} pipe1.errback { failed(conn) } pipe1.callback { processed += 1 pipe1.response_header.status.should == 200 stop.call } pipe2.errback { failed(conn) } pipe2.callback { processed += 1 pipe2.response_header.status.should == 200 stop.call } end end end end em-http-request-1.1.2/spec/external_spec.rb0000644000004100000410000001222012261227270020720 0ustar www-datawww-datarequire 'helper' requires_connection do describe EventMachine::HttpRequest do it "should follow redirects on HEAD method (external)" do EventMachine.run { http = EventMachine::HttpRequest.new('http://www.google.com/').head :redirects => 1 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EM.stop } } end it "should follow redirect to https and initiate the handshake" do EventMachine.run { http = EventMachine::HttpRequest.new('http://github.com/').get :redirects => 5 http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EventMachine.stop } } end it "should perform a streaming GET" do EventMachine.run { # digg.com uses chunked encoding http = EventMachine::HttpRequest.new('http://www.httpwatch.com/httpgallery/chunked/').get http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EventMachine.stop } } end it "should handle a 100 continue" do EventMachine.run { # 8.2.3 Use of the 100 (Continue) Status - http://www.ietf.org/rfc/rfc2616.txt # # An origin server SHOULD NOT send a 100 (Continue) response if # the request message does not include an Expect request-header # field with the "100-continue" expectation, and MUST NOT send a # 100 (Continue) response if such a request comes from an HTTP/1.0 # (or earlier) client. There is an exception to this rule: for # compatibility with RFC 2068, a server MAY send a 100 (Continue) # status in response to an HTTP/1.1 PUT or POST request that does # not include an Expect request-header field with the "100- # continue" expectation. This exception, the purpose of which is # to minimize any client processing delays associated with an # undeclared wait for 100 (Continue) status, applies only to # HTTP/1.1 requests, and not to requests with any other HTTP- # version value. # # 10.1.1: 100 Continue - http://www.ietf.org/rfc/rfc2068.txt # The client may continue with its request. This interim response is # used to inform the client that the initial part of the request has # been received and has not yet been rejected by the server. The client # SHOULD continue by sending the remainder of the request or, if the # request has already been completed, ignore this response. The server # MUST send a final response after the request has been completed. url = 'http://ws.serviceobjects.com/lv/LeadValidation.asmx/ValidateLead_V2' http = EventMachine::HttpRequest.new(url).post :body => {:name => :test} http.errback { failed(http) } http.callback { http.response_header.status.should == 500 http.response.should match('Missing') EventMachine.stop } } end it "should detect deflate encoding" do pending "need an endpoint which supports deflate.. MSN is no longer" EventMachine.run { options = {:head => {"accept-encoding" => "deflate"}, :redirects => 5} http = EventMachine::HttpRequest.new('http://www.msn.com').get options http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "deflate" EventMachine.stop } } end it "should stream chunked gzipped data" do EventMachine.run { options = {:head => {"accept-encoding" => "gzip"}} # GitHub sends chunked gzip, time for a little Inception ;) http = EventMachine::HttpRequest.new('https://github.com/igrigorik/em-http-request/commits/master').get options http.errback { failed(http) } http.callback { http.response_header.status.should == 200 http.response_header["CONTENT_ENCODING"].should == "gzip" http.response.should == '' EventMachine.stop } body = '' http.stream do |chunk| body << chunk end } end context "keepalive" do it "should default to non-keepalive" do EventMachine.run { headers = {'If-Modified-Since' => 'Thu, 05 Aug 2010 22:54:44 GMT'} http = EventMachine::HttpRequest.new('http://www.google.com/images/logos/ps_logo2.png').get :head => headers http.errback { fail } start = Time.now.to_i http.callback { (Time.now.to_i - start).should be_within(2).of(0) EventMachine.stop } } end it "should work with keep-alive servers" do EventMachine.run { http = EventMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get :keepalive => true http.errback { failed(http) } http.callback { http.response_header.status.should == 200 EventMachine.stop } } end end end end em-http-request-1.1.2/spec/multi_spec.rb0000644000004100000410000000563112261227270020240 0ustar www-datawww-datarequire 'helper' require 'stallion' describe EventMachine::MultiRequest do let(:multi) { EventMachine::MultiRequest.new } let(:url) { 'http://127.0.0.1:8090/' } it "should submit multiple requests in parallel and return once all of them are complete" do EventMachine.run { multi.add :a, EventMachine::HttpRequest.new(url).get multi.add :b, EventMachine::HttpRequest.new(url).post multi.add :c, EventMachine::HttpRequest.new(url).head multi.add :d, EventMachine::HttpRequest.new(url).delete multi.add :e, EventMachine::HttpRequest.new(url).put multi.callback { multi.responses[:callback].size.should == 5 multi.responses[:callback].each { |name, response| [ :a, :b, :c, :d, :e ].should include(name) response.response_header.status.should == 200 } multi.responses[:errback].size.should == 0 EventMachine.stop } } end it "should require unique keys for each deferrable" do lambda do multi.add :df1, EM::DefaultDeferrable.new multi.add :df1, EM::DefaultDeferrable.new end.should raise_error("Duplicate Multi key") end describe "#requests" do it "should return the added requests" do request1 = double('request1', :callback => nil, :errback => nil) request2 = double('request2', :callback => nil, :errback => nil) multi.add :a, request1 multi.add :b, request2 multi.requests.should == {:a => request1, :b => request2} end end describe "#responses" do it "should have an empty :callback hash" do multi.responses[:callback].should be_a(Hash) multi.responses[:callback].size.should == 0 end it "should have an empty :errback hash" do multi.responses[:errback].should be_a(Hash) multi.responses[:errback].size.should == 0 end it "should provide access to the requests by name" do EventMachine.run { request1 = EventMachine::HttpRequest.new(url).get request2 = EventMachine::HttpRequest.new(url).post multi.add :a, request1 multi.add :b, request2 multi.callback { multi.responses[:callback][:a].should equal(request1) multi.responses[:callback][:b].should equal(request2) EventMachine.stop } } end end describe "#finished?" do it "should be true when no requests have been added" do multi.should be_finished end it "should be false while the requests are not finished" do EventMachine.run { multi.add :a, EventMachine::HttpRequest.new(url).get multi.should_not be_finished EventMachine.stop } end it "should be finished when all requests are finished" do EventMachine.run { multi.add :a, EventMachine::HttpRequest.new(url).get multi.callback { multi.should be_finished EventMachine.stop } } end end end em-http-request-1.1.2/spec/middleware_spec.rb0000644000004100000410000000676712261227270021236 0ustar www-datawww-datarequire 'helper' describe EventMachine::HttpRequest do class EmptyMiddleware; end class GlobalMiddleware def response(resp) resp.response_header['X-Global'] = 'middleware' end end it "should accept middleware" do EventMachine.run { lambda { conn = EM::HttpRequest.new('http://127.0.0.1:8090') conn.use ResponseMiddleware conn.use EmptyMiddleware EM.stop }.should_not raise_error } end context "configuration" do class ConfigurableMiddleware def initialize(conf, &block) @conf = conf @block = block end def response(resp) resp.response_header['X-Conf'] = @conf resp.response_header['X-Block'] = @block.call end end it "should accept middleware initialization parameters" do EventMachine.run { conn = EM::HttpRequest.new('http://127.0.0.1:8090') conn.use ConfigurableMiddleware, 'conf-value' do 'block-value' end req = conn.get req.callback { req.response_header['X-Conf'].should match('conf-value') req.response_header['X-Block'].should match('block-value') EM.stop } } end end context "request" do class ResponseMiddleware def response(resp) resp.response_header['X-Header'] = 'middleware' resp.response = 'Hello, Middleware!' end end it "should execute response middleware before user callbacks" do EventMachine.run { conn = EM::HttpRequest.new('http://127.0.0.1:8090') conn.use ResponseMiddleware req = conn.get req.callback { req.response_header['X-Header'].should match('middleware') req.response.should match('Hello, Middleware!') EM.stop } } end it "should execute global response middleware before user callbacks" do EventMachine.run { EM::HttpRequest.use GlobalMiddleware conn = EM::HttpRequest.new('http://127.0.0.1:8090') req = conn.get req.callback { req.response_header['X-Global'].should match('middleware') EM.stop } } end end context "request" do class RequestMiddleware def request(client, head, body) head['X-Middleware'] = 'middleware' # insert new header body += ' modified' # modify post body [head, body] end end it "should execute request middleware before dispatching request" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/') conn.use RequestMiddleware req = conn.post :body => "data" req.callback { req.response_header.status.should == 200 req.response.should match(/data modified/) EventMachine.stop } } end end context "jsonify" do class JSONify def request(client, head, body) [head, MultiJson.dump(body)] end def response(resp) resp.response = MultiJson.load(resp.response) end end it "should use middleware to JSON encode and JSON decode the body" do EventMachine.run { conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/') conn.use JSONify req = conn.post :body => {:ruby => :hash} req.callback { req.response_header.status.should == 200 req.response.should == {"ruby" => "hash"} EventMachine.stop } } end end end em-http-request-1.1.2/spec/dns_spec.rb0000644000004100000410000000233012261227270017663 0ustar www-datawww-datarequire 'helper' describe EventMachine::HttpRequest do it "should fail gracefully on an invalid host in Location header" do EventMachine.run { http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/badhost', :connect_timeout => 0.1).get :redirects => 1 http.callback { failed(http) } http.errback { http.error.should match('unable to resolve server address') EventMachine.stop } } end it "should fail GET on DNS timeout" do EventMachine.run { EventMachine.heartbeat_interval = 0.1 http = EventMachine::HttpRequest.new('http://127.1.1.1/', :connect_timeout => 0.1).get http.callback { failed(http) } http.errback { http.response_header.status.should == 0 EventMachine.stop } } end it "should fail GET on invalid host" do EventMachine.run { EventMachine.heartbeat_interval = 0.1 http = EventMachine::HttpRequest.new('http://somethinglocal/', :connect_timeout => 0.1).get http.callback { failed(http) } http.errback { http.error.should match(/unable to resolve server address/) http.response_header.status.should == 0 EventMachine.stop } } end end em-http-request-1.1.2/spec/client_fiber_spec.rb0000644000004100000410000000105612261227270021530 0ustar www-datawww-datarequire 'helper' require 'fiber' describe EventMachine::HttpRequest do context "with fibers" do it "should be transparent to connection errors" do EventMachine.run do Fiber.new do f = Fiber.current fired = false http = EventMachine::HttpRequest.new('http://non-existing.domain/', :connection_timeout => 0.1).get http.callback { failed(http) } http.errback { f.resume :errback } Fiber.yield.should == :errback EM.stop end.resume end end end end em-http-request-1.1.2/spec/stallion.rb0000644000004100000410000002305512261227270017721 0ustar www-datawww-data# #-- # Includes portion originally Copyright (C)2008 Michael Fellinger # license See file LICENSE for details # #-- require 'rack' module Stallion class Mount def initialize(name, *methods, &block) @name, @methods, @block = name, methods, block end def ride @block.call end def match?(request) method = request['REQUEST_METHOD'] right_method = @methods.empty? or @methods.include?(method) end end class Stable attr_reader :request, :response def initialize @boxes = {} end def in(path, *methods, &block) mount = Mount.new(path, *methods, &block) @boxes[[path, methods]] = mount mount end def call(request, response) @request, @response = request, response @boxes.each do |(path, methods), mount| if mount.match?(request) mount.ride end end end end STABLES = {} def self.saddle(name = nil) STABLES[name] = stable = Stable.new yield stable end def self.run(options = {}) options = {:Host => "127.0.0.1", :Port => 8090}.merge(options) Rack::Handler::Mongrel.run(Rack::Lint.new(self), options) end def self.call(env) request = Rack::Request.new(env) response = Rack::Response.new STABLES.each do |name, stable| stable.call(request, response) end response.finish end end Stallion.saddle :spec do |stable| stable.in '/' do if stable.request.path_info == '/fail' stable.response.status = 404 elsif stable.request.path_info == '/fail_with_nonstandard_response' stable.response.status = 420 elsif stable.request.query_string == 'q=test' stable.response.write 'test' elsif stable.request.path_info == '/echo_query' stable.response["ETag"] = "abcdefg" stable.response["Last-Modified"] = "Fri, 13 Aug 2010 17:31:21 GMT" stable.response.write stable.request.query_string elsif stable.request.path_info == '/echo_headers' stable.response["Set-Cookie"] = "test=yes" stable.response["X-Forward-Host"] = "proxy.local" stable.response.write stable.request.query_string elsif stable.request.path_info == '/echo_content_length' stable.response.write stable.request.content_length elsif stable.request.path_info == '/echo_content_length_from_header' stable.response.write "content-length:#{stable.request.env["CONTENT_LENGTH"]}" elsif stable.request.head? && stable.request.path_info == '/' stable.response.status = 200 elsif stable.request.delete? stable.response.status = 200 elsif stable.request.put? stable.response.write stable.request.body.read elsif stable.request.post? || stable.request.patch? if stable.request.path_info == '/echo_content_type' stable.response["Content-Type"] = stable.request.env["CONTENT_TYPE"] || 'text/html' stable.response.write stable.request.env["CONTENT_TYPE"] else stable.response.write stable.request.body.read end elsif stable.request.path_info == '/set_cookie' stable.response["Set-Cookie"] = "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;" stable.response.write "cookie set" elsif stable.request.path_info == '/set_multiple_cookies' stable.response["Set-Cookie"] = [ "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;", "id=2;" ] stable.response.write "cookies set" elsif stable.request.path_info == '/echo_cookie' stable.response.write stable.request.env["HTTP_COOKIE"] elsif stable.request.path_info == '/timeout' sleep(10) stable.response.write 'timeout' elsif stable.request.path_info == '/cookie_parrot' stable.response.status = 200 stable.response["Set-Cookie"] = stable.request.env['HTTP_COOKIE'] elsif stable.request.path_info == '/redirect' stable.response.status = 301 stable.response["Location"] = "/gzip" stable.response.write 'redirect' elsif stable.request.path_info == '/redirect/created' stable.response.status = 201 stable.response["Location"] = "/" stable.response.write 'Hello, World!' elsif stable.request.path_info == '/redirect/multiple-with-cookie' stable.response.status = 301 stable.response["Set-Cookie"] = "another_id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;" stable.response["Location"] = "/redirect" stable.response.write 'redirect' elsif stable.request.path_info == '/redirect/bad' stable.response.status = 301 stable.response["Location"] = "http://127.0.0.1:8090" elsif stable.request.path_info == '/redirect/timeout' stable.response.status = 301 stable.response["Location"] = "http://127.0.0.1:8090/timeout" elsif stable.request.path_info == '/redirect/head' stable.response.status = 301 stable.response["Location"] = "/" elsif stable.request.path_info == '/redirect/middleware_redirects_1' stable.response.status = 301 stable.response["EM-Middleware"] = stable.request.env["HTTP_EM_MIDDLEWARE"] stable.response["Location"] = "/redirect/middleware_redirects_2" elsif stable.request.path_info == '/redirect/middleware_redirects_2' stable.response.status = 301 stable.response["EM-Middleware"] = stable.request.env["HTTP_EM_MIDDLEWARE"] stable.response["Location"] = "/redirect/middleware_redirects_3" elsif stable.request.path_info == '/redirect/middleware_redirects_3' stable.response.status = 200 stable.response["EM-Middleware"] = stable.request.env["HTTP_EM_MIDDLEWARE"] elsif stable.request.path_info == '/redirect/nohost' stable.response.status = 301 stable.response["Location"] = "http:/" elsif stable.request.path_info == '/redirect/badhost' stable.response.status = 301 stable.response["Location"] = "http://$$$@$!%&^" elsif stable.request.path_info == '/redirect/http_no_port' stable.response.status = 301 stable.response["Location"] = "http://host/" elsif stable.request.path_info == '/redirect/https_no_port' stable.response.status = 301 stable.response["Location"] = "https://host/" elsif stable.request.path_info == '/redirect/http_with_port' stable.response.status = 301 stable.response["Location"] = "http://host:80/" elsif stable.request.path_info == '/redirect/https_with_port' stable.response.status = 301 stable.response["Location"] = "https://host:443/" elsif stable.request.path_info == '/gzip' io = StringIO.new gzip = Zlib::GzipWriter.new(io) gzip << "compressed" gzip.close stable.response.write io.string stable.response["Content-Encoding"] = "gzip" elsif stable.request.path_info == '/gzip-large' contents = File.open(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz", 'r') { |f| f.read } stable.response.write contents stable.response["Content-Encoding"] = "gzip" elsif stable.request.path_info == '/deflate' deflater = Zlib::Deflate.new( Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS, # drop the zlib header which causes both Safari and IE to choke Zlib::DEF_MEM_LEVEL, Zlib::DEFAULT_STRATEGY ) deflater.deflate("compressed") stable.response.write deflater.finish stable.response["Content-Encoding"] = "deflate" elsif stable.request.env["HTTP_IF_NONE_MATCH"] stable.response.status = 304 elsif stable.request.path_info == '/auth' && stable.request.env["HTTP_AUTHORIZATION"] stable.response.status = 200 stable.response.write stable.request.env["HTTP_AUTHORIZATION"] elsif stable.request.path_info == '/authtest' auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).split.join if auth == stable.request.env["HTTP_AUTHORIZATION"] stable.response.status = 200 stable.response.write 'success' else stable.response.status = 401 end elsif stable.request.path_info == '/relative-location' stable.response.status = 301 stable.response["Location"] = '/forwarded' elsif stable.request.path_info == '/echo-user-agent' stable.response.write stable.request.env["HTTP_USER_AGENT"].inspect elsif stable.response.write 'Hello, World!' end end end Thread.new do begin Stallion.run :Host => '127.0.0.1', :Port => 8090 rescue Exception => e print e end end # # Simple HTTP Proxy server # Thread.new do server = TCPServer.new('127.0.0.1', 8083) loop do session = server.accept request = "" while (data = session.gets) != "\r\n" request << data end parts = request.split("\r\n") method, destination, http_version = parts.first.split(' ') proxy = parts.find { |part| part =~ /Proxy-Authorization/ } if destination =~ /^http:/ uri = Addressable::URI.parse(destination) absolute_path = uri.path + (uri.query ? "?#{uri.query}" : "") client = TCPSocket.open(uri.host, uri.port || 80) client.write "#{method} #{absolute_path} #{http_version}\r\n" parts[1..-1].each do |part| client.write "#{part}\r\n" end client.write "\r\n" client.flush client.close_write # Take the initial line from the upstream response session.write client.gets if proxy session.write "X-Proxy-Auth: #{proxy}\r\n" end # What (absolute) uri was requested? Send it back in a header session.write "X-The-Requested-URI: #{destination}\r\n" while data = client.gets session.write data end session.flush client.close end session.close end end sleep(1) em-http-request-1.1.2/lib/0000755000004100000410000000000012261227270015356 5ustar www-datawww-dataem-http-request-1.1.2/lib/em-http/0000755000004100000410000000000012261227270016734 5ustar www-datawww-dataem-http-request-1.1.2/lib/em-http/middleware/0000755000004100000410000000000012261227270021051 5ustar www-datawww-dataem-http-request-1.1.2/lib/em-http/middleware/digest_auth.rb0000644000004100000410000000710512261227270023701 0ustar www-datawww-datamodule EventMachine module Middleware require 'digest' require 'securerandom' class DigestAuth include EventMachine::HttpEncoding attr_accessor :auth_digest, :is_digest_auth def initialize(www_authenticate, opts = {}) @nonce_count = -1 @opts = opts @digest_params = { algorithm: 'MD5' # MD5 is the default hashing algorithm } if (@is_digest_auth = www_authenticate =~ /^Digest/) get_params(www_authenticate) end end def request(client, head, body) # Allow HTTP basic auth fallback if @is_digest_auth head['Authorization'] = build_auth_digest(client.req.method, client.req.uri.path, @opts.merge(@digest_params)) else head['Authorization'] = [@opts[:username], @opts[:password]] end [head, body] end def response(resp) # If the server responds with the Authentication-Info header, set the nonce to the new value if @is_digest_auth && (authentication_info = resp.response_header['Authentication-Info']) authentication_info =~ /nextnonce="?(.*?)"?(,|\z)/ @digest_params[:nonce] = $1 end end def build_auth_digest(method, uri, params = nil) params = @opts.merge(@digest_params) if !params nonce_count = next_nonce user = unescape params[:username] password = unescape params[:password] splitted_algorithm = params[:algorithm].split('-') sess = "-sess" if splitted_algorithm[1] raw_algorithm = splitted_algorithm[0] if %w(MD5 SHA1 SHA2 SHA256 SHA384 SHA512 RMD160).include? raw_algorithm algorithm = eval("Digest::#{raw_algorithm}") else raise "Unknown algorithm: #{raw_algorithm}" end qop = params[:qop] cnonce = make_cnonce if qop or sess a1 = if sess [ algorithm.hexdigest("#{params[:username]}:#{params[:realm]}:#{params[:password]}"), params[:nonce], cnonce, ].join ':' else "#{params[:username]}:#{params[:realm]}:#{params[:password]}" end ha1 = algorithm.hexdigest a1 ha2 = algorithm.hexdigest "#{method}:#{uri}" request_digest = [ha1, params[:nonce]] request_digest.push(('%08x' % @nonce_count), cnonce, qop) if qop request_digest << ha2 request_digest = request_digest.join ':' header = [ "Digest username=\"#{params[:username]}\"", "realm=\"#{params[:realm]}\"", "algorithm=#{raw_algorithm}#{sess}", "uri=\"#{uri}\"", "nonce=\"#{params[:nonce]}\"", "response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"", ] if params[:qop] header << "qop=#{qop}" header << "nc=#{'%08x' % @nonce_count}" header << "cnonce=\"#{cnonce}\"" end header << "opaque=\"#{params[:opaque]}\"" if params.key? :opaque header.join(', ') end # Process the WWW_AUTHENTICATE header to get the authentication parameters def get_params(www_authenticate) www_authenticate.scan(/(\w+)="?(.*?)"?(,|\z)/).each do |match| @digest_params[match[0].to_sym] = match[1] end end # Generate a client nonce def make_cnonce Digest::MD5.hexdigest [ Time.now.to_i, $$, SecureRandom.random_number(2**32), ].join ':' end # Keep track of the nounce count def next_nonce @nonce_count += 1 end end end end em-http-request-1.1.2/lib/em-http/middleware/json_response.rb0000644000004100000410000000042012261227270024261 0ustar www-datawww-datarequire 'multi_json' module EventMachine module Middleware class JSONResponse def response(resp) begin body = MultiJson.load(resp.response) resp.response = body rescue Exception => e end end end end end em-http-request-1.1.2/lib/em-http/middleware/oauth.rb0000644000004100000410000000240712261227270022521 0ustar www-datawww-datarequire 'simple_oauth' module EventMachine module Middleware class OAuth include HttpEncoding def initialize(opts = {}) @opts = opts.dup # Allow both `oauth` gem and `simple_oauth` gem opts formats @opts[:token] ||= @opts.delete(:access_token) @opts[:token_secret] ||= @opts.delete(:access_token_secret) end def request(client, head, body) request = client.req uri = request.uri.join(encode_query(request.uri, request.query)) params = {} # from https://github.com/oauth/oauth-ruby/blob/master/lib/oauth/request_proxy/em_http_request.rb if ["POST", "PUT"].include?(request.method) head["content-type"] ||= "application/x-www-form-urlencoded" if body.is_a? Hash form_encoded = head["content-type"].to_s.downcase.start_with?("application/x-www-form-urlencoded") if form_encoded CGI.parse(client.normalize_body(body)).each do |k,v| # Since `CGI.parse` always returns values as an array params[k] = v.size == 1 ? v.first : v end end end head["Authorization"] = SimpleOAuth::Header.new(request.method, uri, params, @opts) [head,body] end end end end em-http-request-1.1.2/lib/em-http/middleware/oauth2.rb0000644000004100000410000000121112261227270022573 0ustar www-datawww-datamodule EventMachine module Middleware class OAuth2 include EM::HttpEncoding attr_accessor :access_token def initialize(opts={}) self.access_token = opts[:access_token] or raise "No :access_token provided" end def request(client, head, body) uri = client.req.uri.dup update_uri! uri client.req.set_uri uri [head, body] end def update_uri!(uri) if uri.query.nil? uri.query = encode_param(:access_token, access_token) else uri.query += "&#{encode_param(:access_token, access_token)}" end end end end end em-http-request-1.1.2/lib/em-http/http_header.rb0000644000004100000410000000345512261227270021557 0ustar www-datawww-datamodule EventMachine # A simple hash is returned for each request made by HttpClient with the # headers that were given by the server for that request. class HttpResponseHeader < Hash # The reason returned in the http response ("OK","File not found",etc.) attr_accessor :http_reason # The HTTP version returned. attr_accessor :http_version # The status code (as a string!) attr_accessor :http_status # Raw headers attr_accessor :raw # E-Tag def etag self[HttpClient::ETAG] end def last_modified self[HttpClient::LAST_MODIFIED] end # HTTP response status as an integer def status @status ||= Integer(http_status) rescue 0 end # Length of content as an integer, or nil if chunked/unspecified def content_length @content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) && (s =~ /^(\d+)$/)) ? $1.to_i : nil end # Cookie header from the server def cookie self[HttpClient::SET_COOKIE] end # Is the transfer encoding chunked? def chunked_encoding? /chunked/i === self[HttpClient::TRANSFER_ENCODING] end def keepalive? /keep-alive/i === self[HttpClient::KEEP_ALIVE] end def compressed? /gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING] end def location self[HttpClient::LOCATION] end def [](key) super(key) || super(key.upcase.gsub('-','_')) end def informational? 100 <= status && 200 > status end def successful? 200 <= status && 300 > status end def redirection? 300 <= status && 400 > status end def client_error? 400 <= status && 500 > status end def server_error? 500 <= status && 600 > status end end end em-http-request-1.1.2/lib/em-http/core_ext/0000755000004100000410000000000012261227270020544 5ustar www-datawww-dataem-http-request-1.1.2/lib/em-http/core_ext/bytesize.rb0000644000004100000410000000017112261227270022726 0ustar www-datawww-data# bytesize was introduced in 1.8.7+ if RUBY_VERSION <= "1.8.6" class String def bytesize; self.size; end end end em-http-request-1.1.2/lib/em-http/decoders.rb0000644000004100000410000001217412261227270021056 0ustar www-datawww-datarequire 'zlib' require 'stringio' ## # Provides a unified callback interface to decompression libraries. module EventMachine::HttpDecoders class DecoderError < StandardError end class << self def accepted_encodings DECODERS.inject([]) { |r, d| r + d.encoding_names } end def decoder_for_encoding(encoding) DECODERS.each { |d| return d if d.encoding_names.include? encoding } nil end end class Base def self.encoding_names name = to_s.split('::').last.downcase [name] end ## # chunk_callback:: [Block] To handle a decompressed chunk def initialize(&chunk_callback) @chunk_callback = chunk_callback end def <<(compressed) return unless compressed && compressed.size > 0 decompressed = decompress(compressed) receive_decompressed decompressed end def finalize! decompressed = finalize receive_decompressed decompressed end private def receive_decompressed(decompressed) if decompressed && decompressed.size > 0 @chunk_callback.call(decompressed) end end protected ## # Must return a part of decompressed def decompress(compressed) nil end ## # May return last part def finalize nil end end class Deflate < Base def decompress(compressed) begin @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS) @zstream.inflate(compressed) rescue Zlib::Error raise DecoderError end end def finalize return nil unless @zstream begin r = @zstream.inflate(nil) @zstream.close r rescue Zlib::Error raise DecoderError end end end ## # Partial implementation of RFC 1952 to extract the deflate stream from a gzip file class GZipHeader def initialize @state = :begin @data = "" @pos = 0 end def finished? @state == :finish end def read(n, buffer) if (@pos + n) <= @data.size buffer << @data[@pos..(@pos + n - 1)] @pos += n return true else return false end end def readbyte if (@pos + 1) <= @data.size @pos += 1 @data.getbyte(@pos - 1) end end def eof? @pos >= @data.size end def extract_stream(compressed) @data << compressed pos = @pos while !eof? && !finished? buffer = "" case @state when :begin break if !read(10, buffer) if buffer.getbyte(0) != 0x1f || buffer.getbyte(1) != 0x8b raise DecoderError.new("magic header not found") end if buffer.getbyte(2) != 0x08 raise DecoderError.new("unknown compression method") end @flags = buffer.getbyte(3) if (@flags & 0xe0).nonzero? raise DecoderError.new("unknown header flags set") end # We don't care about these values, I'm leaving the code for reference # @time = buffer[4..7].unpack("V")[0] # little-endian uint32 # @extra_flags = buffer.getbyte(8) # @os = buffer.getbyte(9) @state = :extra_length when :extra_length if (@flags & 0x04).nonzero? break if !read(2, buffer) @extra_length = buffer.unpack("v")[0] # little-endian uint16 @state = :extra else @state = :extra end when :extra if (@flags & 0x04).nonzero? break if read(@extra_length, buffer) @state = :name else @state = :name end when :name if (@flags & 0x08).nonzero? while !(buffer = readbyte).nil? if buffer == 0 @state = :comment break end end else @state = :comment end when :comment if (@flags & 0x10).nonzero? while !(buffer = readbyte).nil? if buffer == 0 @state = :hcrc break end end else @state = :hcrc end when :hcrc if (@flags & 0x02).nonzero? break if !read(2, buffer) @state = :finish else @state = :finish end end end if finished? compressed[(@pos - pos)..-1] else "" end end end class GZip < Base def self.encoding_names %w(gzip compressed) end def decompress(compressed) @header ||= GZipHeader.new if !@header.finished? compressed = @header.extract_stream(compressed) end @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS) @zstream.inflate(compressed) rescue Zlib::Error raise DecoderError end def finalize if @zstream if !@zstream.finished? r = @zstream.finish end @zstream.close r else nil end rescue Zlib::Error raise DecoderError end end DECODERS = [Deflate, GZip] end em-http-request-1.1.2/lib/em-http/client.rb0000644000004100000410000002100212261227270020532 0ustar www-datawww-datarequire 'cookiejar' module EventMachine class HttpClient include Deferrable include HttpEncoding include HttpStatus TRANSFER_ENCODING="TRANSFER_ENCODING" CONTENT_ENCODING="CONTENT_ENCODING" CONTENT_LENGTH="CONTENT_LENGTH" CONTENT_TYPE="CONTENT_TYPE" LAST_MODIFIED="LAST_MODIFIED" KEEP_ALIVE="CONNECTION" SET_COOKIE="SET_COOKIE" LOCATION="LOCATION" HOST="HOST" ETAG="ETAG" CRLF="\r\n" attr_accessor :state, :response attr_reader :response_header, :error, :content_charset, :req, :cookies def initialize(conn, options) @conn = conn @req = options @stream = nil @headers = nil @cookies = [] @cookiejar = CookieJar.new reset! end def reset! @response_header = HttpResponseHeader.new @state = :response_header @response = '' @error = nil @content_decoder = nil @content_charset = nil end def last_effective_url; @req.uri; end def redirects; @req.followed; end def peer; @conn.peer; end def connection_completed @state = :response_header head, body = build_request, @req.body @conn.middleware.each do |m| head, body = m.request(self, head, body) if m.respond_to?(:request) end send_request(head, body) end def on_request_complete begin @content_decoder.finalize! if @content_decoder rescue HttpDecoders::DecoderError on_error "Content-decoder error" end unbind end def continue? @response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT') end def finished? @state == :finished || (@state == :body && @response_header.content_length.nil?) end def redirect? @response_header.redirection? && @req.follow_redirect? end def unbind(reason = nil) if finished? if redirect? begin @conn.middleware.each do |m| m.response(self) if m.respond_to?(:response) end # one of the injected middlewares could have changed # our redirect settings, check if we still want to # follow the location header if redirect? @req.followed += 1 @cookies.clear @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies @req.set_uri(@response_header.location) @conn.redirect(self) else succeed(self) end rescue Exception => e on_error(e.message) end else succeed(self) end else on_error(reason || 'connection closed by server') end end def on_error(msg = nil) @error = msg fail(self) end alias :close :on_error def stream(&blk); @stream = blk; end def headers(&blk); @headers = blk; end def normalize_body(body) body.is_a?(Hash) ? form_encode_body(body) : body end def build_request head = @req.headers ? munge_header_keys(@req.headers) : {} if @conn.connopts.http_proxy? proxy = @conn.connopts.proxy head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization] end # Set the cookie header if provided if cookie = head['cookie'] @cookies << encode_cookie(cookie) if cookie end head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty? # Set connection close unless keepalive if !@req.keepalive head['connection'] = 'close' end # Set the Host header if it hasn't been specified already head['host'] ||= encode_host # Set the User-Agent if it hasn't been specified if !head.key?('user-agent') head['user-agent'] = "EventMachine HttpClient" elsif head['user-agent'].nil? head.delete('user-agent') end # Set the auth from the URI if given head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo head end def send_request(head, body) body = normalize_body(body) file = @req.file query = @req.query # Set the Content-Length if file is given head['content-length'] = File.size(file) if file # Set the Content-Length if body is given, # or we're doing an empty post or put if body head['content-length'] = body.bytesize elsif @req.method == 'POST' or @req.method == 'PUT' # wont happen if body is set and we already set content-length above head['content-length'] ||= 0 end # Set content-type header if missing and body is a Ruby hash if !head['content-type'] and @req.body.is_a? Hash head['content-type'] = 'application/x-www-form-urlencoded' end request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy) request_header << encode_headers(head) request_header << CRLF @conn.send_data request_header if body @conn.send_data body elsif @req.file @conn.stream_file_data @req.file, :http_chunks => false end end def on_body_data(data) if @content_decoder begin @content_decoder << data rescue HttpDecoders::DecoderError on_error "Content-decoder error" end else on_decoded_body_data(data) end end def on_decoded_body_data(data) data.force_encoding @content_charset if @content_charset if @stream @stream.call(data) else @response << data end end def parse_response_header(header, version, status) @response_header.raw = header header.each do |key, val| @response_header[key.upcase.gsub('-','_')] = val end @response_header.http_version = version.join('.') @response_header.http_status = status @response_header.http_reason = CODE[status] || 'unknown' # invoke headers callback after full parse # if one is specified by the user @headers.call(@response_header) if @headers unless @response_header.http_status and @response_header.http_reason @state = :invalid on_error "no HTTP response" return end # add set-cookie's to cookie list if @response_header.cookie && @req.pass_cookies [@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)} end # correct location header - some servers will incorrectly give a relative URI if @response_header.location begin location = Addressable::URI.parse(@response_header.location) location.path = "/" if location.path.empty? if location.relative? location = @req.uri.join(location) else # if redirect is to an absolute url, check for correct URI structure raise if location.host.nil? end @response_header[LOCATION] = location.to_s rescue on_error "Location header format error" return end end # Fire callbacks immediately after recieving header requests # if the request method is HEAD. In case of a redirect, terminate # current connection and reinitialize the process. if @req.method == "HEAD" @state = :finished return end if @response_header.chunked_encoding? @state = :chunk_header elsif @response_header.content_length @state = :body else @state = :body end if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING]) begin @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end rescue HttpDecoders::DecoderError on_error "Content-decoder error" end end # handle malformed header - Content-Type repetitions. content_type = [response_header[CONTENT_TYPE]].flatten.first if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type) @content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external end end class CookieJar def initialize @jar = ::CookieJar::Jar.new end def set string, uri @jar.set_cookie(uri, string) rescue nil # drop invalid cookies end def get uri uri = URI.parse(uri) rescue nil uri ? @jar.get_cookies(uri) : [] end end # CookieJar end end em-http-request-1.1.2/lib/em-http/request.rb0000644000004100000410000000064412261227270020755 0ustar www-datawww-datamodule EventMachine class HttpRequest @middleware = [] def self.new(uri, options={}) uri = uri.clone connopt = HttpConnectionOptions.new(uri, options) c = HttpConnection.new c.connopts = connopt c.uri = uri c end def self.use(klass, *args, &block) @middleware << klass.new(*args, &block) end def self.middleware @middleware end end end em-http-request-1.1.2/lib/em-http/http_client_options.rb0000644000004100000410000000273612261227270023361 0ustar www-datawww-dataclass HttpClientOptions attr_reader :uri, :method, :host, :port attr_reader :headers, :file, :body, :query, :path attr_reader :keepalive, :pass_cookies, :decoding attr_accessor :followed, :redirects def initialize(uri, options, method) @keepalive = options[:keepalive] || false # default to single request per connection @redirects = options[:redirects] ||= 0 # default number of redirects to follow @followed = options[:followed] ||= 0 # keep track of number of followed requests @method = method.to_s.upcase @headers = options[:head] || {} @query = options[:query] @file = options[:file] @body = options[:body] @pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects @decoding = options.fetch(:decoding, true) # auto-decode compressed response set_uri(uri, options[:path]) end def follow_redirect?; @followed < @redirects; end def ssl?; @uri.scheme == "https" || @uri.port == 443; end def no_body?; @method == "HEAD"; end def set_uri(uri, path = nil) uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s) uri.path = path if path uri.path = '/' if uri.path.empty? @uri = uri @path = uri.path @host = uri.host @port = uri.port # Make sure the ports are set as Addressable::URI doesn't # set the port if it isn't there if @port.nil? @port = @uri.scheme == "https" ? 443 : 80 end end end em-http-request-1.1.2/lib/em-http/multi.rb0000644000004100000410000000264512261227270020422 0ustar www-datawww-datamodule EventMachine # EventMachine based Multi request client, based on a streaming HTTPRequest class, # which allows you to open multiple parallel connections and return only when all # of them finish. (i.e. ideal for parallelizing workloads) # # == Example # # EventMachine.run { # # multi = EventMachine::MultiRequest.new # # # add multiple requests to the multi-handler # multi.add(:a, EventMachine::HttpRequest.new('http://www.google.com/').get) # multi.add(:b, EventMachine::HttpRequest.new('http://www.yahoo.com/').get) # # multi.callback { # p multi.responses[:callback] # p multi.responses[:errback] # # EventMachine.stop # } # } # class MultiRequest include EventMachine::Deferrable attr_reader :requests, :responses def initialize @requests = {} @responses = {:callback => {}, :errback => {}} end def add(name, conn) raise 'Duplicate Multi key' if @requests.key? name @requests[name] = conn conn.callback { @responses[:callback][name] = conn; check_progress } conn.errback { @responses[:errback][name] = conn; check_progress } end def finished? (@responses[:callback].size + @responses[:errback].size) == @requests.size end protected # invoke callback if all requests have completed def check_progress succeed(self) if finished? end end end em-http-request-1.1.2/lib/em-http/version.rb0000644000004100000410000000011012261227270020736 0ustar www-datawww-datamodule EventMachine class HttpRequest VERSION = "1.1.2" end end em-http-request-1.1.2/lib/em-http/http_connection_options.rb0000644000004100000410000000234112261227270024232 0ustar www-datawww-dataclass HttpConnectionOptions attr_reader :host, :port, :tls, :proxy, :bind, :bind_port attr_reader :connect_timeout, :inactivity_timeout def initialize(uri, options) @connect_timeout = options[:connect_timeout] || 5 # default connection setup timeout @inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout @tls = options[:tls] || options[:ssl] || {} @proxy = options[:proxy] if bind = options[:bind] @bind = bind[:host] || '0.0.0.0' # Eventmachine will open a UNIX socket if bind :port # is explicitly set to nil @bind_port = bind[:port] end uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s) @https = uri.scheme == "https" uri.port ||= (@https ? 443 : 80) if proxy = options[:proxy] @host = proxy[:host] @port = proxy[:port] else @host = uri.host @port = uri.port end end def http_proxy? @proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && !@https end def connect_proxy? @proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && @https end def socks_proxy? @proxy && (@proxy[:type] == :socks5) end end em-http-request-1.1.2/lib/em-http/http_encoding.rb0000644000004100000410000000716412261227270022116 0ustar www-datawww-datamodule EventMachine module HttpEncoding HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n" FIELD_ENCODING = "%s: %s\r\n" def escape(s) if defined?(EscapeUtils) EscapeUtils.escape_url(s.to_s) else s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/) { '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase } end end def unescape(s) if defined?(EscapeUtils) EscapeUtils.unescape_url(s.to_s) else s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) { [$1.delete('%')].pack('H*') } end end if ''.respond_to?(:bytesize) def bytesize(string) string.bytesize end else def bytesize(string) string.size end end # Map all header keys to a downcased string version def munge_header_keys(head) head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h } end def encode_host if @req.uri.port.nil? || @req.uri.port == 80 || @req.uri.port == 443 return @req.uri.host else @req.uri.host + ":#{@req.uri.port}" end end def encode_request(method, uri, query, proxy) query = encode_query(uri, query) # Non CONNECT proxies require that you provide the full request # uri in request header, as opposed to a relative path. query = uri.join(query) if proxy HTTP_REQUEST_HEADER % [method.to_s.upcase, query] end def encode_query(uri, query) encoded_query = if query.kind_of?(Hash) query.map { |k, v| encode_param(k, v) }.join('&') else query.to_s end if uri && !uri.query.to_s.empty? encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&") end encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}" end # URL encodes query parameters: # single k=v, or a URL encoded array, if v is an array of values def encode_param(k, v) if v.is_a?(Array) v.map { |e| escape(k) + "[]=" + escape(e) }.join("&") else escape(k) + "=" + escape(v) end end def form_encode_body(obj) pairs = [] recursive = Proc.new do |h, prefix| h.each do |k,v| key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]" if v.is_a? Array nh = Hash.new v.size.times { |t| nh[t] = v[t] } recursive.call(nh, key) elsif v.is_a? Hash recursive.call(v, key) else pairs << "#{key}=#{escape(v)}" end end end recursive.call(obj, '') return pairs.join('&') end # Encode a field in an HTTP header def encode_field(k, v) FIELD_ENCODING % [k, v] end # Encode basic auth in an HTTP header # In: Array ([user, pass]) - for basic auth # String - custom auth string (OAuth, etc) def encode_auth(k,v) if v.is_a? Array FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")] else encode_field(k,v) end end def encode_headers(head) head.inject('') do |result, (key, value)| # Munge keys from foo-bar-baz to Foo-Bar-Baz key = key.split('-').map { |k| k.to_s.capitalize }.join('-') result << case key when 'Authorization', 'Proxy-Authorization' encode_auth(key, value) else encode_field(key, value) end end end def encode_cookie(cookie) if cookie.is_a? Hash cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" } else cookie end end end end em-http-request-1.1.2/lib/em-http/http_status_codes.rb0000644000004100000410000000330712261227270023023 0ustar www-datawww-datamodule EventMachine module HttpStatus CODE = { 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Reserved', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 426 => 'Upgrade Required', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 510 => 'Not Extended' } end end em-http-request-1.1.2/lib/em-http/http_connection.rb0000644000004100000410000001331212261227270022457 0ustar www-datawww-datamodule EventMachine module HTTPMethods def get options = {}, &blk; setup_request(:get, options, &blk); end def head options = {}, &blk; setup_request(:head, options, &blk); end def delete options = {}, &blk; setup_request(:delete, options, &blk); end def put options = {}, &blk; setup_request(:put, options, &blk); end def post options = {}, &blk; setup_request(:post, options, &blk); end def patch options = {}, &blk; setup_request(:patch, options, &blk); end def options options = {}, &blk; setup_request(:options, options, &blk); end end class HttpStubConnection < Connection include Deferrable attr_reader :parent def parent=(p) @parent = p @parent.conn = self end def receive_data(data) @parent.receive_data data end def connection_completed @parent.connection_completed end def unbind(reason=nil) @parent.unbind(reason) end end class HttpConnection include HTTPMethods include Socksify include Connectify attr_reader :deferred attr_accessor :error, :connopts, :uri, :conn def initialize @deferred = true @middleware = [] end def conn=(c) @conn = c @deferred = false end def activate_connection(client) begin EventMachine.bind_connect(@connopts.bind, @connopts.bind_port, @connopts.host, @connopts.port, HttpStubConnection) do |conn| post_init @deferred = false @conn = conn conn.parent = self conn.pending_connect_timeout = @connopts.connect_timeout conn.comm_inactivity_timeout = @connopts.inactivity_timeout end finalize_request(client) rescue EventMachine::ConnectionError => e # # Currently, this can only fire on initial connection setup # since #connect is a synchronous method. Hence, rescue the exception, # and return a failed deferred which fail any client request at next # tick. We fail at next tick to keep a consistent API when the newly # created HttpClient is failed. This approach has the advantage to # remove a state check of @deferred_status after creating a new # HttpRequest. The drawback is that users may setup a callback which we # know won't be used. # # Once there is async-DNS, then we'll iterate over the outstanding # client requests and fail them in order. # # Net outcome: failed connection will invoke the same ConnectionError # message on the connection deferred, and on the client deferred. # EM.next_tick{client.close(e.message)} end end def setup_request(method, options = {}, c = nil) c ||= HttpClient.new(self, HttpClientOptions.new(@uri, options, method)) @deferred ? activate_connection(c) : finalize_request(c) c end def finalize_request(c) @conn.callback { c.connection_completed } middleware.each do |m| c.callback &m.method(:response) if m.respond_to?(:response) end @clients.push c end def middleware [HttpRequest.middleware, @middleware].flatten end def post_init @clients = [] @pending = [] @p = Http::Parser.new @p.header_value_type = :mixed @p.on_headers_complete = proc do |h| client.parse_response_header(h, @p.http_version, @p.status_code) :reset if client.req.no_body? end @p.on_body = proc do |b| client.on_body_data(b) end @p.on_message_complete = proc do if !client.continue? c = @clients.shift c.state = :finished c.on_request_complete end end end def use(klass, *args, &block) @middleware << klass.new(*args, &block) end def peer Socket.unpack_sockaddr_in(@peer)[1] rescue nil end def receive_data(data) begin @p << data rescue HTTP::Parser::Error => e c = @clients.shift c.nil? ? unbind(e.message) : c.on_error(e.message) end end def connection_completed @peer = @conn.get_peername if @connopts.socks_proxy? socksify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start } elsif @connopts.connect_proxy? connectify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start } else start end end def start @conn.start_tls(@connopts.tls) if client && client.req.ssl? @conn.succeed end def redirect(client) @pending.push client end def unbind(reason = nil) @clients.map { |c| c.unbind(reason) } if r = @pending.shift @clients.push r r.reset! @p.reset! begin @conn.set_deferred_status :unknown if @connopts.proxy @conn.reconnect(@connopts.host, @connopts.port) else @conn.reconnect(r.req.host, r.req.port) end @conn.pending_connect_timeout = @connopts.connect_timeout @conn.comm_inactivity_timeout = @connopts.inactivity_timeout @conn.callback { r.connection_completed } rescue EventMachine::ConnectionError => e @clients.pop.close(e.message) end else @deferred = true @conn.close_connection end end alias :close :unbind def send_data(data) @conn.send_data data end def stream_file_data(filename, args = {}) @conn.stream_file_data filename, args end private def client @clients.first end end end em-http-request-1.1.2/lib/em-http.rb0000644000004100000410000000073712261227270017270 0ustar www-datawww-datarequire 'eventmachine' require 'em-socksify' require 'addressable/uri' require 'http/parser' require 'base64' require 'socket' require 'em-http/core_ext/bytesize' require 'em-http/http_connection' require 'em-http/http_header' require 'em-http/http_encoding' require 'em-http/http_status_codes' require 'em-http/http_client_options' require 'em-http/http_connection_options' require 'em-http/client' require 'em-http/multi' require 'em-http/request' require 'em-http/decoders' em-http-request-1.1.2/lib/em-http-request.rb0000644000004100000410000000002212261227270020741 0ustar www-datawww-datarequire 'em-http' em-http-request-1.1.2/metadata.yml0000644000004100000410000001463112261227270017120 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: em-http-request version: !ruby/object:Gem::Version version: 1.1.2 platform: ruby authors: - Ilya Grigorik autorequire: bindir: bin cert_chain: [] date: 2013-12-20 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: addressable requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 2.3.4 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 2.3.4 - !ruby/object:Gem::Dependency name: cookiejar requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: em-socksify requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0.3' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0.3' - !ruby/object:Gem::Dependency name: eventmachine requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.0.3 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.0.3 - !ruby/object:Gem::Dependency name: http_parser.rb requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.6.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.6.0 - !ruby/object:Gem::Dependency name: mongrel requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: 1.2.0.pre2 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: 1.2.0.pre2 - !ruby/object:Gem::Dependency name: multi_json requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rack requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' description: EventMachine based, async HTTP Request client email: - ilya@igvita.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gemtest - .gitignore - .rspec - Changelog.md - Gemfile - README.md - Rakefile - benchmarks/clients.rb - benchmarks/em-excon.rb - benchmarks/em-profile.gif - benchmarks/em-profile.txt - benchmarks/server.rb - em-http-request.gemspec - examples/.gitignore - examples/digest_auth/client.rb - examples/digest_auth/server.rb - examples/fetch.rb - examples/fibered-http.rb - examples/multi.rb - examples/oauth-tweet.rb - examples/socks5.rb - lib/em-http-request.rb - lib/em-http.rb - lib/em-http/client.rb - lib/em-http/core_ext/bytesize.rb - lib/em-http/decoders.rb - lib/em-http/http_client_options.rb - lib/em-http/http_connection.rb - lib/em-http/http_connection_options.rb - lib/em-http/http_encoding.rb - lib/em-http/http_header.rb - lib/em-http/http_status_codes.rb - lib/em-http/middleware/digest_auth.rb - lib/em-http/middleware/json_response.rb - lib/em-http/middleware/oauth.rb - lib/em-http/middleware/oauth2.rb - lib/em-http/multi.rb - lib/em-http/request.rb - lib/em-http/version.rb - spec/client_fiber_spec.rb - spec/client_spec.rb - spec/digest_auth_spec.rb - spec/dns_spec.rb - spec/encoding_spec.rb - spec/external_spec.rb - spec/fixtures/google.ca - spec/fixtures/gzip-sample.gz - spec/gzip_spec.rb - spec/helper.rb - spec/http_proxy_spec.rb - spec/middleware/oauth2_spec.rb - spec/middleware_spec.rb - spec/multi_spec.rb - spec/pipelining_spec.rb - spec/redirect_spec.rb - spec/socksify_proxy_spec.rb - spec/ssl_spec.rb - spec/stallion.rb - spec/stub_server.rb homepage: http://github.com/igrigorik/em-http-request licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: em-http-request rubygems_version: 2.0.6 signing_key: specification_version: 4 summary: EventMachine based, async HTTP Request client test_files: - spec/client_fiber_spec.rb - spec/client_spec.rb - spec/digest_auth_spec.rb - spec/dns_spec.rb - spec/encoding_spec.rb - spec/external_spec.rb - spec/fixtures/google.ca - spec/fixtures/gzip-sample.gz - spec/gzip_spec.rb - spec/helper.rb - spec/http_proxy_spec.rb - spec/middleware/oauth2_spec.rb - spec/middleware_spec.rb - spec/multi_spec.rb - spec/pipelining_spec.rb - spec/redirect_spec.rb - spec/socksify_proxy_spec.rb - spec/ssl_spec.rb - spec/stallion.rb - spec/stub_server.rb em-http-request-1.1.2/.gitignore0000644000004100000410000000010512261227270016574 0ustar www-datawww-data*.bundle *.o Makefile mkmf.log Gemfile.lock misc gems/ vendor/ .idea em-http-request-1.1.2/benchmarks/0000755000004100000410000000000012261227270016725 5ustar www-datawww-dataem-http-request-1.1.2/benchmarks/em-profile.gif0000644000004100000410000047646112261227270021475 0ustar www-datawww-dataGIF87aw 8DBD$"$dbdTRT424trt LJL,*,ljl\Z\<:<|z|DFD$&$dfdTVT464tvt  LNL,.,lnl\^\<><|~|,w 8pH,Ȥrl:ШtJZجvzxL.zn|N~ H*<Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0UjXH͛8sɳg1 3} (ѣH*]ʴӧ6BJ5ԪXjʵׯ`\ K,fӪ]˶۷peKv˷߿ L0+^̸VB1˘3k̹3˞Ck-ӨSKz랭_˞McP7 N\JNμУ9.ֳkνnÇ/ӓ%#˟O5X & 68FVhfvx($rg((@ `pA+C>.` =AH2c7.IbXf営YQ B*3D PDݐ `Gg i%v\j[/`IDC00x iQT*\Ё 8n*mCQ̠( CU6q @ 1`"18kNl^b7L0 @ )0ѶO0 E Df(z0D'31ăk5@?<`C({ zx  Fr,& Q,Z<`@;;as@g!4<@6D(l%9 tIx0EL 42m42c4K;$5!Lg QKQt_"y5@NsEJbG=gS![@`܀N h+ kZ\޸>I41^ɳzzA  pW <(ڇD_M  "g:٬i iB:~v%Qt7Sy 7T/@R(- Gib㊤ $ .JB1%qXBvwp T n ,u nvDs!D!/1~2WGXR~ <#$,%wE}?A|C(,V`k_lpL!0 0'߇.֝'栅{pH(xU3񐏭w!''A?Oy +P8.+_|w/ f ' {pĽQRh"$k*BժճC<}>zWI~D@5gQq,G b+qP:@|: ?50`4(0'8%/VF~;w~X'G_E7,P/*~gz'6TQ' XCW]V`WGWpmuB!v[g^7wfxU7XJwC`/Qy?0"B:oqHAjhH(lsEE5o%/È~8h"U{MGL7CH y?<a$*y]r0vHgUHHxzpHrH}ȋEtC pPP-u!ܔiN:^snw0^uV,u(HbfdᕋH؍nfG&dV!N$]/P|F0$I"I&"8GPDo5y|=0v7Y'  U]e ` n505fP*ɒ.ib/PGKFpR(39K3P )Pbnd#fMSp З*rBXqIyɔ#:p@5#hF0o)URpUQHJ@FBvUuZ DճXF@xiI3cCuB9T9yER:Dɜ9ID0 OTweL dR=wFAxG/p5ztKD6~uɟF蹟YEP&)y1R!C@1V8G3s2=3 8;1IPGJuIf?!BpXu6]F*,'ByO r!CzKڤBPK`im:?*HV^X uc>0'%]t3IGNlab `7aH-jp+&=zA] k۶^ 7C cD9m[1` jf{ +kNњ lӯ..Цҟ9S'00u"S[!+>F~c =)oc[EP?GESK*57@VR-^n`.C/3Ī7}Lx)J>HuhKtE veC*F\yUR@#y^D6D9FP sMP@P!甇'|L/ +Bm]O_c_x?&lG>& H" hqְx_uTF{LdnTZhd%Q$-3]>2_Ċo놼l\/ڀַk[<:aRpF |]h0Ԙ4ʗ3ǯKy/UBP?Ryן-;v|^NVB`B@,.$`,Ғ)?O@zbCyrDo ^̰O0!N䉆!#nkq++n H/ppNCcNt5Uu6Vv7Ww8Xxnd+)!%EJCʣ ezNy E {.M@j].D)g >ݺ5A"C :Rی@ƒ#L1R2A@7 @`I"l\s?6z$"t$bJ)2)HFH@MQdžHɝl,&eWgRB`^ laKw[H ~(ڇjXPpN7X{뮯GHÁX Dn/S"ljLt+t9ap}bǚPd',N䬻 $xx;^Bmƹޢu)yߑ.,7~ S.~$B57φprXW }X0_} $9]Nx.a9 .>Az@L b| COPyEh8.JG ͡( | QIr  R`K>` bX;)8NWJ[O]QŨFѡdrxp",PY^%) h/YH `DBL9 _ђ qIz~(~ >-l [Җc-jAK8\ Onx Y";E.՝dڼ76qEJ~_kRp "pXb/n@P2Т ­\@Xc ?(S4(p;1q8P< X> ˂ 5@ dX .N$҈lq````pM E (@$ bU40@,a"@ nLg;Y {=q;,8SvRȠM5!|YA$uIp%L@$12L)H_A~elb"KM k0l;4Ax l Hu) X>M+T PTH؆"[8;pvdYQ3$lTx¬vޖȸiܶNw(8< %=OK(:, `_YֹuW-ʊZQQq3P+^@dܯFwu/h@҅1)d='JJ|B.I؝9_/w JDfk2^gIV^R?ȣR`Y*7rI^@W(}mz]/HY_OAؼG|滢_}B参?~A&##m8 8 &<\PUOhO@ bZD\h@?T/~.y`iO\^p`1XrA0Ԩ+X=+JЋn P о`( o` }!N+ cˮ@fPzjX`= OnfXS/ WAH &)ѽ~  nL$ӋOA*N]L1  kosQw @DJ|q^ y1^|tqsq@hqZo1M TA[ITNVJ" -d $`JD[x;Xq9\ӫ eA^/2<W7PSR%W%[%_iZ`Fn( RA&i+n2'aҾ(N$ [R((g@gfN90%o 0 Jn>++Q+W!Ϯ  $Gϒ0ҒF, 0@^S4 t+Irߋ1gR373;3?471H?gDs LP4MIe(r@w16 ) #?"Pl{S2`8 8'H8H @J *9Psk!mv R+s;;3=S@~ro9 : @#Er) <5+B/C3TC"3:CGDItC:L79UEAE13  h^>9wt+h fHeHi T QIEIGHH2Y!O 80O"3w%+M}01cAT 3/s?!TO@O)WTQQQR#UR'L b.@`88 < 6T33  C\n L5 P5 VKѳ2I T%5918VWP"T9?1Zp t T8p `VbIGYK2[;urtRU^^^_J`V" S,.A]s\Z.`aYu- GAU vb"WL1V/c]MY7\405ac`4 8 =Zh9f;(T_iijVjcMڃZI ?I± mRnvfkv5@tj mkm躞`V%qt @O.Y!1rpfKq;ml#I5nrN@d֖ڶ@Gxsw@t/ o"WsՖoKuj{wx7``ʠ nt,Fy@^J!m&@p6xi6@- \ Ni+6^`JBu vk= uL\k}w~?$ebM*FU wo~ WϠ B@j?^@ TVd~E\ |Ua"WIuT>җR8$(X/ t ,tft 1 vsF+`ao X-0x|ט؍hf! Z@z+D ԡ@}~ @h^/pb|`֓beƷ@(ktw Le (4 :`4YR * Ua&Ɍ2 <! hْo9wY S V lD$@[\@c VvAq2 n@Im d2l{i֑/3z2;ڣ?CZGKڤO|b'+B@N3p`.|5-@ڈD`F @ SlIrmb`* ~)o`*tr/bn@Բ譯lob/)_ȍ4eRS) #ƚ. jH <ES HS@2#Nz:a2{8  o$ j*0@r`=PT{[|[][*[כ۽[盾۾[TF 'Dd >}) wa 9Fcd(X1n3|_٣(@H> (R0 ^ aig9*FJ ʼn(qA' b; h?ܟo^JTd`Ľę  ;( v|Ϯ̉~4V\| X `>C[^Vdu~x~El5=cAXE~j~< `_ljo83]b2T>6LYSL,:wK JW@0ȅQj< EȘͽLnl@hb40T)N (ml35%?2_OS 3 #n]@(\ z)Z*4 6@pR9P8,   DM0@٧V>.`o#\GJnrZ@u"&*.26:>BFJNRVZ^bfjnr.jht:T1nrֺZ". GT!/{2?CGKOSW[_cgkosw{K~~O0i9 tg(k Gt`_\ CH P("Z& `D(ӯ]A_, f9IQ:5(Ќ7*t)ӦNsu*0Up*d^J*r(,۶!Kҹvͫw/߾~F,oh=\DC^H8JP-! N~[r U 8egh.¹gA7:ޑL0Z֯cϾ`n{%=/[*p? " X1.eA.<.CfCzLz!nZg?:&b $h& `Cz|5HP XG PP HM2PӒ4LɎW}) gkZW&o- Z @z\H(p"}"rP &bbBd(&@‰&٪ 'L*$0p @EJɠ @l4՗:rۭHfᎢ6ўu``'.Hfi'(Dد7 "Z"ڋ%I"bQrܱ_zLJG's!W2|G2) xu~ \7㜳&4@=؀éCT֌qIn `eRB@p?u?ׁ/ !?"۫$wH$7R; E 4?/$c B030 \ բ>LE> TSX %-u`y; 8B0c[ fa`q١Hx$C-8 \<'B1dN"h+jaX D @HE0 4⒃uaáTP Pq vO2hPxYcQ}/zWfb1:&;Iq%);"@b|M9qpdڈgzAZ"@6BK N5ܽ-|Pd]8&N" DwRsML';9u3'U#e%>`~nž !ij + ` XHHT(0*~TȣP؅K ,@ 92|"! KU!>5R% jի2*Wկ5b+Yjֳnu>ֶfTYU*3*3A.кy $lc/0`B@J0e!`yU,ǂqoH8 \'b0N YJ0VY8PA.yO_\`5rܵ>7 Vfb7"譩ZiFBϪA )|Bp $00@@F` p`A 0 4>`\F I킁ƀeA8؁t`D`Z  ,]rS'OP2u.[7/ `v1` @@X 3P@T(Md63etˆx@)Z/H@i3/\ mh8'Zv4 ք$~utApLam~s!Yʺvܭwyv9 , hV c  p!`+0DC$ +@]$mj[i۲{l@;8A!6$ sBV>a yg0z[w|@78unuEkbܔ%O9˗<0LFY8 YPlV@A kfd^7&?J BGiD  7 83`89;>$vm>UҙN\o'o̽qZ;l?6?E"]p]gǀ١<ևCu(=N;pG#DcПM` E$ @@X!J (x ~ܳ|I GtUΦ +( R0 A o^ Q#, @lNOmZ݁ bZ>1WFvA@@4tL HA_&u$tpt܁1j괕jf*J5dKaBmLjnn%+&A1cwyb2'!fs` yP vc$@G #Hg@uFB.!@g h P P W## GB"cfA'fI&hΣn4 @ 䕤-Ac&0!,kĄ \ pΞԤ-h*m( ҇NU`$@jegMbV_!^q2!<'2i)-itX5)ik ̕V@oP\) VV ceX^eL4h |=) (N\@@,l@ D|@ @,8pH>8Nhj|d@^ށ@ D~@ 0XR*j.4bZE M3 J8k\&g+(+0kO"Adx!<"ف0MhHi+ j\ ]"\aLN!@t#nBf!Dm82 k,j*T Ȇ((+mnl͢ L@j@i{F ^΢!"R^kGInm_6BA’|9%i]ׂ +ܖ*CkSy07x }@1†!cz찀wfp`"eFALn(\(BVjVA" ]2n*GA (*1Di)S>(`|#wL dցES -rrrF*iCBM)416 `@0ގ <",@`_swe23DئDr#b+=WA>[z!Hkh!6i,% =GLC @؉ $@.@ cLX@8KcnQo P x@< ,  O+8p@tWXTWUgVtx,@`5?dNMFU҇` Ip^`'`O@6YAФ)3 |@E:XhHЀYh;4ytm&MA<#4ft v'HG!;Dr%&Ɗ6vSH# S$~t; p% n@W@T z z۔Wwp(&#% L\` @RxW}GP0lK.7|o#3ȋ &{h1֝rwvEVSkR&%Yx͉8+ow oL- ܭp V8xw H@(4'H`7%XmX%@ڵ@Y@P@p2hl@|Q 4w9y9 Oy[rȀK_~ @DI A kCSz̀˭ (Z luz9{20A?v!A19}yۅ5(HIBW&@-y1&k280'H X "D/;uD8 t 6򻿋N$*dOYCSXR2 t@ xBuz|#a{"`r3YWFo|WfJsjLw'%6 }ջ8d"̽nyC0}G=2i=o@@~D T 8@Ap Ei- @b$ D @=k={ܶxS i«) @tI# L ND_߁#\.V +p~~'K _#K|GkA $nY_k~GW96K C@ m*}ª_  -P5(l#lj|=A@?h\4Fk2R g8͖w%T<π\/1>%ų/##GJMNOPQRSTUVWM Z[\]^_Ϗ!B!R!T19`BcdRBB3V!}~$XAJHƀÈ`G(r(;a}R>|('o94䙏/JKV@;%9?. aCP#.U~1L!0XxFv@ш! 8EzD)VҮm6:vF1L=5og+A&]9a0‡ BP!vK2 a37!֮a6V{A8+# \¢1 ][vehw fG[QFb~c#j&dgOn4r9Vkw Xb,>c Phvm{_7ÀH8!#܈r ,cj_0:-r"`eW,F\J#x P 9^X%lAhxMJ7v `vн2 \%dbϫ_@~{ {G+9S@MNVԡcf4_5k g -v0?[ 3@ 8p-8A=P#$!*jԣRZ*^ 8zXUP@ D4b cCV|5BD0E w"buBGb$Eh9Xd*.|;cG/'r<'R݃ yPg(,RYqV9hQ["G4rKN8.!(K501[&6@ $f1Ih@-PL#?AlЗ>9fƙ 2[Ōvώ˙p;G#H4)M8l죿̈G4AAC*$8TR9-z5!ZgIEHPAM$=MT]c6-0@J7-b1prA#A@2pJxB(Z3Kx1K/j^.aP9Fkv8[~wuޒ`ٜ.:[ZM=1yUC!Z½T`/! 9b1"\"4Xqq']u/2P\aމh@;S:ՏTl؁bmVygh K]nQPS'޽耀-@h:0L;'1'XZ&X2б(@+&i6_`4` Fǀǚ"ii/]ͯR ͕(+QwCMKa`Rb5F,< # Z$F8"[ۊ;?|@@k:^H8dC*rxXkaxxA$/ ,O?MK!" |<6P9((@W*U*//!6 ".!9` 0L!+P"L>279ޛ4~.`9842D@HaʖiH {U06Sԉ%1KF(K-Rp !< #x(JLL#X`6w: i#Z ea p 8A138 H< :N0%P3GYG9X<¤%:ȺD#*c?TLLL|9c^JNZY UEMWxaQ<@鄼NϸaLXkd;[R ] N`\6Y\1Rsܴ xH @BZ[yD_$`o9 M?MDTXXO` MX#\ +pOØ` a%ߌZFa@_Op'm5XhJ`Ye=x ×!"a4F3NccP;ː<V!`W:^Fpc)9:FHJ/4Ȃ]=05fcPX !^ON#>x`FP}P3(5=~%(pI3"i)Pfb>b)1z\UD$U=d^nfYEPL0iMYa>fyP{g|g}g~gghTn _@elG`s杜0YTi;e:ae~XyZ>H HcR [viE[`Ɂ0 XViJh}gjFUx֕ΑU` VV15:PL9v{u3 Hj790 ^@lfl9.H5H T냸 Ё=ߩ;|XMRm.n4 8hR)kk>)x9x H (cNˀixo٘ok=I8Tx nJO `goRk!icpzm NS@ekS?ɸ+r"+ W8 $ lWrE VpzLM0@9,_ 'iިabKq2oEM8q_`s8kqR <@4%.=%mF(H.e8tGΘtN xeĥs75耪1QUu_ Z^=?)\M`0E 8%F Rlm煖ka?t9_o'K wu[PݨVSxw}Q\ن(s ^t[IsLjaG ]R__mv^]]uuKw/` 3kUjQQp*}W Ok1JJ:avY*qz#:<T|@LiU@{^ ys@UH5?QtTP \pz.dZY/|D?h|]h3O)o18sxQ3vٹH Mq vXo <0X&>x/~8~Hn_ p~:㧁ߧg~ ɘB[˟$ ) % !xIP? EI͆1Z̮}E ʠ HK Bܾ՝!Kb䵢AvsVxEgCBsCBQcBcAcU5a -=MݥQ .>N^n~"p}| 0 〲;o 0$!B!@^D-*UbҦV/OV4@׳o1cá@8 B[gV[]4^ŗ_j]xPFCmȡAKub"Hb- V&ؠ؃ 2Hc6ވc:2 $@<> C0B. 0 `0P`6Z [rfG Y]@L8 J?D@%x"B/<F6fgz"pÜa(+x!#G$! ,pD4SXrcCpFEdG;$@ ]|"@12񂨜:H{ 4}/LĹuB&wfg21*& RFLq_q Nq r"\b|Oav֬̚<%A'HBTTTwFj6^Ҥ<0@ax6/+9#DtuM=uSPjC 1LnDVaLPÏG `6CVwMDowр$ vu1wys /HƆLwRSDLE,?OO}_o}>?&+8@ڇ4ܱ2q86,El|Ѭ {a]ٯrGR ] $8BL>gk?P 掠? p P P8!^]t!9a:$.0V qB~ A_쇿. 6@!T )\b$z KY ʑЄ-–9$ ۡpg(Md9Ӡ&r`[اNVƟ!0eR;"1/LKTŚ?]a#4mdiiUa˅/F2=1j ]"!W΂5i`r׽fFxP^ lX4<@ G#G,*CW^哽p1 }\ mk I;o&0 Ϟg59E/nъc|D-RC7Ñ5 aKfTȴ~* h% Z n& 1GTl@;7ޗA?PP{gr"H%=g)+P3H5h79;ȃ=?A(CHEhGȃR;(%Kj S& F$:Y[:#:&@Pe| 7-><9`#@ rG% &"=@" bB`L?3Ih9:WWY.0E 6ei4 Y`x9* 9,1nY7S5(5 @'  300U `_R45p2Ep` E ;@(.igpرhh$g ىy5Ʃ .1@*YO4@pLQ0 Kp1|AdY  C!v%,JP9)id ڠ?b 9Uw@LLXPD/`0g) pΠ˥   ZFP'0`3&@i?Qzwy7cP*TV'P`H5E.AL)LP@.5#j&Zv(_@d ):gPugXdE 38 tA#,0*z1)0\7,(lɐ< ~?TqN6@Z٪Ɲⶭ&ZUz1f܀PWƑWP0x-ۡ`L0AHԥL0-PP9Q'*`}Pr!ZG1+֭3kq隮8" 7-ʳGG| +(tjrg0uX$pSKOuʐd0LxU2'5D2`&O CJkG5{@B۸&riP>Nۓ?KPМp;,U@JwQ|CjY 'E"tozPd qR$ pp=`pUPkIj`+PO Eڹ 2 jR "Wq 5_6  @% l_sEj!&+* J]0kV @p E$q.yxp^! ; =T֌Y@,,߬# Ӭ#H{T FrpSP[̓0/' (}g˰0mѣ0WX_*H5m {)iL D:*$-$]-c[M#- /Mmࡈu,j= B_ h@F@7S~g=B$01HaKM#k8Bn#+"7 F0:@C>[. 4d } 7]͛͠T )߀ݼѰ]啞1.;UnpSj#L^#.}"?0uq+#E>nO,>0'%0]`^b^gh0ѓ N-Jth SP/ /091@aVn( \}ڀnsqD }1kڐ{#@3e.#PO5&d\hR~F]p# G /Y\ñ B &Ƭ(pB6O"^`I@44O# P/"hsEt/7"I C_B~P<n<~(_PAj1@~* ټ|} ^7`0j2?NP1`qGI)V ,$] !O f6ri.MciR( Aw# N Îf:AK 8T_0gS,p > ni9"S/#@_v& W ߐ8bDt>Q^Y~a\j4fvqwq ^Ѝ8x@v>/n`Tv|*0,R=<Җ 6%(h Pzj<搓y ũ~R"f\X~N 6,H0 (lzLh~@7 8&34&D+%RF =.Y2 prbE&+T`@`gT##4 "P bWxa-22ttD:y*)!|2݇@ (R@„ț9P;sg׎}=ti,܂"?|!吝دq>;;c# >6Aԓ88B 4)A XG8,2#+׿l1 "':`l#!2AnpRXP@'_ FDP@% JUz! 'q%|!\x@##qԑ!An6@3(.@1!j9@"\dmeP _"Xs 8B*(3Gy-5"{!>(pIZg .RFɄ4[q&\y@JH Q@Q7 8A@B#<DX3)).B>RbU .FP6^7!\&8BR `v !&8+jkUfe-" `aʉf%r޹?` gt&B2!%Xa t `3hr``!-; !E h(v HGWqHCh,snh&#nv}]׸r8)>$Daw( '?4kCX,:X026>zאX!<ȁ|%G 2>h0).i@vfal(-G@%\%X !D0%@ɇ<4t3Mp 5 @=0Ocyc/r %&f!#6#`O`;7̓ x"N( A/-"I2(% l|( F^%$Y &MW#Cys}APl$Q# Q%01( Αl9'liy"i]FAS(@ A1{@ƺL|hQX5p@.3$BB6%DoLhPdx =|:{_\d -ir!>AiH"B`Bkl={(|r8'ěO.鈫:`J8y&2oK* {_~YlBB f%kN\P;'!Ph 4`MP(;MN-9;( S `\Bw5q0ĪX0hl@!$Scrt(+ U2tqe9P$d0;H_40! :Υ2`}nPe @% l L Ed>d63&Xӝvb'CʄRR _ :iFpp] /D smeB#=miO85H\mmoA?h+2Pz :1t+mnud%; m8K`?7@b:Z0C,\sM\^" 2}vNp xR<캡($69}sOA\Tav.J{-< :6j<1 4C-L&oo NL fi  Md@BT, ;V @mT H ^q tAg$tAr`J4 x‹@Ho<,+|in Z6rpoI|PL AM֠踌 b >0 j B@O  A1EaP o=MB$Vm`<&dh`0:|X0}s}Rj`zP<" Dr>>S9? hD2Z>h*  n VaLwx@wpʀ501 (1`Pn<==gB CuCvsJj  !Q!K ⧥t @[ ) <O |*d,s RFqJ4QuQ:,<`J@@ : <:`'LNPe&—x RVpn`[hT $_`VkVq0vWXlW !, i0 e*p މߔ@d' ntGxr)F8 O3UU)`J@mL4ei^5V.Pj\#`ub)a Os!cHsֲ  Rd@@^ա6X OF( ;@US; /zn- ΏC+bimQf62@@% h@R .$o@ K<06E$\ֶm6bt00 @Bkun6oy0op//B  P D]D< F@\Hjrt1 C\ > 02Ƭs%p+R)Z !N U-DDFmipn`L 3HlIr r{7s6a|Zv`6`F` ,k H e`*@j@B Zsm`hH@pf $7`tQX }Q8kjRU>`vGgI^Nص(`,Po XK( j<@8x q@}_ DKdt ZƑ4 >xj]`>|8yX x5-_P?2&9 WYx[J0@ss[8 PN35 V@d3T \o)EN(fwJ/|[9^ X"lWn - y z욵M%3~g2Q*d 0X2@9@@ c@Rv N>s 8 Jfh ٜ !up:# K!Y Jwj5MV>,ct`B B #Oܬf Z FqǏ *zX:j\ ΃b9d]c,zj՛uPmrJ` ৙^`PT(M2 @6 r@<˵| ~ȷS(h,<^λ< \|bINq(X^@/]< %})=Q_5Rڃx <`/- M}Yi-էEZJ<^@~a= ҅ؑ_=o2@@t9. #: ǙUؿ`Qq@$+Dس۽`}ߟ>-6  Nܱ )  b`I~$ HU~FTa>e~imq>u~y}>~_^}@>~ꩾe j `Y>Q^]'~پ~1>5^>e ; R~!?%h`Щ4V )o'? Ua?{ B Sb?\nGRn@e?@Օ?Gp'Jea_@ ٿ), 8aZ??A>>#2\2'4*R+6r/8,3;O;.;>?` a!b D#`C &KKAͥh+l,m.o/p0q1r A##Kfr*GI[ y9z:{/Gh<}=~>?l#iFD6BG󗇛._-D!ƌ7rDNOĎ"G,i$ʔ*`H@ܬ Wl&4,j(sA&m)ԨRREG6 2B0F^bCA|x,ܸrRY'$ݼzoL5ZA56j x^hu̚7{3ТG.mzJF YB$ !(mߞ x/n8ױa'3ώ j.D O.C_n<+ <Шqu,,BM\0}I˻;{D .`>ѲŢ#BTƀZ`H4E2705 R&D.HXA#{TxҰs'ٽCx 4Z+G+'qB`DRQ vk~<$" g,JP"@x!CÛ^l _B%E"K3$H1UCP8h*խ./y($e1 cC,BY)>A\=TE"{]*L".@rʋ(C)X® 6,jS/hP@aH ["EmZ#FP~~1:op+]]]pmXgтul(FrM&muK^Nuoy(8:0ַW AOH]z=pNb&Tj@]s8 S]Ąĸ6 $f%L1b;oIrBJ~2-2ᵴpp1Or;Ƭ*8e͘c>3m4.X= i 'Fwx@9*,AG?L K$Χz#m˄axMsZ9`QMy%42iXӺ֣uNziр]I={hJ-jjC+txqRm2+S*2k@@^3`',hq1I<+9aNY$f btAfS;G-ȋYQyQ ϙ;ԁ`/kttYf]çsE145 ŷNKM~{'> 1P_+~]򖿼 uwvt2l4\s|q(8Գtx#pD/gA끣@u {wO?Nf:_CE Vh^IQ*HG/~x_? @c^ԝ$1oT_+ FAL1XDjFeD` WH1^_.P  ^;<Ɓ 0im\V`'| a'܀`4@ X ,n%ءB!CȄDZCq!RĀ ԡ ء ,+xpɡ *a J#aD(@L0[T !'5U`E&bэ ao"!-֢-"..r*VŐbJ`aЄ-&2&/XAT2(!"))l#7v#7x8#997"2vĔQ "\;9@/\@c1$@30@aL,-"#C^dE>Pݝ> 0dFM~@#$MnZ$$ b/LJA2HHjICQz/= @)F(T`NC t: i`#Q$0%TCP`kBVFZ[*0ۥ"L:%^~H@_%`# BZXac>&`ZaLa,"Z[nf .&iif.B5.$&/L!*l^ j&n"X`sDdq*r&qNF&h@TnK͂DWW^ruY*Dg/dg%L.'{F^$hsx&f` gЀ%@GpnB"A6.g@$e V@b/gsZFaFuV!w@&dV 42||JcZMh\J=(v"] x$\׉n)g$ cgJ@PTH!~%U~ݛZg)IIY h,ph .`E.g.njNA aAV !F)JĪLm8p%<@p@0HMNּ ЀZp 8$@@X J(&u ¬vd%@e6>@*ǒ48Tr**(hdQgEξ@@"X@ xgZ̆E@+ԩ| h!IZDeAlEF/&s^@|w: @* *ӹcT *4Ymt- a$@.~X$|-tun ܆ҁ\ @@hh&Yn4 @ W[靮2liWDm@,*o ^ʂ +DV0T})GiAdC pa\@h&{a,! p/iCrH›)$л(S_ @ ` |1uhf l1_ve O[̚h|A {͆:!ļ /h4xll0i$$k8Rd`ʝ@#y2PmJqRA)@ 0Ӳ-/40q$1rTAjܲp8@ x Ā5N 63ҍO@|*@@93|nip@F-\T0KfBko/BB)EL\@2A ГU 6@L4\U"4@sH@bl"PA\h \@x 4Ok LC 45<$ H5 X @H@ A41sA ruE4F'>rEv܇^% \ 쀑F aĀma 6K.;u$va_;03-j@"8@c5v*~q@ |mnqgo s^kDs"v!p7\ xwEv/];4 @ nuU@Qcfw{yOx?7gg{a)iCAg3g[qh &yUm@U @!OJ@fc,  Ć @,@  @A%|SY9.Vӓoxk8P@Pw9܁kۧ48 yx9 2X A62@ cT:cXU:Sm H@6D:c t;9J﹓'"w9`MTwF?Ʈy@5_tyBp@H@8;DU]\3+䈀 lw?GW{{i&#A:_I!>/t.v$7,7wvgF : ;-mc|@g q{6L]Iz最#;m2, x@ ϼU|͓몐 > ~{=}@F@"t.EwD dLj4#XFKTLԀzXDLKZI\#i{`0pH q)0QѩZ+8Lr*0UuYu}u}(ٽXz18|9iMrIhvxɁI8hZp@5??BUB9# ۣ?l`tL)qX1/J<ǟfZO$C0AdA0B 'B ~8#Bf`$*_$PTQ_;舩koZ Hv@4!6(`#ARd ,$)$H*4-?\0X̨|1x8jX*>@  G 3?7xoʽ910K4/@9OE5Ցx [\TH/rXT5W]wW_6Xa áD" #,8  `k۳padR-~aF+}#^vN~~ӄ .@peNf'x`V&H  پX@ FVPKj+LW\h%kXaukzuAD\Пayk[V j'pG_[>byaR"p}pITZP@H(ux' L 9Gqg}xumZD^>-"a 0p) ±l$mA#zn « {BՈ(4`C~7!JXr'WSC!`<P"! +*h"?"D%21 Ob8F2ьgDc86l~E ::{T-v )!_ P#aըUd_57j u;@n xYpI@rtFh9I^qy p&<1z H@VbP0] df MiRDg:չNvSŽNȡ:XE[.JB3 d-~ (Eb0#J#5ёI$l*&lN !$[p-oqь?(Sp c~TdAOE\ P/0S(P=BSUAT*VUխok\:W^7)$/#$Ǚ<%&J]V|% 8ƶu}e#Us^,9!lSV,1 Q%QqeR' 1gG혐\ߞ $^ 6 .].CG0.rֻox;^P w^ PҀIr o<_ _ G,%_tMWŭ"(By..< l!A4S2#6%3nEHY1_>61f Yu=nrb`aBPS`f L }f!a8*kp..?#m`s'Av,POyBB t DxF >h T[[XTD^2xWk.x@m=4zԥ>uWW/dc@ < g_ڟYB6f>3>2炭~qsc,XDf05ë޻R嬵5cEt|7zַa&[z;Չ*p# PZtA`S:V jVzZxEqN:QxZ@_ЇT} /_K?Ͽ$|)(>?|J@?[yK˼Xx.ߚ@@|{؅lBT'XA2z)2*",2, ̈%&B2*# t$t (`CHBI?LȫXBKBYC:@V533A;ID7OP5;8BCȶjUĶk۶`6po-2\/ ^0|WkCW,XZlCQTL"kP"$8E|hTI!j#=ƇFftwx4*HS\ gDÅ?ZE_`~[Fy$܀( Xq4  mx q;L [Ȋ$q$&c8뛿[[ʠJQ!K髾&K+J˿ | L D|©cʡ(53'́KH0"$ʉÅ[D$T1 d. kN$XZ#."[K#@N0L@Liķp)K7R pVY+dX? gihjxc@z#Ёmo%֔<|D'UJ@;M5 JB0TP#8:bBFWwq +KN[PQiC(tZ9()*e&uHPSt\O5C$g}ZG/4}[<y"M'&( cqpxJX,ћJGnt'$@ $YCҐl#! (enK![B\K6K!N VS=nEo[RI5cEP(TRP̘PIˀXNͤIrIȥHr$ ۓ;u噊 CDY H1Wieɋ|$&e;p5AMVYPsǷ숉}؍؎L {|֗ؗˮ Hr@Q"˶Jr= ha)xHЙ)8)~ <ٹƕKꋂֹ>?X*YYJs@Jqu_<[ZH۱۸PDR]+͏j'`\ qSS)Z(p98hf ']HJj$`%>,`άN̑].[v*[*X(%5^B;̑e@ŏeє646%09M5B^U!FG;QrMAړ(SJUjW% 2#m3UC6tUߝ9eMuV `qRd\罏ZTj`ƁUa[WMFb`Y_zat7@u'0)$K{_'8` p$Zz&(]uE_ Vel 1&2F' vXlN * N Tx='-rBB̐St b]7b3~ OPQ&R6SFP&TTvWeW^eMZt'|`6b iYxۋc$`'8 HW"K]ceYL;swxyz|}uqл=fhTP;bv8lNH{ƴMp 4QKR0u၂n陦k5heYpZuU9*H)x]3Ёf9/p& Jލitzi`6F1/E1Ež. $s+k%@Xųk,^N&VŮItkTkYRf'VgÈKaiFԶ 5~ؖ٦yV'@A m[x&Vm!^vWL^ 6ePֈ&7.Hn^pesZn^ivn;=t#2v]wG o1jp^"p^1p9ki7Gq(smUِ2"oq]Oq\!t]Hrq]qqqU&)X4N]rfp$Vf08Ap*/q4g6v#_ ~Xd3q\l>!3wsQ\AGDIp>[2t;+nVt)t(Ou pt9IJT/#UPNOVZ ,hK#Z#axu[w'uewgwR MwȎ Æ]Y vhYGbsG[R]-u y%syLjXotWr?zZH_?k`]~  :O>L'?|?7gy^lY=''Ǥ,Yw\2GE^ix|[3v`_I z, _\]{^`འz-8|| Iǧʇa x{O#,|;ߕ`l|׎ȗWׇ|Utuxvוp! L ֏'W~:-xc'8;}B_~7~'_ ɘ_`4ڀ30P~OuoH,ȤXJ˨tJ"G=vzԄs z}~h%Nxǝk<I|-N!MOK)E< d(k_s{Æ^t/o^DzoiMȓ;DCЯ:Hسkν{vӭOU&>LQU< tm0y 6zVhD Bm$bV#4h8b t+ h̜^9~`LN#5TVih’xxI3\D2F$SOR#p)ʉV*32ej&/p)t4&裐FFEڲh.*M꫰# &d((;/4ꩱj46K *+'ѭkT.,e1D5J+}I ͺk>9 & +fQp7C j;+e gf/&$ k0xƢDLwЬ@M1)&2,j|p u,Ϡ|B?s3Xw-dC  '%0i@P ,` 0gŠsِ}L؀n9ZG-:B߈%27&4>%7"olC>h`)AMpTY+Woe$7#Sb>mME/NEDt/I˿-00PG* G(iI&Á(A0p , Wv| ˃qm?RX 0- H F`?h@@` L Cp0 :G'&#C H:ك IJ@¼p`@`$`B1Hq'Lj %w>=HRe *`A ,`r HA8$@Mgeݤ9@7}x⫁ R@R0/ADYzsGP';F{ &(.aYBTƀ,S ;x~ =8jPry(m`uèF=WiICr㜋hEW^-0$ PQ#K=Q52XͪV'/u#0e6Pjz@ R @` 3n>5 >PQ#&+Pխr#%c#{"dֲ h&0 vHCD  ځ vENl;Ttc(kih)٫R׺%vKqNLg 1+M( x`^7` [~ 0x H@ x Νt a@" pauDwb˘ƼsL" TC!X9xxkby!Hn VÐ<A˦Yp$qa]8(e 2!,n컨08C(4hԵј4u}2>?HE>K-kRhig^oT3jрnAEQ+/ +KPkP(Mu<ﰂK p?\ |ށb4q""`&m ~8,$>8q_ OCz30M3e^<9HwģV 1xQyjt"0=CԷu>Oº+0(h^vAfaۇ  )@:o@Y<6GŁjT{!t@X!Ê!RPtsa_Wv$6֣'=րd.x? '* h@ \z"a?3F}C?np:`@W\VH0 nghm ('6c%G&83pKC@n?? (Cp9`4@!4׀[ID8MrhF!>:qqp!Fn#ЄI8+9>6;w51(:X6 fxI1+0t~jgm8/tg2` LJQZQ5zv9OA~px&z(B(NF؉Ae}ihP;P~ly6(bsH0AFbppl@go% %Ko4@j#pgpp]X>smhs {D`F * p8h7Aj-JJ3Dn2zJk1*v!a7p;3;0vARgKpc Gpy_W7 ęK ,ANb,8`7ڱf#D@Acu>is cֿvnvk*+4 | %y{ `cq,y]I:Lv֡;[ ]wjAp p 0Rc`z԰#Pk[.En%3+o  IR.?<.U^"@>CG~c](Q~`&@G O c>rN=Wu>[~_~}NYa(qXT0u]WmPss~ZNjL2g0tK; 2qn=~y.Λ9h9`02%7.U5A^ҭ:~rBD°``eU_6_0ezSξ;^:S.%@eDZZpFVQh@G@E 8PZcKo^lnc6E,|6np'^!O# 3\f@3?A[KP.@Un'ߤo^c@f>Q0ĕ\_ЎE0XnOoH_HK0ن4TPH\@ ƣaUF ,6ieЁ}HCoo*5i4L$LM$N´2ƄL)P9fb!E$\$U< LKfpM*92@sW^,?) ;Mt>P?N+n,^h_LS+j_KI@P9'U4J$r߂~]eVm׿5. $g#Dceg%q 'q#&Bcɥ%efs$,6Vv7Ww8Xx9Yyٷ Ed#[!je<R ]y"j `M]2@޽!,w)5$‡F?ij \y.\yQ[ 3Ј xh"KnRO:`t'~X i&5{+q Qk)^$bqG=!L$\^`(+0${1*XA^8'Iʇg GJ"A3 fh806֬-O"a6u&:0/KL,~p.i? l\H4 c5txh(.b╭8u` NP~xC(SD2-npr 6j` X^yw:z_뷦(dȀ.uqˍ8t |aga_}gW $(R*yd֙|U'yJ0qSwbh!Q-k dVvb{&dYoW1!j3v{mAXҸЎn*sACCXS L &#IX`tƍ?_x@V?ŹG9^(9/#tC$фTaT>??9"nal n5j U `(`MDJWm+;B.>~!IȽ^ K /A:ĕ=02.СUȆk1)rP!qqK]S*C+~qxr A\@`'qXa1x#?l]c<'T"YEhBm~s G@ӳaYJST%Bwᕭ\e0^؆@k+`^9W2rufz)^3fy'i*H&@?Daϕ)YNsT2dM VxƞܑbdP`j:m0H@h9zS|?vOt`B&AE~=z!`GC;nu-^Z`fھtMpBӚ%P x4]-DtI1 &rYV̅TׅS c-+ZJVqt(>|txȂP\*tJ[ͪo\`׍gHla٪V/$p+]`04Gc:6LNm+bW66` li[[޶yx^/@m| A0!-(nYC]]]nsrev[*Czk 1qzȅ/y' Vb@ L4 Ȱ ^Pga@ ^W$Vp$ p 78[la=lϊ>Kie򌋌%#Y,<g fsNõ͗<ˠsfԌY~8SX,MtMfF f,p,l.XMރ5ԅ47iWֱ`& ]?lm'+׸B Dr$lb<_[vͲ[ھv݅ĴPmrۆ#ZCS҉zS(J 6Z5Ӎnmwv o?'L߹VfBH>H 1,j3匿9M>'+o­NLE-f5X/ؕLu>ͱ3(ʣ>V\q-,s/\73V̓1EOG<<u_1}xv^<Yz׿^32A kR@@> B*8'FpPrPz6ȵt ꨰ[ 1Hb $Ai3k8+0U˲̪ ^G1Bnn&ꆎ#Mq\ .4/@ j/ZiaJ, B8+FYj F/ʫA>  !Gۀb!!! 62:"DL$ lq)&!# #KHQP ݠ(g)@)? )R-Ӳ>` (`l Z.rI($@P> *UbLȈ@Ld ^`ZrXZ.%[sL*'1`ʺȄr)3R'~ f4ä2O/+`5}Y |,6IhpS-9sr = t`x`=bwBA//, @TĬ@L4p4`Z@]Đ2%";<<@dOB'l  5Ui:Z *l6=-6P97@Bɓ0= >9[E_tز- BHSȠx@Y֤;-s: @tHN@6E }.ݼ.J4 (xtS| tSt6I-J)ASitYSܼHJXc`LKO NOSF'R+\-s ZR %2;u`gQ6.G Ɠr<B Vn(A+h#ՠ oS,?K1FF5vL @VilWuWUѐYKNuR 瘮 `N.R1n?o ^\PW,0TEIZ](n؈]]$R/c3ES !K Pnt2ZxF+SY/\L[iV0O^&n@3[1DkAg,Vcj2N`FL@77S >>lY9Ffɥkzqm߀:n+ #=r#[z2$r$ ik!n݀*jq=v D6)'X LRKxV2emfK~G90Ng4pAjwsWwmAFRSr G\=\ؼXט؍tS،(qfe 5ٍ ِ0:'c*ّ#9Y @#.;`@߀li7\x=[ٕ_ّc-kK hr)9ʖY vFx@aYyw/ :FnDe l>QyVrc10 ؙS_uA~ٟE99Kr t lsz Vz1 K=K [4saSj_ kF@` UhoX `gKer@i>uM9G:mbUv :@yd'@2:2 rW>O UlTڮZگ`Z ۰c S}%ňܙz\@*YT$>ay5qڴOQ@Pc[g8u` @jࣵ⸙߹>fFy෻:66h $q`a*J2e[Bʛ[;-=U*`?Y ?{A6;wT"?c׃H 0R`ڜ|`=TX~A]2N8ѳDeګ=2zWF"FՏbjzN)'[}6 =t٧;aݭԼV1Ncai|Mds1]=6ڭd]G>} *]B = gUi,f[$oK1^:>c?I~RI`b|6ro\7z1v"}ß^׾D86V^B& |ngݽ`܉AR>Fޞ[ ^> JyiP@Td#_w=kboK@0"CGgDm c'a]t_oa=@`!B&ph` bɜT`cwo \R~!h<%|>?(U- `u~ÿ46n~m^ = "&*."(1BzERVZ^bfjnrvz~^@L}]V`́"n>:SwWgkS:na{gֲk="^ɓN@0_p\XEg[Ny 8DuȤƎ? )r$ɒ.g2^ xj+XHk.PN7x,0)KIl*u*ժVbj%e3h (GeQtf@{|)kUۋ޾U' {!0B_R/,(vSz h `NPbu]TUZ9Š^Xa/ܴDviDIb23&<otADo#%AȳÑ JƲ%/Q1^"%^мbLi86Vyg: Yڢ|c'qk3\'Yp6iJF2$<u^1 <7[h(AW΂ a&ʊ8^ <E|^:mNy\8jғ(upr!=Eе@^`@~ZF\MPRMh4tFInJU}ƪ@R3*K Q]3-TAn6heKFk *fb\AJ-^jG+gdDIdmã#`2q놦p t1Km{@Z!|~@ᬦ(BZ; >2rT?(R ",H~ck>3?HX0Z ,(jHM^XO1"°oD~#&`8A xnC\6 B0B? @`w n0:0VlY )Dls{w2:] 0 fN9{Яp7l ϷrHTg$[n?`^G`],6+c:aC_/]B H6\{?2ʟ$  _(đ@ 0 ABt!\\-^\GE %pW༼ۉ0 ݽ`aD_-$H)81":>@), \i E2mǎ IJy1 9B:6@9"ĀٰbEHA#-BA/Z< r ~#&cDJ7D*_ 2 X64a.cq3@H@|@KLΤKt@Y,="p$8J?BA.nRR$G1NVre+B@@چUa8HA@$fxS~+fн P^E x@t@ hPQ F%&UaLffLȨlVVn`gvi&#u4 Ț]jm+I~V'% S-sM%@L @ǙcjPĦ&3FX$[% gm"gdD{uclbyf~H Wp0 DhD 8[RC  hV  bJ"`eljh^ \@`v^P(Jdvh)'(zb)i(M7 Q(h@64)gj)n!(D]kh1>mtV-iA | |@_VPљnC}A G]cU: ˜Bo.X/$' z@D,o+@$@.@@Q ШETf/8%\HaCBk~FHB[vBv j ԪDꡂe6bj{|+2z;+&ဌ~⳺B~tdk#t+( *0j:\(*,ږ~^jrBrþl*$S ÿ.lnj'jZB~ Z4k"X'OG~j¯n˒"m*К`)P8h%(*E,)d@ ȀX#;# @R<#=n=^mA<%0-mIm&n;l BrBl,"GLK\m$O$%ö"V$.B.:-ߪ.LK+&zB@ h 42_`ACafb.fc-. xĸچ]a @@5BzZVln `qAs J'ufub'p>71;@t8E @Gf?;37kkWthGCI$9u|)Ľmw+< {C.k:L?! A+gc_pX8?+~ÿu[\}HOKDaO8$Qd6OhT}}WlV}<_pX<&orox\w<֛@ABCDEƛ GIJ1KάPQŽSTWXYZ[[^Y;&VCb3df5fkúkDbnopqr uv wuu7 $XA .dM2.j;C4:6$'UdK1(MF X/\o\2( 8@D&HJ:UTG(| arX vW".hGUqΥ[p$Z|Ӈh`詓6  \`.pGUfk,>aϾ !͂gYlqq}+ON{ #ʀP׀Aw_] F**55jcBA~@5c @?nlZpB +p .P @d :NLY]z o Q&f4H$W 1I&y.%o^Zq>%t =@A BN@tv$P2ƂRt  ;q0s j;hF#uL B *KtV\sՕCw E \uz-oDHYP"9jHW`Tik'h:TPAcӢ:ǂvPkU WVxa'$ea% B "A6:<'6xV=H&WYJ]%@zaA|hiviV~.b :za jcU9|ҙ (F dVo֛o @nB́A2sC]H6FKucwM.n{vODKK;( (pgI܈qOfXqB/I1#\p}Tgqڦn @}no,X78[Wc(G (JȉwR?P. }u?``H)Tw,n#B7Uy! fD( d U$㲃Rqq֐Ijp8`?axGoE?rbm b8 iB~4QN\A#I*VOZbg+DK2FI0?c-myKE CA+,D"x@'"H(΄89O@@h@F +dN(@tA'y`RaL%M93m˜bIys(D9IAdfJ V5dȚfrёsG*uҤR2[piLmzS;i2S>nf H1}(@'>sNE'DȂ:_6 G.pZ+YBw*!^`Bh{嫝+!u_+Wmx& BqЄ( A.M\3zcsVemk]ZضV(b{[V}-[ 0o{\ ouĸk.@- h+`kN -`{  ]bXJBxn+c9ZP# F2~W\0  q\uBR'f$=aޖӈ X*r HdA@c5cC12+C%ri_#&X8N d!KU!c#·Lg!ȈR@2Oe(_Nr\]y5i+#9eb?WL&S%:Yms1ޒ!=d/vKJ4)PW 9pAP=2}>]`@JJ2  }"n72T4@ 5#:R@ p,$< 0`\ УEj 'b:-п?R0=#x<='Hۿx<67{Ճ1S+Kxe23!<҃A?Hj `+#>B0T C8AoHеS8ѳ:f¨*h@+PBCDT@b5I+1 8)Hճ` @%-@ã{&@ViiF-@D,hE%3:?"E%,9Ȼ (!;8"E%; :j)F!+0B?<2Fn Ex;#Q1,Fr eq?L X'?,Ђ &h=02R? sadK+3L]H#GRdƱ,%ȽAk06\ƝĂ#/9 hcǕ{H,|89uHpJZJB0;h%d*#Ёxor,ER\vKut;< 0,L֛AG\ CI!X2ɤLMМLV B-Hh7hTLJIy=7 hK@ XN}e$f,pP&؁>M8FlO LXKc\Dh x- (Ы @*زsNj8L7Cq H ` `0z`@G Ё( ݁ P 01*#% ppEQ}%2 %ԄD$]! HhK"1eR3%R;UR&uQ+H5;Q5<QZ%"y+ݬHhS#< JT'(4 PEN 8TVM-Qx1ޔYVe6ݔ Oj]Vk{(o8  0hȁe"Yqr=ڳ=ܓzo:XvP̢p-% !8&#Lt' hUXD"<mYj$Pu--U%t("IZL49ۡ=ٔ-m (Xږ퓭YR|Z_"(5Y١} }[5\ܢ]<1g]iVն<@'AHB,,Hʣ xYR"AOXP #9UޒD08dXA80=xpȴBH%f{&&r"I`$4_`<_"?HȔ$Ir`e݁A,iIB;4Na (`!@a$V[T/Xǁz`TE>6K%#buleV`ݍcV9 ๡5k(|J$`Fi+hG>A~w\_qhH7_1X<A:E XE2bZU4e"`W/#ejj3&YcP(gI(Hf:~"c]m8kJWؚAF8<(Pd,!kʢ {>ʑ`-?< ΣCHˤ耸(xh강  `hLbj|a!Dy#`.Ddb (ؘiƤ j "V Z(c-P')筦U 쪾~f%"˲N,,mO.%h,'th+hsBhl$0>YPy#c>_ZL0xBڷ Ie"˯nwUXMr18&G`*>j  .Mrgr'o'r#!oԅd&5k2/-3wjl.mZD $&;`14[PϦeV&P!.% tŝnX'h(Y,xȗUԊW "}Pn o]4qRX.gB(؀ n Ȅ2 oE7ߊ_v$t̚ɪw7u!pkHxyg xr7uPx _<~4[P ugw'o ` 8v?"FmN;ECETMzcVp}`Xmw0|=WT>3-tہp_W߁ X"w>{y{|A]t%r(`X׉X}}rp/م`m1x) @{Kp5EVۮ Ъ }m{0=l{Qr 8 8ˋJ]ŏ[aX $^ZeA~ Pl@ z|F×C33 3%ŲsAPs @T!`5E*9bÆ);uR`a!vQ `VvlVW##`2v 0Uts1@V{M7u10b|}kDk0:t2HqGp7RG&VIsc<Сf? vvFǁ|e=8Ō;~ 9ɔ+[9͜;c9ѤKgjZԬ l!50b5ք+u1->%AlB4fv7_1$x QpEX_c%Y%a_aT_ 6+[7Q@XGy IdFdJ.ddRNIYDBX Xjܗ[1hBojl$ 4t@Q?MGeHz-N9ct@AcRVffnfc0+XZ4p HA jL`B"pI,k +A?|P; Ȁ~J0t<PKP +>jk!XC8D@z,DYY 0$@[Y1Q'xP*AK@0Q3H'afeܜ @9^L1*9qrWL8V_uZou^Cubg6 ji(,7y4A "j|@qai3FX AAu\.we>瑊GH36`F} :90 XC ~ '`7 &6B V x 0&?`B0% Oq8@> e? @ HS2<(yڑFN8hp-!Fn& a=d3bg( 0 P1 f|`@(D "HD6xCH?xaMh? Ā(&! ( R@ r,!9C*ribJB *X$%KHBЁcc9ӭP@"5":}$$tĠ.#ְOkC.t%2fgU&$TP0әl#eqF PwLǴ:Rs0XS`hDtGQ3@@@NsJx00l uĝf#K\H@J 4Ҕt,mbҘrMnP h H@|`q8)0`Y<  X}Nj9cIY iK 2S2 X"MԂU"TEg=հX+0 έL_,BL|]\Q5p!R)T>p+IV|aIԥ2M!&MkY_ vLkMV8z h~ppj!,Ì(n,@Ua5%RyEUBW b]*dқ[@wQmB5`k:UP:`*cGSd`NX8q 0lnceWy~|5b_74oc6<RKcCUFdy >g0V@~n 2I˥"4)o{scڏ)M Т~op| I% E>tmgRVCeVC8ΘFV懰z9{f,\z׼{ l:4k@n1 6|pjHmRaFbmh u [ǕP`qS⊁V TUT&=[ p~j`g!<&F"^;x9.p P^êOtP ~F -pr50Xa`$1C@ѵa4CP'p[D|ـ(hM#&.y)KcRCd&4M?ZKP3 k7y/k^޼3l=9Y-ɉk |X U-6uC ?,=4T&@1tAZ}`ʁ/cX.@m3=KJe wpp>>_?p> *C?տ||/`})}ڧ67-@ &F#[# .q ?2 C0t,`&C@`>3Ǒ.vݲ#W h`bdq6*6h#7psS7wj{#~S$Vl=]ȅoC7v7l7wGuhw){g{g0`u8c 0-/e`9H76c(D2P=l` N+@p2c8 ((hp؊r0$7l2lp 5#R~hwIkPI$CIJ$HIŎI4J|(IzhzWV#NU#2' q.MwUĉQpա-I)`B&bp )^)5+ْ?/1)q5F:ɓ>)Y߀. 24iR9IWUyژ5jh9:8\;#8S?A5Tςx_L"?@Sy TBETW阏 )&1uT'T`FTrJ$eE{I9F0)*y)%ɚ(䚿a!HqnIFk`uI̹ !959o!d(E\?ЗE\ AY\5; ʠ6 yP2GRxœ!8`0ix'y&wXu0I"9$qhȁ7A7Xc0pI.7T&c$= SDFJʤ)PEg2F"]ZGKdlp zXç!MG,fKeg=BddMdQ6eV*aEƨLdPiS *cJ6egx gk+P1=z[؆`|pb~yz$/ZYeɔn)WӦ3f$PZ5&7)S;5Ѭ@ꦖUҺxzٺj;aVت:\;wzF2p'9L⵽Ba cPlpi+NCj}qKsb+x[{˟$ c1zq@K([zt/빟 Rⲡkc1p!T#@3t XRSٗPT1鑇{7KBUKHm+%Z[x*!XIdV/u!'Y I6ƼYʩY++-ګ[Apڵn셾۽YYۘ#!˻ | kY3\[̓"rpC}IiU"14Б UR+)Pz6(ʊ$ Qk<`0FDFi&K)CE\H,J|?lgL{U,ħ N\^Ng|H(7Vq,q sWp47rt 6A|pkf ɑ,ɯ`334rpQjwKp!ڨjNƩSf/q `8RƋG$K@c{Bvq0]k@bVqſwW6A팮IppWui.nNjU2V0.,Y|ZA峥 _[6Ҟ:[گj"S 3N:@'e&.&qPRZPʏکzEokG5$r P֐pd%[?[ogN,EnNvh۸$=}^rlNmK;>`A?}EMT'kJyU-`/)4:H 5 k%]cSл崾X+ ,݌qjdoDƣ/χOS2J_a5R$_363:4wt?QݞDžd,0c䎀Rn?qy~ J!*VDtR ^# ;~ce{h o}qTvODۡu)&n$g0l^`E d!ґ^䀑"lZ Z 4}P"ge]`tlR,$R@Q"O*H‹ՙ{NzXK_;lYg"m[{>`u[>k˾k7bKzM'5d.|t IM(8H): DFJLaǖ=vm۷qֽwo^pxqDž3i,Ȓ@:BXpjdr%$Y# aPckl+g|׿ P@gZ A?>XM?l#8OA.|$jZȍX`(# ~*4 . I R!w@"L7&3fY*+Џ/ S12LDPJ e<Z0=- x) Ž@!62@ M& \,T'Ts@R#VrLQOU]yW#Uط^ 3N:.`$LgۢkL $0P@H =<J<"pa NkV4'N< EX)1X@AYIYǂQNYYN ږa,PV`pM&ѵ0֘@V u6,6`TĭAv XHA!:淿qn<䶤[o R[  O\+(!O)W͏YA3mVx!h3}&Vu<.CRPtMqw'#D;bx狩z䱜su~^^9p)y~3-pӥu4) A1m|p!g(4zUbdgTvƲo8Wȕ+[g[3` lTY ;5R/9CT1$<hsռo}7c>l@C.ڑ`nRnN5+|_ǓQ6lY +oR9͗]=H(N̳"`2X\>!(z!Xux\>ֹu[؎oxnD,YPXтR!}gnm>v OK{@4܋]tte 9^Ho~};A^@~70 ; sBGj `jD#xT,{V>2V'Ydy:Y,_c,ȯ-MU~?? x j>O_ @b܏z-%ԏR J%k| !-o&:@((@`b,naeKìh0>@!.&tC2/lpd0MR0 pPHҠaZn! @v,vO& }n ӄ { !AA`@l V ,bP:1{7Ag"AH,ZKPbpAfQb n!lQKH12`.@ Lg¤&&" ׆pʖ qQu1-kx@\N@_1,bL.r-O|Q/Q!oXQ1(T o   !![2&M"kh@X.! %hQ8RQMz`AQTP&o%ń*'(|`A #e!.,$Cb@JvEmH2% P.r*GH*7.2l,!a#+)& KE߾p!2҂2O.s3o0c4Fx2,`@bWt۲292Ӭns7)3_+2urټ|#bNbϠ3:3-Juxrss;sLkNb< fA@Sor@s?I O%A.*<ì;2 4Au=t s"ƻʪ!=?>D8SNA@DU4N@8D`B'=? QCCcDe:/}tE EAIEÊh4 SQ,.H`JHaFHH'S!tÔtҴM J$bKe,BDIO ĴeȔ%t8]M3lMI5RN*058STxF8uHB8 e)r R%u(QcVgiViTFJGr2Gːa*Rue5QN}VmWQZ] WIR1%| tY z!, [uZ#^^B[D@: t0S5@u&rb9b]U_Մ^b-vc Sߖ$T5u 6M6%J``h4e]bC6]@cuLnv?vlsx>+V ::jL @>fkikV.ӖVwr/gmIibf!gvS|tu`ؒn$Y~Jvwq7%ah'6fsorcrxmwR+ޖ ,k@^Kj8@ovj tL`W 7~wjLuU2@ghfCw |x!z6y#rTxX0W|3z SE@# F8٠ZJ V8w+D\xZaw&2b apPc(Cg`wG6>؀xM%(מRz A,Q4QT@  v7 J@V`| v6jRi~`x* v)XVjX`(@Kc@`@ d[wB v{kǘΖ*YMXv$ 9KRmn݀/n X z(8io THkiw` oم9piwلspb>yzByqG򶘂Z_yӤ?h5,, WZ8b5hWF~`G9v8 $  vt kׄ%E=Zlu!tYqlBWY}:MNyM M>@" ڃ[*aPw}<@ h]IwڛIW?‥`a 4@! l@.Z8 ʷ[2d} hڵs~z:;L┹p6XK2!Z` "@~@d PC8:@mw@$Hj`)`5M`\@&@0~ ~ewo`Xy"Bl il$;lawj{uyD?w|n@X<]bVVotaiyjjG)ْ1y@Xjk4b_ 8Y7@8@N`gv< ;CIȻƩt`Z|ud25)<0#|{Z^ 6)R3MB = @+N" Yn"%㨵oYp pi8S4n@Gw4]4oՇaep_}a~SZwy:uq<'`| xDZI= l͝\ Z_T =x|=ޛcÃx@b@4|ev=!ڄ!(vmH1(ڢ13՝kwk\B:ݝ~]8hYL˲Q9}]#`=as^G_NUB6U"ҿG۠/P<]޺]xD[əe ?ګ=: _H` _לqU HvRWIQ_iO(Z vinz_FmzjF1w OJBv@b@ZU62P)t[䇃xˆgJ(;@ۼ_ `+F8^⧐H 0.bWSCF+4X.6o^DW}YsiLb75ز7H7䞍i‡/n8Y3xVq!̴6mV)w<βaօ@oGxT6RIIZk$iH'j,h Zx!m xlrR"x")8˱xa8Tj˃u[/+Ȳ "*IA:?@x\'@ v^Vr"IM͛N5qzwi'L0PeAc*(Ƥ "DJ:b!U q @ :0.AΠ1|9R7'5 m;-Xa "g247gh%F:/N7! <02QeAYw.,rwa da//Xz 2+{:A\+l#+C65f>5DQiyw87fb$V'5[Ô6m  w/b@+u5-,#m<&!zZS/x }'&69bXcET֯wx$^};UH̠j ps~\^O>Ee _D3*?O&5_TD4E\@ $DVبtU4- Un T#e[fyI>\% =^,\`z}(q 9V/U@ ~M N V֎f(蠿h~YUEq0Ux(!/|2 T@ !֡!!!  ԍFZ ~! ">"$"PqHUT<$jB R@v$ )*>e ZEN$4"٢.,t݀q\*N't@ =ɵo"b5n"0†TEm=7Z 4R#,Fk+rcbc6#>c"| ,j!h'<@&6ڄ44Ң>=CVFn?s@F&jE4S8Q2$ㅌ@iEʅd'd' dpFE MO^WGq$$9D@PZ $Sf4 @_~+V l+"ajGmk'H4@%ꧢ\B**FX+kerk@ 8Ƃ5Je%~‰k4vzk 8zHIlHQ@ pNk%(-.^NÒ$핦imsm*NE@xlt'UXzBz5 hH)-l8>B U耔b@x1pJ0~b7oV릯UAG @Â&rTrUx$ 0#ʡ"()V]qêhMa%0⬬ [/%A遀 0iIvpJBX3X=م$-IoȸBJ[A2i($,o(M ,m2);51S@т hqq,(Z2&34®l-F613_3񼨲R1Yll$ @K|*l/RD'` =c6=V4B 6G 7ć+D&q-͊7*?I`aR8V .q m3H'aLt,44O@%S- n32i0[8OTN12t5AA:T5ZO7 P>'. `/Tյ]5^u^[/8K9.`UuUxB*@WZOiV5gwgWWhkZ3J[5(4Tkvlv}2PKNjIKOTwU/ u2\Ch`*$G6uW_ Xug@h7w)su d6$@@ :@0z0@|oi]7!'lCGgm# Dc 6psZnKvv1xߑilw?(kvw~8Bi?] <ր?Bxd`B,IL8@~BHB eo$88tFpXV wD66_U4SŚG5rMW y&c Ley@H%QÒH' t@(,OT880CdsUâE_ћ@tzfyǺ(x"@=ZKC KTQ c.:,):4#({ `c# Jw&s<B{J᳟uHJ7zT{;%: pn 8$E{/y8p开 h H|@@bsL $ T-|@4@f_N|OB1" #;{3uϡ |keN&\C=؜׋(@X=Be4ӻ'?Ͻ$@;4gʇ@@ : Y>A^@X~hdA X@ qxXʼn>g@~+CB®oSB\#?"܀bK/',GB3z-l+/.P?V{S> |WIa@O f~?@vL.HJݯ pCtZ~Cz}: $$*;D\dlt|(\clk%-eNVnv~Ɩ~:fK!8jPJ8HG@ҡ!|zD`3WC@Y ar!ccF\JfDi7VJ &044RNNTС+ތTS DARU^ŚUV]S#eQ2Ζ L͞eYA:.C]vH Co?MT#VՎќJxG?5w q뼐z5VS iݽ}&;fme-aq͝?]M_{Yn@A'Bt\ D< <B l#h.y09Wh86!>40 D;[H.( Xz{Q9qPEYQ11E"A &a6!LaJ`H@^92H1$L3$źe2~ `!@/8kH(tLl2XX:*3vTQGP ;PԂP+2m Q 3̘rl^ H΀xfzY4*dMmVYSjv-[@$8`Z ;q`@B]6^y]˶kuwRHR q~Hz``?L68b'95|X02Pc!>8AA^  Vȁ /ɱdž(uX:!n<@$.PڮVX3~dbHV P%IEx{̖\NW$f 28f~6@ 8 +F=uރr3<$~0 qbXo]]u'xؗ|8H I;n^ha} \9ba;ף=c{ 6t 졂9-Dr[ *24$8x ~@@@^"5>ɠh 0B a LψG̅pQ/aJH#Qcy| xPwO,F6эI^/Ԙ8CИ>,6haB ,7]d1~0 ?A:&#={c g=`+D)9r'!7 Hf [!.A!2"hC:@T9KlU7Ӝ(GhDf&*)H@0.A8T &GGs6ԡoȋyN .PX K+X8 gÐ>8)A>>)Qi A~P<:(Lw, C#qT4r(F,t%IMY/=AG "Ec@P`UgXo AN5X&@A f}(D#Pg@ kJײt\j  h?zBSY@-A X@@$", $ >)ݐUm)p`@6hoս$IE$@ t0, %E @ 0@/ @ Sx6 ́:@2PQ/QݨX?\]KK0׺x\R!*:îqu_Lt5Ex /@ ~㕯#;V0i(*BB݆ J"še(TM,f\y{V5|=@Brxńh z] а_#ot( l7ݛ.Wp@gM/zA嗴9f.q|9y.\ЯzNgG#Ђ,x=I*\@.Ie?^:ԗXuU#t~wI` VsAI͇p"0O~]:t@Lk=sD D AISs7P^r)-&zb#oc:z\9+rF$2Q$H$Rqh"L yiQ! b#|KBz Fx3B(!r? ӛS(# <| ? DЋ;&$@(0!?ԝ&*,"B' kDĄqA!0?D=\nF?64"-IR (ځ P  MZJ$-hE('0$jJ* $hJDї"'BiF {B[:G9u,rGy:K*}l{\ǃȗ!Gz{G;h/-'!H / :ǥLʥ,H,FȥRCbhAPyG\Ht>K$NR0KwP J,=XP@Lۃ 67}E&hQEZ Ѐ4)TPo$Ns5dIvpʣIdKL(HF`abOaA R-`$ &Z (SQ*1J1~Y- 1,t('H_щX(:HBˠhRKxRq@!` p!@:R/Qxh!>!YMTM;48Sq:$TxlhP*Ӫ*-3Q)ҩGQۊGԭ x%?-UeV D%P%-$ 9-HVȌSK)PKmU-C-<>pHS]4SJ(X˸q0 ;O4B2SҸ,ѵ65 U*W ~1Ȯ@u]3s9a=>jZ\9*-ȳ;͵;)I:?:xѕ `W[h12 }p`_gUTlPFߥ#,k@]O8L 4,A"<@Ħe53fVY[A>X\E*=60HP%89J P< 5 @xuIh 7 G eI Ve^_McDepC:X,B#@`?*w^сЀ8 GI$!(̤xKɤ3|dKlʜ$Lm#yfgwNyޚn{} Fb= iqTv6ꗆgzg|}[94D )殖\H!@jnK馮˽D(^V(>IgɄkN6L뻶GJnTfgfhfgf{ h/# 8pLaG:`jfmIxȁxj ԩmab:֍O  ' n0vƤV٦mm߾!h:PlbƦ JoHnnfNkXmn&'f(w˞vP؁ȁ`qv{s |k7qHHX~F&(q.$?6n-&cOq!_q.oq$qg؁B_q'p',,g#0_A'tBpcf|T 0q;P!}[aB. Wu&lRwe8PtYQ/A讃)brbHYu[7\Uwd ex@88t`pBj:cGdrufTw%@D vx1vPKY6=e( ?Ֆt^SJkxKx =hj'iux/y7q:` TmwfBoynUwAyɡx7zY  hp`iU\:S# qsV+{K0դ&{LB ȁ} Ɔ)^j糦^w{w?_|1p|H@C%>&*GWf|:|}=g'ط]Rz7-}'߇/w0it>z'NJz"N: ؑד}o0PpH,ӈ YaeKZ̒-Y!f8_+[rn5uËOyG2?y3Y,uxYM (1&C dР / 0R^~#3\4 0c4a%;t P؄/Xp> ab+ Fo/f\&=7Rrx(BȠ@(a{x|Wz"9 "9#7S_z!{ CYClB !>`'HJNG0h` .B&r=¨H>qZRp* AA;,0qk$kSǪW~iJBP' 7'Ln9|@1/ 3u`D +B4@;-*Q[@ . A>G:E=D A>Qot1i$ؗ(Fˌ49#J `%P`( C 49<`B#l1?bO (a12p8č ^屁@+hJЪ :)Bs̥.wYណS$1 <]A*K |`|$9B6R,1E1 )" :c[h@ñ@0 1R.0aZ$Iw2 $/x< R(ٙ *O}PZg9JЂzv@ݥ|`(=)`M1› az $$N#akAD) h!5sΙ$4W %0&!+hEՄ"Leq(@ psF@ `*B0ᔮgNzжCD0>A_1L[RɣFF`!RIhoMbXT! $  a Y (0۾f/0ImAڳܲ %)NٌTJ oV"iD j" 2)(h $DCn[ ;,╔p^ yqG@l*`nc'\Aze'ufx-wۦ2A1WՇ@ @UA D\ h);1" jc!+ P|@ 'o`<'BLMy"a:{_xM^5y+,5m0gxd&4ilb&Δ:j!U 5V(/8W=W: 4'@x7@m͸@Iζi|D +P* n6RMuaZp8>+ .c@vP4w!dȟ)FN&C.Hp }{Np|>NBhx#:( Dx@H[l@^ 2 A . C@vd/ t S@N'lqKo9Jx!~p)_2y BoKiq p/9nDFpw> !0{ӈ;  0p䟓 ЍĒ@`!UGK9X>jaކ!]0}?C#)@ȀCzP$+PCHt'z x'G ~wi#%T} %~!`u !{7={EV{tvH<`pG.`|w!'K$tHHԑ1LۗrmUX*.>N\wp5MtqtZ 7HP-,۶$qŵ'-S-2-DAat{/Us荕ޱrQV-`n掞ދNa餮D>G4;.>au~뼎=.G`3;g DY彾۹>aFzz P⺀)^X~.F>]^~{_` >0 lw5) @ ˄$_8Hp·mn܀O}HOu-{0"7m E`q@HD*B8ZK/` zf?n'pA5,;pvi.4ʘW%`FoOѭz,2-T0wq}_wp KoVVXo bD'۾ғ/4Aߡ=- "$;k4C/J;# 1Oλ/zge\o:?<7 ow@"@b/NUn]v3V?V/#}}0Pp1QHHF`r1Ss-o4Tt5Uu5ƠU3$h@`A Mpe:Jl(;\|\q(w<?_? }$`䭞V=$S:$ƛ1" o^ɑ+Yto 1›;y(>/$ZL\<q DPy0 ]E.@ՀqʀĔSͶu.: 6}Pax0?&MJFxC?/8JgaqHNa'=kAYvZ\p;mlmݻy~*Dg"=WH"<<̈n.L$|e@vR[-}zd+D\r"~d8[~h* ʃk‡d ?O= Q+<\D"dA3$Fuܑ8rH :h@ @ 4p0<\I?0@dTp@| x@޾̲6M*ϼOթB(9̓B MN'0P,6p4FN=lOIRM)Q-B& pb#NoݕחBT^qR5 Rt.-* X)-jV_=͖pwcZ7p[R_`/!7 uñ|]N\ "j`jM44،=ۂLQCQc^-vee1uOY|@bgQz9{ݘ. =W1괇Ў~)kw4xvS<1eP _mh5nPImDPE?)r1Mo܋A]S}> uwiuZMw H>陙dᭇB )~_>0b =L,"֙D~'t o5K:`Hb7Lqyt<(&}1A $`?w<~,'K>Ț A`gh0QG E)^t;tIKcEL&$sh@﫚*,6TXG;f,gx(IDѠӃzAi#yGGBwj#)0Қ|oVk֞0c`њeH̕/syb0tv Guhug F i!Su@ծ)<3-"o"`feO+PJ 3Y7 '(j`H)VB'c#'T4$KmD"dd ؅E`+ȡv=#pӓ(r,ҶณL1 +) JwPg a摢 uH $$5iOp@ A^iHßYdyCAU_UjQjtlލi1d;'Llqe wa{:26: H(K WTGuYƮϴ n?|)^"#p- b}!`!mOi$L;Y ;yQr(v3sH;O͗g!x0+ü-y b ^v4 #r@ 0` !}pGb1U.AWnniAiXN$S'7 ҇clK*t ği|9NpwS<*ypg@YqQ8@4,8#Po`0] 2?5Ab@B2i/z^/._Pؠf!@k> .p H|@}`1@(A, r.6 VHo {v P  P L`  P { Pr p spzNNa -`@h 5/6 (~@`WP`Q \ @Yp.Q0 ! t+XZ `d+* m&Q M 35LqHqNA 65Bq 'P1 ]4h0nzAfqq@`'x$L $@F@P oqM@`@D 4`@4@6 ytl NpІ$(R(=:@.DH4Ϫ' 8T+%Ȳ(G--o-A`K@tmB, @^EIdp 0`hKjO$Jk֐1DL/z)!9P`/B8@h'nRHN$WajR-. pߘ`9ò+:IJr%8˓''s %X@>i~37Ǻ h "0!4ˊ@n @@` (23?@F$$P &س;qOqr Uq, +pLSMku T@8R(D2Ԕ`JOoSSS2LT}Q  6@d*s4@ @L.PRT`*<ll@B]`"Hu]ua\U`zGw@Idb]i,F@@^S_Vn`` `e aA`bak@]IvbbWmZ UD7 6%VtYYgp 36xB¶*$dVY' { UIib[!h+>`N`32]FNR_ 8`+Y"]7rq܍|@D@#L:Yu! tSNb 6<@DCMuDuw!x v`qs2i{4|Ao6rb sAj}@}oC|jPD6Zk ~B QE5p ְ8 k$`vVZ7o~(05 V8wMTlE' G @)sC`d.|Vka̒5s `,ÜPVB2X7W4`WS  " }O+Yt IAѠw3 v@H#޺DJ`*sa&=axđ2`: :1K,:DH9LL@Z@ϞtKJ bjٲTo,zl@<Ky ;/Hu c G_ kYقxW}w?,YL!Yy!ٜU zi .~ PAhR2"YF @ T ~hCzKSzt|Ӱk܈+Y@V@ ^| f@a pJOktBV@`o ^0K/@ @nDN2V@;ګړxz,nz.:*@l`먄`X@n[`0\[y@N^Z"phЮO@  Lt}3@N_A/iRٟc{b99[7 [۸m7 .*@&V@6`H%K@vy3qS[-9| xj2 ϋP; [ۿ\3+Mqr{#w@iKr@5 LT{Vbk"kPV!]$B܀_E;i^a s2o\DH+ @ R$$(Ȏ̠t!?Wu)\H.[Tω½̥(ãR tۂV_@} )Up4b)z`qT>GA%tq^9>^QÎ>Q^U+vt@ix8NН7^ٽgpV`#] ?+b3_-~ 8?^& M<$ABr(J@ f`($Y~ ,8OZBBvnB`$js?be@&@N9T H?@c9`|;1W$L9[H@UKA~_Fj oQ*Dj21")19ЀeeXİ@<x &-5)~f]<<0əm z&  #+'377D$04D4TD,,L8/ (p "LxBPB}%bl0㼍?l1$ɒ&O{@PdH Ku?N`1l!'t@C*Q ?B1*YfȐ.0`hѣPfaIMb b@{"†#NxCx+[άy3Ξ?]D̢MƗt>ӆ] ;6 dF.8j%l$Ŋ$A h(@, xaMN0"(*T2„ "VmX'vB0`cV @ * bramAt>+fO퀍A':#bH2z5nL {pE0XP(CJ\ 6e'duYD*@%uqͬwٍyڹH'ZbCB 2 4h(ͽ( ؠWdڎ/E1 ~2@SN?@#G\Ȭ 3l pcłҀ> 5\'B P~ Y/QO`C rl 6cgE=s,L=4>W3rB^ȴA6g'C? c=kX")$A P" ɭ@E$p""P 4 n7PH  ;T:h6ꐩ^6Ŋ tzdm?#Ye(?lC:|0 h`@*,T@?Bf ç{E` $=,@A &*.px 6@r֣JTޗ] n`W-h1)PZ.  鎧/yCd~"8 u%=!@ϯd ȀځvA0 Ix#34|H$6s+8y~#8K ~RUd0:ԡl@ `*L@ [ȈYq42 fP{ <4yt/ _@8Q9ґDסD e(̜Ymeȁ (pJh8]>@@thF֭` .LH P p (LMM@ 90}}I_8x@j*`jCbMaaPA 3b @ k\ @fA:acxJxzl `29!["c > @  ¥- *2@u'** `~|ިCX!".Rc5^[ F"!v-z48F @ax Xp `CmU8ZI="pc@"dhxŜI%" |@ A 8d)~C4 @&I;a#1AW;,i(1F1= 4D}@]LD =dødRB% @B740@ @HEf(ϴ` ܷddTe\rJ[f:ʃ<^ƃLXd2 0bb2̀$NM42J`eA,e=4%ezfl 1@z%(͝&(@( Mq٤M]_Ygfo]&@y g"$rĀ1Ϯt"\'F4q"'qvtJ'unf@!'Lc20VԜT}dy&?d&=lbx֩Df@9: X(`u E|egbha𧝼f٘0j<@a p8ed'tr~wgtNu҆fh?hBRۑ@2i]@ZL7~Ɠ㒎AFŗBiH)9fD@-@id2fH,Z$PiC&F R * Lb@@:@VԀՕA (݅ꨪi$dD6F60pc~.PB~"ijNiğj+J ?kA @P4Nk|@ x@t@ h*DTkDZ+j+fC `M CMtYaP1\ L^emg<jbT_t ~· x" "|^FRU2' @OedXʌ*ˊk>kk.,nklZ"1>==p K/?D1?p@рJd1|+B | |@@fskqjE kFK4IMГ@] ߴN'Sj0a@r1c"tqM2gu2(rV1ef܀\n@S0gsX 8p e,3103s2_>s4O(2K"KA| @DVx"+ɜ8ql1쨀 ,@iAC.{d`A8@d@t3e322ks3 H(4BdL4hу <@5FJK, gDtF2; ra(r=Aq<(ٳW@PtOtT f*~n@f4>NKuX5?tk4h:C0KFZ[5btlu34J(2Y0"DC P4@H,,vud 6t@uau2]ih^7^"w"J ` G<P[A =MfHqwshsۃMgPw3fjjswsw2I(>ѐ$UUpqd ';JwF} 1ԀtV2W61{~W2W2q.g73ww+t+d@ @O@q"8s(yuƉ/r  'z8wo727I d8@XC$V 0 i:2뷈_mKDdP'jvHa2>(ܹ0Fr|њ})9/x2#y.蓻c('Gdc8bL L 6Qb}ªO(Ϭ L[C+ơwx>@ 7C he_ 92 9Dieڥ0(I:)i0{6iiK.6P@ 4B"+U fkԾk:J B0 CN$|+Pe"1(blQA|K󻱾\;.3!1";2a%BFoǧR _kK㮿wDDҸ- @C#evo~|77ީV"d--S=c !MdQ lRQ'Q|\_D@|@HB}T?ḷ$అZؤ&ge3"$2R3܆+>2=Tc/=~Չ}Ě}(F}0Yܫpy?+\IՀB@ބOGdRd6d:V !j)k:ivЁu`1FM~; #)FGHIJK̥% NPѤSTUVWXGFZ!3!'`$.% ah! ZPu[&z$!HN 1#صcC%NXXd-؈G'г'DG"$B6G0>!SJUrK$HaB !$uN>Ipu" M$y'0%-?^Q`k~xXK-GyB\xhcȑ%OܘpeXcQ0'~ pdJ5&PǚO>0\FD]nWT$I%J:UHE qٵowl>y v 0W{=$Cƃ"]@|χ$J!܂E`` lx 7h4A Tb.<&LE1QsJ,\eE-AA$p,S4Q2?M%V@0% hAθlOW&T=8P@l'@RL3tS{+_r*1WCL`Cn<@,Z@$X a ex!B ~0@`+VNm PS5\tv\ pAwy裗~z꫷z쳯>- ɶC+@{T%$ א{~qW4_c%P d`D* ksV "8lf:`%aY GECB4`І^m\aK'v QC$bxD$&QKdbC(Fq2:A Ybڤ=r/ L^8Q0AȠB~ҬM8A3-*JQ$d!@ TXG>!ZIAXBx;PBaf]%$"iqe-)J@.0/V DꂂP4@5$1LXG`~@0DO 8(cpiKxSB*F|Sg?OT%hA DrT ehCNJ@T5&!g4𖀃ש0M8D*SiMmzHH4r-^%jQZPTKeaꙊFxjU jXը *QU. C4(ȮȀ9б;QmQ A tFi1|KL 9VgXg _W-O^velg=Y| #עOr< KL; !*"|tkfs-7˂msesmٖjNW_2l >Zы#GkA~t-}X|/7@7oͧ>ŗUw^K":z\@԰y{вܾ~i,,>ķ)% $~-f7|(2ΟH%~_޿wy"7 r?f@%k%Ԅ "@f{@ Z)i@@Z@8@-PA, AA'"Y2A>\AzdA ,‹&Z48? $B#B,!B,\*$d%L ),&k/B3+2kIŕ<5iI̐ɰHG3JĞɦJS4<1>@|K{Jl t̚DK6ʯK|DKK,KKD,VбHAaL6klʰɰ$hK,&0.<'@|MK@Lp0XMq<=!۬޴MM -௜#Mؔ#N&X,@^NHزD@H{| (8Oʸ`;rj-hNTAShO=$`;ENՎ;E&+PG xPPN,(Q&KO P@AH19$QP%X+4@QbXdPj]3# |jQ$? M"-Ri/-M%E &=8!²%lPu'lZ:/MS ө2O#S]hS;E4 )E*=)q*XPSC>@ ?MPD5 E=P9]T*}TXeIU?T 8x!PU,8Ux$l*ĒE|L %V$#VlKcSխ#q@g2hMW&1%ȏoN|W|7Jzp#GӆM͒D7*.y X U|#؋ppk*Xfz&ETMY8YdloRЄY?E ڙ Z$Y˳WoY&t:eZPZtڊZZNZ'G#[-#-&撸:T2"O1\@n7?rdKdX%OXJb"2"8J^ ee["X~DO~F,na`~:]e_#,KfWHe b%X" '++ s0FhfZf 6"͝sYzCpnY Z 8"F  ygTd#e6gV -X!j'3h Ys"ihQL"Zd$fAffV)!*P:: poV؁XU<"k>p3 jU *f_\6pLXppWx0.h6B  !g*'o>(*)_  s%=13w?3/Xslk2&_+w<=w>,'t%":ZG.&8`F &X񏧄/Oz'oʘyHyy#{q {h|(/2fhhh@F pIIXX@#P|Ƈ 2/OW;1ן}%3ȓp_}x'`b_ 0*x~}~NW}VַHGVxt֜=O;2S~x8DŽ7 ~b1`"&߯9` t~ d 8qgND! (8HXhx(wpX3Ó320 IwVi1"CCI;Z+(Ii *J+XBt .>N^n~6-wcR!O_-?uȌYC-xf0ƍ;z2$F|< $XqEd"  " DV5!:XЀB0Z4muAZ_7s\R]Zlh3 ~lALJLH1 0VذB C6` XDnVL|٭!>:լ[[D2e˜@X xo߿~" pH h0C(C ll]"hCl6w@iJ<{Ds^b%g]w .`> I!;Aa./CL ]X"6؄i>uDx#DXd0b'q9IV[|B p<Ԁ(\Vbi+)0Ӭ@\fB=Bod9g~ hz@ɦk^]~e@7`5Di& =H(F$*EwB $P q0'Ȳ+*'%(]c.l> <8K!,- 4CI4C +@BPX%(K\ . B+mZKm8I {N@ Xl (I)fC-J2. s2AhkrS O$ A6= DAi:9AA¿A-'C>fr0FS5({uk0tj? 0 + 7-F D`~eM! ?ؠ߁c>LxuC"z8C"L@v;,@:s?ÛL2Bi޶-'|?Z0SzA -<}C$ JSsFh[94 l@ASz+xo l6@P(Qh`,haC:k LCQRx%*Lp_t@Dv` "І>^aF)ahB\12 # Cp2!|C F"( n) ; fc;Rdb) XblB ~Ċ]3 2d,v` DuKway-oK;l +ea,W$/($%0N|"1T$zGTD0׀"@>@ n` JU@'('ʂdr%)n*t S<x"9 P{ % ذ Q0hoD<\FL,H\Ԁ0P2)R IC2:GGR)jX!-`>fB-48 `l.3Ǵk `rYH c4;ۄ\';XD J#4C Ld`96ǂK  B| "`6AkާjB-BfK(D8k]PӦ֙aOnl 0n5L㤠 r1< g>%~j ( &ͶiH+\F#+"kd (i2 *KyA2 -yddSu(ڄ)IUJ>;QUgGV*ԛ,DЌ(/!Rq~q&bJpBʨ?@@+it ),Unh6. @*AK]݌PHl435Uw]^ ⴡ{E\sFRz(‡w#r)Kڧ:$}9xʖ_Dc*"L3&PU}7zׄ1<&[Ԡm`.Ko||;Qw;n[&<$_o|:dk:ً@oo2?qBk׳win|{wtBAK&ׇ:=-%=gon31_Tv_ @jO7?t w{_<~G~G  ;GOր}F~X~7ZW/p< GP 0L҄ ՔNP$` N41XH&4NMtL@/ H6 Lh%TbH@M$PtLBL\mHG {p!&8 W8M0-H=(QXEN(X rd/PO`GL*3QX86h+@SLxO0capo_M8>5Th%(:@ȋ5jЊX(hX̘Ĩè8hȁȍ837e(e5wBiz/PDP)Q#ݠ@ yYIW"Y؏C9@I5"h]20;CVdue'3 ҎpbeE+}ԁp*B#SUOЕ.oՉQ fii03X5EpZo]aSmPzrqsu镓_%|ɘVks;ws␔+3rpw:S)BoE`?y9@3pdhi٘ɛyҖY YIKY9 I؛ɝd<XDh~  ` Oi s4&QWGp"?0X &53@y?@߀aZ1VNT$`)3Jm9ڡ?p& (@yKF;J9"%z)*G4jWJߩ{ Xxר P7ap+@ذkUb.V-@-51ҲI%/ӥإƈؘTX$pU{BS ]P#-z1*Жj:?06P>Pb7Aj%ZZZZ!&&/ g +0F&* aoG8kV@p.cɒ+0Y"7.J   R 9ky lЙU◀)ZIw)E< G ?Ud!btʦp:-s*kٲ K{GR\_ Pz ega GI7REؙ/ 1LBL$B9X=@O]<`@f.pLrz;s5Z+j{^m0` 4z%,JʡN PR {vV7A@0p SG@+A[-ˤۣ;pQ:,`# "zp;+E r 2??%"J 2λ!IY*`7<D#BPRX!@NMחqBMC1*JF@J3PR$Pp+`*5\&@-c9iK֢8kp5]"7{L $з[Mԍx7V`=;"šK@EdΤd̫k|rpz8䦎v+<`A^_9+@?6|[;;c++A'M( WIf0քS@]Pp+3:f ?OnMv]۫-WYi#d$qalq ^)y*oaƍ~5Bo1?!u嫧sSnmlpogSb4OrRo 4os LYJP~'J?s~NgnQ.!?K/d\՟?/~5-؞I˷꭮˚> 02OFP1FDop,YJ^\.f>q.\^y !##%)+13579;=?ACE44FS^<\NbUgUmisX\r6Tu^0餱ńϋ/op:AOR E!  cM'y)PD-*8dI'QfXFx H q%G5cԹgO KyP1F!"ݤ(=6BHWfպէf^P:WgѦUOT=LپEF0wջo_{kpDOy5>zuh{w?5 i'}{[}&׿Gs9,y:Al!P :

, SL/TC1?53 sKj=L;.SOA -;{ Q#E)t?ʹ8M9GOkQ>nPRYmU9L3uIe{VLѭh#]-$Xc5,em6z׬FdidT1s'Eѕ(?P4ۭn-K{(Z 8w*`!V#߲B16Wa X+ܘ 1؎ 6w`ia '_4-Uy5w'E Nxf⫹nM]i')[zNdxBV[n({ɯɚj;8ϱnN,;vn'=7/ :?r#%GKo _]uitAo]^ٹ?@gHg^QJy1^{?/rszBS' Uo?xݯN_& 폀5.'_=JH 'A 47‘AA"Yp%QCtx ez$9!:F.pD%Bxx@%N1;C.|a؛&:ƒb,n!XLcv/~u8;;+](8䈺8c  PBl[W7(jDCQŅJթn!::aq{vDp@E [maM WĪt تeOha V]X` F0*k3mc.!2F !vʵɵ NPFԲK  zwHY 'z׵r`V DBvmSuݛ_>5j k$!@lF#_ X C A(XHP"K0,mÖD@pkp#B0#`tkz#CB=d< o76|`}EyL?0}ipUAL;[n0m΂@sdYv F!`{Az ^#(Rt1-QHsJ#pPPl~ !xԢ6Cj_ |w8LZ `0 ! _PY6y}MkÍ^U!]BY! LnB%0ump37 O`Vh}n{ޭҷ7X{[՚6(#\)&Nq^@`8>mI#-~qOe|qrz([~Ybyfs_ym|;?Dϣto5u@}Ri~=jxѣu]'{.vK.٭n.JyۻEqukGnyw^}L3?`P|1E`!Bxͷ|W 'fgtp.2 swO|0}D⳴0@/w ( ]h$O7o߄#@P L +p@8Gp٦0Ȅ.΢ƉP>l /@<삤~ LPnft@>н@"J[. h<&D&@` sNdR@/LPl` : pKP>:txM@૖*/2R, ! :A 0ㄯ U*\]]O+ED AQ^F! u$0$0yk ?u1!~@dn18p`/p .XOv;5!"J dQ/@@{ ,Jx//,SjXQ< 0*-s 1z0/P^l 2?0/ :`/` `! 5A8$"#q"6+Jj%iJp'cQ":xҚ qQ:/J Ir* B a>a=eA-;Qj ,b#(P/2|/2 r,'$x&SzX&.S*  r/H~A+4"+",g56p7ɣР3Hh@2Z51!{P`A3'C>$=s=A7*1ۀ7;;b1;a$ b% :8>.Sm3 &DT@e<6pI^@XPCaT;/BBAFKF!!CQt\N3`"*y934I!Gkǁ ^9%FJFG[x@Tj&|t84MyK['AMTGt:@44~CObSJw?#aOhР=!5Rb)eOQSRQ]=%5T$UTMUR) 5?4S_ |s=1VcUYSUAWW#UKG=hu68U6 `7 BX5lURUXW :aZ=WC[@\\[=\ L\]K[@^^_u$USrb"_`_g^JZ`b_@b/af 6 (RJn2T-6KvY.q:udac5ZeS[h6`PVvhGeb jh4r@Bnv H0G1F ¶  4 V.Nfgg5eK(rַd=mnan mjL@ U UZ7 ņ`XNTH| ~ mtt&w H7;Ndo)V vdH@6d@Da:y@xr&`]@ Vh`+ tc08*w VwWfume}ávu| $0,}QO2r;a PX+` ׉WnaIv,jV+@jƬ>dɬvP`z_ `i V T>`px_vx > j{X}c7`w BS!~d widI3J ;!rkVt V(W VWԈN8ւVnj7d :x: &{n`\t @ ֋^T9-x @Ǵ@^@%x;b jp*V`,#p `l@yhm8`+Yr FXx`DPM@`@D6 p*@x4h`#y `@򯠗 U4ET'L`y`"`?Y@BIyRVy \" Pl~. Bq ^ZYZǐ .XfZ ٬` Ԁbxkـ@x3fOA ,`[. xOp0 yPhL> %r.|`*n1`@74` `_;1rs~!Pv\2{6@gj"x ZZ`70; 2 6x b; 0 $ʖ^9RX0] B1 \}J> <KBth@P@tJ@ `>wÇ!^ć`L D@JQ N6'¯ ) .,K@V| lh 8 N@J W b@v ܯ`p <ʃ֯&V@49fe"ɵ!PU* @Xyv']-} @T@0G P@/Qp_և (,ցDʼ%],< ݹiϼ}ϽS`|C`W}ϯ@*Q 1~I`+1 r ^H&!7p@ѓPR7PX pzysKT@A*W8UO wS@F_{-8ŮP-  Ys ٷhs`@ Op}֙ױi@8JQ6&Ԟھ` O7@gړ>vaFWh;>PHLr)}0V3mP_\ "$a@FHZɇHZZ#O hWPK)jː$eiZ0q1r2s3t4u5v6w0pFLQ:ăWG`Dϑċwi Q(C!Ĉi0NJ2>x CB2@D04iiF`L~!DDP@ BΜ!<.CA0M,Ulٴjײm-ܸrҕVvq `_/`We$<& (HA.߾~R f c%0s(l֐!%-|HDeQ>`tUn;‚@C=ӯod2BF\C 0Ag@L;,p%0D ced % ?(D,c}-xW9 F!h ؠ h/Y *` dP 7D9@p7і; ( ǁR4(C+^7'FPȕWNEƕ^d&\K>(l.Zz)j)f3~?׆8H|p :~` Qv(E{t(@}lBP+&!>( 2pXɄknC``Ao4yAp@ .1-(@^U-X :Py]yi]*|0v^$"?$e)!<2%Ǧ?T50 C,0#C9%t`B6ṕT @kA*`!m ɥ@@]?@5OnLCxP&<&@L^@ |РM #wŇG7-mM4]n|#q\r?@ "Ѐr(a,@d!*2XTRd.`(@`"ur$ D"II&8e*Wɡ(f@W M4Nc ( fr?A b?LNO6 Z/s>A 0r3\XW  pc&@W t\!Cю`=@D , @Pe PAOtSzJ^ᱦ6)Ns*zii89ECC c-Hʞ \+`8`^p V =HN 6TxWaW"^! uYxG(25c aN#+RZ}*oQ㨖uO2t [Bj$!pF @L˅F7xt$ o0 ky˄Gq|3X9ЃQ-kA+Y`860~!W(͜`€hx3/@>A8AG T0)p`|k!-C>%3ɒ%2ZfLMe<; -Gt@ 耗X@&h\:}n X jx.K "8'8.pR`э&t .` |@AX 0fÁJXo7 4G);ֶ5Cq}r3S hTÚ2!b~4іBiae^Ђ6(Xh~و!8: ezP Z}ƞke*.S#Yw}nx-5X9wD:Z?~O3 &xZ@)y?t V(Jt&=K*CLB6/ƟOͨ7 |'f?;. xw)_VRd}>m.w*g]nTŧ<#oٽÅUkf?|xЁ_@\.y9AZvh3J_K"c8G85AЏ7ʱ|]+9j ͗A    (a뷴€hfpdEC[ &`M`7hKy-ٟ[8`mz^^* E @ @$mE` BN~BU :C@ 0S3lE&!GjC; `aj|aa! !M."#6eih @B$%6@d@& >:5N XfU5")"-֢-~JE% %Z"c\b&nb'!$v/.`&0~" ~"6""6f ^H`@|@Sx7CS#8^N %66C,&r7>\<~,>#?E.9|c9$:#;21fJ@z8@A^;JE5]=#GvmD HrH.DJt}@|Ez0g%2Q 4dIJ>JdLBb SJBHKL€LҤMfܤ%R[[B x٬D\%@^FBt8Z(% y՝6e)&D%bvgfBC_%^ҥ_hW`&@V^Jj]niem a%HS@h^~}<S >}vn'aH< avfhƑr^  yg047Tyvgr'eg5'4kz 'y''@XPBNLby $@ |(57X Wn`}({D3t?9[ aaE! YȤ0 pA?=X@6CN6f})34%&U^flnfո&ZE QZ,`,TdH>:/706r P=PoW9?4sbӼrD3@S(#~s,t4*+fJ&<>בA Xs4ªQ!>ϴC/hxE7'fP &ERv3,1,DGC&qДc ;7ƒ 4@=I-G>ӴY7pt3xX@QPWd8=|u@x]#dE"AuP\ݴ*@R@6>??'/?c>G~@20׻"eev8CU>/>0??>W???CbxD&_FSjzf[nj4`r|F;rPpy~-Nxlz4f+.lΞ֖ ^^7?GOW_gm_g.Pm >QD-^ĘQFRH%3>5z\eęS\x~ǫPEERM>-T.4(4VoVX|}Zmݾ[Ej\NśW^ҵX FL,?Ycs)PAfΝ=ZhҠFZj~$[l Fn޽.fpōwbr/]t\v~]x *7?yzV·_>lǟ_~F_?@ǫ/dA9+0C\1 ?1D. KBWd1'Egq)kD SqG\EH#\F$qɵzt2J){r*2K- Qr˖0$!a,3M5K6MN;8O?tOz$: E4<ъ1QG 1PH!tҁ4S E SM?u9ICRRTUA䴬TW5VFSk=U\wUWy6hV[FWd%W'…Q9!qygy&Xnbezim ZӣV+ֺkIkŽǎk>Ff;na&lՖ;o9fo Gqpwr*r7G%_[s9ǜ+GGL^A* `lXw -=uϺ xJuGwZ0إyٛ`^'ܓ'}`h ·{zmpw÷__ P]tcE>m~ g@ * :F `1(, \?(AO] x0.4C ux B?ȡٞ@ NnGB :".pB8E @!pcs!D($@B(@aE1qF d 9INjIxdW(NVP#<ҕd)Kp@&I aK^8([ 3$Sܥ1wh:A*TS;F;p3r 4':GpR't  6k1j~&թ8)ˌ5>uZKeUzlUVղTg IWMU"kY:VC%cZW\W|,=J:m_T6@@ʢ4@:TgE N,JP_gG)ЎŬ-3Jjc l5l$ H‹<6"׺#`}n|Z@+Evv#:EV;Սo~ޅ~ F$l;Dp-FT"x<p4~'a3a70#~1IU#L%OF.w -.XJAxP P2^!Ԡ4~.P YL0 p%!yk<恵KG`ډ7Lg//FtIQPaY5zHΰsa@`epRz4*0uU0TT`f43 3ba',xl ֻʪlӐ6}a  RMco{*FwխIӣ1 = O=r) c6_7Ч7O-~/|)Fvk[EYYLkmuFFa$FG`3-2DZJȻPAT|L|p1H,'E:x!E3HK9E\6\BȁC@{  :C {4BB2X3?prpl~J(ʣ JD.s~C~ Ăx\(ELBl oȋ 䃶,tKtL<"<P JXK|I!0p>Eh;|1ǀIxML llKDM؁ (KB\HYi$ ;$߱K E$DLdq 89"B;$;?ĿӬ>;KH'ˬ;L0. "us R>L20JxKA<SkhOR"Hќ/AeSB*փRqT:0RS)VmO Q6OpdU']u@+SHT}@L}g fm@YK!&UѿGTiK,&#@+=k;KN6X&`34X&`Q@[50W 6p,W|'}҃MKX詁 W-SW]TvCVVJ"Vo"=sMZuYKQ 6p}mG (cl x 9؃  )&XΉYs 8\ Ex[5[r [Z}-\0D\O@M[Rp\J@PC³ր٧uh,Ё ( *x=܅$D ZM !`n͂l T鵃׭ax޵_h';+'㝡Ȁ7(@E)p_**j^,h)3YBP:(h -4j^Ztul v1YDȊƤ. _(a^`,xK0֒C'LgD1,p(d Ѐ( 0Pn= Z h,ȁb+؀R`QH Z6~8F v=!. reޘ8Ca'=΁   Zv53N:J-0JKBW1 ^bXR)l2grx.JJ&Hq@ ?H{}<{r>\a )1xOZFWRHHAp@h~ vo$阖gVgv6KnwV~=i9 Bހcy_(.H 4n jNj_ji p00fenޮV<_<ֻzdIe6κ^k츦\@j^iKk}f>9̖;L"Bh0>($ػ6mml@XD&&L@n]&m.mZlo367B0M=Zz HF) 9kŶW7 ??9n @+ߖWYoxte2pضYWw,oJyv<_zIxz`(N8lR f9 'H9wxǒDzPzG{]h<4 @J6ͪÏЏp~P}o}Ѐ W7l4(S1|g߿e&p0r<;I7 (PS~.~/}ׇկ}%B؁ݷG#~K@^G@o,~ql:ШtJZجv-p ܞJLh|N#VIO+M7-C DOC '/). K$HJU^U6D31??D<(9BDFnlw{/8D*B?D! +oTQ(H0@Ly֯ÇQ@Hŋ3*QLILiHOHRCr` /c6~ؑbuȹKO=7o:JJիX:DP|uHY`$Ȱ C>`յnv0˷~ L6?6rC d& e⥓I;_kYh͂Umpk۸sCZ+b [Ń9Č 76QkxK.X1V4$'1,8i`P?#yjMqw܀5\tF(-^e?4qaD ߄U`0Hw6<^b_-q=!AC<DŽ ?d2,ID ͢Ќ"bH&"$p)'n\Ru DXEޙ<৞Cp @+8'>u棔Rcf)_$@ 0D7R$tgtm#'k&n6ȮY`e.ѩ@]>zi+-v5 8 (D :`dbp%Ĵ `(-@ nCҲQ-6jсgBLą#'% 45~,\Eƃ}B| 0/q4[.-]Du8`M?ddYaNԃ8D 2gtީLx3t`+:7"Fc4wވ'DSE3pk@a9֙_;_wgm:KX @>`@fw}M 74NZ̻Wo= kA 2P& KpK+C.C:ĠA ~_[$j9, T09px@"f BXtwD?(La]' "{|^01+\<ЀG?"$TDO*siLF <1%),X^'U n!PxPx2&N儎$U0CpE,Cd.1*Y@#'V-@J4%^'sN( `$ CXJSQ(jɕS0)Ap tB7iF2N#lМ$Iz;g`YҖ%raH`O{:-XpϗDbPKp`X xEL-o i=iP Ռd68$ 6e (4ߘ'RT6FOŃD@XFQc5QQJֲFhlUA2@ \X psqtUtuN)QA^ =Q 2R=)+c5+5*Қ n%Wa@:@PHn,@:L < mL 2q N RtR/BZ܇rfבvMG( - len`ҘnbaBg2 ]Cp? CB3+P<7[r8`3M 2'{%cd )L Uװ( a$|>/B ַi/i!OÎ"?3lPDR@\u5/3Q7Z;~]|Za|G|q|}P{F0DYp" H;0 FppKW~h4' Xw%ǃ`*ˡ `:Wqsq7h XVXr^Z\؅^`b8dXfxhȅQ247#].VC{ykGW3X(!k( f^*=؃;>q}qG=^ф9iXx@؊7<xk,瀌H}N:jYP1%H0aL@h`ט6hqy D/L0$w%4qP"%T2-k58 ؅ hHk) T)mo(:~n}i*@ljELp XRp!q(p6D׎C5(Vc+^+ S+Kp+$"YFyMpN0$ GSN``\K0Uz5Y3 YCVJV1p/>63) .C@.b.:c3`/sv/0L00l_9<oR'Lh Q59& _ `׈y 3d'6rbYi)S@/ՁL(pM#( ,uIE0y i }7#`ٟɩSby [ي i ƃ<\0  /@T  *j- )2 h 6#ԑ3:|X#IJ:5z7:<'BZF] bMjO; >`uc:cP tNWEyX+Z&j|g΂׀" TWj]註ImzG!iSV:eHzaj~ꑬ*Zګ DIz:rZ::Zzؚں_JYJ*:P:ZzD;BϚ ۰B Q { B$2[(+`))4,r20[2(T8۳C>7((4D:JkH۴k%ijP[ôVO +\ԁb`[_l .d۶zpˬj{=r;xoz|uk=~{[:+6r;;cڸS D{Ṩ+*J뺲k;ں렰+MV08#71`Zè>{]䕍KIFJl:Tn } ;^ Vq"dXON jc}qۉiov~u>~>.>t芾`Cy>0>>^̭^~ꨞꪾ*w7+|: ޿m7rW=TydKLKi/PL5 D_-n8M[gYY侢YN:"LJZg{f5sBlDix oCzm?5XBl3 pd / Oa"L2JujI Ԍz,+Ju/P[Ys>5w,*jw;Gd0>X_)PRH h#a fS0Uz]loA{ύ ^SPл5 㜄6XÐ&|ƋOe hة_R^#G330X5tq /7a%9NL@O?c_Osd߇/pଐdspGJ0?Ŏb@EY H$NIj[eVt{=p}0Pp1Q1QCcr3Ss3TtԎ ig kI' u'v6 M5:ZzO{4.ҡi%j}H/?]26KxaB PC)Vx89vȅ}5|bC uH 4Pppȍ*T8`X)?y9+(:(Xd(eD(r";{ :L6qz+Y#-d AA":y*u5ͯaǖ-z4R1=x@E噙aQ;N\)˘y{67!GxyrˣIlytөoX{vg$4("H9?,0\ i0PD!xa>"J@ ~xY %nۯ"s> 3a> t0EY/O#Y1B!hC ]Rpkox=,J'<"1J234eY36W!<tB =[hD}RꠌJ- h|< /(|$~@8g @(DOࡈ @P%< C`a%XR5V]1V) !x@5N$%XB b(aUZ}5[[Y-Twr 0 Q^H 48"Bݷ[!Mab T8TP]~FmIqޙgP- S"pJ Q @_wpBH:J`! Byxۄ8e A#NZ!(Hmn;! b[rn"¶H\p_0̣\@@磉Q5gSdenpCȠT䁨"q^|h~@(:pv‡A*h@D@jb%P>0~7"D?FAǪBmQ8>A!̯~CaC=0(p`A bB R`[d3<'II" ðyc=hF @ؠ>:a%yG?*|6(x`8#L"<"`0i+ ܚ^4r B@Y@a) GRA$H%\D E !]4fm؇0&*E& }m@rp18g:  (  %% F@h`*HP`1π $ 2$@QVH<*5> x)&I!P ig)p 7 \rKeҔC=i,c:j F%BζTMSYgSBQRtbLk;$B80r !A; $%yOn@CIX"QU-BT=IfaI X0L- `<`DլiH}pˏ \@X֫kB(@.X^bR<ҲDo,;j>+"u/k]2d DЩ_"4ѝ~gZE2*{YnvV@ Wqli). @L( Y|)i$РJ- \"L- [#瘷FU\e+cWr+0hf#D5n'?Pf`e @]reR+˞prH@ 31(GB yp[Da?\/y̚~4 1tQ1{x=& j':L[k_Q iv`ZOhֳAN[OYYʆ7_ݘr}.#8^`c 'a]!kh v>)~ A9! QxѠ"ɜ䞇@|/FhxP I+Y(Ի&˾ifNش e#.Plrn_g2+|@@i@HP8{K…T ;X 4':  "0ll}NO {u_>#<}15ж/:4 s]q󹓿/}Ɔa]ws |dثJP|+4*//\ӟ>]c/;8L@Ȯ ocoC&A !p29P.Kp4f,`V( hb@cKB M2oBG> c>i% ,XneȖV`bp f Wpl8R   ~OPO 0 FcR ]T h 7C%fA @YPഠOp1І Cx;FqY@, Hqb{>PcP to߆ XBD^>"FB.`@l`"J0>p=. rk`iW HQ^>qgΕhqz<f2>S?=+\<:j`3@5@tt 4&0ƞ?B=+C)>3"? A@TA[DAaA!toC9F9tB'"g^ԩFFFՎ:P t0`FcTI)BIE4~C`G{TGyHHI<M4NBV MePU 4FQltt#44O P;t$KP@=DuL T^>@Sg#Voq"ULUJTU&UW5ZUSuYNVUGZuN H`.M)O\VUW]CE] [Uum[u]YǵLTf h^'``r <_/c U]7;;@ `v`]Na!VbO.:cc,n 4"fqx`z6g`6d|feisi7fx0qVgyfhhi$jV^-mn9Ac6Cɖn/WAmmn6Ƞ`BpWqan7>Y}tn7DdW WstE*qY{t׀zgsܔDV`|\`2 BORAoj`{~`uw']m>vIGQҨ`TȲ)_f:HL@vzh`*;{s|@Cop cAߗE]Tz/e~& `ʸ,d\nl>@܂tH28xaQa1\>B`f 8_`@rX \.`h Ł`e, ό򖆏۸4Kq6.xLL_xPf 6 I}i^LLm NTO4?y3?Q=@pVtC N tBd3% G`oנoK|ė<0<8sə\fEʑKI9@Ja3B v!NQ`.d[h` V.rs=\]ݱO Ə`0p` ^d/ @>Vd Q. n;]=gYE*ǃȼBf 1b ӏNjS9w۳ÍAY](\=H Ԓ w\bn Iz tx}~ nu ~1@-*r8*Q %af`a9_C^gkoianH1;[ B:M |`o.M b`m3^`*@מ^M&>t}'//f M Vj8hz aN`cX= ߾:J'-`vl2@3<8~1M ? *[rVQ)AW׏wKN@8. s`QS MxNRxD{Z_a_&Q2jg<":@,y$!)v:~/]#0n㏏< "&*.26:>BFJNRVZ^bVdvz~N I q ܭи8}-$@P̸@`ѥn (p \ "Lp!CFB(b(p!? )r$ɒLr%˖ =)s&0k]0@r̜F"Mt)$LB*udѩVb44+WH;0CaOUv-۶nE9}+w.ݺ~ֽͫ~,Q eͪw1ƎY},y2e/cf7s4`C͢sNz5L[Î-ڶOÄYK%{8_O|̟C}ܠSϮ}Jܿ=i<>}㷻X" 2ؠBRXur_~ra"yi8P+آ/3X7☣;أ<WJ'Y$  OBYRQRY%qHZL_dc efsj٦n9Jvy{'PJ"與*ڨp1:RW)rzɠ:Z*g}|+*e+[" հ2,C:J[mLZ`YRu;^|/.b/GG/ /)0;k1Kf0 k\$˃{HgL O D[<ϔ賏I뷿O?G$ G${\`#c H c< !+XE(\YBPY2gx@ 9rhCLBN ?|`|r&BNFH"ȼ~"((JSE+`c$XEld֨&11G:x]x;qKz $!0B> !F:` II>Z&N񓠤'Gi"QLJJF,Z懖.w[r> Ih12/f;=j^`z6n*-8Ys6 eAn*}苧> HMi>M=1Z E(i1S0O9Ң&n RgS!M5Tyc +SUC}.)pV]gZw9b4m5XA $A1~׏kyJX8,B6,e+kBV,g;[Y2V- -٤*ϊ`pahRʃL̀A'?A4i#x|=LUq(  .@D >uv* h,c9HNv0B `"Į _1~0 ?x & D[vALMoP=&8M{ F`6=p7dTBeT>Pt`$&f@(@BF  lN FDNr vxqy3=Q7W8 `)Bp{ar@#(؃{!egO{ Q8[7*Xu# t?@h/.BXV~8 Wz!O'b R-'+?1{WS(ڧ͗^'}$oŽ|y m ]@ qA @dS 9 pYxMuUP\Y,` @MA |@X[^!;! `܀ T ,`u@^ a Y8@,M t\l9 `b@ h !@ @5Iɏ$H @hZ# qp@ ! &~@@ "**A 4d Aa&@. , BU`q] W ,Q\ X@ 8@[@ 4/` (X S0 T'ٜ @6iZ?Y `9dA<$oW:a  @e"bpؔ P9N槑@ohڰ@@ zkfi@LOld>BH"'(& @gur:F!@T@t @X0 @[ HcE Ԁ$R5@ @0( ef8[^E$Murb4ghzdnh]t(ALA DgV Ċ(!hR ҂ )[)*@& `) X*J)W$i'܀ 4D v)<ĀT蟓^)DiIK`|h4T X)Zb `[5Zjfv@`XBi&A |T;@:w:dc `pꣲ*T%Y W&|@ x@t@ 6kjzȳ"`H@@h|Dk t$1(R+%lg)l&,5@dĺ tυ"va`byfC*oJt~@@ Zf,RB2|, yFg@qA{Z _]҄F   ۮ]UܺĀK}@@ɦӢ{D%; B%k8VnL-$܀'<0nfn ̟ؔD @ Fh).A @#G`xD^ Gʪhk < T^Ĕ t}o/n6 A0@"A 42 _x /Bs0upyث"`D Blo pHx%<.$L hT@(9d  D0  +" %V ld@ .. 9@A@X)ZA$Z,)d2C$&" 4dL|@@j›n!2zq&| 0#@g:j2)! *"(K/hn2$y++ q0G' @#| &3AsccxZ$0,d,J,Z-s3T(#|@4!;'4@,t% D@$(&hAt8OlT4&HLCM;ffgf43A^n~34NN[MC5>H5tw./fYRz @@BLSW:FUu=\u"hZA6uu^g^G06D 6"o!o/Lv4v$uee`Cjakdp#dvhhg)hvcdwvlb;Ivvmo(o/Dzq#6q;ƼlKt\ss?wv'N2iwXvww7Bwykx7vz zzw|o{7|}C }w}w#~k7 gxˌ?7+ ;Cx7[S sx/6 x6 xk6 Wy[O67 /3y6K cyˊ5{ WyC5_ 㴛y;4`lΏ9:pLӆz;8Ϣ[x?IA(щ[:zh: zhzzzúDz-wAP:x#Њz;)Uu5c{~ Qw_{;|`qzD{w{B;u{L{3ͽϵ;uL|φ2+óײC|go2[Os|31ȋ @ 㣗<<[@,||V\@4|TlCt}3=S7ԋҫSׄ4k=דK1 ԗ3KP Pسܓ=G@ R=+Q&K O~ׇcV{di狾WkCmk꿖lwr/ޣ~'j# #'>=d~#f#<(a2gC{S[[7j+~o#m$?﵏^;巉Et1T_T:VWl (i_pX<>Zfox\>w@ABCDEFGHIJKLMGVH.NZ[\]M]_`abcdefgLG)(Unٮhqrssuvwxyz{i 1~X`G-dat%NXE5n>~bpc ~b `BRM"OA%Zh$keiONZUYnݓTRa:Yiծe-&[7^Yy_ !NDs/f ocȑ%O\e5$j\0&Rcc]c۹uo x#GU[aE^).f6p٫wŏ$< oQBÊgC>1Vl;bVa pNH*H  D*0f($h(t(A)f @ :r;{G r2~A bz`(ISPQV>E!2R`(x olvx0@02|X~@㦕Pn gHFQH#tRJ+ȼ^"  &.Imx4  zN!ؠP hATRfzYhvZjżZaU,e=Al~p 2X郀*YlHt !'8o @v])]wn:C?bYk#Y+b3X ^@  "dȀF@h$TjdZjNJ :0A 9HX@hXr &䡂pyyVtcm8nn+w@7pWXE/0ycǡ7@ѶYҐX"#ЋhG@jpd'=I|/\LTe+]JXC8ЁBY05d.}K`G*Y^Sdf3@Lc 33ILkfS&4/Ƞ{R.`2x[60jrsg?Ozzjض>$B|ac|phG=Q q H3:9W)qM4 0@BPQ`FAj7TC%D7k:ݨ@PB}BQO~Uc%Z+Kd Ƞ::"LA;D(J1^-ZX&V;+#e/pu 9^є:@S# HؑaI{ZԦQ]L6|o (R_<\ sT|0ZVʴensܭjpJ>Qnw]ưvT.u ^qeo{"ud׽Q|{_o~S_X~_ fpOT@p-|a gXp=aS͂I|bX@]bX3qm|cX;1T#XC&2NR`"c/Ifr#?zKr|eqD˴e0Ba&|f4ci'f8 cq(|g#|=@Dpg労S!LJi{#"#pPPm!|!',+1}0,2I|퟿Ws~'?_b ?;@1@L@3(@l@L:Mx@ ۀpx T A 18Ad(A|{.A7AB!ľ1}_A$\Boc &%B*6 A[B.Bc--0/,C45 x1JCN8C7CV,RC^C=;p1CKC@DJ#K"BALDFDFEDKDtDOPQER,ES =H ȭ,у)x@Ϊa `?HlDN"z%jOF]Pjyqd4AeP }  PՠŃejBD mQ)6( 1ܪQ=Q湧E Q6@I(#$iRQBdQ!R!5P*"- Ҁ@*R4,U"!J!8b!*qYC)3MS>}5M%BD#"D &z("( >}TS4h2ʢT{A1:2<DGTR5+G:D*GbT$$bT RUÑT4PC\Ub^}Bb}V9V78Q`CgVmip')`ɿ:H'Xv2Kx'ŧklVzn6,)xz(&'RW(i%8:)`)x,)) ӝ*2W9ׅ-YK֤zjd*תS!bl4b$nPbldb'bhb*bbhb-bcb0.c5c5645~ R2c<ዂc <uH}:@fPA&dF&C]FgxdC;;dNG>;IPJdR6CNKI.eV;SeQneZ[{Ze[sQ_fcք=2 c~f2{YbflKXlhZfq>g#G5gtnA gugy xygS>eg|g:gNg7h$h`}SNh^Nӆ~hʊ跸h^ Ϗi @@i(Չ&>ui rH,|1P\iF 1~cڣj&Uj|~j#kgü"V>kw svk @i1*ֶsA|ֽp%lĆ^k11Ğlf]l̎1glY8۵G>mb]Fm׮m]m0mmƝ[m mG>NnK(]ĐhnIppn/h)`a+@ʏWooEHȓl1 )h^pC׽)B WoHQA nɌY7S?%A_{ɗoZp q& O50X6>/2FIyQ{rAoVC0$W%mRtS'-u). )Ҏթ1\>u[ml*]G!Subm4TU"BdD,9.bFLZMUL ;")vbGtVUU]$zUUZ$qD"['RSzO;\30/}`\~/|-~5e(*=P?XE,pXB)BؗSY穐utQJ$g*rӨ ْͪt}JzCڵ꫿zy4{7tQ"rol,-Ѳ{0 /\Qr[{2[):.ů`_[{/E8ҿs,_gˆz}[O,}}[{}F~]D'P\Q PfN/0h#T Z -@7x K~ח X\~ĢL*̦ JԪjܮ y (8HXhx؈Xiy *:JZjz +;)v3AK\l| -=}d;6@M ->N^n~~h=r^oߚ0 4ỏ:|'+Z1ƍ;6K8ńaKTʕ,[| 3́ 23Ww 4СDR̎G:lөTZ5VcI\5X(H=6ڵlۺ5ҕ̇ %޽| ]2+N>l2/Ō;~ 92HrN 8;{ :hǃ<5:T~ ;/Kq_w 㯟라zӊ׻?`N(`07`>ai§Onna~H mL!m有*b."bbpQXc:cpB>֜YCdJ.md$mƠV^eZB: u4<Znn g҂HPx1Ih0@ 4Mih.ʨnܙwEEl覞~ j=⁍R@髲JkXixE:DikEUlJ"qjB6%%$됂jPmnвL$ ?l oxkD{DP :ZLp[{,  BLq_l D( `-!;\8#r*1?Hls:̳|0Q2CSio, Rѧ Ny?h *<5p,l!VvPAsqMKp:aAYYlp&Ĝ$`A`a1T[r 0q#0h@d}qll@ kR7q|rEXA>*r\@MBt?x Ճ5}O'? hi.pED!(_ XUX%+&G/ L:p,1d*sÏ5iLU`7 ps䬀|PtsT 6|v3G y0P슨τ*tP':3mЌj4=DP pM|ɍtg9>ʆ@$-ǃRt:?@릓S*u*K@DrԬj5N%Lfud-Yc`ulmB:R,g\P׼u|&JumC:WRCASZJp`&#\  0AVlfAS`4s++BE- iK\X \se.;`@ f[FAрm9k]B E`+X@z%!'&@ [Qxj@ PpiU jc/*>B l*jᾚep`a  bw͐-ockjX 'N~ۄg!抂qP$h0` ͱGJI|ʘY\B|U\w>c7xso| &ms.I&Ko6`t# ] )Gŭ,4 @kddlou}_ Dys Jox]z{d* 0WSR#`v#Av~`; Nw -߉Q.D3 #XL]g,"[ (BI7^w.S}셼(X_KX|b! |6Y HpykE` 0P?@ &?@>`.::h.:pGWO$;D/IWm֖}6B}~WEpQwnIx8pK20 #&5._6=NIMxYYD]?`@~?pI{@C4]h7_$h}1R67t?s*WkҔ޷Dp4lHV~gbPF8PQFOG 7`.wT:5w80G5fщEЊu؆(d.ou.5qJ@s{V}4z\X!@JH`ӈAC?xvSW/k討EgRE H? D,0>)Uu[4L=η (I2e~D:Ei xC;@hE_ia3cPUIp^ fv'&Ԇ Qa*24IQrd03(?`{D@UYYiF`)HPV[P?0BBfiDm(,C$&dJ@0@?pI.ٙ)YBRiG:\Y>F@@ v\ gIe[@JFc)`ٙyqi}RPE.GyEPi)"@wӝع2pI`YYEV"0P(ex9I>)mx `8I.٠JJ`yuw{Fp?GG!0 M ЎD@6Z8,f& GУ7Zj7[[k4-[$u[ Bp *ɉcfp[gIWY?@KuAKEIiJ(mJK$ׇOjmZ&Qs:9e{3xɥ~u`.@#`(4@~p*Pq;`'l6:ꪰJڙ ,h(D૯ b6 洩 өDP᪮ʩT)zJ< 0:; f #@Я k8 +oC`ۀ+#3k [ '`g!K3Kcf,,KHḯ+r QN* 0P: &4`uE,>,FP`[ek6TpLm+ G9 LPwy+(88q(i@r(0s IG?qD)10kv`!g-{D05Kв\|ۻi*?,<@pJP1 _c1THsVE0 pAHJֺ+,.lx;=F*0^a<0BZE(@8@{ps:fD+t~>s4;pF JK`6K3LßaKÝH0оZIP3CE`F!P K͏ į O կ? /oO/o?X4It>QZ^Y~a\6ivq^y !#%;em-http-request-1.1.2/benchmarks/em-profile.txt0000644000004100000410000000734412261227270021535 0ustar www-datawww-dataTotal: 143 samples 67 46.9% 46.9% 109 76.2% Kernel#gem_original_require 17 11.9% 58.7% 17 11.9% garbage_collector 12 8.4% 67.1% 12 8.4% EventMachine.connect_server 11 7.7% 74.8% 11 7.7% Dir.[] 11 7.7% 82.5% 11 7.7% File.file? 4 2.8% 85.3% 4 2.8% Kernel#eval 3 2.1% 87.4% 16 11.2% Enumerable#find 3 2.1% 89.5% 3 2.1% Regexp#initialize 2 1.4% 90.9% 16 11.2% EventMachine.run_machine 2 1.4% 92.3% 2 1.4% Regexp#match 1 0.7% 93.0% 1 0.7% FFI::DynamicLibrary.open 1 0.7% 93.7% 1 0.7% Hash#values 1 0.7% 94.4% 7 4.9% Kernel#load 1 0.7% 95.1% 3 2.1% MIME::Type#initialize 1 0.7% 95.8% 1 0.7% MatchData#captures 1 0.7% 96.5% 13 9.1% Module#bind_connect 1 0.7% 97.2% 17 11.9% Module#searcher 1 0.7% 97.9% 1 0.7% String#downcase 1 0.7% 98.6% 1 0.7% String#scan 1 0.7% 99.3% 1 0.7% String#strip 1 0.7% 100.0% 1 0.7% TCPSocket#initialize 0 0.0% 100.0% 1 0.7% Array#map 0 0.0% 100.0% 14 9.8% Array#reverse_each 0 0.0% 100.0% 14 9.8% Class#from_gems_in 0 0.0% 100.0% 14 9.8% Class#from_installed_gems 0 0.0% 100.0% 14 9.8% Class#load 0 0.0% 100.0% 1 0.7% Class#parse 0 0.0% 100.0% 1 0.7% Class#simplified 0 0.0% 100.0% 6 4.2% Enumerable#each_with_index 0 0.0% 100.0% 1 0.7% Excon::Connection#connect 0 0.0% 100.0% 1 0.7% Excon::Connection#request 0 0.0% 100.0% 1 0.7% Excon::Connection#socket 0 0.0% 100.0% 1 0.7% FFI::Library#ffi_lib 0 0.0% 100.0% 10 7.0% Gem::GemPathSearcher#find 0 0.0% 100.0% 1 0.7% Gem::GemPathSearcher#find_active 0 0.0% 100.0% 16 11.2% Gem::GemPathSearcher#init_gemspecs 0 0.0% 100.0% 16 11.2% Gem::GemPathSearcher#initialize 0 0.0% 100.0% 11 7.7% Gem::GemPathSearcher#matching_file? 0 0.0% 100.0% 11 7.7% Gem::GemPathSearcher#matching_files 0 0.0% 100.0% 14 9.8% Gem::SourceIndex#load_gems_in 0 0.0% 100.0% 14 9.8% Gem::SourceIndex#refresh! 0 0.0% 100.0% 1 0.7% Gem::SourceIndex#search 0 0.0% 100.0% 1 0.7% HttpOptions#initialize 0 0.0% 100.0% 1 0.7% HttpOptions#set_uri 0 0.0% 100.0% 14 9.8% Integer#times 0 0.0% 100.0% 1 0.7% Kernel#loop 0 0.0% 100.0% 109 76.2% Kernel#require 0 0.0% 100.0% 1 0.7% Module#activate 0 0.0% 100.0% 13 9.1% Module#connect 0 0.0% 100.0% 1 0.7% Module#get 0 0.0% 100.0% 2 1.4% Module#load_full_rubygems_library 0 0.0% 100.0% 5 3.5% Module#loaded_path? 0 0.0% 100.0% 16 11.2% Module#meter 0 0.0% 100.0% 16 11.2% Module#run 0 0.0% 100.0% 16 11.2% Module#source_index 0 0.0% 100.0% 30 21.0% Module#try_activate 0 0.0% 100.0% 17 11.9% Object#with_server 0 0.0% 100.0% 2 1.4% Regexp.union 0 0.0% 100.0% 1 0.7% TCPSocket.open 0 0.0% 100.0% 16 11.2% Tach::Meter#initialize 0 0.0% 100.0% 16 11.2% Tach::Meter#run_tach 0 0.0% 100.0% 1 0.7% URI::Parser#initialize 0 0.0% 100.0% 1 0.7% URI::Parser#initialize_regexp 0 0.0% 100.0% 7 4.9% YAML::EngineManager#yamler= em-http-request-1.1.2/benchmarks/em-excon.rb0000644000004100000410000000373612261227270020776 0ustar www-datawww-data$: << './benchmarks' require 'server' url = 'http://127.0.0.1/10k.html' with_server do Tach.meter(100) do excon = Excon.new(url) tach('Excon (persistent)') do excon.request(:method => 'get').body end tach('Excon') do Excon.get(url).body end tach('em-http-request') do |n| EventMachine.run { count = 0 error = 0 n.times do EM.next_tick do http = EventMachine::HttpRequest.new(url, :connect_timeout => 1).get http.callback { count += 1 if count == n p [count, error] EM.stop end } http.errback { count += 1 error += 1 if count == n p [count, error] EM.stop end } end end } end tach('em-http-request (persistent)') do |n| EventMachine.run { count = 0 error = 0 conn = EventMachine::HttpRequest.new(url) n.times do http = conn.get :keepalive => true http.callback { count += 1 if count == n p [count, error] EM.stop end } http.errback { count += 1 error += 1 if count == n p [count, error] EM.stop end } end } end end end # +------------------------------+----------+ # | tach | total | # +------------------------------+----------+ # | em-http-request (persistent) | 0.018133 | # +------------------------------+----------+ # | Excon (persistent) | 0.023975 | # +------------------------------+----------+ # | Excon | 0.032877 | # +------------------------------+----------+ # | em-http-request | 0.042891 | # +------------------------------+----------+em-http-request-1.1.2/benchmarks/clients.rb0000644000004100000410000000771712261227270020727 0ustar www-datawww-data$: << './benchmarks' require 'server' require 'excon' require 'httparty' require 'net/http' require 'open-uri' require 'rest_client' require 'tach' require 'typhoeus' url = 'http://127.0.0.1:9292/data/10000' with_server do Tach.meter(100) do tach('curb (persistent)') do |n| curb = Curl::Easy.new n.times do curb.url = url curb.http_get curb.body_str end end tach('em-http-request') do |n| EventMachine.run { count = 0 error = 0 n.times do http = EventMachine::HttpRequest.new(url).get http.callback { count += 1 if count == n p [count, error] EM.stop end } http.errback { count += 1 error += 1 if count == n p [count, error] EM.stop end } end } end tach('em-http-request (persistent)') do |n| EventMachine.run { count = 0 error = 0 conn = EventMachine::HttpRequest.new(url) n.times do http = conn.get :keepalive => true http.callback { count += 1 if count == n p [count, error] EM.stop end } http.errback { count += 1 error += 1 if count == n p [count, error] EM.stop end } end } end tach('Excon') do Excon.get(url).body end excon = Excon.new(url) tach('Excon (persistent)') do excon.request(:method => 'get').body end tach('HTTParty') do HTTParty.get(url).body end uri = Addressable::URI.parse(url) tach('Net::HTTP') do Net::HTTP.start(uri.host, uri.port) {|http| http.get(uri.path).body } end uri = Addressable::URI.parse(url) Net::HTTP.start(uri.host, uri.port) do |http| tach('Net::HTTP (persistent)') do http.get(uri.path).body end end tach('open-uri') do open(url).read end tach('RestClient') do RestClient.get(url) end streamly = StreamlyFFI::Connection.new tach('StreamlyFFI (persistent)') do streamly.get(url) end tach('Typhoeus') do |n| hydra = Typhoeus::Hydra.new( max_concurrency: 8 ) hydra.disable_memoization count = 0 error = 0 n.times { req = Typhoeus::Request.new( url ) req.on_complete do |res| count += 1 error += 1 if !res.success? p [count, error] if count == n end hydra.queue( req ) } hydra.run end end end #+------------------------------+-----------+ #| tach | total | #+------------------------------+-----------+ #| em-http-request (persistent) | 0.145512 | #+------------------------------+-----------+ #| Excon | 0.181564 | #+------------------------------+-----------+ #| RestClient | 0.253127 | #+------------------------------+-----------+ #| Net::HTTP | 0.294412 | #+------------------------------+-----------+ #| HTTParty | 0.305397 | #+------------------------------+-----------+ #| open-uri | 0.307007 | #+------------------------------+-----------+ #| Net::HTTP (persistent) | 0.313716 | #+------------------------------+-----------+ #| Typhoeus | 0.514725 | #+------------------------------+-----------+ #| curb (persistent) | 3.981700 | #+------------------------------+-----------+ #| StreamlyFFI (persistent) | 3.989063 | #+------------------------------+-----------+ #| Excon (persistent) | 4.018761 | #+------------------------------+-----------+ #| em-http-request | 15.025291 | #+------------------------------+-----------+ em-http-request-1.1.2/benchmarks/server.rb0000644000004100000410000000153212261227270020561 0ustar www-datawww-datarequire 'excon' require 'httparty' require 'net/http' require 'open-uri' require 'rest_client' require 'tach' require 'typhoeus' require 'sinatra/base' require 'streamly_ffi' require 'curb' require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'em-http') module Benchmark class Server < Sinatra::Base def self.run Rack::Handler::WEBrick.run( Benchmark::Server.new, :Port => 9292, :AccessLog => [], :Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR) ) end get '/data/:amount' do |amount| 'x' * amount.to_i end end end def with_server(&block) pid = Process.fork do Benchmark::Server.run end loop do sleep(1) begin #Excon.get('http://localhost:9292/api/foo') break rescue end end yield ensure Process.kill(9, pid) end em-http-request-1.1.2/checksums.yaml.gz0000444000004100000410000000041512261227270020076 0ustar www-datawww-dataRe)[ADN Qݵ0sV# !_<_d{<_??v闗Cch 6'4j/_\2ܧqɒU dk;=[h`-RfN 6mP75iKKs@6v\n fV+[?gR'ձP,@ Ƿ^yt3Zd;AmNrqB3nV`em-http-request-1.1.2/em-http-request.gemspec0000644000004100000410000000230712261227270021223 0ustar www-datawww-data# -*- encoding: utf-8 -*- $:.push File.expand_path('../lib', __FILE__) require 'em-http/version' Gem::Specification.new do |s| s.name = 'em-http-request' s.version = EventMachine::HttpRequest::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Ilya Grigorik"] s.email = ['ilya@igvita.com'] s.homepage = 'http://github.com/igrigorik/em-http-request' s.summary = 'EventMachine based, async HTTP Request client' s.description = s.summary s.license = 'MIT' s.rubyforge_project = 'em-http-request' s.add_dependency 'addressable', '>= 2.3.4' s.add_dependency 'cookiejar' s.add_dependency 'em-socksify', '>= 0.3' s.add_dependency 'eventmachine', '>= 1.0.3' s.add_dependency 'http_parser.rb', '>= 0.6.0' s.add_development_dependency 'mongrel', '~> 1.2.0.pre2' s.add_development_dependency 'multi_json' s.add_development_dependency 'rack' s.add_development_dependency 'rake' s.add_development_dependency 'rspec' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ['lib'] end em-http-request-1.1.2/README.md0000644000004100000410000000677212261227270016103 0ustar www-datawww-data# EM-HTTP-Request Async (EventMachine) HTTP client, with support for: - Asynchronous HTTP API for single & parallel request execution - Keep-Alive and HTTP pipelining support - Auto-follow 3xx redirects with max depth - Automatic gzip & deflate decoding - Streaming response processing - Streaming file uploads - HTTP proxy and SOCKS5 support - Basic Auth & OAuth - Connection-level & Global middleware support - HTTP parser via [http_parser.rb](https://github.com/tmm1/http_parser.rb) - Works wherever EventMachine runs: Rubinius, JRuby, MRI ## Getting started gem install em-http-request - Introductory [screencast](http://everburning.com/news/eventmachine-screencast-em-http-request/) - [Issuing GET/POST/etc requests](https://github.com/igrigorik/em-http-request/wiki/Issuing-Requests) - [Issuing parallel requests with Multi interface](https://github.com/igrigorik/em-http-request/wiki/Parallel-Requests) - [Handling Redirects & Timeouts](https://github.com/igrigorik/em-http-request/wiki/Redirects-and-Timeouts) - [Keep-Alive and HTTP Pipelining](https://github.com/igrigorik/em-http-request/wiki/Keep-Alive-and-HTTP-Pipelining) - [Stream processing responses & uploads](https://github.com/igrigorik/em-http-request/wiki/Streaming) - [Issuing requests through HTTP & SOCKS5 proxies](https://github.com/igrigorik/em-http-request/wiki/Proxy) - [Basic Auth & OAuth](https://github.com/igrigorik/em-http-request/wiki/Basic-Auth-and-OAuth) - [GZIP & Deflate decoding](https://github.com/igrigorik/em-http-request/wiki/Compression) - [EM-HTTP Middleware](https://github.com/igrigorik/em-http-request/wiki/Middleware) ## Extensions Several higher-order Ruby projects have incorporated em-http and other Ruby HTTP clients: - [EM-Synchrony](https://github.com/igrigorik/em-synchrony) - Collection of convenience classes and primitives to help untangle evented code (Ruby 1.9 + Fibers). - [Rack-Client](https://github.com/halorgium/rack-client) - Use Rack API for server, test, and client side. Supports Rack middleware! - [Example in action](https://gist.github.com/802391) - [Faraday](https://github.com/lostisland/faraday) - Modular HTTP client library using middleware heavily inspired by Rack. - [Example in action](https://gist.github.com/802395) ## Testing - [WebMock](https://github.com/bblimke/webmock) - Library for stubbing and setting expectations on HTTP requests in Ruby. - Example of [using WebMock, VCR & EM-HTTP](https://gist.github.com/802553) ## Other libraries & applications using EM-HTTP - [VMWare CloudFoundry](https://github.com/cloudfoundry) - The open platform-as-a-service project - [PubSubHubbub](https://github.com/igrigorik/PubSubHubbub) - Asynchronous PubSubHubbub ruby client - [em-net-http](https://github.com/jfairbairn/em-net-http) - Monkeypatching Net::HTTP to play ball with EventMachine - [chirpstream](https://github.com/joshbuddy/chirpstream) - EM client for Twitters Chirpstream API - [rsolr-async](https://github.com/mwmitchell/rsolr-async) - An asynchronus connection adapter for RSolr - [Firering](https://github.com/EmmanuelOga/firering) - Eventmachine powered Campfire API - [RDaneel](https://github.com/hasmanydevelopers/RDaneel) - Ruby crawler which respects robots.txt - [em-eventsource](https://github.com/AF83/em-eventsource) - EventSource client for EventMachine - [sinatra-synchrony](https://github.com/kyledrake/sinatra-synchrony) - Sinatra plugin for synchronous use of EM - and many others.. drop me a link if you want yours included! ### License (MIT License) - Copyright (c) 2011 Ilya Grigorik