escape individual cookie values from $cookie_*
[clogger.git] / lib / clogger.rb
blobbe1bdce5c590e1a32430a1dce5f927c45132abe8
1 # -*- encoding: binary -*-
2 require 'rack'
4 # See the README for usage instructions
5 class Clogger
7   # :stopdoc:
8   OP_LITERAL = 0
9   OP_REQUEST = 1
10   OP_RESPONSE = 2
11   OP_SPECIAL = 3
12   OP_EVAL = 4
13   OP_TIME_LOCAL = 5
14   OP_TIME_UTC = 6
15   OP_REQUEST_TIME = 7
16   OP_TIME = 8
17   OP_COOKIE = 9
19   # support nginx variables that are less customizable than our own
20   ALIASES = {
21     '$request_time' => '$request_time{3}',
22     '$msec' => '$time{3}',
23     '$usec' => '$time{6}',
24     '$http_content_length' => '$content_length',
25     '$http_content_type' => '$content_type',
26   }
28   SPECIAL_VARS = {
29     :body_bytes_sent => 0,
30     :status => 1,
31     :request => 2, # REQUEST_METHOD PATH_INFO?QUERY_STRING HTTP_VERSION
32     :request_length => 3, # env['rack.input'].size
33     :response_length => 4, # like body_bytes_sent, except "-" instead of "0"
34     :ip => 5, # HTTP_X_FORWARDED_FOR || REMOTE_ADDR || -
35     :pid => 6, # getpid()
36     :request_uri => 7,
37     :time_iso8601 => 8,
38     :time_local => 9,
39     :time_utc => 10,
40   }
42 private
44   CGI_ENV = Regexp.new('\A\$(' <<
45       %w(request_method content_length content_type
46          remote_addr remote_ident remote_user
47          path_info query_string script_name
48          server_name server_port
49          auth_type gateway_interface server_software path_translated
50          ).join('|') << ')\z')
52   SCAN = /([^$]*)(\$+(?:env\{\w+(?:\.[\w\.]+)?\}|
53                         e\{[^\}]+\}|
54                         (?:request_)?time\{\d+\}|
55                         time_(?:utc|local)\{[^\}]+\}|
56                         \w*))?([^$]*)/x
58   def compile_format(str, opt = {})
59     str = Clogger::Format.const_get(str) if Symbol === str
60     longest_day = Time.at(26265600) # "Saturday, November 01, 1970 00:00:00"
61     rv = []
62     opt ||= {}
63     str.scan(SCAN).each do |pre,tok,post|
64       rv << [ OP_LITERAL, pre ] if pre && pre != ""
66       unless tok.nil?
67         if tok.sub!(/\A(\$+)\$/, '$')
68           rv << [ OP_LITERAL, $1 ]
69         end
71         compat = ALIASES[tok] and tok = compat
73         case tok
74         when /\A(\$*)\z/
75           rv << [ OP_LITERAL, $1 ]
76         when /\A\$env\{(\w+(?:\.[\w\.]+))\}\z/
77           rv << [ OP_REQUEST, $1 ]
78         when /\A\$e\{([^\}]+)\}\z/
79           rv << [ OP_EVAL, $1 ]
80         when /\A\$cookie_(\w+)\z/
81           rv << [ OP_COOKIE, $1 ]
82         when CGI_ENV, /\A\$(http_\w+)\z/
83           rv << [ OP_REQUEST, $1.upcase ]
84         when /\A\$sent_http_(\w+)\z/
85           rv << [ OP_RESPONSE, $1.downcase.tr('_','-') ]
86         when /\A\$time_local\{([^\}]+)\}\z/
87           fmt = $1
88           rv << [ OP_TIME_LOCAL, fmt, longest_day.strftime(fmt) ]
89         when /\A\$time_utc\{([^\}]+)\}\z/
90           fmt = $1
91           rv << [ OP_TIME_UTC, fmt, longest_day.strftime(fmt) ]
92         when /\A\$time\{(\d+)\}\z/
93           rv << [ OP_TIME, *usec_conv_pair(tok, $1.to_i) ]
94         when /\A\$request_time\{(\d+)\}\z/
95           rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, $1.to_i) ]
96         else
97           tok_sym = tok[1..-1].to_sym
98           if special_code = SPECIAL_VARS[tok_sym]
99             rv << [ OP_SPECIAL, special_code ]
100           else
101             raise ArgumentError, "unable to make sense of token: #{tok}"
102           end
103         end
104       end
106       rv << [ OP_LITERAL, post ] if post && post != ""
107     end
109     # auto-append a newline
110     last = rv.last or return rv
111     op = last.first
112     ors = opt[:ORS] || "\n"
113     if (op == OP_LITERAL && /#{ors}\z/ !~ last.last) || op != OP_LITERAL
114       rv << [ OP_LITERAL, ors ] if ors.size > 0
115     end
117     rv
118   end
120   def usec_conv_pair(tok, prec)
121     if prec == 0
122       [ "%d", 1 ] # stupid...
123     elsif prec > 6
124       raise ArgumentError, "#{tok}: too high precision: #{prec} (max=6)"
125     else
126       [ "%d.%0#{prec}d", 10 ** (6 - prec) ]
127     end
128   end
130   def need_response_headers?(fmt_ops)
131     fmt_ops.any? { |op| OP_RESPONSE == op[0] }
132   end
134   def need_wrap_body?(fmt_ops)
135     fmt_ops.any? do |op|
136       (OP_REQUEST_TIME == op[0]) || (OP_SPECIAL == op[0] &&
137         (SPECIAL_VARS[:body_bytes_sent] == op[1] ||
138          SPECIAL_VARS[:response_length] == op[1]))
139     end
140   end
142 private
143   def method_missing(*args, &block)
144     body.__send__(*args, &block)
145   end
146   # :startdoc:
149 require 'clogger/format'
151 begin
152   raise LoadError if ENV['CLOGGER_PURE'].to_i != 0
153   require 'clogger_ext'
154 rescue LoadError
155   require 'clogger/pure'