diff options
Diffstat (limited to 'jni/ruby/lib/webrick')
34 files changed, 6468 insertions, 0 deletions
| diff --git a/jni/ruby/lib/webrick/accesslog.rb b/jni/ruby/lib/webrick/accesslog.rb new file mode 100644 index 0000000..4df27ef --- /dev/null +++ b/jni/ruby/lib/webrick/accesslog.rb @@ -0,0 +1,158 @@ +#-- +# accesslog.rb -- Access log handling utilities +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2002 keita yamaguchi +# Copyright (c) 2002 Internet Programming with Ruby writers +# +# $IPR: accesslog.rb,v 1.1 2002/10/01 17:16:32 gotoyuzo Exp $ + +module WEBrick + +  ## +  # AccessLog provides logging to various files in various formats. +  # +  # Multiple logs may be written to at the same time: +  # +  #   access_log = [ +  #     [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT], +  #     [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT], +  #   ] +  # +  #   server = WEBrick::HTTPServer.new :AccessLog => access_log +  # +  # Custom log formats may be defined.  WEBrick::AccessLog provides a subset +  # of the formatting from Apache's mod_log_config +  # http://httpd.apache.org/docs/mod/mod_log_config.html#formats.  See +  # AccessLog::setup_params for a list of supported options + +  module AccessLog + +    ## +    # Raised if a parameter such as %e, %i, %o or %n is used without fetching +    # a specific field. + +    class AccessLogError < StandardError; end + +    ## +    # The Common Log Format's time format + +    CLF_TIME_FORMAT     = "[%d/%b/%Y:%H:%M:%S %Z]" + +    ## +    # Common Log Format + +    COMMON_LOG_FORMAT   = "%h %l %u %t \"%r\" %s %b" + +    ## +    # Short alias for Common Log Format + +    CLF                 = COMMON_LOG_FORMAT + +    ## +    # Referer Log Format + +    REFERER_LOG_FORMAT  = "%{Referer}i -> %U" + +    ## +    # User-Agent Log Format + +    AGENT_LOG_FORMAT    = "%{User-Agent}i" + +    ## +    # Combined Log Format + +    COMBINED_LOG_FORMAT = "#{CLF} \"%{Referer}i\" \"%{User-agent}i\"" + +    module_function + +    # This format specification is a subset of mod_log_config of Apache: +    # +    # %a:: Remote IP address +    # %b:: Total response size +    # %e{variable}:: Given variable in ENV +    # %f:: Response filename +    # %h:: Remote host name +    # %{header}i:: Given request header +    # %l:: Remote logname, always "-" +    # %m:: Request method +    # %{attr}n:: Given request attribute from <tt>req.attributes</tt> +    # %{header}o:: Given response header +    # %p:: Server's request port +    # %{format}p:: The canonical port of the server serving the request or the +    #              actual port or the client's actual port.  Valid formats are +    #              canonical, local or remote. +    # %q:: Request query string +    # %r:: First line of the request +    # %s:: Request status +    # %t:: Time the request was received +    # %T:: Time taken to process the request +    # %u:: Remote user from auth +    # %U:: Unparsed URI +    # %%:: Literal % + +    def setup_params(config, req, res) +      params = Hash.new("") +      params["a"] = req.peeraddr[3] +      params["b"] = res.sent_size +      params["e"] = ENV +      params["f"] = res.filename || "" +      params["h"] = req.peeraddr[2] +      params["i"] = req +      params["l"] = "-" +      params["m"] = req.request_method +      params["n"] = req.attributes +      params["o"] = res +      params["p"] = req.port +      params["q"] = req.query_string +      params["r"] = req.request_line.sub(/\x0d?\x0a\z/o, '') +      params["s"] = res.status       # won't support "%>s" +      params["t"] = req.request_time +      params["T"] = Time.now - req.request_time +      params["u"] = req.user || "-" +      params["U"] = req.unparsed_uri +      params["v"] = config[:ServerName] +      params +    end + +    ## +    # Formats +params+ according to +format_string+ which is described in +    # setup_params. + +    def format(format_string, params) +      format_string.gsub(/\%(?:\{(.*?)\})?>?([a-zA-Z%])/){ +         param, spec = $1, $2 +         case spec[0] +         when ?e, ?i, ?n, ?o +           raise AccessLogError, +             "parameter is required for \"#{spec}\"" unless param +           (param = params[spec][param]) ? escape(param) : "-" +         when ?t +           params[spec].strftime(param || CLF_TIME_FORMAT) +         when ?p +           case param +           when 'remote' +             escape(params["i"].peeraddr[1].to_s) +           else +             escape(params["p"].to_s) +           end +         when ?% +           "%" +         else +           escape(params[spec].to_s) +         end +      } +    end + +    ## +    # Escapes control characters in +data+ + +    def escape(data) +      if data.tainted? +        data.gsub(/[[:cntrl:]\\]+/) {$&.dump[1...-1]}.untaint +      else +        data +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/cgi.rb b/jni/ruby/lib/webrick/cgi.rb new file mode 100644 index 0000000..ee19055 --- /dev/null +++ b/jni/ruby/lib/webrick/cgi.rb @@ -0,0 +1,308 @@ +# +# cgi.rb -- Yet another CGI library +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $Id: cgi.rb 38945 2013-01-26 01:12:54Z drbrain $ + +require "webrick/httprequest" +require "webrick/httpresponse" +require "webrick/config" +require "stringio" + +module WEBrick + +  # A CGI library using WEBrick requests and responses. +  # +  # Example: +  # +  #   class MyCGI < WEBrick::CGI +  #     def do_GET req, res +  #       res.body = 'it worked!' +  #       res.status = 200 +  #     end +  #   end +  # +  #   MyCGI.new.start + +  class CGI + +    # The CGI error exception class + +    CGIError = Class.new(StandardError) + +    ## +    # The CGI configuration.  This is based on WEBrick::Config::HTTP + +    attr_reader :config + +    ## +    # The CGI logger + +    attr_reader :logger + +    ## +    # Creates a new CGI interface. +    # +    # The first argument in +args+ is a configuration hash which would update +    # WEBrick::Config::HTTP. +    # +    # Any remaining arguments are stored in the <code>@options</code> instance +    # variable for use by a subclass. + +    def initialize(*args) +      if defined?(MOD_RUBY) +        unless ENV.has_key?("GATEWAY_INTERFACE") +          Apache.request.setup_cgi_env +        end +      end +      if %r{HTTP/(\d+\.\d+)} =~ ENV["SERVER_PROTOCOL"] +        httpv = $1 +      end +      @config = WEBrick::Config::HTTP.dup.update( +        :ServerSoftware => ENV["SERVER_SOFTWARE"] || "null", +        :HTTPVersion    => HTTPVersion.new(httpv || "1.0"), +        :RunOnCGI       => true,   # to detect if it runs on CGI. +        :NPH            => false   # set true to run as NPH script. +      ) +      if config = args.shift +        @config.update(config) +      end +      @config[:Logger] ||= WEBrick::BasicLog.new($stderr) +      @logger = @config[:Logger] +      @options = args +    end + +    ## +    # Reads +key+ from the configuration + +    def [](key) +      @config[key] +    end + +    ## +    # Starts the CGI process with the given environment +env+ and standard +    # input and output +stdin+ and +stdout+. + +    def start(env=ENV, stdin=$stdin, stdout=$stdout) +      sock = WEBrick::CGI::Socket.new(@config, env, stdin, stdout) +      req = HTTPRequest.new(@config) +      res = HTTPResponse.new(@config) +      unless @config[:NPH] or defined?(MOD_RUBY) +        def res.setup_header +          unless @header["status"] +            phrase = HTTPStatus::reason_phrase(@status) +            @header["status"] = "#{@status} #{phrase}" +          end +          super +        end +        def res.status_line +          "" +        end +      end + +      begin +        req.parse(sock) +        req.script_name = (env["SCRIPT_NAME"] || File.expand_path($0)).dup +        req.path_info = (env["PATH_INFO"] || "").dup +        req.query_string = env["QUERY_STRING"] +        req.user = env["REMOTE_USER"] +        res.request_method = req.request_method +        res.request_uri = req.request_uri +        res.request_http_version = req.http_version +        res.keep_alive = req.keep_alive? +        self.service(req, res) +      rescue HTTPStatus::Error => ex +        res.set_error(ex) +      rescue HTTPStatus::Status => ex +        res.status = ex.code +      rescue Exception => ex +        @logger.error(ex) +        res.set_error(ex, true) +      ensure +        req.fixup +        if defined?(MOD_RUBY) +          res.setup_header +          Apache.request.status_line = "#{res.status} #{res.reason_phrase}" +          Apache.request.status = res.status +          table = Apache.request.headers_out +          res.header.each{|key, val| +            case key +            when /^content-encoding$/i +              Apache::request.content_encoding = val +            when /^content-type$/i +              Apache::request.content_type = val +            else +              table[key] = val.to_s +            end +          } +          res.cookies.each{|cookie| +            table.add("Set-Cookie", cookie.to_s) +          } +          Apache.request.send_http_header +          res.send_body(sock) +        else +          res.send_response(sock) +        end +      end +    end + +    ## +    # Services the request +req+ which will fill in the response +res+.  See +    # WEBrick::HTTPServlet::AbstractServlet#service for details. + +    def service(req, res) +      method_name = "do_" + req.request_method.gsub(/-/, "_") +      if respond_to?(method_name) +        __send__(method_name, req, res) +      else +        raise HTTPStatus::MethodNotAllowed, +              "unsupported method `#{req.request_method}'." +      end +    end + +    ## +    # Provides HTTP socket emulation from the CGI environment + +    class Socket # :nodoc: +      include Enumerable + +      private + +      def initialize(config, env, stdin, stdout) +        @config = config +        @env = env +        @header_part = StringIO.new +        @body_part = stdin +        @out_port = stdout +        @out_port.binmode + +        @server_addr = @env["SERVER_ADDR"] || "0.0.0.0" +        @server_name = @env["SERVER_NAME"] +        @server_port = @env["SERVER_PORT"] +        @remote_addr = @env["REMOTE_ADDR"] +        @remote_host = @env["REMOTE_HOST"] || @remote_addr +        @remote_port = @env["REMOTE_PORT"] || 0 + +        begin +          @header_part << request_line << CRLF +          setup_header +          @header_part << CRLF +          @header_part.rewind +        rescue Exception +          raise CGIError, "invalid CGI environment" +        end +      end + +      def request_line +        meth = @env["REQUEST_METHOD"] || "GET" +        unless url = @env["REQUEST_URI"] +          url = (@env["SCRIPT_NAME"] || File.expand_path($0)).dup +          url << @env["PATH_INFO"].to_s +          url = WEBrick::HTTPUtils.escape_path(url) +          if query_string = @env["QUERY_STRING"] +            unless query_string.empty? +              url << "?" << query_string +            end +          end +        end +        # we cannot get real HTTP version of client ;) +        httpv = @config[:HTTPVersion] +        return "#{meth} #{url} HTTP/#{httpv}" +      end + +      def setup_header +        @env.each{|key, value| +          case key +          when "CONTENT_TYPE", "CONTENT_LENGTH" +            add_header(key.gsub(/_/, "-"), value) +          when /^HTTP_(.*)/ +            add_header($1.gsub(/_/, "-"), value) +          end +        } +      end + +      def add_header(hdrname, value) +        unless value.empty? +          @header_part << hdrname << ": " << value << CRLF +        end +      end + +      def input +        @header_part.eof? ? @body_part : @header_part +      end + +      public + +      def peeraddr +        [nil, @remote_port, @remote_host, @remote_addr] +      end + +      def addr +        [nil, @server_port, @server_name, @server_addr] +      end + +      def gets(eol=LF, size=nil) +        input.gets(eol, size) +      end + +      def read(size=nil) +        input.read(size) +      end + +      def each +        input.each{|line| yield(line) } +      end + +      def eof? +        input.eof? +      end + +      def <<(data) +        @out_port << data +      end + +      def cert +        return nil unless defined?(OpenSSL) +        if pem = @env["SSL_SERVER_CERT"] +          OpenSSL::X509::Certificate.new(pem) unless pem.empty? +        end +      end + +      def peer_cert +        return nil unless defined?(OpenSSL) +        if pem = @env["SSL_CLIENT_CERT"] +          OpenSSL::X509::Certificate.new(pem) unless pem.empty? +        end +      end + +      def peer_cert_chain +        return nil unless defined?(OpenSSL) +        if @env["SSL_CLIENT_CERT_CHAIN_0"] +          keys = @env.keys +          certs = keys.sort.collect{|k| +            if /^SSL_CLIENT_CERT_CHAIN_\d+$/ =~ k +              if pem = @env[k] +                OpenSSL::X509::Certificate.new(pem) unless pem.empty? +              end +            end +          } +          certs.compact +        end +      end + +      def cipher +        return nil unless defined?(OpenSSL) +        if cipher = @env["SSL_CIPHER"] +          ret = [ cipher ] +          ret << @env["SSL_PROTOCOL"] +          ret << @env["SSL_CIPHER_USEKEYSIZE"] +          ret << @env["SSL_CIPHER_ALGKEYSIZE"] +          ret +        end +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/compat.rb b/jni/ruby/lib/webrick/compat.rb new file mode 100644 index 0000000..d2bc3ef --- /dev/null +++ b/jni/ruby/lib/webrick/compat.rb @@ -0,0 +1,35 @@ +# +# compat.rb -- cross platform compatibility +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2002 GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: compat.rb,v 1.6 2002/10/01 17:16:32 gotoyuzo Exp $ + +## +# System call error module used by webrick for cross platform compatibility. +# +# EPROTO:: protocol error +# ECONNRESET:: remote host reset the connection request +# ECONNABORTED:: Client sent TCP reset (RST) before server has accepted the +#                connection requested by client. +# +module Errno +  ## +  # Protocol error. + +  class EPROTO       < SystemCallError; end + +  ## +  # Remote host reset the connection request. + +  class ECONNRESET   < SystemCallError; end + +  ## +  # Client sent TCP reset (RST) before server has accepted the connection +  # requested by client. + +  class ECONNABORTED < SystemCallError; end +end diff --git a/jni/ruby/lib/webrick/config.rb b/jni/ruby/lib/webrick/config.rb new file mode 100644 index 0000000..c347da4 --- /dev/null +++ b/jni/ruby/lib/webrick/config.rb @@ -0,0 +1,151 @@ +# +# config.rb -- Default configurations. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $ + +require 'webrick/version' +require 'webrick/httpversion' +require 'webrick/httputils' +require 'webrick/utils' +require 'webrick/log' + +module WEBrick +  module Config +    LIBDIR = File::dirname(__FILE__) # :nodoc: + +    # for GenericServer +    General = { +      :ServerName     => Utils::getservername, +      :BindAddress    => nil,   # "0.0.0.0" or "::" or nil +      :Port           => nil,   # users MUST specify this!! +      :MaxClients     => 100,   # maximum number of the concurrent connections +      :ServerType     => nil,   # default: WEBrick::SimpleServer +      :Logger         => nil,   # default: WEBrick::Log.new +      :ServerSoftware => "WEBrick/#{WEBrick::VERSION} " + +                         "(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})", +      :TempDir        => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp', +      :DoNotListen    => false, +      :StartCallback  => nil, +      :StopCallback   => nil, +      :AcceptCallback => nil, +      :DoNotReverseLookup => nil, +      :ShutdownSocketWithoutClose => false, +    } + +    # for HTTPServer, HTTPRequest, HTTPResponse ... +    HTTP = General.dup.update( +      :Port           => 80, +      :RequestTimeout => 30, +      :HTTPVersion    => HTTPVersion.new("1.1"), +      :AccessLog      => nil, +      :MimeTypes      => HTTPUtils::DefaultMimeTypes, +      :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"], +      :DocumentRoot   => nil, +      :DocumentRootOptions => { :FancyIndexing => true }, +      :RequestCallback => nil, +      :ServerAlias    => nil, +      :InputBufferSize  => 65536, # input buffer size in reading request body +      :OutputBufferSize => 65536, # output buffer size in sending File or IO + +      # for HTTPProxyServer +      :ProxyAuthProc  => nil, +      :ProxyContentHandler => nil, +      :ProxyVia       => true, +      :ProxyTimeout   => true, +      :ProxyURI       => nil, + +      :CGIInterpreter => nil, +      :CGIPathEnv     => nil, + +      # workaround: if Request-URIs contain 8bit chars, +      # they should be escaped before calling of URI::parse(). +      :Escape8bitURI  => false +    ) + +    ## +    # Default configuration for WEBrick::HTTPServlet::FileHandler +    # +    # :AcceptableLanguages:: +    #   Array of languages allowed for accept-language.  There is no default +    # :DirectoryCallback:: +    #   Allows preprocessing of directory requests.  There is no default +    #   callback. +    # :FancyIndexing:: +    #   If true, show an index for directories.  The default is true. +    # :FileCallback:: +    #   Allows preprocessing of file requests.  There is no default callback. +    # :HandlerCallback:: +    #   Allows preprocessing of requests.  There is no default callback. +    # :HandlerTable:: +    #   Maps file suffixes to file handlers.  DefaultFileHandler is used by +    #   default but any servlet can be used. +    # :NondisclosureName:: +    #   Do not show files matching this array of globs.  .ht* and *~ are +    #   excluded by default. +    # :UserDir:: +    #   Directory inside ~user to serve content from for /~user requests. +    #   Only works if mounted on /.  Disabled by default. + +    FileHandler = { +      :NondisclosureName => [".ht*", "*~"], +      :FancyIndexing     => false, +      :HandlerTable      => {}, +      :HandlerCallback   => nil, +      :DirectoryCallback => nil, +      :FileCallback      => nil, +      :UserDir           => nil,  # e.g. "public_html" +      :AcceptableLanguages => []  # ["en", "ja", ... ] +    } + +    ## +    # Default configuration for WEBrick::HTTPAuth::BasicAuth +    # +    # :AutoReloadUserDB:: Reload the user database provided by :UserDB +    #                     automatically? + +    BasicAuth = { +      :AutoReloadUserDB     => true, +    } + +    ## +    # Default configuration for WEBrick::HTTPAuth::DigestAuth. +    # +    # :Algorithm:: MD5, MD5-sess (default), SHA1, SHA1-sess +    # :Domain:: An Array of URIs that define the protected space +    # :Qop:: 'auth' for authentication, 'auth-int' for integrity protection or +    #        both +    # :UseOpaque:: Should the server send opaque values to the client?  This +    #              helps prevent replay attacks. +    # :CheckNc:: Should the server check the nonce count?  This helps the +    #            server detect replay attacks. +    # :UseAuthenticationInfoHeader:: Should the server send an +    #                                AuthenticationInfo header? +    # :AutoReloadUserDB:: Reload the user database provided by :UserDB +    #                     automatically? +    # :NonceExpirePeriod:: How long should we store used nonces?  Default is +    #                      30 minutes. +    # :NonceExpireDelta:: How long is a nonce valid?  Default is 1 minute +    # :InternetExplorerHack:: Hack which allows Internet Explorer to work. +    # :OperaHack:: Hack which allows Opera to work. + +    DigestAuth = { +      :Algorithm            => 'MD5-sess', # or 'MD5' +      :Domain               => nil,        # an array includes domain names. +      :Qop                  => [ 'auth' ], # 'auth' or 'auth-int' or both. +      :UseOpaque            => true, +      :UseNextNonce         => false, +      :CheckNc              => false, +      :UseAuthenticationInfoHeader => true, +      :AutoReloadUserDB     => true, +      :NonceExpirePeriod    => 30*60, +      :NonceExpireDelta     => 60, +      :InternetExplorerHack => true, +      :OperaHack            => true, +    } +  end +end diff --git a/jni/ruby/lib/webrick/cookie.rb b/jni/ruby/lib/webrick/cookie.rb new file mode 100644 index 0000000..d8df231 --- /dev/null +++ b/jni/ruby/lib/webrick/cookie.rb @@ -0,0 +1,171 @@ +# +# cookie.rb -- Cookie class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $ + +require 'time' +require 'webrick/httputils' + +module WEBrick + +  ## +  # Processes HTTP cookies + +  class Cookie + +    ## +    # The cookie name + +    attr_reader :name + +    ## +    # The cookie value + +    attr_accessor :value + +    ## +    # The cookie version + +    attr_accessor :version + +    ## +    # The cookie domain +    attr_accessor :domain + +    ## +    # The cookie path + +    attr_accessor :path + +    ## +    # Is this a secure cookie? + +    attr_accessor :secure + +    ## +    # The cookie comment + +    attr_accessor :comment + +    ## +    # The maximum age of the cookie + +    attr_accessor :max_age + +    #attr_accessor :comment_url, :discard, :port + +    ## +    # Creates a new cookie with the given +name+ and +value+ + +    def initialize(name, value) +      @name = name +      @value = value +      @version = 0     # Netscape Cookie + +      @domain = @path = @secure = @comment = @max_age = +      @expires = @comment_url = @discard = @port = nil +    end + +    ## +    # Sets the cookie expiration to the time +t+.  The expiration time may be +    # a false value to disable expiration or a Time or HTTP format time string +    # to set the expiration date. + +    def expires=(t) +      @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s) +    end + +    ## +    # Retrieves the expiration time as a Time + +    def expires +      @expires && Time.parse(@expires) +    end + +    ## +    # The cookie string suitable for use in an HTTP header + +    def to_s +      ret = "" +      ret << @name << "=" << @value +      ret << "; " << "Version=" << @version.to_s if @version > 0 +      ret << "; " << "Domain="  << @domain  if @domain +      ret << "; " << "Expires=" << @expires if @expires +      ret << "; " << "Max-Age=" << @max_age.to_s if @max_age +      ret << "; " << "Comment=" << @comment if @comment +      ret << "; " << "Path="    << @path if @path +      ret << "; " << "Secure"   if @secure +      ret +    end + +    ## +    # Parses a Cookie field sent from the user-agent.  Returns an array of +    # cookies. + +    def self.parse(str) +      if str +        ret = [] +        cookie = nil +        ver = 0 +        str.split(/[;,]\s+/).each{|x| +          key, val = x.split(/=/,2) +          val = val ? HTTPUtils::dequote(val) : "" +          case key +          when "$Version"; ver = val.to_i +          when "$Path";    cookie.path = val +          when "$Domain";  cookie.domain = val +          when "$Port";    cookie.port = val +          else +            ret << cookie if cookie +            cookie = self.new(key, val) +            cookie.version = ver +          end +        } +        ret << cookie if cookie +        ret +      end +    end + +    ## +    # Parses the cookie in +str+ + +    def self.parse_set_cookie(str) +      cookie_elem = str.split(/;/) +      first_elem = cookie_elem.shift +      first_elem.strip! +      key, value = first_elem.split(/=/, 2) +      cookie = new(key, HTTPUtils.dequote(value)) +      cookie_elem.each{|pair| +        pair.strip! +        key, value = pair.split(/=/, 2) +        if value +          value = HTTPUtils.dequote(value.strip) +        end +        case key.downcase +        when "domain"  then cookie.domain  = value +        when "path"    then cookie.path    = value +        when "expires" then cookie.expires = value +        when "max-age" then cookie.max_age = Integer(value) +        when "comment" then cookie.comment = value +        when "version" then cookie.version = Integer(value) +        when "secure"  then cookie.secure = true +        end +      } +      return cookie +    end + +    ## +    # Parses the cookies in +str+ + +    def self.parse_set_cookies(str) +      return str.split(/,(?=[^;,]*=)|,$/).collect{|c| +        parse_set_cookie(c) +      } +    end +  end +end diff --git a/jni/ruby/lib/webrick/htmlutils.rb b/jni/ruby/lib/webrick/htmlutils.rb new file mode 100644 index 0000000..4cb3d0d --- /dev/null +++ b/jni/ruby/lib/webrick/htmlutils.rb @@ -0,0 +1,29 @@ +#-- +# htmlutils.rb -- HTMLUtils Module +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htmlutils.rb,v 1.7 2002/09/21 12:23:35 gotoyuzo Exp $ + +module WEBrick +  module HTMLUtils + +    ## +    # Escapes &, ", > and < in +string+ + +    def escape(string) +      return "" unless string +      str = string.b +      str.gsub!(/&/n, '&') +      str.gsub!(/\"/n, '"') +      str.gsub!(/>/n, '>') +      str.gsub!(/</n, '<') +      str.force_encoding(string.encoding) +    end +    module_function :escape + +  end +end diff --git a/jni/ruby/lib/webrick/httpauth.rb b/jni/ruby/lib/webrick/httpauth.rb new file mode 100644 index 0000000..96d479b --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth.rb @@ -0,0 +1,95 @@ +# +# httpauth.rb -- HTTP access authentication +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $ + +require 'webrick/httpauth/basicauth' +require 'webrick/httpauth/digestauth' +require 'webrick/httpauth/htpasswd' +require 'webrick/httpauth/htdigest' +require 'webrick/httpauth/htgroup' + +module WEBrick + +  ## +  # HTTPAuth provides both basic and digest authentication. +  # +  # To enable authentication for requests in WEBrick you will need a user +  # database and an authenticator.  To start, here's an Htpasswd database for +  # use with a DigestAuth authenticator: +  # +  #   config = { :Realm => 'DigestAuth example realm' } +  # +  #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' +  #   htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth +  #   htpasswd.set_passwd config[:Realm], 'username', 'password' +  #   htpasswd.flush +  # +  # The +:Realm+ is used to provide different access to different groups +  # across several resources on a server.  Typically you'll need only one +  # realm for a server. +  # +  # This database can be used to create an authenticator: +  # +  #   config[:UserDB] = htpasswd +  # +  #   digest_auth = WEBrick::HTTPAuth::DigestAuth.new config +  # +  # To authenticate a request call #authenticate with a request and response +  # object in a servlet: +  # +  #   def do_GET req, res +  #     @authenticator.authenticate req, res +  #   end +  # +  # For digest authentication the authenticator must not be created every +  # request, it must be passed in as an option via WEBrick::HTTPServer#mount. + +  module HTTPAuth +    module_function + +    def _basic_auth(req, res, realm, req_field, res_field, err_type, +                    block) # :nodoc: +      user = pass = nil +      if /^Basic\s+(.*)/o =~ req[req_field] +        userpass = $1 +        user, pass = userpass.unpack("m*")[0].split(":", 2) +      end +      if block.call(user, pass) +        req.user = user +        return +      end +      res[res_field] = "Basic realm=\"#{realm}\"" +      raise err_type +    end + +    ## +    # Simple wrapper for providing basic authentication for a request.  When +    # called with a request +req+, response +res+, authentication +realm+ and +    # +block+ the block will be called with a +username+ and +password+.  If +    # the block returns true the request is allowed to continue, otherwise an +    # HTTPStatus::Unauthorized error is raised. + +    def basic_auth(req, res, realm, &block) # :yield: username, password +      _basic_auth(req, res, realm, "Authorization", "WWW-Authenticate", +                  HTTPStatus::Unauthorized, block) +    end + +    ## +    # Simple wrapper for providing basic authentication for a proxied request. +    # When called with a request +req+, response +res+, authentication +realm+ +    # and +block+ the block will be called with a +username+ and +password+. +    # If the block returns true the request is allowed to continue, otherwise +    # an HTTPStatus::ProxyAuthenticationRequired error is raised. + +    def proxy_basic_auth(req, res, realm, &block) # :yield: username, password +      _basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate", +                  HTTPStatus::ProxyAuthenticationRequired, block) +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/authenticator.rb b/jni/ruby/lib/webrick/httpauth/authenticator.rb new file mode 100644 index 0000000..f6d4ab8 --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/authenticator.rb @@ -0,0 +1,116 @@ +#-- +# httpauth/authenticator.rb -- Authenticator mix-in module. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $ + +module WEBrick +  module HTTPAuth + +    ## +    # Module providing generic support for both Digest and Basic +    # authentication schemes. + +    module Authenticator + +      RequestField      = "Authorization" # :nodoc: +      ResponseField     = "WWW-Authenticate" # :nodoc: +      ResponseInfoField = "Authentication-Info" # :nodoc: +      AuthException     = HTTPStatus::Unauthorized # :nodoc: + +      ## +      # Method of authentication, must be overridden by the including class + +      AuthScheme        = nil + +      ## +      # The realm this authenticator covers + +      attr_reader :realm + +      ## +      # The user database for this authenticator + +      attr_reader :userdb + +      ## +      # The logger for this authenticator + +      attr_reader :logger + +      private + +      # :stopdoc: + +      ## +      # Initializes the authenticator from +config+ + +      def check_init(config) +        [:UserDB, :Realm].each{|sym| +          unless config[sym] +            raise ArgumentError, "Argument #{sym.inspect} missing." +          end +        } +        @realm     = config[:Realm] +        @userdb    = config[:UserDB] +        @logger    = config[:Logger] || Log::new($stderr) +        @reload_db = config[:AutoReloadUserDB] +        @request_field   = self::class::RequestField +        @response_field  = self::class::ResponseField +        @resp_info_field = self::class::ResponseInfoField +        @auth_exception  = self::class::AuthException +        @auth_scheme     = self::class::AuthScheme +      end + +      ## +      # Ensures +req+ has credentials that can be authenticated. + +      def check_scheme(req) +        unless credentials = req[@request_field] +          error("no credentials in the request.") +          return nil +        end +        unless match = /^#{@auth_scheme}\s+/i.match(credentials) +          error("invalid scheme in %s.", credentials) +          info("%s: %s", @request_field, credentials) if $DEBUG +          return nil +        end +        return match.post_match +      end + +      def log(meth, fmt, *args) +        msg = format("%s %s: ", @auth_scheme, @realm) +        msg << fmt % args +        @logger.send(meth, msg) +      end + +      def error(fmt, *args) +        if @logger.error? +          log(:error, fmt, *args) +        end +      end + +      def info(fmt, *args) +        if @logger.info? +          log(:info, fmt, *args) +        end +      end + +      # :startdoc: +    end + +    ## +    # Module providing generic support for both Digest and Basic +    # authentication schemes for proxies. + +    module ProxyAuthenticator +      RequestField  = "Proxy-Authorization" # :nodoc: +      ResponseField = "Proxy-Authenticate" # :nodoc: +      InfoField     = "Proxy-Authentication-Info" # :nodoc: +      AuthException = HTTPStatus::ProxyAuthenticationRequired # :nodoc: +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/basicauth.rb b/jni/ruby/lib/webrick/httpauth/basicauth.rb new file mode 100644 index 0000000..3ff20b5 --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/basicauth.rb @@ -0,0 +1,108 @@ +# +# httpauth/basicauth.rb -- HTTP basic access authentication +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ + +require 'webrick/config' +require 'webrick/httpstatus' +require 'webrick/httpauth/authenticator' + +module WEBrick +  module HTTPAuth + +    ## +    # Basic Authentication for WEBrick +    # +    # Use this class to add basic authentication to a WEBrick servlet. +    # +    # Here is an example of how to set up a BasicAuth: +    # +    #   config = { :Realm => 'BasicAuth example realm' } +    # +    #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' +    #   htpasswd.set_passwd config[:Realm], 'username', 'password' +    #   htpasswd.flush +    # +    #   config[:UserDB] = htpasswd +    # +    #   basic_auth = WEBrick::HTTPAuth::BasicAuth.new config + +    class BasicAuth +      include Authenticator + +      AuthScheme = "Basic" # :nodoc: + +      ## +      # Used by UserDB to create a basic password entry + +      def self.make_passwd(realm, user, pass) +        pass ||= "" +        pass.crypt(Utils::random_string(2)) +      end + +      attr_reader :realm, :userdb, :logger + +      ## +      # Creates a new BasicAuth instance. +      # +      # See WEBrick::Config::BasicAuth for default configuration entries +      # +      # You must supply the following configuration entries: +      # +      # :Realm:: The name of the realm being protected. +      # :UserDB:: A database of usernames and passwords. +      #           A WEBrick::HTTPAuth::Htpasswd instance should be used. + +      def initialize(config, default=Config::BasicAuth) +        check_init(config) +        @config = default.dup.update(config) +      end + +      ## +      # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if +      # the authentication was not correct. + +      def authenticate(req, res) +        unless basic_credentials = check_scheme(req) +          challenge(req, res) +        end +        userid, password = basic_credentials.unpack("m*")[0].split(":", 2) +        password ||= "" +        if userid.empty? +          error("user id was not given.") +          challenge(req, res) +        end +        unless encpass = @userdb.get_passwd(@realm, userid, @reload_db) +          error("%s: the user is not allowed.", userid) +          challenge(req, res) +        end +        if password.crypt(encpass) != encpass +          error("%s: password unmatch.", userid) +          challenge(req, res) +        end +        info("%s: authentication succeeded.", userid) +        req.user = userid +      end + +      ## +      # Returns a challenge response which asks for for authentication +      # information + +      def challenge(req, res) +        res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\"" +        raise @auth_exception +      end +    end + +    ## +    # Basic authentication for proxy servers.  See BasicAuth for details. + +    class ProxyBasicAuth < BasicAuth +      include ProxyAuthenticator +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/digestauth.rb b/jni/ruby/lib/webrick/httpauth/digestauth.rb new file mode 100644 index 0000000..0eea947 --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/digestauth.rb @@ -0,0 +1,408 @@ +# +# httpauth/digestauth.rb -- HTTP digest access authentication +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. +# Copyright (c) 2003 H.M. +# +# The original implementation is provided by H.M. +#   URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name= +#        %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB +# +# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ + +require 'webrick/config' +require 'webrick/httpstatus' +require 'webrick/httpauth/authenticator' +require 'digest/md5' +require 'digest/sha1' + +module WEBrick +  module HTTPAuth + +    ## +    # RFC 2617 Digest Access Authentication for WEBrick +    # +    # Use this class to add digest authentication to a WEBrick servlet. +    # +    # Here is an example of how to set up DigestAuth: +    # +    #   config = { :Realm => 'DigestAuth example realm' } +    # +    #   htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' +    #   htdigest.set_passwd config[:Realm], 'username', 'password' +    #   htdigest.flush +    # +    #   config[:UserDB] = htdigest +    # +    #   digest_auth = WEBrick::HTTPAuth::DigestAuth.new config +    # +    # When using this as with a servlet be sure not to create a new DigestAuth +    # object in the servlet's #initialize.  By default WEBrick creates a new +    # servlet instance for every request and the DigestAuth object must be +    # used across requests. + +    class DigestAuth +      include Authenticator + +      AuthScheme = "Digest" # :nodoc: + +      ## +      # Struct containing the opaque portion of the digest authentication + +      OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc: + +      ## +      # Digest authentication algorithm + +      attr_reader :algorithm + +      ## +      # Quality of protection.  RFC 2617 defines "auth" and "auth-int" + +      attr_reader :qop + +      ## +      # Used by UserDB to create a digest password entry + +      def self.make_passwd(realm, user, pass) +        pass ||= "" +        Digest::MD5::hexdigest([user, realm, pass].join(":")) +      end + +      ## +      # Creates a new DigestAuth instance.  Be sure to use the same DigestAuth +      # instance for multiple requests as it saves state between requests in +      # order to perform authentication. +      # +      # See WEBrick::Config::DigestAuth for default configuration entries +      # +      # You must supply the following configuration entries: +      # +      # :Realm:: The name of the realm being protected. +      # :UserDB:: A database of usernames and passwords. +      #           A WEBrick::HTTPAuth::Htdigest instance should be used. + +      def initialize(config, default=Config::DigestAuth) +        check_init(config) +        @config                 = default.dup.update(config) +        @algorithm              = @config[:Algorithm] +        @domain                 = @config[:Domain] +        @qop                    = @config[:Qop] +        @use_opaque             = @config[:UseOpaque] +        @use_next_nonce         = @config[:UseNextNonce] +        @check_nc               = @config[:CheckNc] +        @use_auth_info_header   = @config[:UseAuthenticationInfoHeader] +        @nonce_expire_period    = @config[:NonceExpirePeriod] +        @nonce_expire_delta     = @config[:NonceExpireDelta] +        @internet_explorer_hack = @config[:InternetExplorerHack] + +        case @algorithm +        when 'MD5','MD5-sess' +          @h = Digest::MD5 +        when 'SHA1','SHA1-sess'  # it is a bonus feature :-) +          @h = Digest::SHA1 +        else +          msg = format('Algorithm "%s" is not supported.', @algorithm) +          raise ArgumentError.new(msg) +        end + +        @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid) +        @opaques = {} +        @last_nonce_expire = Time.now +        @mutex = Mutex.new +      end + +      ## +      # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if +      # the authentication was not correct. + +      def authenticate(req, res) +        unless result = @mutex.synchronize{ _authenticate(req, res) } +          challenge(req, res) +        end +        if result == :nonce_is_stale +          challenge(req, res, true) +        end +        return true +      end + +      ## +      # Returns a challenge response which asks for for authentication +      # information + +      def challenge(req, res, stale=false) +        nonce = generate_next_nonce(req) +        if @use_opaque +          opaque = generate_opaque(req) +          @opaques[opaque].nonce = nonce +        end + +        param = Hash.new +        param["realm"]  = HTTPUtils::quote(@realm) +        param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain +        param["nonce"]  = HTTPUtils::quote(nonce) +        param["opaque"] = HTTPUtils::quote(opaque) if opaque +        param["stale"]  = stale.to_s +        param["algorithm"] = @algorithm +        param["qop"]    = HTTPUtils::quote(@qop.to_a.join(",")) if @qop + +        res[@response_field] = +          "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ") +        info("%s: %s", @response_field, res[@response_field]) if $DEBUG +        raise @auth_exception +      end + +      private + +      # :stopdoc: + +      MustParams = ['username','realm','nonce','uri','response'] +      MustParamsAuth = ['cnonce','nc'] + +      def _authenticate(req, res) +        unless digest_credentials = check_scheme(req) +          return false +        end + +        auth_req = split_param_value(digest_credentials) +        if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" +          req_params = MustParams + MustParamsAuth +        else +          req_params = MustParams +        end +        req_params.each{|key| +          unless auth_req.has_key?(key) +            error('%s: parameter missing. "%s"', auth_req['username'], key) +            raise HTTPStatus::BadRequest +          end +        } + +        if !check_uri(req, auth_req) +          raise HTTPStatus::BadRequest +        end + +        if auth_req['realm'] != @realm +          error('%s: realm unmatch. "%s" for "%s"', +                auth_req['username'], auth_req['realm'], @realm) +          return false +        end + +        auth_req['algorithm'] ||= 'MD5' +        if auth_req['algorithm'].upcase != @algorithm.upcase +          error('%s: algorithm unmatch. "%s" for "%s"', +                auth_req['username'], auth_req['algorithm'], @algorithm) +          return false +        end + +        if (@qop.nil? && auth_req.has_key?('qop')) || +           (@qop && (! @qop.member?(auth_req['qop']))) +          error('%s: the qop is not allowed. "%s"', +                auth_req['username'], auth_req['qop']) +          return false +        end + +        password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db) +        unless password +          error('%s: the user is not allowed.', auth_req['username']) +          return false +        end + +        nonce_is_invalid = false +        if @use_opaque +          info("@opaque = %s", @opaque.inspect) if $DEBUG +          if !(opaque = auth_req['opaque']) +            error('%s: opaque is not given.', auth_req['username']) +            nonce_is_invalid = true +          elsif !(opaque_struct = @opaques[opaque]) +            error('%s: invalid opaque is given.', auth_req['username']) +            nonce_is_invalid = true +          elsif !check_opaque(opaque_struct, req, auth_req) +            @opaques.delete(auth_req['opaque']) +            nonce_is_invalid = true +          end +        elsif !check_nonce(req, auth_req) +          nonce_is_invalid = true +        end + +        if /-sess$/i =~ auth_req['algorithm'] +          ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce']) +        else +          ha1 = password +        end + +        if auth_req['qop'] == "auth" || auth_req['qop'] == nil +          ha2 = hexdigest(req.request_method, auth_req['uri']) +          ha2_res = hexdigest("", auth_req['uri']) +        elsif auth_req['qop'] == "auth-int" +          ha2 = hexdigest(req.request_method, auth_req['uri'], +                          hexdigest(req.body)) +          ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body)) +        end + +        if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" +          param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key| +            auth_req[key] +          }.join(':') +          digest     = hexdigest(ha1, param2, ha2) +          digest_res = hexdigest(ha1, param2, ha2_res) +        else +          digest     = hexdigest(ha1, auth_req['nonce'], ha2) +          digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res) +        end + +        if digest != auth_req['response'] +          error("%s: digest unmatch.", auth_req['username']) +          return false +        elsif nonce_is_invalid +          error('%s: digest is valid, but nonce is not valid.', +                auth_req['username']) +          return :nonce_is_stale +        elsif @use_auth_info_header +          auth_info = { +            'nextnonce' => generate_next_nonce(req), +            'rspauth'   => digest_res +          } +          if @use_opaque +            opaque_struct.time  = req.request_time +            opaque_struct.nonce = auth_info['nextnonce'] +            opaque_struct.nc    = "%08x" % (auth_req['nc'].hex + 1) +          end +          if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" +            ['qop','cnonce','nc'].each{|key| +              auth_info[key] = auth_req[key] +            } +          end +          res[@resp_info_field] = auth_info.keys.map{|key| +            if key == 'nc' +              key + '=' + auth_info[key] +            else +              key + "=" + HTTPUtils::quote(auth_info[key]) +            end +          }.join(', ') +        end +        info('%s: authentication succeeded.', auth_req['username']) +        req.user = auth_req['username'] +        return true +      end + +      def split_param_value(string) +        ret = {} +        while string.bytesize != 0 +          case string +          when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/ +            key = $1 +            matched = $2 +            string = $' +            ret[key] = matched.gsub(/\\(.)/, "\\1") +          when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/ +            key = $1 +            matched = $2 +            string = $' +            ret[key] = matched.clone +          when /^s*^,/ +            string = $' +          else +            break +          end +        end +        ret +      end + +      def generate_next_nonce(req) +        now = "%012d" % req.request_time.to_i +        pk  = hexdigest(now, @instance_key)[0,32] +        nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars. +        nonce +      end + +      def check_nonce(req, auth_req) +        username = auth_req['username'] +        nonce = auth_req['nonce'] + +        pub_time, pk = nonce.unpack("m*")[0].split(":", 2) +        if (!pub_time || !pk) +          error("%s: empty nonce is given", username) +          return false +        elsif (hexdigest(pub_time, @instance_key)[0,32] != pk) +          error("%s: invalid private-key: %s for %s", +                username, hexdigest(pub_time, @instance_key)[0,32], pk) +          return false +        end + +        diff_time = req.request_time.to_i - pub_time.to_i +        if (diff_time < 0) +          error("%s: difference of time-stamp is negative.", username) +          return false +        elsif diff_time > @nonce_expire_period +          error("%s: nonce is expired.", username) +          return false +        end + +        return true +      end + +      def generate_opaque(req) +        @mutex.synchronize{ +          now = req.request_time +          if now - @last_nonce_expire > @nonce_expire_delta +            @opaques.delete_if{|key,val| +              (now - val.time) > @nonce_expire_period +            } +            @last_nonce_expire = now +          end +          begin +            opaque = Utils::random_string(16) +          end while @opaques[opaque] +          @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001') +          opaque +        } +      end + +      def check_opaque(opaque_struct, req, auth_req) +        if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce) +          error('%s: nonce unmatched. "%s" for "%s"', +                auth_req['username'], auth_req['nonce'], opaque_struct.nonce) +          return false +        elsif !check_nonce(req, auth_req) +          return false +        end +        if (@check_nc && auth_req['nc'] != opaque_struct.nc) +          error('%s: nc unmatched."%s" for "%s"', +                auth_req['username'], auth_req['nc'], opaque_struct.nc) +          return false +        end +        true +      end + +      def check_uri(req, auth_req) +        uri = auth_req['uri'] +        if uri != req.request_uri.to_s && uri != req.unparsed_uri && +           (@internet_explorer_hack && uri != req.path) +          error('%s: uri unmatch. "%s" for "%s"', auth_req['username'], +                auth_req['uri'], req.request_uri.to_s) +          return false +        end +        true +      end + +      def hexdigest(*args) +        @h.hexdigest(args.join(":")) +      end + +      # :startdoc: +    end + +    ## +    # Digest authentication for proxy servers.  See DigestAuth for details. + +    class ProxyDigestAuth < DigestAuth +      include ProxyAuthenticator + +      private +      def check_uri(req, auth_req) # :nodoc: +        return true +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/htdigest.rb b/jni/ruby/lib/webrick/httpauth/htdigest.rb new file mode 100644 index 0000000..5fb0635 --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/htdigest.rb @@ -0,0 +1,131 @@ +# +# httpauth/htdigest.rb -- Apache compatible htdigest file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ + +require 'webrick/httpauth/userdb' +require 'webrick/httpauth/digestauth' +require 'tempfile' + +module WEBrick +  module HTTPAuth + +    ## +    # Htdigest accesses apache-compatible digest password files.  Passwords are +    # matched to a realm where they are valid.  For security, the path for a +    # digest password database should be stored outside of the paths available +    # to the HTTP server. +    # +    # Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and +    # stores passwords using cryptographic hashes. +    # +    #   htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' +    #   htpasswd.set_passwd 'my realm', 'username', 'password' +    #   htpasswd.flush + +    class Htdigest +      include UserDB + +      ## +      # Open a digest password database at +path+ + +      def initialize(path) +        @path = path +        @mtime = Time.at(0) +        @digest = Hash.new +        @mutex = Mutex::new +        @auth_type = DigestAuth +        open(@path,"a").close unless File::exist?(@path) +        reload +      end + +      ## +      # Reloads passwords from the database + +      def reload +        mtime = File::mtime(@path) +        if mtime > @mtime +          @digest.clear +          open(@path){|io| +            while line = io.gets +              line.chomp! +              user, realm, pass = line.split(/:/, 3) +              unless @digest[realm] +                @digest[realm] = Hash.new +              end +              @digest[realm][user] = pass +            end +          } +          @mtime = mtime +        end +      end + +      ## +      # Flush the password database.  If +output+ is given the database will +      # be written there instead of to the original path. + +      def flush(output=nil) +        output ||= @path +        tmp = Tempfile.create("htpasswd", File::dirname(output)) +        renamed = false +        begin +          each{|item| tmp.puts(item.join(":")) } +          tmp.close +          File::rename(tmp.path, output) +          renamed = true +        ensure +          tmp.close if !tmp.closed? +          File.unlink(tmp.path) if !renamed +        end +      end + +      ## +      # Retrieves a password from the database for +user+ in +realm+.  If +      # +reload_db+ is true the database will be reloaded first. + +      def get_passwd(realm, user, reload_db) +        reload() if reload_db +        if hash = @digest[realm] +          hash[user] +        end +      end + +      ## +      # Sets a password in the database for +user+ in +realm+ to +pass+. + +      def set_passwd(realm, user, pass) +        @mutex.synchronize{ +          unless @digest[realm] +            @digest[realm] = Hash.new +          end +          @digest[realm][user] = make_passwd(realm, user, pass) +        } +      end + +      ## +      # Removes a password from the database for +user+ in +realm+. + +      def delete_passwd(realm, user) +        if hash = @digest[realm] +          hash.delete(user) +        end +      end + +      ## +      # Iterate passwords in the database. + +      def each # :yields: [user, realm, password_hash] +        @digest.keys.sort.each{|realm| +          hash = @digest[realm] +          hash.keys.sort.each{|user| +            yield([user, realm, hash[user]]) +          } +        } +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/htgroup.rb b/jni/ruby/lib/webrick/httpauth/htgroup.rb new file mode 100644 index 0000000..0ecabef --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/htgroup.rb @@ -0,0 +1,93 @@ +# +# httpauth/htgroup.rb -- Apache compatible htgroup file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htgroup.rb,v 1.1 2003/02/16 22:22:56 gotoyuzo Exp $ + +require 'tempfile' + +module WEBrick +  module HTTPAuth + +    ## +    # Htgroup accesses apache-compatible group files.  Htgroup can be used to +    # provide group-based authentication for users.  Currently Htgroup is not +    # directly integrated with any authenticators in WEBrick.  For security, +    # the path for a digest password database should be stored outside of the +    # paths available to the HTTP server. +    # +    # Example: +    # +    #   htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file' +    #   htgroup.add 'superheroes', %w[spiderman batman] +    # +    #   htgroup.members('superheroes').include? 'magneto' # => false + +    class Htgroup + +      ## +      # Open a group database at +path+ + +      def initialize(path) +        @path = path +        @mtime = Time.at(0) +        @group = Hash.new +        open(@path,"a").close unless File::exist?(@path) +        reload +      end + +      ## +      # Reload groups from the database + +      def reload +        if (mtime = File::mtime(@path)) > @mtime +          @group.clear +          open(@path){|io| +            while line = io.gets +              line.chomp! +              group, members = line.split(/:\s*/) +              @group[group] = members.split(/\s+/) +            end +          } +          @mtime = mtime +        end +      end + +      ## +      # Flush the group database.  If +output+ is given the database will be +      # written there instead of to the original path. + +      def flush(output=nil) +        output ||= @path +        tmp = Tempfile.new("htgroup", File::dirname(output)) +        begin +          @group.keys.sort.each{|group| +            tmp.puts(format("%s: %s", group, self.members(group).join(" "))) +          } +          tmp.close +          File::rename(tmp.path, output) +        rescue +          tmp.close(true) +        end +      end + +      ## +      # Retrieve the list of members from +group+ + +      def members(group) +        reload +        @group[group] || [] +      end + +      ## +      # Add an Array of +members+ to +group+ + +      def add(group, members) +        @group[group] = members(group) | members +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/htpasswd.rb b/jni/ruby/lib/webrick/httpauth/htpasswd.rb new file mode 100644 index 0000000..69b739f --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/htpasswd.rb @@ -0,0 +1,124 @@ +# +# httpauth/htpasswd -- Apache compatible htpasswd file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ + +require 'webrick/httpauth/userdb' +require 'webrick/httpauth/basicauth' +require 'tempfile' + +module WEBrick +  module HTTPAuth + +    ## +    # Htpasswd accesses apache-compatible password files.  Passwords are +    # matched to a realm where they are valid.  For security, the path for a +    # password database should be stored outside of the paths available to the +    # HTTP server. +    # +    # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. +    # +    # To create an Htpasswd database with a single user: +    # +    #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' +    #   htpasswd.set_passwd 'my realm', 'username', 'password' +    #   htpasswd.flush + +    class Htpasswd +      include UserDB + +      ## +      # Open a password database at +path+ + +      def initialize(path) +        @path = path +        @mtime = Time.at(0) +        @passwd = Hash.new +        @auth_type = BasicAuth +        open(@path,"a").close unless File::exist?(@path) +        reload +      end + +      ## +      # Reload passwords from the database + +      def reload +        mtime = File::mtime(@path) +        if mtime > @mtime +          @passwd.clear +          open(@path){|io| +            while line = io.gets +              line.chomp! +              case line +              when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z! +                user, pass = line.split(":") +              when /:\$/, /:{SHA}/ +                raise NotImplementedError, +                      'MD5, SHA1 .htpasswd file not supported' +              else +                raise StandardError, 'bad .htpasswd file' +              end +              @passwd[user] = pass +            end +          } +          @mtime = mtime +        end +      end + +      ## +      # Flush the password database.  If +output+ is given the database will +      # be written there instead of to the original path. + +      def flush(output=nil) +        output ||= @path +        tmp = Tempfile.create("htpasswd", File::dirname(output)) +        renamed = false +        begin +          each{|item| tmp.puts(item.join(":")) } +          tmp.close +          File::rename(tmp.path, output) +          renamed = true +        ensure +          tmp.close if !tmp.closed? +          File.unlink(tmp.path) if !renamed +        end +      end + +      ## +      # Retrieves a password from the database for +user+ in +realm+.  If +      # +reload_db+ is true the database will be reloaded first. + +      def get_passwd(realm, user, reload_db) +        reload() if reload_db +        @passwd[user] +      end + +      ## +      # Sets a password in the database for +user+ in +realm+ to +pass+. + +      def set_passwd(realm, user, pass) +        @passwd[user] = make_passwd(realm, user, pass) +      end + +      ## +      # Removes a password from the database for +user+ in +realm+. + +      def delete_passwd(realm, user) +        @passwd.delete(user) +      end + +      ## +      # Iterate passwords in the database. + +      def each # :yields: [user, password] +        @passwd.keys.sort.each{|user| +          yield([user, @passwd[user]]) +        } +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpauth/userdb.rb b/jni/ruby/lib/webrick/httpauth/userdb.rb new file mode 100644 index 0000000..005c18d --- /dev/null +++ b/jni/ruby/lib/webrick/httpauth/userdb.rb @@ -0,0 +1,52 @@ +#-- +# httpauth/userdb.rb -- UserDB mix-in module. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: userdb.rb,v 1.2 2003/02/20 07:15:48 gotoyuzo Exp $ + +module WEBrick +  module HTTPAuth + +    ## +    # User database mixin for HTTPAuth.  This mixin dispatches user record +    # access to the underlying auth_type for this database. + +    module UserDB + +      ## +      # The authentication type. +      # +      # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are +      # built-in. + +      attr_accessor :auth_type + +      ## +      # Creates an obscured password in +realm+ with +user+ and +password+ +      # using the auth_type of this database. + +      def make_passwd(realm, user, pass) +        @auth_type::make_passwd(realm, user, pass) +      end + +      ## +      # Sets a password in +realm+ with +user+ and +password+ for the +      # auth_type of this database. + +      def set_passwd(realm, user, pass) +        self[user] = pass +      end + +      ## +      # Retrieves a password in +realm+ for +user+ for the auth_type of this +      # database.  +reload_db+ is a dummy value. + +      def get_passwd(realm, user, reload_db=false) +        make_passwd(realm, user, self[user]) +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpproxy.rb b/jni/ruby/lib/webrick/httpproxy.rb new file mode 100644 index 0000000..cbba2d8 --- /dev/null +++ b/jni/ruby/lib/webrick/httpproxy.rb @@ -0,0 +1,337 @@ +# +# httpproxy.rb -- HTTPProxy Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2002 GOTO Kentaro +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $ +# $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $ + +require "webrick/httpserver" +require "net/http" + +module WEBrick + +  NullReader = Object.new # :nodoc: +  class << NullReader # :nodoc: +    def read(*args) +      nil +    end +    alias gets read +  end + +  FakeProxyURI = Object.new # :nodoc: +  class << FakeProxyURI # :nodoc: +    def method_missing(meth, *args) +      if %w(scheme host port path query userinfo).member?(meth.to_s) +        return nil +      end +      super +    end +  end + +  # :startdoc: + +  ## +  # An HTTP Proxy server which proxies GET, HEAD and POST requests. +  # +  # To create a simple proxy server: +  # +  #   require 'webrick' +  #   require 'webrick/httpproxy' +  # +  #   proxy = WEBrick::HTTPProxyServer.new Port: 8000 +  # +  #   trap 'INT'  do proxy.shutdown end +  #   trap 'TERM' do proxy.shutdown end +  # +  #   proxy.start +  # +  # See ::new for proxy-specific configuration items. +  # +  # == Modifying proxied responses +  # +  # To modify content the proxy server returns use the +:ProxyContentHandler+ +  # option: +  # +  #   handler = proc do |req, res| +  #     if res['content-type'] == 'text/plain' then +  #       res.body << "\nThis content was proxied!\n" +  #     end +  #   end +  # +  #   proxy = +  #     WEBrick::HTTPProxyServer.new Port: 8000, ProxyContentHandler: handler + +  class HTTPProxyServer < HTTPServer + +    ## +    # Proxy server configurations.  The proxy server handles the following +    # configuration items in addition to those supported by HTTPServer: +    # +    # :ProxyAuthProc:: Called with a request and response to authorize a +    #                  request +    # :ProxyVia:: Appended to the via header +    # :ProxyURI:: The proxy server's URI +    # :ProxyContentHandler:: Called with a request and response and allows +    #                        modification of the response +    # :ProxyTimeout:: Sets the proxy timeouts to 30 seconds for open and 60 +    #                 seconds for read operations + +    def initialize(config={}, default=Config::HTTP) +      super(config, default) +      c = @config +      @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}" +    end + +    # :stopdoc: +    def service(req, res) +      if req.request_method == "CONNECT" +        do_CONNECT(req, res) +      elsif req.unparsed_uri =~ %r!^http://! +        proxy_service(req, res) +      else +        super(req, res) +      end +    end + +    def proxy_auth(req, res) +      if proc = @config[:ProxyAuthProc] +        proc.call(req, res) +      end +      req.header.delete("proxy-authorization") +    end + +    def proxy_uri(req, res) +      # should return upstream proxy server's URI +      return @config[:ProxyURI] +    end + +    def proxy_service(req, res) +      # Proxy Authentication +      proxy_auth(req, res) + +      begin +        self.send("do_#{req.request_method}", req, res) +      rescue NoMethodError +        raise HTTPStatus::MethodNotAllowed, +          "unsupported method `#{req.request_method}'." +      rescue => err +        logger.debug("#{err.class}: #{err.message}") +        raise HTTPStatus::ServiceUnavailable, err.message +      end + +      # Process contents +      if handler = @config[:ProxyContentHandler] +        handler.call(req, res) +      end +    end + +    def do_CONNECT(req, res) +      # Proxy Authentication +      proxy_auth(req, res) + +      ua = Thread.current[:WEBrickSocket]  # User-Agent +      raise HTTPStatus::InternalServerError, +        "[BUG] cannot get socket" unless ua + +      host, port = req.unparsed_uri.split(":", 2) +      # Proxy authentication for upstream proxy server +      if proxy = proxy_uri(req, res) +        proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0" +        if proxy.userinfo +          credentials = "Basic " + [proxy.userinfo].pack("m").delete("\n") +        end +        host, port = proxy.host, proxy.port +      end + +      begin +        @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.") +        os = TCPSocket.new(host, port)     # origin server + +        if proxy +          @logger.debug("CONNECT: sending a Request-Line") +          os << proxy_request_line << CRLF +          @logger.debug("CONNECT: > #{proxy_request_line}") +          if credentials +            @logger.debug("CONNECT: sending a credentials") +            os << "Proxy-Authorization: " << credentials << CRLF +          end +          os << CRLF +          proxy_status_line = os.gets(LF) +          @logger.debug("CONNECT: read a Status-Line form the upstream server") +          @logger.debug("CONNECT: < #{proxy_status_line}") +          if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line +            while line = os.gets(LF) +              break if /\A(#{CRLF}|#{LF})\z/om =~ line +            end +          else +            raise HTTPStatus::BadGateway +          end +        end +        @logger.debug("CONNECT #{host}:#{port}: succeeded") +        res.status = HTTPStatus::RC_OK +      rescue => ex +        @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'") +        res.set_error(ex) +        raise HTTPStatus::EOFError +      ensure +        if handler = @config[:ProxyContentHandler] +          handler.call(req, res) +        end +        res.send_response(ua) +        access_log(@config, req, res) + +        # Should clear request-line not to send the response twice. +        # see: HTTPServer#run +        req.parse(NullReader) rescue nil +      end + +      begin +        while fds = IO::select([ua, os]) +          if fds[0].member?(ua) +            buf = ua.sysread(1024); +            @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") +            os.syswrite(buf) +          elsif fds[0].member?(os) +            buf = os.sysread(1024); +            @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") +            ua.syswrite(buf) +          end +        end +      rescue +        os.close +        @logger.debug("CONNECT #{host}:#{port}: closed") +      end + +      raise HTTPStatus::EOFError +    end + +    def do_GET(req, res) +      perform_proxy_request(req, res) do |http, path, header| +        http.get(path, header) +      end +    end + +    def do_HEAD(req, res) +      perform_proxy_request(req, res) do |http, path, header| +        http.head(path, header) +      end +    end + +    def do_POST(req, res) +      perform_proxy_request(req, res) do |http, path, header| +        http.post(path, req.body || "", header) +      end +    end + +    def do_OPTIONS(req, res) +      res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT" +    end + +    private + +    # Some header fields should not be transferred. +    HopByHop = %w( connection keep-alive proxy-authenticate upgrade +                   proxy-authorization te trailers transfer-encoding ) +    ShouldNotTransfer = %w( set-cookie proxy-connection ) +    def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end + +    def choose_header(src, dst) +      connections = split_field(src['connection']) +      src.each{|key, value| +        key = key.downcase +        if HopByHop.member?(key)          || # RFC2616: 13.5.1 +           connections.member?(key)       || # RFC2616: 14.10 +           ShouldNotTransfer.member?(key)    # pragmatics +          @logger.debug("choose_header: `#{key}: #{value}'") +          next +        end +        dst[key] = value +      } +    end + +    # Net::HTTP is stupid about the multiple header fields. +    # Here is workaround: +    def set_cookie(src, dst) +      if str = src['set-cookie'] +        cookies = [] +        str.split(/,\s*/).each{|token| +          if /^[^=]+;/o =~ token +            cookies[-1] << ", " << token +          elsif /=/o =~ token +            cookies << token +          else +            cookies[-1] << ", " << token +          end +        } +        dst.cookies.replace(cookies) +      end +    end + +    def set_via(h) +      if @config[:ProxyVia] +        if  h['via'] +          h['via'] << ", " << @via +        else +          h['via'] = @via +        end +      end +    end + +    def setup_proxy_header(req, res) +      # Choose header fields to transfer +      header = Hash.new +      choose_header(req, header) +      set_via(header) +      return header +    end + +    def setup_upstream_proxy_authentication(req, res, header) +      if upstream = proxy_uri(req, res) +        if upstream.userinfo +          header['proxy-authorization'] = +            "Basic " + [upstream.userinfo].pack("m").delete("\n") +        end +        return upstream +      end +      return FakeProxyURI +    end + +    def perform_proxy_request(req, res) +      uri = req.request_uri +      path = uri.path.dup +      path << "?" << uri.query if uri.query +      header = setup_proxy_header(req, res) +      upstream = setup_upstream_proxy_authentication(req, res, header) +      response = nil + +      http = Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port) +      http.start do +        if @config[:ProxyTimeout] +          ##################################   these issues are +          http.open_timeout = 30   # secs  #   necessary (maybe because +          http.read_timeout = 60   # secs  #   Ruby's bug, but why?) +          ################################## +        end +        response = yield(http, path, header) +      end + +      # Persistent connection requirements are mysterious for me. +      # So I will close the connection in every response. +      res['proxy-connection'] = "close" +      res['connection'] = "close" + +      # Convert Net::HTTP::HTTPResponse to WEBrick::HTTPResponse +      res.status = response.code.to_i +      choose_header(response, res) +      set_cookie(response, res) +      set_via(res) +      res.body = response.body +    end + +    # :stopdoc: +  end +end diff --git a/jni/ruby/lib/webrick/httprequest.rb b/jni/ruby/lib/webrick/httprequest.rb new file mode 100644 index 0000000..6aa2d1c --- /dev/null +++ b/jni/ruby/lib/webrick/httprequest.rb @@ -0,0 +1,584 @@ +# +# httprequest.rb -- HTTPRequest Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $ + +require 'uri' +require 'webrick/httpversion' +require 'webrick/httpstatus' +require 'webrick/httputils' +require 'webrick/cookie' + +module WEBrick + +  ## +  # An HTTP request.  This is consumed by service and do_* methods in +  # WEBrick servlets + +  class HTTPRequest + +    BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc: + +    # :section: Request line + +    ## +    # The complete request line such as: +    # +    #   GET / HTTP/1.1 + +    attr_reader :request_line + +    ## +    # The request method, GET, POST, PUT, etc. + +    attr_reader :request_method + +    ## +    # The unparsed URI of the request + +    attr_reader :unparsed_uri + +    ## +    # The HTTP version of the request + +    attr_reader :http_version + +    # :section: Request-URI + +    ## +    # The parsed URI of the request + +    attr_reader :request_uri + +    ## +    # The request path + +    attr_reader :path + +    ## +    # The script name (CGI variable) + +    attr_accessor :script_name + +    ## +    # The path info (CGI variable) + +    attr_accessor :path_info + +    ## +    # The query from the URI of the request + +    attr_accessor :query_string + +    # :section: Header and entity body + +    ## +    # The raw header of the request + +    attr_reader :raw_header + +    ## +    # The parsed header of the request + +    attr_reader :header + +    ## +    # The parsed request cookies + +    attr_reader :cookies + +    ## +    # The Accept header value + +    attr_reader :accept + +    ## +    # The Accept-Charset header value + +    attr_reader :accept_charset + +    ## +    # The Accept-Encoding header value + +    attr_reader :accept_encoding + +    ## +    # The Accept-Language header value + +    attr_reader :accept_language + +    # :section: + +    ## +    # The remote user (CGI variable) + +    attr_accessor :user + +    ## +    # The socket address of the server + +    attr_reader :addr + +    ## +    # The socket address of the client + +    attr_reader :peeraddr + +    ## +    # Hash of request attributes + +    attr_reader :attributes + +    ## +    # Is this a keep-alive connection? + +    attr_reader :keep_alive + +    ## +    # The local time this request was received + +    attr_reader :request_time + +    ## +    # Creates a new HTTP request.  WEBrick::Config::HTTP is the default +    # configuration. + +    def initialize(config) +      @config = config +      @buffer_size = @config[:InputBufferSize] +      @logger = config[:Logger] + +      @request_line = @request_method = +        @unparsed_uri = @http_version = nil + +      @request_uri = @host = @port = @path = nil +      @script_name = @path_info = nil +      @query_string = nil +      @query = nil +      @form_data = nil + +      @raw_header = Array.new +      @header = nil +      @cookies = [] +      @accept = [] +      @accept_charset = [] +      @accept_encoding = [] +      @accept_language = [] +      @body = "" + +      @addr = @peeraddr = nil +      @attributes = {} +      @user = nil +      @keep_alive = false +      @request_time = nil + +      @remaining_size = nil +      @socket = nil + +      @forwarded_proto = @forwarded_host = @forwarded_port = +        @forwarded_server = @forwarded_for = nil +    end + +    ## +    # Parses a request from +socket+.  This is called internally by +    # WEBrick::HTTPServer. + +    def parse(socket=nil) +      @socket = socket +      begin +        @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : [] +        @addr = socket.respond_to?(:addr) ? socket.addr : [] +      rescue Errno::ENOTCONN +        raise HTTPStatus::EOFError +      end + +      read_request_line(socket) +      if @http_version.major > 0 +        read_header(socket) +        @header['cookie'].each{|cookie| +          @cookies += Cookie::parse(cookie) +        } +        @accept = HTTPUtils.parse_qvalues(self['accept']) +        @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset']) +        @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding']) +        @accept_language = HTTPUtils.parse_qvalues(self['accept-language']) +      end +      return if @request_method == "CONNECT" +      return if @unparsed_uri == "*" + +      begin +        setup_forwarded_info +        @request_uri = parse_uri(@unparsed_uri) +        @path = HTTPUtils::unescape(@request_uri.path) +        @path = HTTPUtils::normalize_path(@path) +        @host = @request_uri.host +        @port = @request_uri.port +        @query_string = @request_uri.query +        @script_name = "" +        @path_info = @path.dup +      rescue +        raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'." +      end + +      if /close/io =~ self["connection"] +        @keep_alive = false +      elsif /keep-alive/io =~ self["connection"] +        @keep_alive = true +      elsif @http_version < "1.1" +        @keep_alive = false +      else +        @keep_alive = true +      end +    end + +    ## +    # Generate HTTP/1.1 100 continue response if the client expects it, +    # otherwise does nothing. + +    def continue # :nodoc: +      if self['expect'] == '100-continue' && @config[:HTTPVersion] >= "1.1" +        @socket << "HTTP/#{@config[:HTTPVersion]} 100 continue#{CRLF}#{CRLF}" +        @header.delete('expect') +      end +    end + +    ## +    # Returns the request body. + +    def body(&block) # :yields: body_chunk +      block ||= Proc.new{|chunk| @body << chunk } +      read_body(@socket, block) +      @body.empty? ? nil : @body +    end + +    ## +    # Request query as a Hash + +    def query +      unless @query +        parse_query() +      end +      @query +    end + +    ## +    # The content-length header + +    def content_length +      return Integer(self['content-length']) +    end + +    ## +    # The content-type header + +    def content_type +      return self['content-type'] +    end + +    ## +    # Retrieves +header_name+ + +    def [](header_name) +      if @header +        value = @header[header_name.downcase] +        value.empty? ? nil : value.join(", ") +      end +    end + +    ## +    # Iterates over the request headers + +    def each +      if @header +        @header.each{|k, v| +          value = @header[k] +          yield(k, value.empty? ? nil : value.join(", ")) +        } +      end +    end + +    ## +    # The host this request is for + +    def host +      return @forwarded_host || @host +    end + +    ## +    # The port this request is for + +    def port +      return @forwarded_port || @port +    end + +    ## +    # The server name this request is for + +    def server_name +      return @forwarded_server || @config[:ServerName] +    end + +    ## +    # The client's IP address + +    def remote_ip +      return self["client-ip"] || @forwarded_for || @peeraddr[3] +    end + +    ## +    # Is this an SSL request? + +    def ssl? +      return @request_uri.scheme == "https" +    end + +    ## +    # Should the connection this request was made on be kept alive? + +    def keep_alive? +      @keep_alive +    end + +    def to_s # :nodoc: +      ret = @request_line.dup +      @raw_header.each{|line| ret << line } +      ret << CRLF +      ret << body if body +      ret +    end + +    ## +    # Consumes any remaining body and updates keep-alive status + +    def fixup() # :nodoc: +      begin +        body{|chunk| }   # read remaining body +      rescue HTTPStatus::Error => ex +        @logger.error("HTTPRequest#fixup: #{ex.class} occurred.") +        @keep_alive = false +      rescue => ex +        @logger.error(ex) +        @keep_alive = false +      end +    end + +    # This method provides the metavariables defined by the revision 3 +    # of "The WWW Common Gateway Interface Version 1.1" +    # To browse the current document of CGI Version 1.1, see below: +    # http://tools.ietf.org/html/rfc3875 + +    def meta_vars +      meta = Hash.new + +      cl = self["Content-Length"] +      ct = self["Content-Type"] +      meta["CONTENT_LENGTH"]    = cl if cl.to_i > 0 +      meta["CONTENT_TYPE"]      = ct.dup if ct +      meta["GATEWAY_INTERFACE"] = "CGI/1.1" +      meta["PATH_INFO"]         = @path_info ? @path_info.dup : "" +     #meta["PATH_TRANSLATED"]   = nil      # no plan to be provided +      meta["QUERY_STRING"]      = @query_string ? @query_string.dup : "" +      meta["REMOTE_ADDR"]       = @peeraddr[3] +      meta["REMOTE_HOST"]       = @peeraddr[2] +     #meta["REMOTE_IDENT"]      = nil      # no plan to be provided +      meta["REMOTE_USER"]       = @user +      meta["REQUEST_METHOD"]    = @request_method.dup +      meta["REQUEST_URI"]       = @request_uri.to_s +      meta["SCRIPT_NAME"]       = @script_name.dup +      meta["SERVER_NAME"]       = @host +      meta["SERVER_PORT"]       = @port.to_s +      meta["SERVER_PROTOCOL"]   = "HTTP/" + @config[:HTTPVersion].to_s +      meta["SERVER_SOFTWARE"]   = @config[:ServerSoftware].dup + +      self.each{|key, val| +        next if /^content-type$/i =~ key +        next if /^content-length$/i =~ key +        name = "HTTP_" + key +        name.gsub!(/-/o, "_") +        name.upcase! +        meta[name] = val +      } + +      meta +    end + +    private + +    # :stopdoc: + +    MAX_URI_LENGTH = 2083 # :nodoc: + +    def read_request_line(socket) +      @request_line = read_line(socket, MAX_URI_LENGTH) if socket +      if @request_line.bytesize >= MAX_URI_LENGTH and @request_line[-1, 1] != LF +        raise HTTPStatus::RequestURITooLarge +      end +      @request_time = Time.now +      raise HTTPStatus::EOFError unless @request_line +      if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line +        @request_method = $1 +        @unparsed_uri   = $2 +        @http_version   = HTTPVersion.new($3 ? $3 : "0.9") +      else +        rl = @request_line.sub(/\x0d?\x0a\z/o, '') +        raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'." +      end +    end + +    def read_header(socket) +      if socket +        while line = read_line(socket) +          break if /\A(#{CRLF}|#{LF})\z/om =~ line +          @raw_header << line +        end +      end +      @header = HTTPUtils::parse_header(@raw_header.join) +    end + +    def parse_uri(str, scheme="http") +      if @config[:Escape8bitURI] +        str = HTTPUtils::escape8bit(str) +      end +      str.sub!(%r{\A/+}o, '/') +      uri = URI::parse(str) +      return uri if uri.absolute? +      if @forwarded_host +        host, port = @forwarded_host, @forwarded_port +      elsif self["host"] +        pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n +        host, port = *self['host'].scan(pattern)[0] +      elsif @addr.size > 0 +        host, port = @addr[2], @addr[1] +      else +        host, port = @config[:ServerName], @config[:Port] +      end +      uri.scheme = @forwarded_proto || scheme +      uri.host = host +      uri.port = port ? port.to_i : nil +      return URI::parse(uri.to_s) +    end + +    def read_body(socket, block) +      return unless socket +      if tc = self['transfer-encoding'] +        case tc +        when /chunked/io then read_chunked(socket, block) +        else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}." +        end +      elsif self['content-length'] || @remaining_size +        @remaining_size ||= self['content-length'].to_i +        while @remaining_size > 0 +          sz = [@buffer_size, @remaining_size].min +          break unless buf = read_data(socket, sz) +          @remaining_size -= buf.bytesize +          block.call(buf) +        end +        if @remaining_size > 0 && @socket.eof? +          raise HTTPStatus::BadRequest, "invalid body size." +        end +      elsif BODY_CONTAINABLE_METHODS.member?(@request_method) +        raise HTTPStatus::LengthRequired +      end +      return @body +    end + +    def read_chunk_size(socket) +      line = read_line(socket) +      if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line +        chunk_size = $1.hex +        chunk_ext = $2 +        [ chunk_size, chunk_ext ] +      else +        raise HTTPStatus::BadRequest, "bad chunk `#{line}'." +      end +    end + +    def read_chunked(socket, block) +      chunk_size, = read_chunk_size(socket) +      while chunk_size > 0 +        data = read_data(socket, chunk_size) # read chunk-data +        if data.nil? || data.bytesize != chunk_size +          raise BadRequest, "bad chunk data size." +        end +        read_line(socket)                    # skip CRLF +        block.call(data) +        chunk_size, = read_chunk_size(socket) +      end +      read_header(socket)                    # trailer + CRLF +      @header.delete("transfer-encoding") +      @remaining_size = 0 +    end + +    def _read_data(io, method, *arg) +      begin +        WEBrick::Utils.timeout(@config[:RequestTimeout]){ +          return io.__send__(method, *arg) +        } +      rescue Errno::ECONNRESET +        return nil +      rescue Timeout::Error +        raise HTTPStatus::RequestTimeout +      end +    end + +    def read_line(io, size=4096) +      _read_data(io, :gets, LF, size) +    end + +    def read_data(io, size) +      _read_data(io, :read, size) +    end + +    def parse_query() +      begin +        if @request_method == "GET" || @request_method == "HEAD" +          @query = HTTPUtils::parse_query(@query_string) +        elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/ +          @query = HTTPUtils::parse_query(body) +        elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/ +          boundary = HTTPUtils::dequote($1) +          @query = HTTPUtils::parse_form_data(body, boundary) +        else +          @query = Hash.new +        end +      rescue => ex +        raise HTTPStatus::BadRequest, ex.message +      end +    end + +    PrivateNetworkRegexp = / +      ^unknown$| +      ^((::ffff:)?127.0.0.1|::1)$| +      ^(::ffff:)?(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\. +    /ixo + +    # It's said that all X-Forwarded-* headers will contain more than one +    # (comma-separated) value if the original request already contained one of +    # these headers. Since we could use these values as Host header, we choose +    # the initial(first) value. (apr_table_mergen() adds new value after the +    # existing value with ", " prefix) +    def setup_forwarded_info +      if @forwarded_server = self["x-forwarded-server"] +        @forwarded_server = @forwarded_server.split(",", 2).first +      end +      @forwarded_proto = self["x-forwarded-proto"] +      if host_port = self["x-forwarded-host"] +        host_port = host_port.split(",", 2).first +        @forwarded_host, tmp = host_port.split(":", 2) +        @forwarded_port = (tmp || (@forwarded_proto == "https" ? 443 : 80)).to_i +      end +      if addrs = self["x-forwarded-for"] +        addrs = addrs.split(",").collect(&:strip) +        addrs.reject!{|ip| PrivateNetworkRegexp =~ ip } +        @forwarded_for = addrs.first +      end +    end + +    # :startdoc: +  end +end diff --git a/jni/ruby/lib/webrick/httpresponse.rb b/jni/ruby/lib/webrick/httpresponse.rb new file mode 100644 index 0000000..e897e8c --- /dev/null +++ b/jni/ruby/lib/webrick/httpresponse.rb @@ -0,0 +1,466 @@ +# +# httpresponse.rb -- HTTPResponse Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $ + +require 'time' +require 'webrick/httpversion' +require 'webrick/htmlutils' +require 'webrick/httputils' +require 'webrick/httpstatus' + +module WEBrick +  ## +  # An HTTP response.  This is filled in by the service or do_* methods of a +  # WEBrick HTTP Servlet. + +  class HTTPResponse + +    ## +    # HTTP Response version + +    attr_reader :http_version + +    ## +    # Response status code (200) + +    attr_reader :status + +    ## +    # Response header + +    attr_reader :header + +    ## +    # Response cookies + +    attr_reader :cookies + +    ## +    # Response reason phrase ("OK") + +    attr_accessor :reason_phrase + +    ## +    # Body may be a String or IO-like object that responds to #read and +    # #readpartial. + +    attr_accessor :body + +    ## +    # Request method for this response + +    attr_accessor :request_method + +    ## +    # Request URI for this response + +    attr_accessor :request_uri + +    ## +    # Request HTTP version for this response + +    attr_accessor :request_http_version + +    ## +    # Filename of the static file in this response.  Only used by the +    # FileHandler servlet. + +    attr_accessor :filename + +    ## +    # Is this a keep-alive response? + +    attr_accessor :keep_alive + +    ## +    # Configuration for this response + +    attr_reader :config + +    ## +    # Bytes sent in this response + +    attr_reader :sent_size + +    ## +    # Creates a new HTTP response object.  WEBrick::Config::HTTP is the +    # default configuration. + +    def initialize(config) +      @config = config +      @buffer_size = config[:OutputBufferSize] +      @logger = config[:Logger] +      @header = Hash.new +      @status = HTTPStatus::RC_OK +      @reason_phrase = nil +      @http_version = HTTPVersion::convert(@config[:HTTPVersion]) +      @body = '' +      @keep_alive = true +      @cookies = [] +      @request_method = nil +      @request_uri = nil +      @request_http_version = @http_version  # temporary +      @chunked = false +      @filename = nil +      @sent_size = 0 +    end + +    ## +    # The response's HTTP status line + +    def status_line +      "HTTP/#@http_version #@status #@reason_phrase #{CRLF}" +    end + +    ## +    # Sets the response's status to the +status+ code + +    def status=(status) +      @status = status +      @reason_phrase = HTTPStatus::reason_phrase(status) +    end + +    ## +    # Retrieves the response header +field+ + +    def [](field) +      @header[field.downcase] +    end + +    ## +    # Sets the response header +field+ to +value+ + +    def []=(field, value) +      @header[field.downcase] = value.to_s +    end + +    ## +    # The content-length header + +    def content_length +      if len = self['content-length'] +        return Integer(len) +      end +    end + +    ## +    # Sets the content-length header to +len+ + +    def content_length=(len) +      self['content-length'] = len.to_s +    end + +    ## +    # The content-type header + +    def content_type +      self['content-type'] +    end + +    ## +    # Sets the content-type header to +type+ + +    def content_type=(type) +      self['content-type'] = type +    end + +    ## +    # Iterates over each header in the response + +    def each +      @header.each{|field, value|  yield(field, value) } +    end + +    ## +    # Will this response body be returned using chunked transfer-encoding? + +    def chunked? +      @chunked +    end + +    ## +    # Enables chunked transfer encoding. + +    def chunked=(val) +      @chunked = val ? true : false +    end + +    ## +    # Will this response's connection be kept alive? + +    def keep_alive? +      @keep_alive +    end + +    ## +    # Sends the response on +socket+ + +    def send_response(socket) # :nodoc: +      begin +        setup_header() +        send_header(socket) +        send_body(socket) +      rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex +        @logger.debug(ex) +        @keep_alive = false +      rescue Exception => ex +        @logger.error(ex) +        @keep_alive = false +      end +    end + +    ## +    # Sets up the headers for sending + +    def setup_header() # :nodoc: +      @reason_phrase    ||= HTTPStatus::reason_phrase(@status) +      @header['server'] ||= @config[:ServerSoftware] +      @header['date']   ||= Time.now.httpdate + +      # HTTP/0.9 features +      if @request_http_version < "1.0" +        @http_version = HTTPVersion.new("0.9") +        @keep_alive = false +      end + +      # HTTP/1.0 features +      if @request_http_version < "1.1" +        if chunked? +          @chunked = false +          ver = @request_http_version.to_s +          msg = "chunked is set for an HTTP/#{ver} request. (ignored)" +          @logger.warn(msg) +        end +      end + +      # Determine the message length (RFC2616 -- 4.4 Message Length) +      if @status == 304 || @status == 204 || HTTPStatus::info?(@status) +        @header.delete('content-length') +        @body = "" +      elsif chunked? +        @header["transfer-encoding"] = "chunked" +        @header.delete('content-length') +      elsif %r{^multipart/byteranges} =~ @header['content-type'] +        @header.delete('content-length') +      elsif @header['content-length'].nil? +        unless @body.is_a?(IO) +          @header['content-length'] = @body ? @body.bytesize : 0 +        end +      end + +      # Keep-Alive connection. +      if @header['connection'] == "close" +         @keep_alive = false +      elsif keep_alive? +        if chunked? || @header['content-length'] || @status == 304 || @status == 204 || HTTPStatus.info?(@status) +          @header['connection'] = "Keep-Alive" +        else +          msg = "Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true" +          @logger.warn(msg) +          @header['connection'] = "close" +          @keep_alive = false +        end +      else +        @header['connection'] = "close" +      end + +      # Location is a single absoluteURI. +      if location = @header['location'] +        if @request_uri +          @header['location'] = @request_uri.merge(location) +        end +      end +    end + +    ## +    # Sends the headers on +socket+ + +    def send_header(socket) # :nodoc: +      if @http_version.major > 0 +        data = status_line() +        @header.each{|key, value| +          tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase } +          data << "#{tmp}: #{value}" << CRLF +        } +        @cookies.each{|cookie| +          data << "Set-Cookie: " << cookie.to_s << CRLF +        } +        data << CRLF +        _write_data(socket, data) +      end +    end + +    ## +    # Sends the body on +socket+ + +    def send_body(socket) # :nodoc: +      if @body.respond_to? :readpartial then +        send_body_io(socket) +      else +        send_body_string(socket) +      end +    end + +    def to_s # :nodoc: +      ret = "" +      send_response(ret) +      ret +    end + +    ## +    # Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+. +    # +    # Example: +    # +    #   res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect + +    def set_redirect(status, url) +      @body = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\n" +      @header['location'] = url.to_s +      raise status +    end + +    ## +    # Creates an error page for exception +ex+ with an optional +backtrace+ + +    def set_error(ex, backtrace=false) +      case ex +      when HTTPStatus::Status +        @keep_alive = false if HTTPStatus::error?(ex.code) +        self.status = ex.code +      else +        @keep_alive = false +        self.status = HTTPStatus::RC_INTERNAL_SERVER_ERROR +      end +      @header['content-type'] = "text/html; charset=ISO-8859-1" + +      if respond_to?(:create_error_page) +        create_error_page() +        return +      end + +      if @request_uri +        host, port = @request_uri.host, @request_uri.port +      else +        host, port = @config[:ServerName], @config[:Port] +      end + +      @body = '' +      @body << <<-_end_of_html_ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"> +<HTML> +  <HEAD><TITLE>#{HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD> +  <BODY> +    <H1>#{HTMLUtils::escape(@reason_phrase)}</H1> +    #{HTMLUtils::escape(ex.message)} +    <HR> +      _end_of_html_ + +      if backtrace && $DEBUG +        @body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' " +        @body << "#{HTMLUtils::escape(ex.message)}" +        @body << "<PRE>" +        ex.backtrace.each{|line| @body << "\t#{line}\n"} +        @body << "</PRE><HR>" +      end + +      @body << <<-_end_of_html_ +    <ADDRESS> +     #{HTMLUtils::escape(@config[:ServerSoftware])} at +     #{host}:#{port} +    </ADDRESS> +  </BODY> +</HTML> +      _end_of_html_ +    end + +    private + +    # :stopdoc: + +    def send_body_io(socket) +      begin +        if @request_method == "HEAD" +          # do nothing +        elsif chunked? +          begin +            buf  = '' +            data = '' +            while true +              @body.readpartial( @buffer_size, buf ) # there is no need to clear buf? +              data << format("%x", buf.bytesize) << CRLF +              data << buf << CRLF +              _write_data(socket, data) +              data.clear +              @sent_size += buf.bytesize +            end +          rescue EOFError # do nothing +          end +          _write_data(socket, "0#{CRLF}#{CRLF}") +        else +          size = @header['content-length'].to_i +          _send_file(socket, @body, 0, size) +          @sent_size = size +        end +      ensure +        @body.close +      end +    end + +    def send_body_string(socket) +      if @request_method == "HEAD" +        # do nothing +      elsif chunked? +        body ? @body.bytesize : 0 +        while buf = @body[@sent_size, @buffer_size] +          break if buf.empty? +          data = "" +          data << format("%x", buf.bytesize) << CRLF +          data << buf << CRLF +          _write_data(socket, data) +          @sent_size += buf.bytesize +        end +        _write_data(socket, "0#{CRLF}#{CRLF}") +      else +        if @body && @body.bytesize > 0 +          _write_data(socket, @body) +          @sent_size = @body.bytesize +        end +      end +    end + +    def _send_file(output, input, offset, size) +      while offset > 0 +        sz = @buffer_size < size ? @buffer_size : size +        buf = input.read(sz) +        offset -= buf.bytesize +      end + +      if size == 0 +        while buf = input.read(@buffer_size) +          _write_data(output, buf) +        end +      else +        while size > 0 +          sz = @buffer_size < size ? @buffer_size : size +          buf = input.read(sz) +          _write_data(output, buf) +          size -= buf.bytesize +        end +      end +    end + +    def _write_data(socket, data) +      socket << data +    end + +    # :startdoc: +  end + +end diff --git a/jni/ruby/lib/webrick/https.rb b/jni/ruby/lib/webrick/https.rb new file mode 100644 index 0000000..9194f94 --- /dev/null +++ b/jni/ruby/lib/webrick/https.rb @@ -0,0 +1,86 @@ +# +# https.rb -- SSL/TLS enhancement for HTTPServer +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: https.rb,v 1.15 2003/07/22 19:20:42 gotoyuzo Exp $ + +require 'webrick/ssl' + +module WEBrick +  module Config +    HTTP.update(SSL) +  end + +  ## +  #-- +  # Adds SSL functionality to WEBrick::HTTPRequest + +  class HTTPRequest + +    ## +    # HTTP request SSL cipher + +    attr_reader :cipher + +    ## +    # HTTP request server certificate + +    attr_reader :server_cert + +    ## +    # HTTP request client certificate + +    attr_reader :client_cert + +    # :stopdoc: + +    alias orig_parse parse + +    def parse(socket=nil) +      if socket.respond_to?(:cert) +        @server_cert = socket.cert || @config[:SSLCertificate] +        @client_cert = socket.peer_cert +        @client_cert_chain = socket.peer_cert_chain +        @cipher      = socket.cipher +      end +      orig_parse(socket) +    end + +    alias orig_parse_uri parse_uri + +    def parse_uri(str, scheme="https") +      if server_cert +        return orig_parse_uri(str, scheme) +      end +      return orig_parse_uri(str) +    end +    private :parse_uri + +    alias orig_meta_vars meta_vars + +    def meta_vars +      meta = orig_meta_vars +      if server_cert +        meta["HTTPS"] = "on" +        meta["SSL_SERVER_CERT"] = @server_cert.to_pem +        meta["SSL_CLIENT_CERT"] = @client_cert ? @client_cert.to_pem : "" +        if @client_cert_chain +          @client_cert_chain.each_with_index{|cert, i| +            meta["SSL_CLIENT_CERT_CHAIN_#{i}"] = cert.to_pem +          } +        end +        meta["SSL_CIPHER"] = @cipher[0] +        meta["SSL_PROTOCOL"] = @cipher[1] +        meta["SSL_CIPHER_USEKEYSIZE"] = @cipher[2].to_s +        meta["SSL_CIPHER_ALGKEYSIZE"] = @cipher[3].to_s +      end +      meta +    end + +    # :startdoc: +  end +end diff --git a/jni/ruby/lib/webrick/httpserver.rb b/jni/ruby/lib/webrick/httpserver.rb new file mode 100644 index 0000000..0618489 --- /dev/null +++ b/jni/ruby/lib/webrick/httpserver.rb @@ -0,0 +1,278 @@ +# +# httpserver.rb -- HTTPServer Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $ + +require 'webrick/server' +require 'webrick/httputils' +require 'webrick/httpstatus' +require 'webrick/httprequest' +require 'webrick/httpresponse' +require 'webrick/httpservlet' +require 'webrick/accesslog' + +module WEBrick +  class HTTPServerError < ServerError; end + +  ## +  # An HTTP Server + +  class HTTPServer < ::WEBrick::GenericServer +    ## +    # Creates a new HTTP server according to +config+ +    # +    # An HTTP server uses the following attributes: +    # +    # :AccessLog:: An array of access logs.  See WEBrick::AccessLog +    # :BindAddress:: Local address for the server to bind to +    # :DocumentRoot:: Root path to serve files from +    # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler +    # :HTTPVersion:: The HTTP version of this server +    # :Port:: Port to listen on +    # :RequestCallback:: Called with a request and response before each +    #                    request is serviced. +    # :RequestTimeout:: Maximum time to wait between requests +    # :ServerAlias:: Array of alternate names for this server for virtual +    #                hosting +    # :ServerName:: Name for this server for virtual hosting + +    def initialize(config={}, default=Config::HTTP) +      super(config, default) +      @http_version = HTTPVersion::convert(@config[:HTTPVersion]) + +      @mount_tab = MountTable.new +      if @config[:DocumentRoot] +        mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot], +              @config[:DocumentRootOptions]) +      end + +      unless @config[:AccessLog] +        @config[:AccessLog] = [ +          [ $stderr, AccessLog::COMMON_LOG_FORMAT ], +          [ $stderr, AccessLog::REFERER_LOG_FORMAT ] +        ] +      end + +      @virtual_hosts = Array.new +    end + +    ## +    # Processes requests on +sock+ + +    def run(sock) +      while true +        res = HTTPResponse.new(@config) +        req = HTTPRequest.new(@config) +        server = self +        begin +          timeout = @config[:RequestTimeout] +          while timeout > 0 +            break if IO.select([sock], nil, nil, 0.5) +            break if @status != :Running +            timeout -= 0.5 +          end +          raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running +          raise HTTPStatus::EOFError if sock.eof? +          req.parse(sock) +          res.request_method = req.request_method +          res.request_uri = req.request_uri +          res.request_http_version = req.http_version +          res.keep_alive = req.keep_alive? +          server = lookup_server(req) || self +          if callback = server[:RequestCallback] +            callback.call(req, res) +          elsif callback = server[:RequestHandler] +            msg = ":RequestHandler is deprecated, please use :RequestCallback" +            @logger.warn(msg) +            callback.call(req, res) +          end +          server.service(req, res) +        rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex +          res.set_error(ex) +        rescue HTTPStatus::Error => ex +          @logger.error(ex.message) +          res.set_error(ex) +        rescue HTTPStatus::Status => ex +          res.status = ex.code +        rescue StandardError => ex +          @logger.error(ex) +          res.set_error(ex, true) +        ensure +          if req.request_line +            if req.keep_alive? && res.keep_alive? +              req.fixup() +            end +            res.send_response(sock) +            server.access_log(@config, req, res) +          end +        end +        break if @http_version < "1.1" +        break unless req.keep_alive? +        break unless res.keep_alive? +      end +    end + +    ## +    # Services +req+ and fills in +res+ + +    def service(req, res) +      if req.unparsed_uri == "*" +        if req.request_method == "OPTIONS" +          do_OPTIONS(req, res) +          raise HTTPStatus::OK +        end +        raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found." +      end + +      servlet, options, script_name, path_info = search_servlet(req.path) +      raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet +      req.script_name = script_name +      req.path_info = path_info +      si = servlet.get_instance(self, *options) +      @logger.debug(format("%s is invoked.", si.class.name)) +      si.service(req, res) +    end + +    ## +    # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS +    # requests are allowed. + +    def do_OPTIONS(req, res) +      res["allow"] = "GET,HEAD,POST,OPTIONS" +    end + +    ## +    # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation +    # time + +    def mount(dir, servlet, *options) +      @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir)) +      @mount_tab[dir] = [ servlet, options ] +    end + +    ## +    # Mounts +proc+ or +block+ on +dir+ and calls it with a +    # WEBrick::HTTPRequest and WEBrick::HTTPResponse + +    def mount_proc(dir, proc=nil, &block) +      proc ||= block +      raise HTTPServerError, "must pass a proc or block" unless proc +      mount(dir, HTTPServlet::ProcHandler.new(proc)) +    end + +    ## +    # Unmounts +dir+ + +    def unmount(dir) +      @logger.debug(sprintf("unmount %s.", dir)) +      @mount_tab.delete(dir) +    end +    alias umount unmount + +    ## +    # Finds a servlet for +path+ + +    def search_servlet(path) +      script_name, path_info = @mount_tab.scan(path) +      servlet, options = @mount_tab[script_name] +      if servlet +        [ servlet, options, script_name, path_info ] +      end +    end + +    ## +    # Adds +server+ as a virtual host. + +    def virtual_host(server) +      @virtual_hosts << server +      @virtual_hosts = @virtual_hosts.sort_by{|s| +        num = 0 +        num -= 4 if s[:BindAddress] +        num -= 2 if s[:Port] +        num -= 1 if s[:ServerName] +        num +      } +    end + +    ## +    # Finds the appropriate virtual host to handle +req+ + +    def lookup_server(req) +      @virtual_hosts.find{|s| +        (s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) && +        (s[:Port].nil?        || req.port == s[:Port])           && +        ((s[:ServerName].nil?  || req.host == s[:ServerName]) || +         (!s[:ServerAlias].nil? && s[:ServerAlias].find{|h| h === req.host})) +      } +    end + +    ## +    # Logs +req+ and +res+ in the access logs.  +config+ is used for the +    # server name. + +    def access_log(config, req, res) +      param = AccessLog::setup_params(config, req, res) +      @config[:AccessLog].each{|logger, fmt| +        logger << AccessLog::format(fmt+"\n", param) +      } +    end + +    ## +    # Mount table for the path a servlet is mounted on in the directory space +    # of the server.  Users of WEBrick can only access this indirectly via +    # WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and +    # WEBrick::HTTPServer#search_servlet + +    class MountTable # :nodoc: +      def initialize +        @tab = Hash.new +        compile +      end + +      def [](dir) +        dir = normalize(dir) +        @tab[dir] +      end + +      def []=(dir, val) +        dir = normalize(dir) +        @tab[dir] = val +        compile +        val +      end + +      def delete(dir) +        dir = normalize(dir) +        res = @tab.delete(dir) +        compile +        res +      end + +      def scan(path) +        @scanner =~ path +        [ $&, $' ] +      end + +      private + +      def compile +        k = @tab.keys +        k.sort! +        k.reverse! +        k.collect!{|path| Regexp.escape(path) } +        @scanner = Regexp.new("^(" + k.join("|") +")(?=/|$)") +      end + +      def normalize(dir) +        ret = dir ? dir.dup : "" +        ret.sub!(%r|/+$|, "") +        ret +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet.rb b/jni/ruby/lib/webrick/httpservlet.rb new file mode 100644 index 0000000..ac7c022 --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet.rb @@ -0,0 +1,22 @@ +# +# httpservlet.rb -- HTTPServlet Utility File +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpservlet.rb,v 1.21 2003/02/23 12:24:46 gotoyuzo Exp $ + +require 'webrick/httpservlet/abstract' +require 'webrick/httpservlet/filehandler' +require 'webrick/httpservlet/cgihandler' +require 'webrick/httpservlet/erbhandler' +require 'webrick/httpservlet/prochandler' + +module WEBrick +  module HTTPServlet +    FileHandler.add_handler("cgi", CGIHandler) +    FileHandler.add_handler("rhtml", ERBHandler) +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet/abstract.rb b/jni/ruby/lib/webrick/httpservlet/abstract.rb new file mode 100644 index 0000000..d3b00ab --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/abstract.rb @@ -0,0 +1,153 @@ +# +# httpservlet.rb -- HTTPServlet Module +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $ + +require 'thread' + +require 'webrick/htmlutils' +require 'webrick/httputils' +require 'webrick/httpstatus' + +module WEBrick +  module HTTPServlet +    class HTTPServletError < StandardError; end + +    ## +    # AbstractServlet allows HTTP server modules to be reused across multiple +    # servers and allows encapsulation of functionality. +    # +    # By default a servlet will respond to GET, HEAD (through an alias to GET) +    # and OPTIONS requests. +    # +    # By default a new servlet is initialized for every request.  A servlet +    # instance can be reused by overriding ::get_instance in the +    # AbstractServlet subclass. +    # +    # == A Simple Servlet +    # +    #  class Simple < WEBrick::HTTPServlet::AbstractServlet +    #    def do_GET request, response +    #      status, content_type, body = do_stuff_with request +    # +    #      response.status = status +    #      response['Content-Type'] = content_type +    #      response.body = body +    #    end +    # +    #    def do_stuff_with request +    #      return 200, 'text/plain', 'you got a page' +    #    end +    #  end +    # +    # This servlet can be mounted on a server at a given path: +    # +    #   server.mount '/simple', Simple +    # +    # == Servlet Configuration +    # +    # Servlets can be configured via initialize.  The first argument is the +    # HTTP server the servlet is being initialized for. +    # +    #  class Configurable < Simple +    #    def initialize server, color, size +    #      super server +    #      @color = color +    #      @size = size +    #    end +    # +    #    def do_stuff_with request +    #      content = "<p " \ +    #                %q{style="color: #{@color}; font-size: #{@size}"} \ +    #                ">Hello, World!" +    # +    #      return 200, "text/html", content +    #    end +    #  end +    # +    # This servlet must be provided two arguments at mount time: +    # +    #   server.mount '/configurable', Configurable, 'red', '2em' + +    class AbstractServlet + +      ## +      # Factory for servlet instances that will handle a request from +server+ +      # using +options+ from the mount point.  By default a new servlet +      # instance is created for every call. + +      def self.get_instance(server, *options) +        self.new(server, *options) +      end + +      ## +      # Initializes a new servlet for +server+ using +options+ which are +      # stored as-is in +@options+.  +@logger+ is also provided. + +      def initialize(server, *options) +        @server = @config = server +        @logger = @server[:Logger] +        @options = options +      end + +      ## +      # Dispatches to a +do_+ method based on +req+ if such a method is +      # available.  (+do_GET+ for a GET request).  Raises a MethodNotAllowed +      # exception if the method is not implemented. + +      def service(req, res) +        method_name = "do_" + req.request_method.gsub(/-/, "_") +        if respond_to?(method_name) +          __send__(method_name, req, res) +        else +          raise HTTPStatus::MethodNotAllowed, +                "unsupported method `#{req.request_method}'." +        end +      end + +      ## +      # Raises a NotFound exception + +      def do_GET(req, res) +        raise HTTPStatus::NotFound, "not found." +      end + +      ## +      # Dispatches to do_GET + +      def do_HEAD(req, res) +        do_GET(req, res) +      end + +      ## +      # Returns the allowed HTTP request methods + +      def do_OPTIONS(req, res) +        m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1} +        m.sort! +        res["allow"] = m.join(",") +      end + +      private + +      ## +      # Redirects to a path ending in / + +      def redirect_to_directory_uri(req, res) +        if req.path[-1] != ?/ +          location = WEBrick::HTTPUtils.escape_path(req.path + "/") +          if req.query_string && req.query_string.bytesize > 0 +            location << "?" << req.query_string +          end +          res.set_redirect(HTTPStatus::MovedPermanently, location) +        end +      end +    end + +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet/cgi_runner.rb b/jni/ruby/lib/webrick/httpservlet/cgi_runner.rb new file mode 100644 index 0000000..32ecb6f --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/cgi_runner.rb @@ -0,0 +1,46 @@ +# +# cgi_runner.rb -- CGI launcher. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $ + +def sysread(io, size) +  buf = "" +  while size > 0 +    tmp = io.sysread(size) +    buf << tmp +    size -= tmp.bytesize +  end +  return buf +end + +STDIN.binmode + +len = sysread(STDIN, 8).to_i +out = sysread(STDIN, len) +STDOUT.reopen(open(out, "w")) + +len = sysread(STDIN, 8).to_i +err = sysread(STDIN, len) +STDERR.reopen(open(err, "w")) + +len  = sysread(STDIN, 8).to_i +dump = sysread(STDIN, len) +hash = Marshal.restore(dump) +ENV.keys.each{|name| ENV.delete(name) } +hash.each{|k, v| ENV[k] = v if v } + +dir = File::dirname(ENV["SCRIPT_FILENAME"]) +Dir::chdir dir + +if ARGV[0] +  argv = ARGV.dup +  argv << ENV["SCRIPT_FILENAME"] +  exec(*argv) +  # NOTREACHED +end +exec ENV["SCRIPT_FILENAME"] diff --git a/jni/ruby/lib/webrick/httpservlet/cgihandler.rb b/jni/ruby/lib/webrick/httpservlet/cgihandler.rb new file mode 100644 index 0000000..3210041 --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/cgihandler.rb @@ -0,0 +1,120 @@ +# +# cgihandler.rb -- CGIHandler Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: cgihandler.rb,v 1.27 2003/03/21 19:56:01 gotoyuzo Exp $ + +require 'rbconfig' +require 'tempfile' +require 'webrick/config' +require 'webrick/httpservlet/abstract' + +module WEBrick +  module HTTPServlet + +    ## +    # Servlet for handling CGI scripts +    # +    # Example: +    # +    #  server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler, +    #               '/path/to/my_script') + +    class CGIHandler < AbstractServlet +      Ruby = RbConfig.ruby # :nodoc: +      CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: + +      ## +      # Creates a new CGI script servlet for the script at +name+ + +      def initialize(server, name) +        super(server, name) +        @script_filename = name +        @tempdir = server[:TempDir] +        @cgicmd = "#{CGIRunner} #{server[:CGIInterpreter]}" +      end + +      # :stopdoc: + +      def do_GET(req, res) +        cgi_in = IO::popen(@cgicmd, "wb") +        cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY) +        cgi_out.set_encoding("ASCII-8BIT") +        cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY) +        cgi_err.set_encoding("ASCII-8BIT") +        begin +          cgi_in.sync = true +          meta = req.meta_vars +          meta["SCRIPT_FILENAME"] = @script_filename +          meta["PATH"] = @config[:CGIPathEnv] +          if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM +            meta["SystemRoot"] = ENV["SystemRoot"] +          end +          dump = Marshal.dump(meta) + +          cgi_in.write("%8d" % cgi_out.path.bytesize) +          cgi_in.write(cgi_out.path) +          cgi_in.write("%8d" % cgi_err.path.bytesize) +          cgi_in.write(cgi_err.path) +          cgi_in.write("%8d" % dump.bytesize) +          cgi_in.write(dump) + +          if req.body and req.body.bytesize > 0 +            cgi_in.write(req.body) +          end +        ensure +          cgi_in.close +          status = $?.exitstatus +          sleep 0.1 if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM +          data = cgi_out.read +          cgi_out.close(true) +          if errmsg = cgi_err.read +            if errmsg.bytesize > 0 +              @logger.error("CGIHandler: #{@script_filename}:\n" + errmsg) +            end +          end +          cgi_err.close(true) +        end + +        if status != 0 +          @logger.error("CGIHandler: #{@script_filename} exit with #{status}") +        end + +        data = "" unless data +        raw_header, body = data.split(/^[\xd\xa]+/, 2) +        raise HTTPStatus::InternalServerError, +          "Premature end of script headers: #{@script_filename}" if body.nil? + +        begin +          header = HTTPUtils::parse_header(raw_header) +          if /^(\d+)/ =~ header['status'][0] +            res.status = $1.to_i +            header.delete('status') +          end +          if header.has_key?('location') +            # RFC 3875 6.2.3, 6.2.4 +            res.status = 302 unless (300...400) === res.status +          end +          if header.has_key?('set-cookie') +            header['set-cookie'].each{|k| +              res.cookies << Cookie.parse_set_cookie(k) +            } +            header.delete('set-cookie') +          end +          header.each{|key, val| res[key] = val.join(", ") } +        rescue => ex +          raise HTTPStatus::InternalServerError, ex.message +        end +        res.body = body +      end +      alias do_POST do_GET + +      # :startdoc: +    end + +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet/erbhandler.rb b/jni/ruby/lib/webrick/httpservlet/erbhandler.rb new file mode 100644 index 0000000..1b8a82d --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/erbhandler.rb @@ -0,0 +1,87 @@ +# +# erbhandler.rb -- ERBHandler Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $ + +require 'webrick/httpservlet/abstract.rb' + +require 'erb' + +module WEBrick +  module HTTPServlet + +    ## +    # ERBHandler evaluates an ERB file and returns the result.  This handler +    # is automatically used if there are .rhtml files in a directory served by +    # the FileHandler. +    # +    # ERBHandler supports GET and POST methods. +    # +    # The ERB file is evaluated with the local variables +servlet_request+ and +    # +servlet_response+ which are a WEBrick::HTTPRequest and +    # WEBrick::HTTPResponse respectively. +    # +    # Example .rhtml file: +    # +    #   Request to <%= servlet_request.request_uri %> +    # +    #   Query params <%= servlet_request.query.inspect %> + +    class ERBHandler < AbstractServlet + +      ## +      # Creates a new ERBHandler on +server+ that will evaluate and serve the +      # ERB file +name+ + +      def initialize(server, name) +        super(server, name) +        @script_filename = name +      end + +      ## +      # Handles GET requests + +      def do_GET(req, res) +        unless defined?(ERB) +          @logger.warn "#{self.class}: ERB not defined." +          raise HTTPStatus::Forbidden, "ERBHandler cannot work." +        end +        begin +          data = open(@script_filename){|io| io.read } +          res.body = evaluate(ERB.new(data), req, res) +          res['content-type'] ||= +            HTTPUtils::mime_type(@script_filename, @config[:MimeTypes]) +        rescue StandardError +          raise +        rescue Exception => ex +          @logger.error(ex) +          raise HTTPStatus::InternalServerError, ex.message +        end +      end + +      ## +      # Handles POST requests + +      alias do_POST do_GET + +      private + +      ## +      # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as +      # local variables. + +      def evaluate(erb, servlet_request, servlet_response) +        Module.new.module_eval{ +          servlet_request.meta_vars +          servlet_request.query +          erb.result(binding) +        } +      end +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet/filehandler.rb b/jni/ruby/lib/webrick/httpservlet/filehandler.rb new file mode 100644 index 0000000..cc9db4a --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/filehandler.rb @@ -0,0 +1,521 @@ +# +# filehandler.rb -- FileHandler Module +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $ + +require 'thread' +require 'time' + +require 'webrick/htmlutils' +require 'webrick/httputils' +require 'webrick/httpstatus' + +module WEBrick +  module HTTPServlet + +    ## +    # Servlet for serving a single file.  You probably want to use the +    # FileHandler servlet instead as it handles directories and fancy indexes. +    # +    # Example: +    # +    #   server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler, +    #                '/path/to/my_page.txt') +    # +    # This servlet handles If-Modified-Since and Range requests. + +    class DefaultFileHandler < AbstractServlet + +      ## +      # Creates a DefaultFileHandler instance for the file at +local_path+. + +      def initialize(server, local_path) +        super(server, local_path) +        @local_path = local_path +      end + +      # :stopdoc: + +      def do_GET(req, res) +        st = File::stat(@local_path) +        mtime = st.mtime +        res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i) + +        if not_modified?(req, res, mtime, res['etag']) +          res.body = '' +          raise HTTPStatus::NotModified +        elsif req['range'] +          make_partial_content(req, res, @local_path, st.size) +          raise HTTPStatus::PartialContent +        else +          mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes]) +          res['content-type'] = mtype +          res['content-length'] = st.size +          res['last-modified'] = mtime.httpdate +          res.body = open(@local_path, "rb") +        end +      end + +      def not_modified?(req, res, mtime, etag) +        if ir = req['if-range'] +          begin +            if Time.httpdate(ir) >= mtime +              return true +            end +          rescue +            if HTTPUtils::split_header_value(ir).member?(res['etag']) +              return true +            end +          end +        end + +        if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime +          return true +        end + +        if (inm = req['if-none-match']) && +           HTTPUtils::split_header_value(inm).member?(res['etag']) +          return true +        end + +        return false +      end + +      def make_partial_content(req, res, filename, filesize) +        mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes]) +        unless ranges = HTTPUtils::parse_range_header(req['range']) +          raise HTTPStatus::BadRequest, +            "Unrecognized range-spec: \"#{req['range']}\"" +        end +        open(filename, "rb"){|io| +          if ranges.size > 1 +            time = Time.now +            boundary = "#{time.sec}_#{time.usec}_#{Process::pid}" +            body = '' +            ranges.each{|range| +              first, last = prepare_range(range, filesize) +              next if first < 0 +              io.pos = first +              content = io.read(last-first+1) +              body << "--" << boundary << CRLF +              body << "Content-Type: #{mtype}" << CRLF +              body << "Content-Range: bytes #{first}-#{last}/#{filesize}" << CRLF +              body << CRLF +              body << content +              body << CRLF +            } +            raise HTTPStatus::RequestRangeNotSatisfiable if body.empty? +            body << "--" << boundary << "--" << CRLF +            res["content-type"] = "multipart/byteranges; boundary=#{boundary}" +            res.body = body +          elsif range = ranges[0] +            first, last = prepare_range(range, filesize) +            raise HTTPStatus::RequestRangeNotSatisfiable if first < 0 +            if last == filesize - 1 +              content = io.dup +              content.pos = first +            else +              io.pos = first +              content = io.read(last-first+1) +            end +            res['content-type'] = mtype +            res['content-range'] = "bytes #{first}-#{last}/#{filesize}" +            res['content-length'] = last - first + 1 +            res.body = content +          else +            raise HTTPStatus::BadRequest +          end +        } +      end + +      def prepare_range(range, filesize) +        first = range.first < 0 ? filesize + range.first : range.first +        return -1, -1 if first < 0 || first >= filesize +        last = range.last < 0 ? filesize + range.last : range.last +        last = filesize - 1 if last >= filesize +        return first, last +      end + +      # :startdoc: +    end + +    ## +    # Serves a directory including fancy indexing and a variety of other +    # options. +    # +    # Example: +    # +    #   server.mount '/assets', WEBrick::FileHandler, '/path/to/assets' + +    class FileHandler < AbstractServlet +      HandlerTable = Hash.new # :nodoc: + +      ## +      # Allow custom handling of requests for files with +suffix+ by class +      # +handler+ + +      def self.add_handler(suffix, handler) +        HandlerTable[suffix] = handler +      end + +      ## +      # Remove custom handling of requests for files with +suffix+ + +      def self.remove_handler(suffix) +        HandlerTable.delete(suffix) +      end + +      ## +      # Creates a FileHandler servlet on +server+ that serves files starting +      # at directory +root+ +      # +      # +options+ may be a Hash containing keys from +      # WEBrick::Config::FileHandler or +true+ or +false+. +      # +      # If +options+ is true or false then +:FancyIndexing+ is enabled or +      # disabled respectively. + +      def initialize(server, root, options={}, default=Config::FileHandler) +        @config = server.config +        @logger = @config[:Logger] +        @root = File.expand_path(root) +        if options == true || options == false +          options = { :FancyIndexing => options } +        end +        @options = default.dup.update(options) +      end + +      # :stopdoc: + +      def service(req, res) +        # if this class is mounted on "/" and /~username is requested. +        # we're going to override path informations before invoking service. +        if defined?(Etc) && @options[:UserDir] && req.script_name.empty? +          if %r|^(/~([^/]+))| =~ req.path_info +            script_name, user = $1, $2 +            path_info = $' +            begin +              passwd = Etc::getpwnam(user) +              @root = File::join(passwd.dir, @options[:UserDir]) +              req.script_name = script_name +              req.path_info = path_info +            rescue +              @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed" +            end +          end +        end +        prevent_directory_traversal(req, res) +        super(req, res) +      end + +      def do_GET(req, res) +        unless exec_handler(req, res) +          set_dir_list(req, res) +        end +      end + +      def do_POST(req, res) +        unless exec_handler(req, res) +          raise HTTPStatus::NotFound, "`#{req.path}' not found." +        end +      end + +      def do_OPTIONS(req, res) +        unless exec_handler(req, res) +          super(req, res) +        end +      end + +      # ToDo +      # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV +      # +      # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE +      # LOCK UNLOCK + +      # RFC3253: Versioning Extensions to WebDAV +      #          (Web Distributed Authoring and Versioning) +      # +      # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT +      # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY + +      private + +      def trailing_pathsep?(path) +        # check for trailing path separator: +        #   File.dirname("/aaaa/bbbb/")      #=> "/aaaa") +        #   File.dirname("/aaaa/bbbb/x")     #=> "/aaaa/bbbb") +        #   File.dirname("/aaaa/bbbb")       #=> "/aaaa") +        #   File.dirname("/aaaa/bbbbx")      #=> "/aaaa") +        return File.dirname(path) != File.dirname(path+"x") +      end + +      def prevent_directory_traversal(req, res) +        # Preventing directory traversal on Windows platforms; +        # Backslashes (0x5c) in path_info are not interpreted as special +        # character in URI notation. So the value of path_info should be +        # normalize before accessing to the filesystem. + +        # dirty hack for filesystem encoding; in nature, File.expand_path +        # should not be used for path normalization.  [Bug #3345] +        path = req.path_info.dup.force_encoding(Encoding.find("filesystem")) +        if trailing_pathsep?(req.path_info) +          # File.expand_path removes the trailing path separator. +          # Adding a character is a workaround to save it. +          #  File.expand_path("/aaa/")        #=> "/aaa" +          #  File.expand_path("/aaa/" + "x")  #=> "/aaa/x" +          expanded = File.expand_path(path + "x") +          expanded.chop!  # remove trailing "x" +        else +          expanded = File.expand_path(path) +        end +        expanded.force_encoding(req.path_info.encoding) +        req.path_info = expanded +      end + +      def exec_handler(req, res) +        raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root +        if set_filename(req, res) +          handler = get_handler(req, res) +          call_callback(:HandlerCallback, req, res) +          h = handler.get_instance(@config, res.filename) +          h.service(req, res) +          return true +        end +        call_callback(:HandlerCallback, req, res) +        return false +      end + +      def get_handler(req, res) +        suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase +        if /\.(\w+)\.([\w\-]+)\z/ =~ res.filename +          if @options[:AcceptableLanguages].include?($2.downcase) +            suffix2 = $1.downcase +          end +        end +        handler_table = @options[:HandlerTable] +        return handler_table[suffix1] || handler_table[suffix2] || +               HandlerTable[suffix1] || HandlerTable[suffix2] || +               DefaultFileHandler +      end + +      def set_filename(req, res) +        res.filename = @root.dup +        path_info = req.path_info.scan(%r|/[^/]*|) + +        path_info.unshift("")  # dummy for checking @root dir +        while base = path_info.first +          break if base == "/" +          break unless File.directory?(File.expand_path(res.filename + base)) +          shift_path_info(req, res, path_info) +          call_callback(:DirectoryCallback, req, res) +        end + +        if base = path_info.first +          if base == "/" +            if file = search_index_file(req, res) +              shift_path_info(req, res, path_info, file) +              call_callback(:FileCallback, req, res) +              return true +            end +            shift_path_info(req, res, path_info) +          elsif file = search_file(req, res, base) +            shift_path_info(req, res, path_info, file) +            call_callback(:FileCallback, req, res) +            return true +          else +            raise HTTPStatus::NotFound, "`#{req.path}' not found." +          end +        end + +        return false +      end + +      def check_filename(req, res, name) +        if nondisclosure_name?(name) || windows_ambiguous_name?(name) +          @logger.warn("the request refers nondisclosure name `#{name}'.") +          raise HTTPStatus::NotFound, "`#{req.path}' not found." +        end +      end + +      def shift_path_info(req, res, path_info, base=nil) +        tmp = path_info.shift +        base = base || tmp +        req.path_info = path_info.join +        req.script_name << base +        res.filename = File.expand_path(res.filename + base) +        check_filename(req, res, File.basename(res.filename)) +      end + +      def search_index_file(req, res) +        @config[:DirectoryIndex].each{|index| +          if file = search_file(req, res, "/"+index) +            return file +          end +        } +        return nil +      end + +      def search_file(req, res, basename) +        langs = @options[:AcceptableLanguages] +        path = res.filename + basename +        if File.file?(path) +          return basename +        elsif langs.size > 0 +          req.accept_language.each{|lang| +            path_with_lang = path + ".#{lang}" +            if langs.member?(lang) && File.file?(path_with_lang) +              return basename + ".#{lang}" +            end +          } +          (langs - req.accept_language).each{|lang| +            path_with_lang = path + ".#{lang}" +            if File.file?(path_with_lang) +              return basename + ".#{lang}" +            end +          } +        end +        return nil +      end + +      def call_callback(callback_name, req, res) +        if cb = @options[callback_name] +          cb.call(req, res) +        end +      end + +      def windows_ambiguous_name?(name) +        return true if /[. ]+\z/ =~ name +        return true if /::\$DATA\z/ =~ name +        return false +      end + +      def nondisclosure_name?(name) +        @options[:NondisclosureName].each{|pattern| +          if File.fnmatch(pattern, name, File::FNM_CASEFOLD) +            return true +          end +        } +        return false +      end + +      def set_dir_list(req, res) +        redirect_to_directory_uri(req, res) +        unless @options[:FancyIndexing] +          raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'" +        end +        local_path = res.filename +        list = Dir::entries(local_path).collect{|name| +          next if name == "." || name == ".." +          next if nondisclosure_name?(name) +          next if windows_ambiguous_name?(name) +          st = (File::stat(File.join(local_path, name)) rescue nil) +          if st.nil? +            [ name, nil, -1 ] +          elsif st.directory? +            [ name + "/", st.mtime, -1 ] +          else +            [ name, st.mtime, st.size ] +          end +        } +        list.compact! + +        query = req.query + +        d0 = nil +        idx = nil +        %w[N M S].each_with_index do |q, i| +          if d = query.delete(q) +            idx ||= i +            d0 ||= d +          end +        end +        d0 ||= "A" +        idx ||= 0 +        d1 = (d0 == "A") ? "D" : "A" + +        if d0 == "A" +          list.sort!{|a,b| a[idx] <=> b[idx] } +        else +          list.sort!{|a,b| b[idx] <=> a[idx] } +        end + +        namewidth = query["NameWidth"] +        if namewidth == "*" +          namewidth = nil +        elsif !namewidth or (namewidth = namewidth.to_i) < 2 +          namewidth = 25 +        end +        query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")} + +        type = "text/html" +        case enc = Encoding.find('filesystem') +        when Encoding::US_ASCII, Encoding::ASCII_8BIT +        else +          type << "; charset=\"#{enc.name}\"" +        end +        res['content-type'] = type + +        title = "Index of #{HTMLUtils::escape(req.path)}" +        res.body = <<-_end_of_html_ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<HTML> +  <HEAD> +    <TITLE>#{title}</TITLE> +    <style type="text/css"> +    <!-- +    .name, .mtime { text-align: left; } +    .size { text-align: right; } +    td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } +    table { border-collapse: collapse; } +    tr th { border-bottom: 2px groove; } +    //--> +    </style> +  </HEAD> +  <BODY> +    <H1>#{title}</H1> +        _end_of_html_ + +        res.body << "<TABLE width=\"100%\"><THEAD><TR>\n" +        res.body << "<TH class=\"name\"><A HREF=\"?N=#{d1}#{query}\">Name</A></TH>" +        res.body << "<TH class=\"mtime\"><A HREF=\"?M=#{d1}#{query}\">Last modified</A></TH>" +        res.body << "<TH class=\"size\"><A HREF=\"?S=#{d1}#{query}\">Size</A></TH>\n" +        res.body << "</TR></THEAD>\n" +        res.body << "<TBODY>\n" + +        query.sub!(/\A&/, '?') +        list.unshift [ "..", File::mtime(local_path+"/.."), -1 ] +        list.each{ |name, time, size| +          if name == ".." +            dname = "Parent Directory" +          elsif namewidth and name.size > namewidth +            dname = name[0...(namewidth - 2)] << '..' +          else +            dname = name +          end +          s =  "<TR><TD class=\"name\"><A HREF=\"#{HTTPUtils::escape(name)}#{query if name.end_with?('/')}\">#{HTMLUtils::escape(dname)}</A></TD>" +          s << "<TD class=\"mtime\">" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "</TD>" +          s << "<TD class=\"size\">" << (size >= 0 ? size.to_s : "-") << "</TD></TR>\n" +          res.body << s +        } +        res.body << "</TBODY></TABLE>" +        res.body << "<HR>" + +        res.body << <<-_end_of_html_ +    <ADDRESS> +     #{HTMLUtils::escape(@config[:ServerSoftware])}<BR> +     at #{req.host}:#{req.port} +    </ADDRESS> +  </BODY> +</HTML> +        _end_of_html_ +      end + +      # :startdoc: +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpservlet/prochandler.rb b/jni/ruby/lib/webrick/httpservlet/prochandler.rb new file mode 100644 index 0000000..2f5aa66 --- /dev/null +++ b/jni/ruby/lib/webrick/httpservlet/prochandler.rb @@ -0,0 +1,46 @@ +# +# prochandler.rb -- ProcHandler Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: prochandler.rb,v 1.7 2002/09/21 12:23:42 gotoyuzo Exp $ + +require 'webrick/httpservlet/abstract.rb' + +module WEBrick +  module HTTPServlet + +    ## +    # Mounts a proc at a path that accepts a request and response. +    # +    # Instead of mounting this servlet with WEBrick::HTTPServer#mount use +    # WEBrick::HTTPServer#mount_proc: +    # +    #   server.mount_proc '/' do |req, res| +    #     res.body = 'it worked!' +    #     res.status = 200 +    #   end + +    class ProcHandler < AbstractServlet +      # :stopdoc: +      def get_instance(server, *options) +        self +      end + +      def initialize(proc) +        @proc = proc +      end + +      def do_GET(request, response) +        @proc.call(request, response) +      end + +      alias do_POST do_GET +      # :startdoc: +    end + +  end +end diff --git a/jni/ruby/lib/webrick/httpstatus.rb b/jni/ruby/lib/webrick/httpstatus.rb new file mode 100644 index 0000000..7ffda64 --- /dev/null +++ b/jni/ruby/lib/webrick/httpstatus.rb @@ -0,0 +1,196 @@ +#-- +# httpstatus.rb -- HTTPStatus Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpstatus.rb,v 1.11 2003/03/24 20:18:55 gotoyuzo Exp $ + +require 'webrick/accesslog' + +module WEBrick + +  ## +  # This module is used to manager HTTP status codes. +  # +  # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more +  # information. +  module HTTPStatus + +    ## +    # Root of the HTTP status class hierarchy +    class Status < StandardError +      def initialize(*args) # :nodoc: +        args[0] = AccessLog.escape(args[0]) unless args.empty? +        super(*args) +      end +      class << self +        attr_reader :code, :reason_phrase # :nodoc: +      end + +      # Returns the HTTP status code +      def code() self::class::code end + +      # Returns the HTTP status description +      def reason_phrase() self::class::reason_phrase end + +      alias to_i code # :nodoc: +    end + +    # Root of the HTTP info statuses +    class Info        < Status; end +    # Root of the HTTP success statuses +    class Success     < Status; end +    # Root of the HTTP redirect statuses +    class Redirect    < Status; end +    # Root of the HTTP error statuses +    class Error       < Status; end +    # Root of the HTTP client error statuses +    class ClientError < Error; end +    # Root of the HTTP server error statuses +    class ServerError < Error; end + +    class EOFError < StandardError; end + +    # HTTP status codes and descriptions +    StatusMessage = { # :nodoc: +      100 => 'Continue', +      101 => 'Switching Protocols', +      200 => 'OK', +      201 => 'Created', +      202 => 'Accepted', +      203 => 'Non-Authoritative Information', +      204 => 'No Content', +      205 => 'Reset Content', +      206 => 'Partial Content', +      207 => 'Multi-Status', +      300 => 'Multiple Choices', +      301 => 'Moved Permanently', +      302 => 'Found', +      303 => 'See Other', +      304 => 'Not Modified', +      305 => 'Use Proxy', +      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 Large', +      415 => 'Unsupported Media Type', +      416 => 'Request Range Not Satisfiable', +      417 => 'Expectation Failed', +      422 => 'Unprocessable Entity', +      423 => 'Locked', +      424 => 'Failed Dependency', +      426 => 'Upgrade Required', +      428 => 'Precondition Required', +      429 => 'Too Many Requests', +      431 => 'Request Header Fields Too Large', +      500 => 'Internal Server Error', +      501 => 'Not Implemented', +      502 => 'Bad Gateway', +      503 => 'Service Unavailable', +      504 => 'Gateway Timeout', +      505 => 'HTTP Version Not Supported', +      507 => 'Insufficient Storage', +      511 => 'Network Authentication Required', +    } + +    # Maps a status code to the corresponding Status class +    CodeToError = {} # :nodoc: + +    # Creates a status or error class for each status code and +    # populates the CodeToError map. +    StatusMessage.each{|code, message| +      message.freeze +      var_name = message.gsub(/[ \-]/,'_').upcase +      err_name = message.gsub(/[ \-]/,'') + +      case code +      when 100...200; parent = Info +      when 200...300; parent = Success +      when 300...400; parent = Redirect +      when 400...500; parent = ClientError +      when 500...600; parent = ServerError +      end + +      const_set("RC_#{var_name}", code) +      err_class = Class.new(parent) +      err_class.instance_variable_set(:@code, code) +      err_class.instance_variable_set(:@reason_phrase, message) +      const_set(err_name, err_class) +      CodeToError[code] = err_class +    } + +    ## +    # Returns the description corresponding to the HTTP status +code+ +    # +    #   WEBrick::HTTPStatus.reason_phrase 404 +    #   => "Not Found" +    def reason_phrase(code) +      StatusMessage[code.to_i] +    end + +    ## +    # Is +code+ an informational status? +    def info?(code) +      code.to_i >= 100 and code.to_i < 200 +    end + +    ## +    # Is +code+ a successful status? +    def success?(code) +      code.to_i >= 200 and code.to_i < 300 +    end + +    ## +    # Is +code+ a redirection status? +    def redirect?(code) +      code.to_i >= 300 and code.to_i < 400 +    end + +    ## +    # Is +code+ an error status? +    def error?(code) +      code.to_i >= 400 and code.to_i < 600 +    end + +    ## +    # Is +code+ a client error status? +    def client_error?(code) +      code.to_i >= 400 and code.to_i < 500 +    end + +    ## +    # Is +code+ a server error status? +    def server_error?(code) +      code.to_i >= 500 and code.to_i < 600 +    end + +    ## +    # Returns the status class corresponding to +code+ +    # +    #   WEBrick::HTTPStatus[302] +    #   => WEBrick::HTTPStatus::NotFound +    # +    def self.[](code) +      CodeToError[code] +    end + +    module_function :reason_phrase +    module_function :info?, :success?, :redirect?, :error? +    module_function :client_error?, :server_error? +  end +end diff --git a/jni/ruby/lib/webrick/httputils.rb b/jni/ruby/lib/webrick/httputils.rb new file mode 100644 index 0000000..a5f0632 --- /dev/null +++ b/jni/ruby/lib/webrick/httputils.rb @@ -0,0 +1,509 @@ +# +# httputils.rb -- HTTPUtils Module +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httputils.rb,v 1.34 2003/06/05 21:34:08 gotoyuzo Exp $ + +require 'socket' +require 'tempfile' + +module WEBrick +  CR   = "\x0d"     # :nodoc: +  LF   = "\x0a"     # :nodoc: +  CRLF = "\x0d\x0a" # :nodoc: + +  ## +  # HTTPUtils provides utility methods for working with the HTTP protocol. +  # +  # This module is generally used internally by WEBrick + +  module HTTPUtils + +    ## +    # Normalizes a request path.  Raises an exception if the path cannot be +    # normalized. + +    def normalize_path(path) +      raise "abnormal path `#{path}'" if path[0] != ?/ +      ret = path.dup + +      ret.gsub!(%r{/+}o, '/')                    # //      => / +      while ret.sub!(%r'/\.(?:/|\Z)', '/'); end  # /.      => / +      while ret.sub!(%r'/(?!\.\./)[^/]+/\.\.(?:/|\Z)', '/'); end # /foo/.. => /foo + +      raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret +      ret +    end +    module_function :normalize_path + +    ## +    # Default mime types + +    DefaultMimeTypes = { +      "ai"    => "application/postscript", +      "asc"   => "text/plain", +      "avi"   => "video/x-msvideo", +      "bin"   => "application/octet-stream", +      "bmp"   => "image/bmp", +      "class" => "application/octet-stream", +      "cer"   => "application/pkix-cert", +      "crl"   => "application/pkix-crl", +      "crt"   => "application/x-x509-ca-cert", +     #"crl"   => "application/x-pkcs7-crl", +      "css"   => "text/css", +      "dms"   => "application/octet-stream", +      "doc"   => "application/msword", +      "dvi"   => "application/x-dvi", +      "eps"   => "application/postscript", +      "etx"   => "text/x-setext", +      "exe"   => "application/octet-stream", +      "gif"   => "image/gif", +      "htm"   => "text/html", +      "html"  => "text/html", +      "jpe"   => "image/jpeg", +      "jpeg"  => "image/jpeg", +      "jpg"   => "image/jpeg", +      "js"    => "application/javascript", +      "lha"   => "application/octet-stream", +      "lzh"   => "application/octet-stream", +      "mov"   => "video/quicktime", +      "mpe"   => "video/mpeg", +      "mpeg"  => "video/mpeg", +      "mpg"   => "video/mpeg", +      "pbm"   => "image/x-portable-bitmap", +      "pdf"   => "application/pdf", +      "pgm"   => "image/x-portable-graymap", +      "png"   => "image/png", +      "pnm"   => "image/x-portable-anymap", +      "ppm"   => "image/x-portable-pixmap", +      "ppt"   => "application/vnd.ms-powerpoint", +      "ps"    => "application/postscript", +      "qt"    => "video/quicktime", +      "ras"   => "image/x-cmu-raster", +      "rb"    => "text/plain", +      "rd"    => "text/plain", +      "rtf"   => "application/rtf", +      "sgm"   => "text/sgml", +      "sgml"  => "text/sgml", +      "svg"   => "image/svg+xml", +      "tif"   => "image/tiff", +      "tiff"  => "image/tiff", +      "txt"   => "text/plain", +      "xbm"   => "image/x-xbitmap", +      "xhtml" => "text/html", +      "xls"   => "application/vnd.ms-excel", +      "xml"   => "text/xml", +      "xpm"   => "image/x-xpixmap", +      "xwd"   => "image/x-xwindowdump", +      "zip"   => "application/zip", +    } + +    ## +    # Loads Apache-compatible mime.types in +file+. + +    def load_mime_types(file) +      open(file){ |io| +        hash = Hash.new +        io.each{ |line| +          next if /^#/ =~ line +          line.chomp! +          mimetype, ext0 = line.split(/\s+/, 2) +          next unless ext0 +          next if ext0.empty? +          ext0.split(/\s+/).each{ |ext| hash[ext] = mimetype } +        } +        hash +      } +    end +    module_function :load_mime_types + +    ## +    # Returns the mime type of +filename+ from the list in +mime_tab+.  If no +    # mime type was found application/octet-stream is returned. + +    def mime_type(filename, mime_tab) +      suffix1 = (/\.(\w+)$/ =~ filename && $1.downcase) +      suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ filename && $1.downcase) +      mime_tab[suffix1] || mime_tab[suffix2] || "application/octet-stream" +    end +    module_function :mime_type + +    ## +    # Parses an HTTP header +raw+ into a hash of header fields with an Array +    # of values. + +    def parse_header(raw) +      header = Hash.new([].freeze) +      field = nil +      raw.each_line{|line| +        case line +        when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om +          field, value = $1, $2 +          field.downcase! +          header[field] = [] unless header.has_key?(field) +          header[field] << value +        when /^\s+(.*?)\s*\z/om +          value = $1 +          unless field +            raise HTTPStatus::BadRequest, "bad header '#{line}'." +          end +          header[field][-1] << " " << value +        else +          raise HTTPStatus::BadRequest, "bad header '#{line}'." +        end +      } +      header.each{|key, values| +        values.each{|value| +          value.strip! +          value.gsub!(/\s+/, " ") +        } +      } +      header +    end +    module_function :parse_header + +    ## +    # Splits a header value +str+ according to HTTP specification. + +    def split_header_value(str) +      str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) +                    (?:,\s*|\Z)'xn).flatten +    end +    module_function :split_header_value + +    ## +    # Parses a Range header value +ranges_specifier+ + +    def parse_range_header(ranges_specifier) +      if /^bytes=(.*)/ =~ ranges_specifier +        byte_range_set = split_header_value($1) +        byte_range_set.collect{|range_spec| +          case range_spec +          when /^(\d+)-(\d+)/ then $1.to_i .. $2.to_i +          when /^(\d+)-/      then $1.to_i .. -1 +          when /^-(\d+)/      then -($1.to_i) .. -1 +          else return nil +          end +        } +      end +    end +    module_function :parse_range_header + +    ## +    # Parses q values in +value+ as used in Accept headers. + +    def parse_qvalues(value) +      tmp = [] +      if value +        parts = value.split(/,\s*/) +        parts.each {|part| +          if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) +            val = m[1] +            q = (m[2] or 1).to_f +            tmp.push([val, q]) +          end +        } +        tmp = tmp.sort_by{|val, q| -q} +        tmp.collect!{|val, q| val} +      end +      return tmp +    end +    module_function :parse_qvalues + +    ## +    # Removes quotes and escapes from +str+ + +    def dequote(str) +      ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup +      ret.gsub!(/\\(.)/, "\\1") +      ret +    end +    module_function :dequote + +    ## +    # Quotes and escapes quotes in +str+ + +    def quote(str) +      '"' << str.gsub(/[\\\"]/o, "\\\1") << '"' +    end +    module_function :quote + +    ## +    # Stores multipart form data.  FormData objects are created when +    # WEBrick::HTTPUtils.parse_form_data is called. + +    class FormData < String +      EmptyRawHeader = [].freeze # :nodoc: +      EmptyHeader = {}.freeze # :nodoc: + +      ## +      # The name of the form data part + +      attr_accessor :name + +      ## +      # The filename of the form data part + +      attr_accessor :filename + +      attr_accessor :next_data # :nodoc: +      protected :next_data + +      ## +      # Creates a new FormData object. +      # +      # +args+ is an Array of form data entries.  One FormData will be created +      # for each entry. +      # +      # This is called by WEBrick::HTTPUtils.parse_form_data for you + +      def initialize(*args) +        @name = @filename = @next_data = nil +        if args.empty? +          @raw_header = [] +          @header = nil +          super("") +        else +          @raw_header = EmptyRawHeader +          @header = EmptyHeader +          super(args.shift) +          unless args.empty? +            @next_data = self.class.new(*args) +          end +        end +      end + +      ## +      # Retrieves the header at the first entry in +key+ + +      def [](*key) +        begin +          @header[key[0].downcase].join(", ") +        rescue StandardError, NameError +          super +        end +      end + +      ## +      # Adds +str+ to this FormData which may be the body, a header or a +      # header entry. +      # +      # This is called by WEBrick::HTTPUtils.parse_form_data for you + +      def <<(str) +        if @header +          super +        elsif str == CRLF +          @header = HTTPUtils::parse_header(@raw_header.join) +          if cd = self['content-disposition'] +            if /\s+name="(.*?)"/ =~ cd then @name = $1 end +            if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end +          end +        else +          @raw_header << str +        end +        self +      end + +      ## +      # Adds +data+ at the end of the chain of entries +      # +      # This is called by WEBrick::HTTPUtils.parse_form_data for you. + +      def append_data(data) +        tmp = self +        while tmp +          unless tmp.next_data +            tmp.next_data = data +            break +          end +          tmp = tmp.next_data +        end +        self +      end + +      ## +      # Yields each entry in this FormData + +      def each_data +        tmp = self +        while tmp +          next_data = tmp.next_data +          yield(tmp) +          tmp = next_data +        end +      end + +      ## +      # Returns all the FormData as an Array + +      def list +        ret = [] +        each_data{|data| +          ret << data.to_s +        } +        ret +      end + +      ## +      # A FormData will behave like an Array + +      alias :to_ary :list + +      ## +      # This FormData's body + +      def to_s +        String.new(self) +      end +    end + +    ## +    # Parses the query component of a URI in +str+ + +    def parse_query(str) +      query = Hash.new +      if str +        str.split(/[&;]/).each{|x| +          next if x.empty? +          key, val = x.split(/=/,2) +          key = unescape_form(key) +          val = unescape_form(val.to_s) +          val = FormData.new(val) +          val.name = key +          if query.has_key?(key) +            query[key].append_data(val) +            next +          end +          query[key] = val +        } +      end +      query +    end +    module_function :parse_query + +    ## +    # Parses form data in +io+ with the given +boundary+ + +    def parse_form_data(io, boundary) +      boundary_regexp = /\A--#{Regexp.quote(boundary)}(--)?#{CRLF}\z/ +      form_data = Hash.new +      return form_data unless io +      data = nil +      io.each_line{|line| +        if boundary_regexp =~ line +          if data +            data.chop! +            key = data.name +            if form_data.has_key?(key) +              form_data[key].append_data(data) +            else +              form_data[key] = data +            end +          end +          data = FormData.new +          next +        else +          if data +            data << line +          end +        end +      } +      return form_data +    end +    module_function :parse_form_data + +    ##### + +    reserved = ';/?:@&=+$,' +    num      = '0123456789' +    lowalpha = 'abcdefghijklmnopqrstuvwxyz' +    upalpha  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +    mark     = '-_.!~*\'()' +    unreserved = num + lowalpha + upalpha + mark +    control  = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f" +    space    = " " +    delims   = '<>#%"' +    unwise   = '{}|\\^[]`' +    nonascii = (0x80..0xff).collect{|c| c.chr }.join + +    module_function + +    # :stopdoc: + +    def _make_regex(str) /([#{Regexp.escape(str)}])/n end +    def _make_regex!(str) /([^#{Regexp.escape(str)}])/n end +    def _escape(str, regex) +      str = str.b +      str.gsub!(regex) {"%%%02X" % $1.ord} +      # %-escaped string should contain US-ASCII only +      str.force_encoding(Encoding::US_ASCII) +    end +    def _unescape(str, regex) +      str = str.b +      str.gsub!(regex) {$1.hex.chr} +      # encoding of %-unescaped string is unknown +      str +    end + +    UNESCAPED = _make_regex(control+space+delims+unwise+nonascii) +    UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii) +    NONASCII  = _make_regex(nonascii) +    ESCAPED   = /%([0-9a-fA-F]{2})/ +    UNESCAPED_PCHAR = _make_regex!(unreserved+":@&=+$,") + +    # :startdoc: + +    ## +    # Escapes HTTP reserved and unwise characters in +str+ + +    def escape(str) +      _escape(str, UNESCAPED) +    end + +    ## +    # Unescapes HTTP reserved and unwise characters in +str+ + +    def unescape(str) +      _unescape(str, ESCAPED) +    end + +    ## +    # Escapes form reserved characters in +str+ + +    def escape_form(str) +      ret = _escape(str, UNESCAPED_FORM) +      ret.gsub!(/ /, "+") +      ret +    end + +    ## +    # Unescapes form reserved characters in +str+ + +    def unescape_form(str) +      _unescape(str.gsub(/\+/, " "), ESCAPED) +    end + +    ## +    # Escapes path +str+ + +    def escape_path(str) +      result = "" +      str.scan(%r{/([^/]*)}).each{|i| +        result << "/" << _escape(i[0], UNESCAPED_PCHAR) +      } +      return result +    end + +    ## +    # Escapes 8 bit characters in +str+ + +    def escape8bit(str) +      _escape(str, NONASCII) +    end +  end +end diff --git a/jni/ruby/lib/webrick/httpversion.rb b/jni/ruby/lib/webrick/httpversion.rb new file mode 100644 index 0000000..cdfb957 --- /dev/null +++ b/jni/ruby/lib/webrick/httpversion.rb @@ -0,0 +1,75 @@ +#-- +# HTTPVersion.rb -- presentation of HTTP version +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: httpversion.rb,v 1.5 2002/09/21 12:23:37 gotoyuzo Exp $ + +module WEBrick + +  ## +  # Represents an HTTP protocol version + +  class HTTPVersion +    include Comparable + +    ## +    # The major protocol version number + +    attr_accessor :major + +    ## +    # The minor protocol version number + +    attr_accessor :minor + +    ## +    # Converts +version+ into an HTTPVersion + +    def self.convert(version) +      version.is_a?(self) ? version : new(version) +    end + +    ## +    # Creates a new HTTPVersion from +version+. + +    def initialize(version) +      case version +      when HTTPVersion +        @major, @minor = version.major, version.minor +      when String +        if /^(\d+)\.(\d+)$/ =~ version +          @major, @minor = $1.to_i, $2.to_i +        end +      end +      if @major.nil? || @minor.nil? +        raise ArgumentError, +          format("cannot convert %s into %s", version.class, self.class) +      end +    end + +    ## +    # Compares this version with +other+ according to the HTTP specification +    # rules. + +    def <=>(other) +      unless other.is_a?(self.class) +        other = self.class.new(other) +      end +      if (ret = @major <=> other.major) == 0 +        return @minor <=> other.minor +      end +      return ret +    end + +    ## +    # The HTTP version as show in the HTTP request and response.  For example, +    # "1.1" + +    def to_s +      format("%d.%d", @major, @minor) +    end +  end +end diff --git a/jni/ruby/lib/webrick/log.rb b/jni/ruby/lib/webrick/log.rb new file mode 100644 index 0000000..41cde4a --- /dev/null +++ b/jni/ruby/lib/webrick/log.rb @@ -0,0 +1,155 @@ +#-- +# log.rb -- Log Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: log.rb,v 1.26 2002/10/06 17:06:10 gotoyuzo Exp $ + +module WEBrick + +  ## +  # A generic logging class + +  class BasicLog + +    # Fatal log level which indicates a server crash + +    FATAL = 1 + +    # Error log level which indicates a recoverable error + +    ERROR = 2 + +    # Warning log level which indicates a possible problem + +    WARN  = 3 + +    # Information log level which indicates possibly useful information + +    INFO  = 4 + +    # Debugging error level for messages used in server development or +    # debugging + +    DEBUG = 5 + +    # log-level, messages above this level will be logged +    attr_accessor :level + +    ## +    # Initializes a new logger for +log_file+ that outputs messages at +level+ +    # or higher.  +log_file+ can be a filename, an IO-like object that +    # responds to #<< or nil which outputs to $stderr. +    # +    # If no level is given INFO is chosen by default + +    def initialize(log_file=nil, level=nil) +      @level = level || INFO +      case log_file +      when String +        @log = open(log_file, "a+") +        @log.sync = true +        @opened = true +      when NilClass +        @log = $stderr +      else +        @log = log_file  # requires "<<". (see BasicLog#log) +      end +    end + +    ## +    # Closes the logger (also closes the log device associated to the logger) +    def close +      @log.close if @opened +      @log = nil +    end + +    ## +    # Logs +data+ at +level+ if the given level is above the current log +    # level. + +    def log(level, data) +      if @log && level <= @level +        data += "\n" if /\n\Z/ !~ data +        @log << data +      end +    end + +    ## +    # Synonym for log(INFO, obj.to_s) +    def <<(obj) +      log(INFO, obj.to_s) +    end + +    # Shortcut for logging a FATAL message +    def fatal(msg) log(FATAL, "FATAL " << format(msg)); end +    # Shortcut for logging an ERROR message +    def error(msg) log(ERROR, "ERROR " << format(msg)); end +    # Shortcut for logging a WARN message +    def warn(msg)  log(WARN,  "WARN  " << format(msg)); end +    # Shortcut for logging an INFO message +    def info(msg)  log(INFO,  "INFO  " << format(msg)); end +    # Shortcut for logging a DEBUG message +    def debug(msg) log(DEBUG, "DEBUG " << format(msg)); end + +    # Will the logger output FATAL messages? +    def fatal?; @level >= FATAL; end +    # Will the logger output ERROR messages? +    def error?; @level >= ERROR; end +    # Will the logger output WARN messages? +    def warn?;  @level >= WARN; end +    # Will the logger output INFO messages? +    def info?;  @level >= INFO; end +    # Will the logger output DEBUG messages? +    def debug?; @level >= DEBUG; end + +    private + +    ## +    # Formats +arg+ for the logger +    # +    # * If +arg+ is an Exception, it will format the error message and +    #   the back trace. +    # * If +arg+ responds to #to_str, it will return it. +    # * Otherwise it will return +arg+.inspect. +    def format(arg) +      if arg.is_a?(Exception) +        "#{arg.class}: #{arg.message}\n\t" << +        arg.backtrace.join("\n\t") << "\n" +      elsif arg.respond_to?(:to_str) +        arg.to_str +      else +        arg.inspect +      end +    end +  end + +  ## +  # A logging class that prepends a timestamp to each message. + +  class Log < BasicLog +    # Format of the timestamp which is applied to each logged line.  The +    # default is <tt>"[%Y-%m-%d %H:%M:%S]"</tt> +    attr_accessor :time_format + +    ## +    # Same as BasicLog#initialize +    # +    # You can set the timestamp format through #time_format +    def initialize(log_file=nil, level=nil) +      super(log_file, level) +      @time_format = "[%Y-%m-%d %H:%M:%S]" +    end + +    ## +    # Same as BasicLog#log +    def log(level, data) +      tmp = Time.now.strftime(@time_format) +      tmp << " " << data +      super(level, tmp) +    end +  end +end diff --git a/jni/ruby/lib/webrick/server.rb b/jni/ruby/lib/webrick/server.rb new file mode 100644 index 0000000..4aafd1e --- /dev/null +++ b/jni/ruby/lib/webrick/server.rb @@ -0,0 +1,365 @@ +# +# server.rb -- GenericServer Class +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: server.rb,v 1.62 2003/07/22 19:20:43 gotoyuzo Exp $ + +require 'thread' +require 'socket' +require 'webrick/config' +require 'webrick/log' + +module WEBrick + +  ## +  # Server error exception + +  class ServerError < StandardError; end + +  ## +  # Base server class + +  class SimpleServer + +    ## +    # A SimpleServer only yields when you start it + +    def SimpleServer.start +      yield +    end +  end + +  ## +  # A generic module for daemonizing a process + +  class Daemon + +    ## +    # Performs the standard operations for daemonizing a process.  Runs a +    # block, if given. + +    def Daemon.start +      exit!(0) if fork +      Process::setsid +      exit!(0) if fork +      Dir::chdir("/") +      File::umask(0) +      STDIN.reopen("/dev/null") +      STDOUT.reopen("/dev/null", "w") +      STDERR.reopen("/dev/null", "w") +      yield if block_given? +    end +  end + +  ## +  # Base TCP server class.  You must subclass GenericServer and provide a #run +  # method. + +  class GenericServer + +    ## +    # The server status.  One of :Stop, :Running or :Shutdown + +    attr_reader :status + +    ## +    # The server configuration + +    attr_reader :config + +    ## +    # The server logger.  This is independent from the HTTP access log. + +    attr_reader :logger + +    ## +    # Tokens control the number of outstanding clients.  The +    # <code>:MaxClients</code> configuration sets this. + +    attr_reader :tokens + +    ## +    # Sockets listening for connections. + +    attr_reader :listeners + +    ## +    # Creates a new generic server from +config+.  The default configuration +    # comes from +default+. + +    def initialize(config={}, default=Config::General) +      @config = default.dup.update(config) +      @status = :Stop +      @config[:Logger] ||= Log::new +      @logger = @config[:Logger] + +      @tokens = SizedQueue.new(@config[:MaxClients]) +      @config[:MaxClients].times{ @tokens.push(nil) } + +      webrickv = WEBrick::VERSION +      rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" +      @logger.info("WEBrick #{webrickv}") +      @logger.info("ruby #{rubyv}") + +      @listeners = [] +      @shutdown_pipe = nil +      unless @config[:DoNotListen] +        if @config[:Listen] +          warn(":Listen option is deprecated; use GenericServer#listen") +        end +        listen(@config[:BindAddress], @config[:Port]) +        if @config[:Port] == 0 +          @config[:Port] = @listeners[0].addr[1] +        end +      end +    end + +    ## +    # Retrieves +key+ from the configuration + +    def [](key) +      @config[key] +    end + +    ## +    # Adds listeners from +address+ and +port+ to the server.  See +    # WEBrick::Utils::create_listeners for details. + +    def listen(address, port) +      @listeners += Utils::create_listeners(address, port, @logger) +      setup_shutdown_pipe +    end + +    ## +    # Starts the server and runs the +block+ for each connection.  This method +    # does not return until the server is stopped from a signal handler or +    # another thread using #stop or #shutdown. +    # +    # If the block raises a subclass of StandardError the exception is logged +    # and ignored.  If an IOError or Errno::EBADF exception is raised the +    # exception is ignored.  If an Exception subclass is raised the exception +    # is logged and re-raised which stops the server. +    # +    # To completely shut down a server call #shutdown from ensure: +    # +    #   server = WEBrick::GenericServer.new +    #   # or WEBrick::HTTPServer.new +    # +    #   begin +    #     server.start +    #   ensure +    #     server.shutdown +    #   end + +    def start(&block) +      raise ServerError, "already started." if @status != :Stop +      server_type = @config[:ServerType] || SimpleServer + +      server_type.start{ +        @logger.info \ +          "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}" +        call_callback(:StartCallback) + +        shutdown_pipe = @shutdown_pipe + +        thgroup = ThreadGroup.new +        @status = :Running +        begin +          while @status == :Running +            begin +              if svrs = IO.select([shutdown_pipe[0], *@listeners], nil, nil, 2.0) +                if svrs[0].include? shutdown_pipe[0] +                  break +                end +                svrs[0].each{|svr| +                  @tokens.pop          # blocks while no token is there. +                  if sock = accept_client(svr) +                    sock.do_not_reverse_lookup = config[:DoNotReverseLookup] +                    th = start_thread(sock, &block) +                    th[:WEBrickThread] = true +                    thgroup.add(th) +                  else +                    @tokens.push(nil) +                  end +                } +              end +            rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex +              # if the listening socket was closed in GenericServer#shutdown, +              # IO::select raise it. +            rescue StandardError => ex +              msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" +              @logger.error msg +            rescue Exception => ex +              @logger.fatal ex +              raise +            end +          end +        ensure +          cleanup_shutdown_pipe(shutdown_pipe) +          cleanup_listener +          @status = :Shutdown +          @logger.info "going to shutdown ..." +          thgroup.list.each{|th| th.join if th[:WEBrickThread] } +          call_callback(:StopCallback) +          @logger.info "#{self.class}#start done." +          @status = :Stop +        end +      } +    end + +    ## +    # Stops the server from accepting new connections. + +    def stop +      if @status == :Running +        @status = :Shutdown +      end +    end + +    ## +    # Shuts down the server and all listening sockets.  New listeners must be +    # provided to restart the server. + +    def shutdown +      stop + +      shutdown_pipe = @shutdown_pipe # another thread may modify @shutdown_pipe. +      if shutdown_pipe +        if !shutdown_pipe[1].closed? +          begin +            shutdown_pipe[1].close +          rescue IOError # closed by another thread. +          end +        end +      end +    end + +    ## +    # You must subclass GenericServer and implement \#run which accepts a TCP +    # client socket + +    def run(sock) +      @logger.fatal "run() must be provided by user." +    end + +    private + +    # :stopdoc: + +    ## +    # Accepts a TCP client socket from the TCP server socket +svr+ and returns +    # the client socket. + +    def accept_client(svr) +      sock = nil +      begin +        sock = svr.accept +        sock.sync = true +        Utils::set_non_blocking(sock) +        Utils::set_close_on_exec(sock) +      rescue Errno::ECONNRESET, Errno::ECONNABORTED, +             Errno::EPROTO, Errno::EINVAL +      rescue StandardError => ex +        msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" +        @logger.error msg +      end +      return sock +    end + +    ## +    # Starts a server thread for the client socket +sock+ that runs the given +    # +block+. +    # +    # Sets the socket to the <code>:WEBrickSocket</code> thread local variable +    # in the thread. +    # +    # If any errors occur in the block they are logged and handled. + +    def start_thread(sock, &block) +      Thread.start{ +        begin +          Thread.current[:WEBrickSocket] = sock +          begin +            addr = sock.peeraddr +            @logger.debug "accept: #{addr[3]}:#{addr[1]}" +          rescue SocketError +            @logger.debug "accept: <address unknown>" +            raise +          end +          call_callback(:AcceptCallback, sock) +          block ? block.call(sock) : run(sock) +        rescue Errno::ENOTCONN +          @logger.debug "Errno::ENOTCONN raised" +        rescue ServerError => ex +          msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" +          @logger.error msg +        rescue Exception => ex +          @logger.error ex +        ensure +          @tokens.push(nil) +          Thread.current[:WEBrickSocket] = nil +          if addr +            @logger.debug "close: #{addr[3]}:#{addr[1]}" +          else +            @logger.debug "close: <address unknown>" +          end +          sock.close unless sock.closed? +        end +      } +    end + +    ## +    # Calls the callback +callback_name+ from the configuration with +args+ + +    def call_callback(callback_name, *args) +      if cb = @config[callback_name] +        cb.call(*args) +      end +    end + +    def setup_shutdown_pipe +      if !@shutdown_pipe +        @shutdown_pipe = IO.pipe +      end +      @shutdown_pipe +    end + +    def cleanup_shutdown_pipe(shutdown_pipe) +      @shutdown_pipe = nil +      shutdown_pipe.each {|io| +        if !io.closed? +          begin +            io.close +          rescue IOError # another thread closed io. +          end +        end +      } +    end + +    def cleanup_listener +      @listeners.each{|s| +        if @logger.debug? +          addr = s.addr +          @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})") +        end +        begin +          s.shutdown +        rescue Errno::ENOTCONN +          # when `Errno::ENOTCONN: Socket is not connected' on some platforms, +          # call #close instead of #shutdown. +          # (ignore @config[:ShutdownSocketWithoutClose]) +          s.close +        else +          unless @config[:ShutdownSocketWithoutClose] +            s.close +          end +        end +      } +      @listeners.clear +    end +  end    # end of GenericServer +end diff --git a/jni/ruby/lib/webrick/ssl.rb b/jni/ruby/lib/webrick/ssl.rb new file mode 100644 index 0000000..f6433fb --- /dev/null +++ b/jni/ruby/lib/webrick/ssl.rb @@ -0,0 +1,196 @@ +# +# ssl.rb -- SSL/TLS enhancement for GenericServer +# +# Copyright (c) 2003 GOTOU Yuuzou All rights reserved. +# +# $Id: ssl.rb 48362 2014-11-10 11:05:00Z akr $ + +require 'webrick' +require 'openssl' + +module WEBrick +  module Config +    svrsoft = General[:ServerSoftware] +    osslv = ::OpenSSL::OPENSSL_VERSION.split[1] + +    ## +    # Default SSL server configuration. +    # +    # WEBrick can automatically create a self-signed certificate if +    # <code>:SSLCertName</code> is set.  For more information on the various +    # SSL options see OpenSSL::SSL::SSLContext. +    # +    # :ServerSoftware       :: +    #   The server software name used in the Server: header. +    # :SSLEnable            :: false, +    #   Enable SSL for this server.  Defaults to false. +    # :SSLCertificate       :: +    #   The SSL certificate for the server. +    # :SSLPrivateKey        :: +    #   The SSL private key for the server certificate. +    # :SSLClientCA          :: nil, +    #   Array of certificates that will be sent to the client. +    # :SSLExtraChainCert    :: nil, +    #   Array of certificates that will be added to the certificate chain +    # :SSLCACertificateFile :: nil, +    #   Path to a CA certificate file +    # :SSLCACertificatePath :: nil, +    #   Path to a directory containing CA certificates +    # :SSLCertificateStore  :: nil, +    #   OpenSSL::X509::Store used for certificate validation of the client +    # :SSLTmpDhCallback     :: nil, +    #   Callback invoked when DH parameters are required. +    # :SSLVerifyClient      :: +    #   Sets whether the client is verified.  This defaults to VERIFY_NONE +    #   which is typical for an HTTPS server. +    # :SSLVerifyDepth       :: +    #   Number of CA certificates to walk when verifying a certificate chain +    # :SSLVerifyCallback    :: +    #   Custom certificate verification callback +    # :SSLTimeout           :: +    #   Maximum session lifetime +    # :SSLOptions           :: +    #   Various SSL options +    # :SSLStartImmediately  :: +    #   Immediately start SSL upon connection?  Defaults to true +    # :SSLCertName          :: +    #   SSL certificate name.  Must be set to enable automatic certificate +    #   creation. +    # :SSLCertComment       :: +    #   Comment used during automatic certificate creation. + +    SSL = { +      :ServerSoftware       => "#{svrsoft} OpenSSL/#{osslv}", +      :SSLEnable            => false, +      :SSLCertificate       => nil, +      :SSLPrivateKey        => nil, +      :SSLClientCA          => nil, +      :SSLExtraChainCert    => nil, +      :SSLCACertificateFile => nil, +      :SSLCACertificatePath => nil, +      :SSLCertificateStore  => nil, +      :SSLTmpDhCallback     => nil, +      :SSLVerifyClient      => ::OpenSSL::SSL::VERIFY_NONE, +      :SSLVerifyDepth       => nil, +      :SSLVerifyCallback    => nil,   # custom verification +      :SSLTimeout           => nil, +      :SSLOptions           => nil, +      :SSLStartImmediately  => true, +      # Must specify if you use auto generated certificate. +      :SSLCertName          => nil, +      :SSLCertComment       => "Generated by Ruby/OpenSSL" +    } +    General.update(SSL) +  end + +  module Utils +    ## +    # Creates a self-signed certificate with the given number of +bits+, +    # the issuer +cn+ and a +comment+ to be stored in the certificate. + +    def create_self_signed_cert(bits, cn, comment) +      rsa = OpenSSL::PKey::RSA.new(bits){|p, n| +        case p +        when 0; $stderr.putc "."  # BN_generate_prime +        when 1; $stderr.putc "+"  # BN_generate_prime +        when 2; $stderr.putc "*"  # searching good prime, +                                  # n = #of try, +                                  # but also data from BN_generate_prime +        when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q, +                                  # but also data from BN_generate_prime +        else;   $stderr.putc "*"  # BN_generate_prime +        end +      } +      cert = OpenSSL::X509::Certificate.new +      cert.version = 2 +      cert.serial = 1 +      name = OpenSSL::X509::Name.new(cn) +      cert.subject = name +      cert.issuer = name +      cert.not_before = Time.now +      cert.not_after = Time.now + (365*24*60*60) +      cert.public_key = rsa.public_key + +      ef = OpenSSL::X509::ExtensionFactory.new(nil,cert) +      ef.issuer_certificate = cert +      cert.extensions = [ +        ef.create_extension("basicConstraints","CA:FALSE"), +        ef.create_extension("keyUsage", "keyEncipherment"), +        ef.create_extension("subjectKeyIdentifier", "hash"), +        ef.create_extension("extendedKeyUsage", "serverAuth"), +        ef.create_extension("nsComment", comment), +      ] +      aki = ef.create_extension("authorityKeyIdentifier", +                                "keyid:always,issuer:always") +      cert.add_extension(aki) +      cert.sign(rsa, OpenSSL::Digest::SHA1.new) + +      return [ cert, rsa ] +    end +    module_function :create_self_signed_cert +  end + +  ## +  #-- +  # Updates WEBrick::GenericServer with SSL functionality + +  class GenericServer + +    ## +    # SSL context for the server when run in SSL mode + +    def ssl_context # :nodoc: +      @ssl_context ||= nil +    end + +    undef listen + +    ## +    # Updates +listen+ to enable SSL when the SSL configuration is active. + +    def listen(address, port) # :nodoc: +      listeners = Utils::create_listeners(address, port, @logger) +      if @config[:SSLEnable] +        unless ssl_context +          @ssl_context = setup_ssl_context(@config) +          @logger.info("\n" + @config[:SSLCertificate].to_text) +        end +        listeners.collect!{|svr| +          ssvr = ::OpenSSL::SSL::SSLServer.new(svr, ssl_context) +          ssvr.start_immediately = @config[:SSLStartImmediately] +          ssvr +        } +      end +      @listeners += listeners +      setup_shutdown_pipe +    end + +    ## +    # Sets up an SSL context for +config+ + +    def setup_ssl_context(config) # :nodoc: +      unless config[:SSLCertificate] +        cn = config[:SSLCertName] +        comment = config[:SSLCertComment] +        cert, key = Utils::create_self_signed_cert(1024, cn, comment) +        config[:SSLCertificate] = cert +        config[:SSLPrivateKey] = key +      end +      ctx = OpenSSL::SSL::SSLContext.new +      ctx.key = config[:SSLPrivateKey] +      ctx.cert = config[:SSLCertificate] +      ctx.client_ca = config[:SSLClientCA] +      ctx.extra_chain_cert = config[:SSLExtraChainCert] +      ctx.ca_file = config[:SSLCACertificateFile] +      ctx.ca_path = config[:SSLCACertificatePath] +      ctx.cert_store = config[:SSLCertificateStore] +      ctx.tmp_dh_callback = config[:SSLTmpDhCallback] +      ctx.verify_mode = config[:SSLVerifyClient] +      ctx.verify_depth = config[:SSLVerifyDepth] +      ctx.verify_callback = config[:SSLVerifyCallback] +      ctx.timeout = config[:SSLTimeout] +      ctx.options = config[:SSLOptions] +      ctx +    end +  end +end diff --git a/jni/ruby/lib/webrick/utils.rb b/jni/ruby/lib/webrick/utils.rb new file mode 100644 index 0000000..606ede5 --- /dev/null +++ b/jni/ruby/lib/webrick/utils.rb @@ -0,0 +1,230 @@ +# +# utils.rb -- Miscellaneous utilities +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou +# Copyright (c) 2002 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: utils.rb,v 1.10 2003/02/16 22:22:54 gotoyuzo Exp $ + +require 'socket' +require 'fcntl' +require 'etc' + +module WEBrick +  module Utils +    ## +    # Sets IO operations on +io+ to be non-blocking +    def set_non_blocking(io) +      flag = File::NONBLOCK +      if defined?(Fcntl::F_GETFL) +        flag |= io.fcntl(Fcntl::F_GETFL) +      end +      io.fcntl(Fcntl::F_SETFL, flag) +    end +    module_function :set_non_blocking + +    ## +    # Sets the close on exec flag for +io+ +    def set_close_on_exec(io) +      if defined?(Fcntl::FD_CLOEXEC) +        io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) +      end +    end +    module_function :set_close_on_exec + +    ## +    # Changes the process's uid and gid to the ones of +user+ +    def su(user) +      if pw = Etc.getpwnam(user) +        Process::initgroups(user, pw.gid) +        Process::Sys::setgid(pw.gid) +        Process::Sys::setuid(pw.uid) +      else +        warn("WEBrick::Utils::su doesn't work on this platform") +      end +    end +    module_function :su + +    ## +    # The server hostname +    def getservername +      host = Socket::gethostname +      begin +        Socket::gethostbyname(host)[0] +      rescue +        host +      end +    end +    module_function :getservername + +    ## +    # Creates TCP server sockets bound to +address+:+port+ and returns them. +    # +    # It will create IPV4 and IPV6 sockets on all interfaces. +    def create_listeners(address, port, logger=nil) +      unless port +        raise ArgumentError, "must specify port" +      end +      sockets = Socket.tcp_server_sockets(address, port) +      sockets = sockets.map {|s| +        s.autoclose = false +        ts = TCPServer.for_fd(s.fileno) +        s.close +        ts +      } +      return sockets +    end +    module_function :create_listeners + +    ## +    # Characters used to generate random strings +    RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +                 "0123456789" + +                 "abcdefghijklmnopqrstuvwxyz" + +    ## +    # Generates a random string of length +len+ +    def random_string(len) +      rand_max = RAND_CHARS.bytesize +      ret = "" +      len.times{ ret << RAND_CHARS[rand(rand_max)] } +      ret +    end +    module_function :random_string + +    ########### + +    require "thread" +    require "timeout" +    require "singleton" + +    ## +    # Class used to manage timeout handlers across multiple threads. +    # +    # Timeout handlers should be managed by using the class methods which are +    # synchronized. +    # +    #   id = TimeoutHandler.register(10, Timeout::Error) +    #   begin +    #     sleep 20 +    #     puts 'foo' +    #   ensure +    #     TimeoutHandler.cancel(id) +    #   end +    # +    # will raise Timeout::Error +    # +    #   id = TimeoutHandler.register(10, Timeout::Error) +    #   begin +    #     sleep 5 +    #     puts 'foo' +    #   ensure +    #     TimeoutHandler.cancel(id) +    #   end +    # +    # will print 'foo' +    # +    class TimeoutHandler +      include Singleton + +      class Thread < ::Thread; end + +      ## +      # Mutex used to synchronize access across threads +      TimeoutMutex = Mutex.new # :nodoc: + +      ## +      # Registers a new timeout handler +      # +      # +time+:: Timeout in seconds +      # +exception+:: Exception to raise when timeout elapsed +      def TimeoutHandler.register(seconds, exception) +        TimeoutMutex.synchronize{ +          instance.register(Thread.current, Time.now + seconds, exception) +        } +      end + +      ## +      # Cancels the timeout handler +id+ +      def TimeoutHandler.cancel(id) +        TimeoutMutex.synchronize{ +          instance.cancel(Thread.current, id) +        } +      end + +      ## +      # Creates a new TimeoutHandler.  You should use ::register and ::cancel +      # instead of creating the timeout handler directly. +      def initialize +        @timeout_info = Hash.new +        Thread.start{ +          while true +            now = Time.now +            @timeout_info.keys.each{|thread| +              ary = @timeout_info[thread] +              next unless ary +              ary.dup.each{|info| +                time, exception = *info +                interrupt(thread, info.object_id, exception) if time < now +              } +            } +            sleep 0.5 +          end +        } +      end + +      ## +      # Interrupts the timeout handler +id+ and raises +exception+ +      def interrupt(thread, id, exception) +        TimeoutMutex.synchronize{ +          if cancel(thread, id) && thread.alive? +            thread.raise(exception, "execution timeout") +          end +        } +      end + +      ## +      # Registers a new timeout handler +      # +      # +time+:: Timeout in seconds +      # +exception+:: Exception to raise when timeout elapsed +      def register(thread, time, exception) +        @timeout_info[thread] ||= Array.new +        @timeout_info[thread] << [time, exception] +        return @timeout_info[thread].last.object_id +      end + +      ## +      # Cancels the timeout handler +id+ +      def cancel(thread, id) +        if ary = @timeout_info[thread] +          ary.delete_if{|info| info.object_id == id } +          if ary.empty? +            @timeout_info.delete(thread) +          end +          return true +        end +        return false +      end +    end + +    ## +    # Executes the passed block and raises +exception+ if execution takes more +    # than +seconds+. +    # +    # If +seconds+ is zero or nil, simply executes the block +    def timeout(seconds, exception=Timeout::Error) +      return yield if seconds.nil? or seconds.zero? +      # raise ThreadError, "timeout within critical session" if Thread.critical +      id = TimeoutHandler.register(seconds, exception) +      begin +        yield(seconds) +      ensure +        TimeoutHandler.cancel(id) +      end +    end +    module_function :timeout +  end +end diff --git a/jni/ruby/lib/webrick/version.rb b/jni/ruby/lib/webrick/version.rb new file mode 100644 index 0000000..48bdc6d --- /dev/null +++ b/jni/ruby/lib/webrick/version.rb @@ -0,0 +1,17 @@ +#-- +# version.rb -- version and release date +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: version.rb,v 1.74 2003/07/22 19:20:43 gotoyuzo Exp $ + +module WEBrick + +  ## +  # The WEBrick version + +  VERSION      = "1.3.1" +end | 
