Permalink
Browse files

Initial revision

  • Loading branch information...
1 parent ca8b596 commit 863c5ad56a9e3f49f3b75193860bd7678a7e1314 nahi committed Oct 21, 2002
Showing with 1,624 additions and 0 deletions.
  1. +32 −0 install.rb
  2. +1,001 −0 lib/http-access2.rb
  3. +447 −0 lib/http-access2/http.rb
  4. +52 −0 sample/dav.rb
  5. +46 −0 sample/howto.rb
  6. +25 −0 sample/thread.rb
  7. +21 −0 sample/wcat.rb
View
32 install.rb
@@ -0,0 +1,32 @@
+#!/usr/bin/env ruby
+#
+# Installer for http-access2
+# Copyright (C) 2001 Michael Neumann and NAKAMURA, Hiroshi.
+
+require "rbconfig"
+require "ftools"
+include Config
+
+RV = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+DSTPATH = CONFIG["sitedir"] + "/" + RV
+
+begin
+ unless FileTest.directory?( "lib/http-access2" )
+ raise RuntimeError.new( "'lib/http-access2' not found." )
+ end
+
+ File.mkpath DSTPATH + "/http-access2", true
+ Dir["lib/http-access2/*.rb"].each do |name|
+ File.install name, "#{DSTPATH}/http-access2/#{File.basename name}", 0644, true
+ end
+
+ Dir["lib/*.rb"].each do |name|
+ File.install name, "#{DSTPATH}/#{File.basename name}", 0644, true
+ end
+
+rescue
+ puts "install failed!"
+ puts $!
+else
+ puts "install succeed!"
+end
View
1,001 lib/http-access2.rb
@@ -0,0 +1,1001 @@
+# HTTPAccess2 - HTTP accessing library.
+# Copyright (C) 2000, 2001, 2002 NAKAMURA, Hiroshi.
+#
+# This module is copyrighted free software by NAKAMURA, Hiroshi.
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# http-access2.rb is based on http-access.rb in http-access/0.0.4. Some part
+# of code in http-access.rb was recycled in http-access2.rb. Those part is
+# copyrighted by Maehashi-san who made and distribute http-access/0.0.4. Many
+# thanks to Maehashi-san.
+
+
+# Ruby standard library
+require 'singleton'
+require 'timeout'
+require 'uri'
+require 'socket'
+require 'thread'
+
+# Extra library
+require 'http-access2/http'
+
+
+module HTTPAccess2
+
+
+VERSION = '1.1'
+
+RUBY_VERSION_STRING =
+ "ruby #{ RUBY_VERSION } (#{ RUBY_RELEASE_DATE }) [#{ RUBY_PLATFORM }]"
+
+/: (\S+),v (\S+)/ =~
+ %q$Id: http-access2.rb,v 1.1 2002/10/21 04:12:34 nahi Exp $
+RCS_FILE, RCS_REVISION = $1, $2
+
+RS = "\r\n"
+FS = "\r\n\t"
+
+
+# DESCRIPTION
+# HTTPAccess2::Client -- Client to retrieve web resources via HTTP.
+#
+# How to create your client.
+# 1. Create simple client.
+# clnt = HTTPAccess2::Client.new
+#
+# 2. Accessing resources through HTTP proxy.
+# clnt = HTTPAccess2::Client.new( "http://myproxy:8080" )
+#
+# 3. Set User-Agent and From in HTTP request header.(nil means "No proxy")
+# clnt = HTTPAccess2::Client.new( nil, "MyAgent", "nahi@keynauts.com" )
+#
+# How to retrieve web resources.
+# 1. Get content of specified URL.
+# puts clnt.getContent( "http://www.ruby-lang.org/en/" )
+#
+# 2. Do HEAD request.
+# res = clnt.head( uri )
+#
+# 3. Do GET request with query.
+# res = clnt.get( uri )
+#
+# 4. Do POST request.
+# res = clnt.post( uri )
+# res = clnt.get|post|head( uri, proxy )
+#
+class Client
+ attr_reader :agentName # Name of this client.
+ attr_reader :from # Owner of this client.
+
+ attr_accessor :proxy # Proxy
+
+ attr_reader :debugDev # Device for dumping log for debugging.
+
+ attr_reader :sessionManager # Session manager.
+
+ # SYNOPSIS
+ # Client.new( proxy = nil, agentName = nil, from = nil )
+ #
+ # ARGS
+ # proxy A String of HTTP proxy URL. ex. "http://proxy:8080"
+ # agentName A String for "User-Agent" HTTP request header.
+ # from A String for "From" HTTP request header.
+ #
+ # DESCRIPTION
+ # Create an instance.
+ #
+ def initialize( proxy = nil, agentName = nil, from = nil )
+ @proxy = proxy
+ @agentName = agentName
+ @from = from
+ @basicAuth = BasicAuth.new
+
+ @debugDev = nil
+
+ @sessionManager = SessionManager.instance
+ @sessionManager.agentName = @agentName
+ @sessionManager.from = @from
+ end
+
+ # SYNOPSIS
+ # Client#debugDev=( dev )
+ #
+ # ARGS
+ # dev Device for debugging. nil for 'no debugging device'
+ #
+ # DEBT
+ # dev must respond to '<<' method.
+ #
+ # DESCRIPTION
+ # Set debug device. Messages for debugging is dumped to the device.
+ #
+ def debugDev=( dev )
+ @debugDev = dev
+ @sessionManager.debugDev = dev
+ end
+
+ def setBasicAuth( uri, userId, passwd )
+ unless uri.is_a?( URI )
+ uri = URI.parse( uri )
+ end
+ @basicAuth.set( uri, userId, passwd )
+ end
+
+ # SYNOPSIS
+ # Client#getContent( uri, query = nil, extraHeader = {}, &block = nil )
+ #
+ # ARGS
+ # uri anURI or aString of uri to connect.
+ # query aHash or anArray of query part. e.g. { "a" => "b" }.
+ # Give an array to pass multiple value like
+ # [[ "a" => "b" ], [ "a" => "c" ]].
+ # extraHeader
+ # aHash of extra headers like { "SOAPAction" => "urn:foo" }.
+ # &block Give a block to get chunked message-body of response like
+ # getContent( uri ) { | chunkedBody | ... }
+ # Size of each chunk might not be the same.
+ #
+ # DESCRIPTION
+ # Get aString of message-body of response.
+ #
+ # BUGS
+ # getContent should handle 302, etc. Not yet.
+ #
+ def getContent( uri, query = nil, extraHeader = {}, &block )
+ retryNumber = 0
+ while retryNumber < 10
+ res = get( uri, query, extraHeader, &block )
+ case res.status
+ when HTTP::Status::OK
+ return res.body.content
+ when HTTP::Status::MOVED_PERMANENTLY, HTTP::Status::MOVED_TEMPORARILY
+ uri = res.header[ 'location' ][ 0 ]
+ query = nil
+ retryNumber += 1
+ puts "Redirect to: #{ uri }" if $DEBUG
+ else
+ raise RuntimeError.new( "Unexpected response: #{ res.header.inspect }" )
+ end
+ end
+ raise RuntimeError.new( "Retry count exceeded." )
+ end
+
+ def head( uri, query = nil, extraHeader = {} )
+ request( 'HEAD', uri, query, nil, extraHeader )
+ end
+
+ def get( uri, query = nil, extraHeader = {}, &block )
+ request( 'GET', uri, query, nil, extraHeader, &block )
+ end
+
+ def post( uri, body = nil, extraHeader = {}, &block )
+ request( 'POST', uri, nil, body, extraHeader, &block )
+ end
+
+ def put( uri, body = nil, extraHeader = {}, &block )
+ request( 'PUT', uri, nil, body, extraHeader, &block )
+ end
+
+ def delete( uri, extraHeader = {}, &block )
+ request( 'DELETE', uri, nil, nil, extraHeader, &block )
+ end
+
+ def options( uri, extraHeader = {}, &block )
+ request( 'OPTIONS', uri, nil, nil, extraHeader, &block )
+ end
+
+ def trace( uri, query = nil, body = nil, extraHeader = {}, &block )
+ request( 'TRACE', uri, query, body, extraHeader, &block )
+ end
+
+ def request( method, uri, query = nil, body = nil, extraHeader = {}, &block )
+ @debugDev << "= Request\n\n" if @debugDev
+ req = createRequest( method, uri, query, body, extraHeader )
+ sess = @sessionManager.query( req, @proxy )
+ @debugDev << "\n\n= Response\n\n" if @debugDev
+ conn = Connection.new
+ begin
+ doGet( sess, conn, &block )
+ rescue Session::KeepAliveDisconnected
+ # Try again.
+ sess = @sessionManager.query( req, @proxy )
+ doGet( sess, conn, &block )
+ end
+ conn.pop
+ end
+
+ def reset( uri )
+ @sessionManager.reset( uri )
+ end
+
+ ##
+ # Async interface.
+
+ def headAsync( uri, query = nil, extraHeader = {} )
+ requestAsync( 'HEAD', uri, query, nil, extraHeader )
+ end
+
+ def getAsync( uri, query = nil, extraHeader = {}, &block )
+ requestAsync( 'GET', uri, query, nil, extraHeader, &block )
+ end
+
+ def postAsync( uri, body = nil, extraHeader = {}, &block )
+ requestAsync( 'POST', uri, nil, body, extraHeader, &block )
+ end
+
+ def putAsync( uri, body = nil, extraHeader = {}, &block )
+ requestAsync( 'PUT', uri, nil, body, extraHeader, &block )
+ end
+
+ def deleteAsync( uri, extraHeader = {}, &block )
+ requestAsync( 'DELETE', uri, nil, nil, extraHeader, &block )
+ end
+
+ def options( uri, extraHeader = {}, &block )
+ requestAsync( 'OPTIONS', uri, nil, nil, extraHeader, &block )
+ end
+
+ def trace( uri, query = nil, body = nil, extraHeader = {}, &block )
+ requestAsync( 'TRACE', uri, query, body, extraHeader, &block )
+ end
+
+ def requestAsync( method, uri, query = nil, body = nil, extraHeader = {},
+ &block )
+ @debugDev << "= Request\n\n" if @debugDev
+ req = createRequest( method, uri, query, body, extraHeader )
+ sess = @sessionManager.query( req, @proxy )
+ @debugDev << "\n\n= Response\n\n" if @debugDev
+ responseConn = Connection.new
+ t = Thread.new( responseConn ) { | conn |
+ begin
+ doGet( sess, conn, &block )
+ rescue Session::KeepAliveDisconnected
+ # Try again.
+ sess = @sessionManager.query( req, @proxy )
+ doGet( sess, conn, &block )
+ end
+ }
+ responseConn.asyncThread = t
+ responseConn
+ end
+
+ ##
+ # Multiple call interface.
+
+ # ???
+
+private
+ def createRequest( method, uri, query, body, extraHeader )
+ if extraHeader.is_a?( Hash )
+ extraHeader = extraHeader.collect { | key, value | [ key, value ] }
+ end
+ unless uri.is_a?( URI )
+ uri = URI.parse( uri )
+ end
+ cred = @basicAuth.get( uri )
+ if cred
+ extraHeader << [ 'Authorization', "Basic " << cred ]
+ end
+ req = HTTP::Message.newRequest( method, uri, query, body, @proxy )
+ extraHeader.each do | key, value |
+ req.header.set( key, value )
+ end
+ req
+ end
+
+ # !! CAUTION !!
+ # Method 'doGet' runs under MT conditon. Be careful to change.
+ def doGet( sess, conn, &block )
+ res = HTTP::Message.newResponse
+ res.version, res.status, res.reason = sess.getStatus
+ sess.getHeaders().each do | line |
+ unless /^([^:]+)\s*:\s*(.*)$/ =~ line
+ raise RuntimeError.new( "Unparsable header: '#{ line }'." ) if $DEBUG
+ end
+ res.header.set( $1, $2 )
+ end
+ sess.getData() do | str |
+ if block
+ block.call( str )
+ end
+ res.body.load( str )
+ end
+ conn.push( res )
+ @sessionManager.keep( sess ) unless sess.closed?
+ end
+end
+
+
+###
+## HTTPAccess2::BasicAuth -- BasicAuth repository
+#
+class BasicAuth # :nodoc:
+ def initialize
+ @auth = {}
+ end
+
+ def set( uri, userId, passwd )
+ uri = uri.clone
+ uri.path = uri.path.sub( /\/[^\/]*$/, '/' )
+ @auth[ uri ] = [ "#{ userId }:#{ passwd }" ].pack( 'm' ).strip
+ end
+
+ def get( uri )
+ @auth.each do | realmUri, cred |
+ if (( realmUri.host == uri.host ) and
+ ( realmUri.scheme == uri.scheme ) and
+ ( realmUri.port == uri.port ) and
+ uri.path.index( realmUri.path ) == 0)
+ return cred
+ end
+ end
+ nil
+ end
+end
+
+
+###
+## HTTPAccess2::Site -- manage a site( host and port )
+#
+class Site # :nodoc:
+ attr_accessor :host
+ attr_reader :port
+
+ def initialize( host = 'localhost', port = 0 )
+ @host = host
+ @port = port.to_i
+ end
+
+ def addr
+ "http://#{ @host }:#{ @port.to_s }"
+ end
+
+ def port=( port )
+ @port = port.to_i
+ end
+
+ def ==( rhs )
+ if rhs.is_a?( Site )
+ (( @host == rhs.host ) and ( @port == rhs.port ))
+ else
+ false
+ end
+ end
+end
+
+###
+## HTTPAccess2::Connection -- magage a connection(one request and response to it).
+#
+class Connection # :nodoc:
+ attr_accessor :asyncThread
+
+ def initialize( headersQueue = [], bodyQueue = [] )
+ @headers = headersQueue
+ @body = bodyQueue
+ @asyncThread = nil
+ @queue = Queue.new
+ end
+
+ def finished?
+ if !@asyncThread
+ # Not in async mode.
+ true
+ elsif @asyncThread.alive?
+ # Working...
+ false
+ else
+ # Async thread have been finished.
+ @asyncThread.join
+ true
+ end
+ end
+
+ def pop
+ @queue.pop
+ end
+
+ def push( result )
+ @queue.push( result )
+ end
+
+ def join
+ unless @asyncThread
+ false
+ else
+ @asyncThread.join
+ end
+ end
+end
+
+private
+
+###
+## HTTPAccess2::SessionManager -- singleton class to manage several sessions.
+#
+class SessionManager # :nodoc:
+ include Singleton
+
+ attr_accessor :agentName # Name of this client.
+ attr_accessor :from # Owner of this client.
+
+ attr_accessor :protocolVersion # Requested protocol version
+ attr_accessor :chunkSize # Chunk size for chunked request
+ attr_accessor :debugDev # Device for dumping log for debugging
+
+ # Those parameters are not used now...
+ attr_accessor :connectTimeout
+ attr_accessor :connectRetry
+ attr_accessor :sendTimeout
+ attr_accessor :receiveTimeout
+ attr_accessor :readBlockSize
+
+ def initialize
+ @proxy = nil
+
+ @agentName = nil
+ @from = nil
+
+ @protocolVersion = nil
+ @debugDev = nil
+ @chunkSize = 102400
+
+ @connectTimeout = 60
+ @connectRetry = 1
+ @sendTimeout = 120
+ @receiveTimeout = 60 # For each readBlockSize bytes...
+ @readBlockSize = 102400
+
+ @sessPool = []
+ @sessPoolMutex = Mutex.new
+ end
+
+ def proxy=( proxyStr )
+ unless proxyStr
+ @proxy = nil
+ return
+ end
+ uri = URI.parse( proxyStr )
+ @proxy = Site.new( uri.host, uri.port )
+ end
+
+ def query( req, proxyStr )
+ destSite = Site.new( req.header.requestUri.host, req.header.requestUri.port )
+ proxySite = if proxyStr
+ proxyUri = URI.parse( proxyStr )
+ Site.new( proxyUri.host, proxyUri.port )
+ else
+ @proxy
+ end
+ sess = open( destSite, proxySite )
+ begin
+ sess.query( req )
+ rescue
+ # Retry...
+ close( destSite )
+ sess = open( destSite, proxySite )
+ sess.query( req )
+ end
+
+ sess
+ end
+
+ def reset( uri )
+ unless uri.is_a?( URI )
+ uri = URI.parse( uri.to_s )
+ end
+ site = Site.new( uri.host, uri.port )
+ close( site )
+ end
+
+ def keep( sess )
+ addCachedSession( sess )
+ end
+
+private
+ def open( dest, proxy = nil )
+ sess = nil
+ if ( cached = getCachedSession( dest ))
+ sess = cached
+ else
+ sess = Session.new( dest, @agentName, @from )
+ sess.proxy = proxy
+ sess.requestedVersion = @protocolVersion if @protocolVersion
+ sess.connectTimeout = @connectTimeout
+ sess.connectRetry = @connectRetry
+ sess.sendTimeout = @sendTimeout
+ sess.receiveTimeout = @receiveTimeout
+ sess.readBlockSize = @readBlockSize
+ sess.debugDev = @debugDev
+ sess.chunkSize = @chunkSize
+ end
+ sess
+ end
+
+ def close( dest )
+ if ( cached = getCachedSession( dest ))
+ cached.close
+ true
+ else
+ false
+ end
+ end
+
+ def getCachedSession( dest )
+ cached = nil
+ @sessPoolMutex.synchronize do
+ newPool = []
+ @sessPool.each do | s |
+ if s.dest == dest
+ cached = s
+ else
+ newPool << s
+ end
+ end
+ @sessPool = newPool
+ end
+ cached
+ end
+
+ def addCachedSession( sess )
+ @sessPoolMutex.synchronize do
+ @sessPool << sess
+ end
+ end
+end
+
+###
+## HTTPAccess2::DebugSocket -- debugging support
+#
+class DebugSocket
+public
+ attr_accessor :debugDev # Device for dumping log for debugging.
+
+ def initialize( host, port, debugDev )
+ @debugDev = debugDev
+ @socket = TCPSocket.new( host, port )
+ @debugDev << '! CONNECTION ESTABLISHED' << "\n"
+ end
+
+ def addr
+ @socket.addr
+ end
+
+ def close
+ @debugDev << '! CONNECTION CLOSED' << "\n"
+ @socket.close
+ end
+
+ def closed?
+ @socket.closed?
+ end
+
+ def eof?
+ @socket.eof?
+ end
+
+ def gets( *args )
+ str = @socket.gets( *args )
+ @debugDev << str
+ str
+ end
+
+ def read( *args )
+ str = @socket.read( *args )
+ @debugDev << str
+ str
+ end
+
+ def <<( str )
+ dump( str )
+ end
+
+private
+ def dump( str )
+ @socket << str
+ @debugDev << str
+ end
+end
+
+
+###
+## HTTPAccess2::Session -- manage http session with one site.
+## One ore more TCP sessions with the site may be created.
+#
+class Session # :nodoc:
+
+ class Error < StandardError # :nodoc:
+ end
+
+ class InvalidState < Error # :nodoc:
+ end
+
+ class BadResponse < Error # :nodoc:
+ end
+
+ class KeepAliveDisconnected < Error # :nodoc:
+ end
+
+ attr_reader :dest # Destination site
+ attr_reader :src # Source site
+ attr_accessor :proxy # Proxy site
+
+ attr_accessor :requestedVersion # Requested protocol version
+
+ attr_accessor :chunkSize # Chunk size for chunked request
+ attr_accessor :debugDev # Device for dumping log for debugging
+
+ # Those session parameters are not used now...
+ attr_accessor :connectTimeout
+ attr_accessor :connectRetry
+ attr_accessor :sendTimeout
+ attr_accessor :receiveTimeout
+ attr_accessor :readBlockSize
+
+ def initialize( dest, user_agent, from )
+ @dest = dest
+ @src = Site.new
+ @proxy = nil
+ @requestedVersion = VERSION
+
+ @chunkSize = 102400
+ @debugDev = nil
+
+ @connectTimeout = nil
+ @connectRetry = 1
+ @sendTimeout = nil
+ @receiveTimeout = nil
+ @readBlockSize = nil
+
+ @user_agent = user_agent
+ @from = from
+ @state = :INIT
+
+ @requests = []
+
+ @status = nil
+ @reason = nil
+ @headers = []
+ end
+
+ # Send a request to the server
+ def query( req )
+ connect() if @state == :INIT
+
+ begin
+ timeout( @sendTimeout ) do
+ setHeaders( req )
+ req.dump( @socket )
+ end
+ rescue TimeoutError
+ close
+ raise
+ end
+
+ @state = :META if @state == :WAIT
+ @next_connection = nil
+ @requests.push( req )
+ end
+
+ def close
+ unless @socket.nil?
+ @socket.close unless @socket.closed?
+ end
+ @state = :INIT
+ end
+
+ def closed?
+ @state == :INIT
+ end
+
+ def getStatus
+ version = status = reason = nil
+ begin
+ if @state != :META
+ raise RuntimeError.new( "getStatus must be called at the beginning of a session." )
+ end
+ version, status, reason = readHeaders()
+ rescue
+ close
+ raise
+ end
+ return version, status, reason
+ end
+
+ def getHeaders( &block )
+ begin
+ readHeaders() if @state == :META
+ rescue
+ close
+ raise
+ end
+ if block
+ @headers.each do | line |
+ block.call( line )
+ end
+ else
+ @headers
+ end
+ end
+
+ def eof?
+ if @content_length == 0
+ true
+ elsif @readbuf.length > 0
+ false
+ else
+ @socket.closed? or @socket.eof?
+ end
+ end
+
+ def getData( &block )
+ begin
+ readHeaders() if @state == :META
+ return nil if @state != :DATA
+ unless @state == :DATA
+ raise InvalidState.new( 'state != DATA' )
+ end
+ data = nil
+ if block
+ until eof?
+ begin
+ timeout( @receiveTimeout ) do
+ data = readBody()
+ end
+ rescue TimeoutError
+ raise
+ end
+ block.call( data ) if data
+ end
+ data = nil # Calling with block returns nil.
+ else
+ begin
+ timeout( @receiveTimeout ) do
+ data = readBody()
+ end
+ rescue TimeoutError
+ raise
+ end
+ end
+ rescue
+ close
+ raise
+ end
+ if eof?
+ if @next_connection
+ @state = :WAIT
+ else
+ close
+ end
+ end
+ data
+ end
+
+private
+ LibNames = "( #{ RCS_FILE }/#{ RCS_REVISION }, #{ RUBY_VERSION_STRING } )"
+
+ def setHeaders( req )
+ if @user_agent
+ req.header.set( 'User-Agent', "#{ @user_agent } #{ LibNames }" )
+ end
+ if @from
+ req.header.set( 'From', @from )
+ end
+ req.header.set( 'Date', Time.now )
+ end
+
+ # Connect to the server
+ def connect
+ site = @proxy || @dest
+ begin
+ retryNumber = 0
+ timeout( @connectTimeout ) do
+ @socket = if @debugDev
+ DebugSocket.new( site.host, site.port, @debugDev )
+ else
+ TCPSocket.new( site.host, site.port )
+ end
+ end
+ rescue TimeoutError
+ if @connectRetry == 0
+ retry
+ else
+ retryNumber += 1
+ retry if retryNumber < @connectRetry
+ end
+ close
+ raise
+ end
+
+ @src.host = @socket.addr[ 3 ]
+ @src.port = @socket.addr[ 1 ]
+ @state = :WAIT
+ @readbuf = ''
+ end
+
+ # Read status block.
+ StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+(.*))?#{ RS }\z)
+ def readHeaders
+ if @state == :DATA
+ get_data {}
+ check_state()
+ end
+ unless @state == :META
+ raise InvalidState, 'state != :META'
+ end
+
+ begin
+ timeout( @receiveTimeout ) do
+ begin
+ @status_line = @socket.gets( RS )
+ if @status_line.nil?
+ raise KeepAliveDisconnected.new
+ end
+ StatusParseRegexp =~ @status_line
+ unless $1
+ raise BadResponse.new( @status_line )
+ end
+ @version, @status, @reason = $1, $2.to_i, $3
+ @next_connection = if keepAliveEnabled?( @version )
+ true
+ else
+ false
+ end
+
+ @headers = []
+ until (( line = @socket.gets( RS )) == RS )
+ unless line
+ raise BadResponse.new( 'Unexpected EOF.' )
+ end
+ line.sub!( "#{ RS }\\z", '' )
+ if line.sub!( /^\t/, '' )
+ @headers[-1] << line
+ else
+ @headers.push( line )
+ end
+ end
+ end while ( @version == '1.1' && @status == 100 )
+ end
+ rescue TimeoutError
+ raise
+ end
+
+ @content_length = nil
+ @chunked = false
+ @headers.each do | line |
+ case line
+ when /^Content-Length:\s+(\d+)/i
+ @content_length = $1.to_i
+ when /^Transfer-Encoding:\s+chunked/i
+ @chunked = true
+ @content_length = true # how?
+ @chunk_length = 0
+ when /^Connection:\s+([-\w]+)/i, /^Proxy-Connection:\s+([-\w]+)/i
+ case $1
+ when /^Keep-Alive$/i
+ @next_connection = true
+ when /^close$/i
+ @next_connection = false
+ end
+ else
+ # Nothing to parse.
+ end
+ end
+
+ # Head of the request has been parsed.
+ @state = :DATA
+ req = @requests.shift
+
+ if req.header.requestMethod == 'HEAD'
+ @content_length = 0
+ if @next_connection
+ @state = :WAIT
+ else
+ close
+ end
+ end
+
+ @next_connection = false unless @content_length
+
+ return [ @version, @status, @reason ]
+ end
+
+ def readBody
+ if @chunked
+ return readBodyChunked()
+ elsif @content_length == 0
+ return nil
+ elsif @content_length
+ return readBodyLength()
+ else
+ if @readbuf.length > 0
+ data = @readbuf
+ @readbuf = ''
+ return data
+ else
+ data = @socket.read( @readBlockSize )
+ data = nil if data.empty? # Absorbing interface mismatch.
+ return data
+ end
+ end
+ end
+
+ def readBodyLength
+ maxbytes = @readBlockSize
+ if @readbuf.length > 0
+ data = @readbuf[0, @content_length]
+ @readbuf[0, @content_length] = ''
+ @content_length -= data.length
+ return data
+ end
+ maxbytes = @content_length if maxbytes > @content_length
+ data = @socket.read( maxbytes )
+ if data
+ @content_length -= data.length
+ else
+ @content_length = 0
+ end
+ return data
+ end
+
+ def readBodyChunked
+ if @chunk_length == 0
+ until ( i = @readbuf.index( RS ))
+ @readbuf << @socket.gets( RS )
+ end
+ i += 2
+ if @readbuf[0, i] == "0" << RS
+ @content_length = 0
+ unless ( @readbuf[0, 5] == "0" << RS << RS )
+ @readbuf << @socket.gets( RS )
+ end
+ @readbuf[0, 5] = ''
+ return nil
+ end
+ @chunk_length = @readbuf[0, i].hex
+ @readbuf[0, i] = ''
+ end
+ while @readbuf.length < @chunk_length + 2
+ @readbuf << @socket.read( @chunk_length + 2 - @readbuf.length )
+ end
+ data = @readbuf[0, @chunk_length]
+ @readbuf[0, @chunk_length + 2] = ''
+ @chunk_length = 0
+ return data
+ end
+
+ def check_state
+ if @state == :DATA
+ if eof?
+ if @next_connection
+ if @requests.empty?
+ @state = :WAIT
+ else
+ @state = :META
+ end
+ end
+ end
+ end
+ end
+
+ ProtocolVersionRegexp = Regexp.new( '^(\d+)\.(\d+)$' )
+
+ # Persistent connection is usable in 1.1 or later.
+ def keepAliveEnabled?( version )
+ ProtocolVersionRegexp =~ version
+ bEnabled = if ( $1 && ( $1.to_i > 1 ))
+ true
+ elsif ( $2 && ( $2.to_i >= 1 ))
+ true
+ else
+ false
+ end
+ return bEnabled
+ end
+end
+
+
+end
+
+
+HTTPClient = HTTPAccess2::Client
View
447 lib/http-access2/http.rb
@@ -0,0 +1,447 @@
+# HTTP - HTTP container.
+# Copyright (C) 2001, 2002 NAKAMURA, Hiroshi.
+#
+# This module is copyrighted free software by NAKAMURA, Hiroshi.
+# You can redistribute it and/or modify it under the same term as Ruby.
+
+require 'uri'
+
+
+module HTTP
+
+
+module Status
+ OK = 200
+ MOVED_PERMANENTLY = 301
+ MOVED_TEMPORARILY = 302
+ BAD_REQUEST = 400
+ INTERNAL = 500
+end
+
+
+class Error < StandardError; end
+class BadResponseError < Error; end
+
+ class << self
+ def httpDate( aTime )
+ aTime.gmtime.strftime( "%a, %d %b %Y %H:%M:%S GMT" )
+ end
+
+ ProtocolVersionRegexp = Regexp.new( '^(?:HTTP/|)(\d+)\.(\d+)$' )
+ def keepAliveEnabled?( version )
+ ProtocolVersionRegexp =~ version
+ if ( $1 && ( $1.to_i > 1 ))
+ true
+ elsif ( $2 && ( $2.to_i >= 1 ))
+ true
+ else
+ false
+ end
+ end
+ end
+
+
+# HTTP::Message -- HTTP message.
+#
+# DESCRIPTION
+# A class that describes 1 HTTP request / response message.
+#
+class Message
+ CRLF = "\r\n"
+
+ # HTTP::Message::Headers -- HTTP message header.
+ #
+ # DESCRIPTION
+ # A class that describes header part of HTTP message.
+ #
+ class Headers
+ # HTTP version string in a HTTP header.
+ attr_accessor :httpVersion
+ # Content-type.
+ attr_accessor :bodyType
+ # Charset.
+ attr_accessor :bodyCharset
+ # Size of body.
+ attr_reader :bodySize
+ # A milestone of body.
+ attr_accessor :bodyDate
+ # Chunked or not.
+ attr_reader :chunked
+ # Request method.
+ attr_reader :requestMethod
+ # Requested URI.
+ attr_reader :requestUri
+ # HTTP status reason phrase.
+ attr_accessor :reasonPhrase
+
+ StatusCodeMap = {
+ Status::OK => 'OK',
+ Status::MOVED_PERMANENTLY => 'Object moved',
+ Status::MOVED_TEMPORARILY => 'Object moved',
+ Status::BAD_REQUEST => 'Bad Request',
+ Status::INTERNAL => 'Internal Server Error',
+ }
+
+ CharsetMap = {
+ 'NONE' => 'us-ascii',
+ 'EUC' => 'euc-jp',
+ 'SJIS' => 'shift_jis',
+ 'UTF8' => 'utf-8',
+ }
+
+ # SYNOPSIS
+ # HTTP::Message.new
+ #
+ # ARGS
+ # N/A
+ #
+ # DESCRIPTION
+ # Create a instance of HTTP request or HTTP response. Specify
+ # statusCode for HTTP response.
+ #
+ def initialize
+ @isRequest = nil # true, false and nil
+ @httpVersion = 'HTTP/1.1'
+ @bodyType = nil
+ @bodyCharset = nil
+ @bodySize = nil
+ @bodyDate = nil
+ @headerItem = []
+ @chunked = false
+ @responseStatusCode = nil
+ @reasonPhrase = nil
+ @requestMethod = nil
+ @requestUri = nil
+ @requestQueryUri = nil
+ @requestViaProxy = false
+ end
+
+ def initRequest( method, uri, query = nil, viaProxy = false )
+ @isRequest = true
+ @requestMethod = method
+ @requestUri = if uri.is_a?( URI )
+ uri
+ else
+ URI.parse( uri.to_s )
+ end
+ @requestQueryUri = createQueryUri( @requestUri, query )
+ @requestViaProxy = viaProxy
+ end
+
+ def initResponse( statusCode )
+ @isRequest = false
+ self.responseStatusCode = statusCode
+ end
+
+ attr_accessor :requestViaProxy
+
+ attr_reader :responseStatusCode
+ def responseStatusCode=( statusCode )
+ @responseStatusCode = statusCode
+ @reasonPhrase = StatusCodeMap[ @responseStatusCode ]
+ end
+
+ def bodySize=( bodySize )
+ @bodySize = bodySize
+ if @bodySize
+ @chunked = false
+ else
+ @chunked = true
+ end
+ end
+
+ def dump
+ setHeader
+ str = ""
+ if @isRequest
+ str << requestLine
+ else
+ str << responseStatusLine
+ end
+ @headerItem.each do | key, value |
+ str << dumpLine( "#{ key }: #{ value }" )
+ end
+ str
+ end
+
+ def set( key, value )
+ @headerItem.push( [ key, value ] )
+ end
+
+ def get( key = nil )
+ if !key
+ @headerItem
+ else
+ @headerItem.find_all { | pair | pair[ 0 ].upcase == key.upcase }
+ end
+ end
+
+ def []=( key, value )
+ set( key, value )
+ end
+
+ def []( key )
+ get( key ).collect { |item| item[ 1 ] }
+ end
+
+ private
+
+ def requestLine
+ path = unless @requestViaProxy
+ @requestQueryUri
+ else
+ if @requestUri.port
+ "http://#{ @requestUri.host }:#{ @requestUri.port }#{ @requestQueryUri }"
+ else
+ "http://#{ @requestUri.host }#{ @requestQueryUri }"
+ end
+ end
+ dumpLine( "#{ @requestMethod } #{ path } #{ @httpVersion }" )
+ end
+
+ def responseStatusLine
+ if defined?( Apache )
+ dumpLine( "#{ @httpVersion } #{ responseStatusCode } #{ @reasonPhrase }" )
+ else
+ dumpLine( "Status: #{ responseStatusCode } #{ @reasonPhrase }" )
+ end
+ end
+
+ def setHeader
+ if defined?( Apache )
+ set( 'Date', HTTP.httpDate( Time.now ))
+ end
+
+ unless HTTP.keepAliveEnabled?( @httpVersion )
+ set( 'Connection', 'close' )
+ end
+
+ if @chunked
+ set( 'Transfer-Encoding', 'chunked' )
+ else
+ set( 'Content-Length', @bodySize.to_s )
+ end
+
+ if @bodyDate
+ set( 'Last-Modified', HTTP.httpDate( @bodyDate ))
+ end
+
+ if @isRequest == true
+ set( 'Host', @requestUri.host )
+ elsif @isRequest == false
+ set( 'Content-Type', "#{ @bodyType || 'text/html' }; charset=#{ CharsetMap[ @bodyCharset || $KCODE ] }" )
+ end
+ end
+
+ def dumpLine( str )
+ str + CRLF
+ end
+
+ def createQueryUri( uri, query )
+ path = uri.path.dup
+ path = '/' if path.empty?
+ queryStr = nil
+ if uri.query
+ queryStr = uri.query
+ end
+ if query
+ if queryStr
+ queryStr << '&' << createQueryPartStr( query )
+ else
+ queryStr = Message.createQueryPartStr( query )
+ end
+ end
+ if queryStr
+ path << '?' << queryStr
+ end
+ path
+ end
+
+ end
+
+ class Body
+ attr_accessor :type, :charset, :date
+
+ def initialize( body = nil, date = nil, type = nil, charset = nil )
+ @body = body || ''
+ @type = type
+ @charset = charset
+ @date = date
+ end
+
+ def size
+ if @body.is_a?( IO )
+ nil
+ else
+ @body.size
+ end
+ end
+
+ def dump
+ @body
+ end
+
+ def load( str )
+ @body << str
+ end
+
+ def content
+ @body
+ end
+ end
+
+ def initialize
+ @body = @header = nil
+ @chunkSize = 102400
+ end
+
+ class << self
+ alias __new new
+ undef new
+ end
+
+ def self.newRequest( method, uri, query = nil, body = nil, proxy = nil )
+ m = self.__new
+ m.header = Headers.new
+ m.header.initRequest( method, uri, query, proxy )
+ m.body = Body.new( body )
+ m
+ end
+
+ def self.newResponse( body = '' )
+ m = self.__new
+ m.header = Headers.new
+ m.header.initResponse( Status::OK )
+ m.body = Body.new( body )
+ m
+ end
+
+ def dump( dev = '' )
+ syncHeader
+ dev << header.dump
+ dev << dumpEOH
+ if body
+ if header.chunked
+ while !body.content.eof
+ chunk = body.content.read( @chunkSize )
+ dev << dumpChunk( chunk )
+ end
+ dev << dumpLastChunk << dumpEOH
+ else
+ dev << body.dump
+ end
+ end
+ dev
+ end
+
+ def load( str )
+ buf = str.dup
+ unless self.header.load( buf )
+ self.body.load( buf )
+ end
+ end
+
+ def header
+ @header
+ end
+
+ def header=( header )
+ @header = header
+ syncBody
+ end
+
+ def body
+ @body
+ end
+
+ def body=( body )
+ @body = body
+ syncHeader
+ end
+
+ def status
+ @header.responseStatusCode
+ end
+
+ def status=( status )
+ @header.responseStatusCode = status
+ end
+
+ def version
+ @header.httpVersion
+ end
+
+ def version=( version )
+ @header.httpVersion = version
+ end
+
+ def reason
+ @header.reasonPhrase
+ end
+
+ def reason=( reason )
+ @header.reasonPhrase = reason
+ end
+
+ class << self
+ public
+ def createQueryPartStr( query )
+ return case query
+ when Array, Hash
+ escape_query( query )
+ when NilClass
+ nil
+ else
+ query.to_s
+ end
+ end
+
+ def escape_query( query )
+ data = ''
+ query.each do |attr, value|
+ data << '&' if !data.empty?
+ data << URI.escape( attr.to_s ) << '=' << URI.escape( value.to_s )
+ end
+ data
+ end
+ end
+
+private
+
+ def syncHeader
+ if @header and @body
+ @header.bodyType = @body.type
+ @header.bodyCharset = @body.charset
+ @header.bodySize = @body.size
+ @header.bodyDate = @body.date
+ end
+ end
+
+ def syncBody
+ if @header and @body
+ @body.type = @header.bodyType
+ @body.charset = @header.bodyCharset
+ @body.size = @header.bodySize
+ @body.date = @header.bodyDate
+ end
+ end
+
+ def dumpEOH
+ CRLF
+ end
+
+ def dumpChunk( str )
+ dumpChunkSize( str.size ) << str << CRLF
+ end
+
+ def dumpLastChunk
+ dumpChunkSize( 0 )
+ end
+
+ def dumpChunkSize( size )
+ sprintf( "%x", size ) << CRLF
+ end
+end
+
+
+end
View
52 sample/dav.rb
@@ -0,0 +1,52 @@
+require 'uri'
+require 'http-access2'
+
+class DAV
+ attr_reader :headers
+
+ def initialize( uri = nil )
+ @uri = nil
+ @headers = {}
+ open( uri ) if uri
+ proxy = ENV[ 'HTTP_PROXY' ] || ENV[ 'http_proxy' ] || nil
+ @client = HTTPAccess2::Client.new( proxy )
+ end
+
+ def out
+ STDOUT
+ end
+
+ def open( uri )
+ @uri = if uri.is_a?( URI )
+ uri
+ else
+ URI.parse( uri )
+ end
+ end
+
+ def setBasicAuth( userId, passwd )
+ @client.setBasicAuth( @uri, userId, passwd )
+ end
+
+ def get( target, local = nil )
+ local ||= target
+ targetUri = @uri + target
+ if FileTest.exist?( local )
+ raise RuntimeError.new( "File #{ local } exists." )
+ end
+ f = File.open( local, "wb" )
+ res = @client.get( targetUri, nil, @headers ) do | data |
+ f << data
+ end
+ f.close
+ out.puts( "#{ res.header[ 'content-length' ][ 0 ] } bytes saved to file #{ target }." )
+ end
+
+ def put( local, target = nil )
+ target ||= local
+ targetUri = @uri + target
+ out.puts( "Sending file #{ local }." )
+ res = @client.put( targetUri, File.open( local, "rb" ), @headers )
+ out.puts res.body.content
+ end
+end
View
46 sample/howto.rb
@@ -0,0 +1,46 @@
+#!/usr/bin/env ruby
+
+$:.unshift( File.join( '..', 'lib' ))
+require 'http-access2'
+
+proxy = ENV[ 'HTTP_PROXY' ]
+clnt = HTTPAccess2::Client.new( proxy )
+target = ARGV.shift || "http://localhost/foo.cgi"
+
+puts
+puts '= Get content directly'
+puts clnt.getContent( target )
+
+
+puts '= Get result object'
+result = clnt.get( target )
+puts '== Header object'
+p result.header
+puts "== Content-type"
+p result.header[ 'content-type' ][ 0 ]
+puts '== Body object'
+p result.body
+puts '== Content'
+print result.body.content
+
+puts
+puts '= GET with query'
+puts clnt.get( target, { "foo" => "bar", "baz" => "quz" } ).body.content
+
+puts
+puts '= GET with query 2'
+puts clnt.get( target, [[ "foo", "bar1" ], [ "foo", "bar2" ]] ).body.content
+
+
+clnt.debugDev = STDERR
+puts
+puts '= GET with extraHeader'
+puts clnt.get( target, nil, { "SOAPAction" => "HelloWorld" } ).body.content
+
+puts
+puts '= GET with extraHeader 2'
+puts clnt.get( target, nil, [[ "Accept", "text/plain" ], [ "Accept", "text/html" ]] ).body.content
+
+clnt.debugDev = STDERR
+clnt.debugDev = nil
+clnt.reset( target )
View
25 sample/thread.rb
@@ -0,0 +1,25 @@
+$:.unshift( File.join( '..', 'lib' ))
+require 'http-access2'
+
+urlstr = ARGV.shift
+
+proxy = ENV[ 'HTTP_PROXY' ] || ENV[ 'http_proxy' ]
+h = HTTPAccess2::Client.new( proxy )
+
+res = []
+g = []
+for i in 0..29
+ g << Thread.new {
+ res[ i ] = h.get( urlstr )
+ }
+end
+
+g.each do | th |
+ th.join
+end
+
+for i in 0..28
+ raise unless ( res[ i ].body.content == res[ i + 1 ].body.content )
+end
+
+puts 'ok'
View
21 sample/wcat.rb
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+
+# wcat for http-access2
+# Copyright (C) 2001 TAKAHASHI Masayoshi
+
+$:.unshift( File.join( '..', 'lib' ))
+require 'http-access2'
+
+if ENV['HTTP_PROXY']
+ h = HTTPAccess2::Client.new(ENV['HTTP_PROXY'])
+else
+ h = HTTPAccess2::Client.new()
+end
+
+while urlstr = ARGV.shift
+ response = h.get(urlstr){ |data|
+ print data
+ }
+ p response.header[ 'content-type' ]
+ p response.body.size
+end

0 comments on commit 863c5ad

Please sign in to comment.