minhttp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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