minhttp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ require_relative 'lib/min_http'
2
+
3
+ data = <<-HTTP
4
+ GET / HTTP/1.0\r
5
+ Host: www.google.com\r
6
+
7
+ HTTP
8
+
9
+ EventMachine::run do
10
+ Http::Min.connect("www.google.com", data) do |raw_response, parsed_response|
11
+ puts "Received #{parsed_response.status_code} status from Google"
12
+ puts "First 100 characters of raw HTTP response:"
13
+ puts raw_response[0..100]
14
+ EM::stop
15
+ end
16
+ end
data/lib/minhttp.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'http/parser'
2
+ require 'eventmachine'
3
+ require 'logger'
4
+ require_relative "ssl_validator"
5
+
6
+ #
7
+ # The minimal HTTP client
8
+ # Sends a raw http request (bytes)
9
+ # Parses the response and provides both the parsed and the raw response
10
+ # Supports ssl
11
+ #
12
+ module Http
13
+ class Min < EventMachine::Connection
14
+
15
+ attr_accessor :host, :ssl, :callback, :request_data
16
+
17
+ def self.connections
18
+ @@connections
19
+ end
20
+
21
+ def self.configure(options={})
22
+ @@options = options
23
+ @@logger = options[:logger] || Logger.new(STDOUT)
24
+ @@connections = 0
25
+ end
26
+
27
+ def self.configured?
28
+ class_variable_defined?("@@logger")
29
+ end
30
+
31
+ def self.connect(host, data, port=80, ssl=false, &callback)
32
+ configure unless configured?
33
+
34
+ EventMachine.connect(host, port, self) do |c|
35
+ # this code runs after 'post_init', before 'connection_completed'
36
+ c.host = host
37
+ c.ssl = ssl
38
+ c.callback = callback
39
+ c.request_data = data
40
+ end
41
+ end
42
+
43
+ def post_init
44
+ begin
45
+ @@connections += 1
46
+ @parser = Http::Parser.new
47
+ @response_data = ""
48
+ rescue Exception => e
49
+ @@logger.error("Error in post_init: #{e}")
50
+ raise e
51
+ end
52
+ end
53
+
54
+ def connection_completed
55
+ begin
56
+ start_tls(:verify_peer => true) if @ssl
57
+ send_data @request_data
58
+ rescue Exception => e
59
+ puts "Error in connection_completed: #{e}"
60
+ end
61
+ end
62
+
63
+ def receive_data(data)
64
+ @response_data << data
65
+ begin
66
+ @parser << data
67
+ rescue HTTP::Parser::Error => e
68
+ @@logger.warn "Failed to parse: #{data}"
69
+ raise e
70
+ end
71
+ end
72
+
73
+ def unbind
74
+ begin
75
+ @@connections -= 1
76
+ @callback.call(@response_data, @parser)
77
+ rescue Exception => e
78
+ @@logger.error("Error in unbind: #{e}")
79
+ raise e
80
+ end
81
+ end
82
+
83
+ #
84
+ # Called once per cert received
85
+ # The certs aren't verified until the handshake is completed
86
+ #
87
+ def ssl_verify_peer(cert)
88
+ begin
89
+ @certs ||= []
90
+ @certs << cert unless @certs.include?(cert)
91
+ true
92
+ rescue Exception => e
93
+ @@logger.error("Error in ssl_verify_peer: #{e}")
94
+ raise e
95
+ end
96
+ end
97
+
98
+ #
99
+ # Verify the certs and throw an exception if they are not valid
100
+ #
101
+ def ssl_handshake_completed
102
+ begin
103
+ return unless @@options[:verify_ssl]
104
+ close_connection unless Http::SSLValidator.validate(@certs, @host)
105
+ rescue Exception => e
106
+ @@logger.error("Error in ssl_handshake_completed: #{e}")
107
+ raise e
108
+ end
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,80 @@
1
+ require 'openssl'
2
+ require 'logger'
3
+
4
+ module Http
5
+ class SSLValidator
6
+ class << self
7
+
8
+ def configure(logger)
9
+ @@logger = logger || Logger.new(STDOUT)
10
+ create_store
11
+ end
12
+
13
+ def configured?
14
+ class_variable_defined?("@@logger")
15
+ end
16
+
17
+ #
18
+ # Completes the 3 steps to certificate chain verification
19
+ # Also applies if there is just one cert in the chain, but the last
20
+ # step won't run
21
+ #
22
+ def validate(certs, host)
23
+ certs = certs.collect { |c| OpenSSL::X509::Certificate.new(c) }
24
+ @@logger.debug("Verifying certs for #{host}")
25
+
26
+ # 1. Verify that the last cert has a valid hostname
27
+ unless OpenSSL::SSL.verify_certificate_identity(certs.last, host)
28
+ @@logger.error("Hostname #{host} does not match cert: #{certs.last}")
29
+ return false
30
+ end
31
+
32
+ # 2. Verify that the first cert can be validated by a root certificate
33
+ unless @@store.verify(certs.first)
34
+ @@logger.error("Cert not validated by any of the root certificates in my store: #{certs.first}")
35
+ return false
36
+ end
37
+
38
+ # 3. Verify that every cert in the chain is validated by the cert after it
39
+ (certs.length - 1).times do |i|
40
+ cert_a = certs[i+1]
41
+ cert_b = certs[i]
42
+ unless cert_a.verify(cert_b.public_key)
43
+ @@logger.error("Broken link in certificate chain for #{host} between #{cert_a} and #{cert_b}")
44
+ return false
45
+ end
46
+ end
47
+ true
48
+ end
49
+
50
+
51
+ private
52
+
53
+ #
54
+ # Create a Ruby OpenSSL certificate store with all the certs
55
+ # in the given root certificate file. Ruby doesn't supply a clean
56
+ # way to get the root certs out of a root cert file so I have to split
57
+ # it manually.
58
+ #
59
+ def create_store
60
+ configure unless configured?
61
+
62
+ # Root certs downloaded from: http://curl.haxx.se/ca/cacert.pem
63
+ ca_file_path = File.join(File.dirname(__FILE__), "../cacert.pem")
64
+
65
+ @@store = OpenSSL::X509::Store.new
66
+ splitter = "END CERTIFICATE-----"
67
+ File.read(ca_file_path).strip.split(splitter).each do |c|
68
+ begin
69
+ c << splitter
70
+ @@store.add_cert OpenSSL::X509::Certificate.new(c)
71
+ rescue OpenSSL::X509::CertificateError
72
+ @@logger.warn "Error loading cert from #{c} from #{ca_file_path}"
73
+ end
74
+ end
75
+ @@store
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ require 'minitest/autorun'
2
+ require_relative "../lib/minhttp"
3
+
4
+ class SimpleTest < MiniTest::Unit::TestCase
5
+ def test_simple_google
6
+ data = <<-HTTP
7
+ GET / HTTP/1.0\r
8
+ Host: www.google.com\r
9
+
10
+ HTTP
11
+
12
+ EventMachine::run do
13
+ Http::Min.connect("www.google.com", data) do |raw_response, parsed_response|
14
+ assert(parsed_response.status_code == 200, "Response from google should be 200 but is #{parsed_response.status_code}")
15
+ assert(raw_response.length > 0, "Raw response from google should be have size larger than 0")
16
+ EM::stop
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+
@@ -0,0 +1,10 @@
1
+ require 'minitest/autorun'
2
+
3
+ class SimpleTest < MiniTest::Unit::TestCase
4
+ #TEST_DIR = File.dirname(__FILE__)
5
+
6
+ def simple_test(test_name)
7
+
8
+ end
9
+ end
10
+
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minhttp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease: !!null
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Farmer
9
+ autorequire: !!null
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-05-10 00:00:00.000000000 -07:00
13
+ default_executable: !!null
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: http_parser.rb
17
+ requirement: &9355460 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *9355460
26
+ - !ruby/object:Gem::Dependency
27
+ name: eventmachine
28
+ requirement: &9338000 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *9338000
37
+ description: MinHTTP allows one to send and receive raw HTTP requests. It's a very
38
+ thin wrapper around EventMachine's connect method with some SSL validation added.
39
+ email:
40
+ - ahfarmer@gmail.com
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - README.md
46
+ - VERSION
47
+ - Gemfile
48
+ - Rakefile
49
+ - cacert.pem
50
+ - examples/readme_example.rb
51
+ - lib/ssl_validator.rb
52
+ - lib/minhttp.rb
53
+ - test/simple_test.rb~
54
+ - test/simple_test.rb
55
+ has_rdoc: true
56
+ homepage: http://github.com/ahfarmer/minhttp
57
+ licenses: []
58
+ post_install_message: !!null
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project: minhttp
76
+ rubygems_version: 1.5.0
77
+ signing_key: !!null
78
+ specification_version: 3
79
+ summary: An HTTP library for the minimalist.
80
+ test_files:
81
+ - test/simple_test.rb~
82
+ - test/simple_test.rb