Permalink
Browse files

Merge branch 'master' of github.com:elhu/em-http-request

  • Loading branch information...
2 parents 7b25670 + 8605374 commit 25c2ada07fa337fe488d4a937f7056f1abf9d12a @igrigorik igrigorik committed Jun 22, 2013
Showing with 213 additions and 0 deletions.
  1. +25 −0 examples/digest_auth/client.rb
  2. +28 −0 examples/digest_auth/server.rb
  3. +112 −0 lib/em-http/middleware/digest_auth.rb
  4. +48 −0 spec/digest_auth_spec.rb
@@ -0,0 +1,25 @@
+$: << '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
@@ -0,0 +1,28 @@
+require '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
@@ -0,0 +1,112 @@
+module 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
View
@@ -0,0 +1,48 @@
+require '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

0 comments on commit 25c2ada

Please sign in to comment.