correctly rebuild simple HTTP/0.9 GET requests
[clogger.git] / lib / clogger / pure.rb
blobd8752b38b4336247ce2994fb34b89b4a44619b99
1 # -*- encoding: binary -*-
2 # :stopdoc:
4 # Not at all optimized for performance, this was written based on
5 # the original C extension code so it's not very Ruby-ish...
6 class Clogger
8   def initialize(app, opts = {})
9     @app = app
10     @logger = opts[:logger]
11     @fmt_ops = compile_format(opts[:format] || Format::Common)
12     @wrap_body = need_wrap_body?(@fmt_ops)
13     @reentrant = nil
14     @body_bytes_sent = 0
15   end
17   def call(env)
18     @start = Time.now
19     resp = @app.call(env)
20     unless resp.instance_of?(Array) && resp.size == 3
21       log(env, 500, {})
22       raise TypeError, "app response not a 3 element Array: #{resp.inspect}"
23     end
24     status, headers, body = resp
25     if wrap_body?
26       @reentrant = env['rack.multithread']
27       @env, @status, @headers, @body = env, status, headers, body
28       return [ status, headers, reentrant? ? self.dup : self ]
29     end
30     log(env, status, headers)
31     [ status, headers, body ]
32   end
34   def each
35     @body_bytes_sent = 0
36     @body.each do |part|
37       @body_bytes_sent += part.size
38       yield part
39     end
40     ensure
41       log(@env, @status, @headers)
42   end
44   def close
45     @body.close
46   end
48   def reentrant?
49     @reentrant
50   end
52   def wrap_body?
53     @wrap_body
54   end
56   def fileno
57     @logger.fileno rescue nil
58   end
60 private
62   def byte_xs(s)
63     s = s.dup
64     s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
65     s.gsub!(/(['"\x00-\x1f])/) { |x| "\\x#{$1.unpack('H2').first.upcase}" }
66     s
67   end
69   SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
71   def request_uri(env)
72     ru = env['REQUEST_URI'] and return byte_xs(ru)
73     qs = env['QUERY_STRING']
74     qs.empty? or qs = "?#{byte_xs(qs)}"
75     "#{byte_xs(env['PATH_INFO'])}#{qs}"
76   end
78   def special_var(special_nr, env, status, headers)
79     case SPECIAL_RMAP[special_nr]
80     when :body_bytes_sent
81       @body_bytes_sent.to_s
82     when :status
83       status = status.to_i
84       status >= 100 && status <= 999 ? ('%03d' % status) : '-'
85     when :request
86       version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
87       qs = env['QUERY_STRING']
88       qs.empty? or qs = "?#{byte_xs(qs)}"
89       "#{env['REQUEST_METHOD']} " \
90         "#{request_uri(env)}#{version}"
91     when :request_uri
92       request_uri(env)
93     when :request_length
94       env['rack.input'].size.to_s
95     when :response_length
96       @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
97     when :ip
98       xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
99       env['REMOTE_ADDR'] || '-'
100     when :pid
101       $$.to_s
102     else
103       raise "EDOOFUS #{special_nr}"
104     end
105   end
107   def time_format(sec, usec, format, div)
108     format % [ sec, usec / div ]
109   end
111   def log(env, status, headers)
112     (@logger || env['rack.errors']) << @fmt_ops.map { |op|
113       case op[0]
114       when OP_LITERAL; op[1]
115       when OP_REQUEST; byte_xs(env[op[1]] || "-")
116       when OP_RESPONSE; byte_xs(get_sent_header(headers, op[1]))
117       when OP_SPECIAL; special_var(op[1], env, status, headers)
118       when OP_EVAL; eval(op[1]).to_s rescue "-"
119       when OP_TIME_LOCAL; Time.now.strftime(op[1])
120       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
121       when OP_REQUEST_TIME
122         t = Time.now - @start
123         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
124       when OP_TIME
125         t = Time.now
126         time_format(t.sec, t.usec, op[1], op[2])
127       when OP_COOKIE
128         (env['rack.request.cookie_hash'][op[1]] rescue "-") || "-"
129       else
130         raise "EDOOFUS #{op.inspect}"
131       end
132     }.join('')
133   end
135   def get_sent_header(headers, match)
136     headers.each do |pair|
137       Array === pair && pair.size >= 2 or
138         raise TypeError, "headers not returning pairs"
139       key, value = pair
140       match == key.downcase and return value
141     end
142     "-"
143   end