Class: Rodsec::Rack

Inherits:
Object
  • Object
show all
Includes:
ReadConfig
Defined in:
lib/rodsec/rack.rb

Overview

Thanks to rack-contrib/deflect for the basic idea, and some of the docs.

Constant Summary collapse

REQUEST_URI =
'REQUEST_URI'.freeze
REMOTE_HOST =
'REMOTE_HOST'.freeze
REMOTE_ADDR =
'REMOTE_ADDR'.freeze
SERVER_NAME =
'SERVER_NAME'.freeze
HTTP_HOST =
'HTTP_HOST'.freeze
SERVER_PORT =
'SERVER_PORT'.freeze
HTTP_VERSION =
'HTTP_VERSION'.freeze
REQUEST_METHOD =
'REQUEST_METHOD'.freeze
SLASH =
'/'.freeze
HTTP_HEADER_RX =
/HTTP_(.*)|(CONTENT_.*)/.freeze
DASH =
'-'.freeze
UNDERSCORE =
'_'.freeze
EMPTY =
String.new.freeze
RACK_INPUT =
'rack.input'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ReadConfig

read_combined_config, read_config

Constructor Details

#initialize(app, config:, rules: nil, logger: nil, log_blk: nil) ⇒ Rack

Required Options:

:config   Proc, or the directory containing the ModSecurity config files
          modsecurity.conf and crs-setup.conf. If it's a Proc, which
          must return a Rodsec::Ruleset instance containing all the
          rules you want.

Optional Options:

:rules    the directory containing the ModSecurity rules files.
          Defaults to ${config}/rules. Ignored if you pass a proc to config

:logger   must respond_to #puts which takes a string. Defaults to a StringIO at #logger

:log_blk  a callable that takes |tag,string| Defaults to sending
          only the string to logger. The ModSecurity logs are highly
          structured and you might want to parse them, so the tag
          helps disambiguate the source of the logs.

? :msi_blk  called with [status, headers, body] if there's an intervention from ModSecurity.

Examples:

use Rodsec::Rack, config: 'your_config_path', log: (mylogger = StringIO.new)
use Rodsec::Rack, config: 'your_config_path', log_blk: -> src_class, str { my_funky_parse_msi_to_hash str }


32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rodsec/rack.rb', line 32

def initialize app, config:, rules: nil, logger: nil, log_blk: nil
  @app = app

  @log_blk = log_blk || -> _tag, str{self.logger.puts str}
  @msc = Rodsec::Modsec.new{|tag,str| @log_blk.call tag, str}

  @logger = logger || StringIO.new

  @log_blk.call self.class, "#{self.class} starting with #{@msc.version_info}"

  set_rules config, rules
end

Instance Attribute Details

#log_blkObject (readonly)

Returns the value of attribute log_blk.



45
46
47
# File 'lib/rodsec/rack.rb', line 45

def log_blk
  @log_blk
end

#loggerObject (readonly)

Returns the value of attribute logger.



45
46
47
# File 'lib/rodsec/rack.rb', line 45

def logger
  @logger
end

Instance Method Details

#call(env) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/rodsec/rack.rb', line 74

def call env
  txn = Rodsec::Transaction.new @msc, @rules, txn_log_tag: env[REQUEST_URI]

  ################
  # incoming

  # uri! scope for variables
  lambda do
    remote_addr = env[REMOTE_HOST] || env[REMOTE_ADDR]
    server_addr = env[HTTP_HOST] || env[SERVER_NAME]
    txn.connection! remote_addr, 0, server_addr, (env[SERVER_PORT] || 0)

    _, version = env[HTTP_VERSION]&.split(SLASH)

    txn.uri! env[REQUEST_URI], env[REQUEST_METHOD], version
  end.call

  # request_headers! - another scope for variables
  lambda do
    http_headers = env.map do |key,val|
      key =~ HTTP_HEADER_RX or next
      header_name = $1 || $2
      dashified = header_name.split(UNDERSCORE).map(&:capitalize).join(DASH)
      [dashified, val]
    end.compact.to_h

    txn.request_headers! http_headers
  end.call

  # request_body! MUST be called (even with an empty body is fine),
  # otherwise ModSecurity never triggers the rules, even though ModSecurity
  # can detect something dodgy in the headers. That needs what they call
  # self-contained mode.
  env[RACK_INPUT].tap do |rack_input|
    # ruby-2.3 syntax :-|
    begin
      # What about a DOS from a very large body?
      #
      # Rack spec says rack.input must be rewindable at the http-server
      # level, so it's all in memory by now anyway, nothing we can do to
      # affect that here.
      txn.request_body! rack_input
    ensure
      # Have to rewind input, otherwise other rack apps can't get the content
      rack_input.rewind
    end
  end

  ################
  # rack chain
  status, headers, body = @app.call env

  ################
  # outgoing
  txn.response_headers! status, env[HTTP_VERSION], headers

  # TODO handle hijacking? Not sure.
  # body is an Enumerable, which response_body! will handle
  txn.response_body! body

  # Logging. From ModSecurity's point of view this could be in a separate
  # thread. Dunno how rack will handle that though. Also, there's no way to
  # wait for a thread doing that logging. So it would have to be spawned and
  # then left to die. Alone. In the rain.
  txn.logging

  # all ok
  return status, headers, body

rescue Rodsec::Intervention => iex
  log_blk.call :intervention, iex.msi.log
  # rack interface specification says we have to call close on the body, if
  # it responds to close
  body.respond_to?(:close) && body.close
  # Intervention!
  return iex.msi.status, {'Content-Type' => 'text/plain'}, [ ::Rack::Utils::HTTP_STATUS_CODES[iex.msi.status] ].compact
end