webrick源码分析——http请求

http服务器的主要工作就是解析http请求,然后返回http应答。http请求从socket读入,就是一段特定格式的字符串,下面是访问huangzhimn.com首页的http请求

GET / HTTP/1.1\r\n
Host: www.huangzhimin.com\r\n
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-us,en;q=0.5\r\n
Accept-Encoding: gzip,deflate\r\n
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n
Keep-Alive: 300\r\n
Connection: keep-alive\r\n
\r\n

那么webrick是解析这段字符串的呢?之前在分析webrick主要流程的时候讲到,在http服务器从socket读取到数据时,立刻交给WEBrick::HTTPRequest类来解析,解析方法如下

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
    @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

第3-8行,读取对方和自己的地址信息(host, port, id)

第10行,解析http请求的第一行数据,内容为“GET / HTTP/1.1\r\n”,具体解析方法如下

def read_request_line(socket)
  @request_line = read_line(socket) if socket
  @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

读取http请求的第一行,读取之后通过正则匹配获取@request_method为'GET',@unparsed_url为'/',@http_version为1.1

第11-20行,当http版本为1.0或1.1时,对http头部进行处理

首先,读取http头,读取方法如下:

def read_header(socket)
  if socket
    while line = read_line(socket)
      break if /\A(#{CRLF}|#{LF})\z/om =~ line
      @raw_header  line
    end
  end
  begin
    @header = HTTPUtils::parse_header(@raw_header)
  rescue = ex
    raise  HTTPStatus::BadRequest, ex.message
  end
end

从socket一行一行地读取数据,直到一行为\r\n,并通过HTTPUTils::parse_header方法将字符串数组@raw_header转换为散列@header

接着,读取cookies,将cookie字符串解析为Cookie对象

然后是读取accept, accept-charset, accept-encoding, accept-language值,这些值都是多选的,比如Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8\r\n,所以通过HTTPUtils::parse_qvalues解析出来的结果是一个数组,而且按照q值来排序

第21行,当request_method为CONNECT时(用于http代理,http1.1协议新增的),不再继续

第25行,将字符串@unparsed_uri转换成正规的URI实例@parsed_uri

第26-30行,通过@parsed_uri获取@path, @host, @port和@query_string

最后,第37-45行,设置keep-alive值。

WEBrick::HTTPRequest类另外一个重要的方法是body

def body()
  block ||= Proc.new{|chunk| @body  chunk }
  read_body(@socket, block)
  @body.empty? ? nil : @body
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 = BUFSIZE  @remaining_size ? BUFSIZE : @remaining_size
      break unless buf = read_data(socket, sz)
      @remaining_size -= buf.size
      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

如果http body为空,则返回nil

http body分为两种,一种是数据一次性全部传入,另一种是一段一段分批传输(chunked)。

第8-18行就是处理一次性全部传入的数据,根据header中content-length来读取指定长度的数据。

第3-7行读取chunked分段数据,读取方法为

def read_chunked(socket, block)
  chunk_size, = read_chunk_size(socket)
  while chunk_size  0
    data =
    while data.size  chunk_size
      tmp = read_data(socket, chunk_size-data.size) # read chunk-data
      break unless tmp
      data  tmp
    end
    if data.nil? || data.size != 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

chunked分段数据,第一行表明这一段数据的长度,用十六进制表示,第二行开始为需要读取的分段数据。所以读取chunked数据就是读一行chunk_size,读一行chunk data,直到读完为止。

最后看看WEBrick::HTTPRequest的meta方法,对CGI的理解很有帮助

def meta_vars
  # This method provides the metavariables defined by the revision 3
  # of ``The WWW Common Gateway Interface Version 1.1''.
  # (http://Web.Golux.Com/coar/cgi/)

  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

Posted in  webrick ruby


paperclip和id_partition

很多网站都允许用户上传文件,如何管理这些上传的文件呢?以paperclip为例,其默认文件布局结构为

:url  => "/system/:attachment/:id/:style/:filename",
:path => ":rails_root/public:url",

每个id都会占据一个目录,问题是文件系统的子目录数量是有限制的,ext3是32k,ext4是64k,所以网站的数据量达到规模时,默认的文件布局并不合适。比较好的方式是采用id_partition,即把id表示成九位,并且分成3级目录,比如:

1 => 000/000/001

10000 => 000/010/000

100000000 => 100/000/000

这样就无须为文件系统的子目录数量限制担忧了。实现上同样以papaerclip为例,只需要修改其默认的配置参数

Paperclip::Attachment.default_options.merge!(
  :path => ":rails_root/public/pictures/:class/:attachment/:id_partition/:basename_:style.:extension",
  :url => "/pictures/:class/:attachment/:id_partition/:basename_:style.:extension"
)

其中的:id_partition是paperclip内部支持的

Posted in  rails


activerecord属性保护

最近在看rails安全方面的书,第一部分就是关于生成activerecord对象的参数保护问题。平时一直使用,今天心血来潮想起要看看源代码是如何实现的。

activerecord属性保护就是通过attr_accessible和attr_protected来声明哪些属性可以访问,哪些不可以访问。当然,这些保护只是针对new, create和update_attributes方法,对于直接使用attribute=就无能为力了。

attr_accessible的源码为

def attr_protected(*attributes)
  write_inheritable_attribute(:attr_protected, Set.new(attributes.map()) + (protected_attributes || []))
end

原来activerecord会生成一个attr_protected属性,来记录所有的需要被保护的字段

同样attr_accessible会生成attr_accessible属性

def attr_accessible(*attributes)
  write_inheritable_attribute(:attr_accessible, Set.new(attributes.map()) + (accessible_attributes || []))
end

然后,在传递attributes的时候会调remove_attributes_protected_from_mass_assignment

def remove_attributes_protected_from_mass_assignment(attributes)
  safe_attributes =
    if self.class.accessible_attributes.nil?  self.class.protected_attributes.nil?
      attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    elsif self.class.protected_attributes.nil?
      attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    elsif self.class.accessible_attributes.nil?
      attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    else
      raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
    end

  removed_attributes = attributes.keys - safe_attributes.keys

  if removed_attributes.any?
    log_protected_attribute_removal(removed_attributes)
  end

  safe_attributes
end

如果没有定义attr_accessible和attr_protected,会防止修改默认的属性(primary_key属性,一般是id和inheritance属性,即type)

如果没有定义attr_protected,就只允许修改attr_accessible定义的属性,还会防止修改默认的属性

如果没有定义attr_accessible,就防止修改attr_protected定义的属性,也会防止修改默认的属性

需要注意的是,如果同时定义attr_protected和attr_accessible的话,就会抛异常

Posted in  rails activerecord


webrick源码分析──路由

webrick的路由是由WEBrick::HTTPServer::MountTable定义的

MountTable由@tab和@scanner组成,@tab是一个由script_name到Servlet的Hash,@scanner一个可以匹配所有script_name的正则表达式。其定义如下:

class MountTable
  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
end

MountTable只提供了四个方法:

[] 根据script_name获取相应的Servlet []= 定义scrpt_name与Servlet的对应关系 delete 删除script_name到Servlet的映射 scan 根据request的path返回相应的script_name和path_info

另外normalize和compile是MountTable的私有方法,normalize会删除url最后的'/',compile生成可以匹配所有script_name的正则表达式

看完定义之后,先来看看我们是如何定义路由的

1. 定义根目录

doc_root = '/home/flyerhzm'
server.mount("/", WEBrick::HTTPServlet::FileHandler, doc_root, {:FancyIndexing=>true})

2. 定义任意目录

cgi_dir = '/home/flyerhzm/cgi-bin'
server.mount("/cgi-bin", WEBrick::HTTPServlet::FileHandler, cgi_dir, {:FancyIndexing=>true})

上面定义了两个由FileHandler处理的路由,当path为/'时,在'/home/flyerhzm'目录下查找相应的文件,当path为'/cgi-bin'时,在'/home/flyerhzm/cgi-bin'目录下查找相应的文件,选项:FancyIndexing=true表示,在path对应为某个目录时,显示目录下的所有文件。对应到MountTable的@tab为

""=>[WEBrick::HTTPServlet::FileHandler, ["/home/flyerhzm", {:FancyIndexing=>true}]],
/cgi-bin=[WEBrick::HTTPServlet::FileHandler, [/home/flyerhzm/cgi-bin, {:FancyIndexing=true}]]

3. 定义Servlet路径

class GreetingServlet  WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    if req.query['name']
      resp.body = #{@options[0]} #{req.query['name']}. #{@options[1]}
      raise WEBrick::HTTPStatus::OK
    else
      raise WEBrick::HTTPStatus::PreconditionFailed.new(missing attribute: 'name')
    end
  end
  alias do_POST do_GET
end
server.mount('/greet', GreetingServlet, 'Hi', 'Are you having a nice day?')

当path为'/greet'时,由GreetingServlet来处理,选项options = ['Hi', 'Are you having a nice day?'],其对应到MountTable的@tab为

"/greet"=>[GreetingServlet, ["Hi", "Are you having a nice day?"]]

4. webrick还可以mount一个proc

server.mount_proc('/myblock') {|req, resp|
  resp.body = a block mounted at #{req.script_name}
}

当path为'/myblock'时,执行这个proc,其对应到MountTable的@tab为

"/myproc"=>[#WEBrick::HTTPServlet::ProcHandler:0x5ce54 @proc=#Proc:0x00026c8c@webrick_test.rb:18>>, []]

接下来,让我们看看webrick是如何执行mount操作的

在httpserver初始化的时候,执行

@mount_tab = MountTable.new
if @config[:DocumentRoot]
  mount(/, HTTPServlet::FileHandler, @config[:DocumentRoot],
        @config[:DocumentRootOptions])
end

初始化MountTable,同时检查DocumentRoot参数是否设置,如果设置的话,就mount到根目录

mount, mount_proc和unmount方法定义如下

def mount(dir, servlet, *options)
  @logger.debug(sprintf(%s is mounted on %s., servlet.inspect, dir))
  @mount_tab[dir] = [ servlet, options ]
end

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

def unmount(dir)
  @logger.debug(sprintf(unmount %s., dir))
  @mount_tab.delete(dir)
end
alias umount unmount

非常简单,只是调用MountTable提供的方法。

然后来看看webrick是如何根据url来找到相应的servlet。其关键是search_servlet方法

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

参数path就是request的path,经过MountTable#scan解析,分解为script_name和path_info,而通过script_name就能从MountTable中获取servlet类型和选项,WEBrick再根据这个servlet类型和选项,实例化一个servlet,执行用户请求。

Posted in  webrick ruby


困惑

昨天发了几个Ticket给Hostmonster,控诉自己的server无法访问中国的网站,得到的结果和预期一样,它们没有限制我访问中国的网站,肯定是被中国给block了,买了dedicated ip就不能更换。

虽然我还是能够通过google的代理访问,但是终觉不爽。其实我弄个服务器,就是想写点自己的网站,一来练练手,增加点经验;二来呢也指望着赚点零花钱,做得好说不定也是可以创业的基础。相信有不少站长有和我相同的想法。但是现实是网络在中国并不自由。我之所以购买国外的服务器,除了国外的性价比比较高之外,国内的服务器受到GFW的限制,太多的网站无法访问到,再加上动不动就会受到牵连的审查实在不是我这样的懒人能够忍受的。放在国外呢,也提心吊胆,生怕哪天被墙了,就完蛋了。

最近看到不少抨击中国网络的文章,说实话我并不是很关心政治的人,但是我却是一个希望能够在自由的网络世界谋生的程序员,有时候我在想,要是哪天中国的网络真的被封锁了,网络公司纷纷外逃,我还能拿什么来谋生呢?

Posted in  life


Fork me on GitHub