class Rack::Prevoty::ContentMiddleware

Public Class Methods

build_result(mode, request, input, result) click to toggle source
# File lib/rack/content_middleware.rb, line 109
def self.build_result(mode, request, input, result)
  data = {
    product: 'content',
    mode: mode,
    version: '1',
    input: input,
    timestamp: Time.now.utc.strftime('%b %d %Y %H:%M:%S %Z'),
    request_url: request.path,
    session_id: request.session["session_id"],
    cookies: request.cookies,
    http_method: request.request_method,
    src_ip: request.ip,
    dest_host: request.host,
    dest_port: request.port
  }

  # these are hacks due to differences between protect and monitor
  if mode === 'protect'
    data[:statistics] = result.statistics
    data[:output] = CGI::unescape(result.output)
  else
    data[:statistics] = result
  end

  ::Prevoty::ContentPayload.new(data)
end
new(app, opts) click to toggle source
# File lib/rack/content_middleware.rb, line 9
def initialize(app, opts)
  @app = app
  @base = opts[:api_base] ||= 'https://api.prevoty.com'
  @client = ::Prevoty::Client.new(opts[:api_key], @base)
  @policy_key = opts[:policy_key] ||= ''
  @mode = opts[:mode] ||= 'monitor'
  @log_verbosity = opts[:log_verbosity] ||= 'incident'
  @paths = opts[:paths] ||= ['/']
  @blacklist = opts[:blacklist] ||= []
  @traffic_percentage = opts[:traffic_percentage] ||= 100
  if @mode === 'monitor'
    @monitor = ::Prevoty::ContentMonitor.new(@client, opts)
  end
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/content_middleware.rb, line 24
def call(env)
  req = Rack::Request.new(env)

  # Thread local storage for communicating between content and query
  Thread.current[:request_storage] = {}

  # only wrapping this in a begin so we can use ensure to clean out
  # thread storage
  begin
    if rand(100) > @traffic_percentage
      Thread.current[:request_storage][:skip_processing] = true
      return @app.call(env)
    end

    # passthru if not listed in paths
    return @app.call(env) if @paths.detect {|p| req.path.start_with?(p)}.nil?

    # passthru if blacklisted
    return @app.call(env) unless @blacklist.detect {|p| req.path.start_with?(p)}.nil?

    # TODO: implement support for multipart. The Rack multipart
    # implementation doesn't support parsing and re-creating the
    # mutlipart data so a custom implementation needs to be written
    return @app.call(env) if req.media_type === 'multipart/form-data'

    unless env['QUERY_STRING'].empty?
      querystring = env['QUERY_STRING']
      if @mode === 'protect'
        begin
          Timeout::timeout(@timeout) do
            resp = nil
            if defined? ActiveSupport::Notifications
              ActiveSupport::Notifications.instrument('prevoty:content:protect') do |payload|
                resp = payload[:response] = @client.bulk_filter(querystring, @policy_key)
              end
            end
            env['QUERY_STRING'] = resp.output
            result = self.class.build_result(@mode, req, querystring, resp)
            if resp.statistics.is_significant? || @log_verbosity === 'all'
                ::Prevoty::LOGGER << result.to_json + "\n"
            end
          end
        rescue Exception => e
          env['QUERY_STRING'] = escape_query(CGI::parse(env['QUERY_STRING']))
          Rails.logger.warn e.message
        end
      else
        @monitor.process({mode: @mode, input: env['QUERY_STRING'], request: req})
      end
    end

    if ['POST', 'PUT', 'PATCH'].member?(req.request_method)
      body = URI.unescape(req.body.read.encode('utf-8'))
      unless body.empty?
        if @mode === 'protect'
          begin
            resp = nil
            Timeout::timeout(@timeout) do
              if defined? ActiveSupport::Notifications
                ActiveSupport::Notifications.instrument('prevoty:content:protect') do |payload|
                  resp = payload[:response] = @client.bulk_filter(body, @policy_key)
                end
              end
              env['rack.input'] = StringIO.new(resp.output)
              result = self.class.build_result(@mode, req, body, resp)
              if resp.statistics.is_significant? || @log_verbosity === 'all'
                ::Prevoty::LOGGER << result.to_json + "\n"
              end
            end
          rescue Exception => e
            env['rack.input'] = StringIO.new(escape_query(CGI::parse(body)))
            Rails.logger.warn e.message
          end
        else
          @monitor.process({mode: @mode, input: body, request: req})
        end
      end
    end

    @app.call(env)
  ensure
    Thread.current[:request_storage] = {}
  end
end

Protected Instance Methods

escape_query(params) click to toggle source
# File lib/rack/content_middleware.rb, line 137
def escape_query(params)
  params.map do |name,values|
    values.map do |value|
      "#{CGI.escape name}=#{CGI.escape CGI.escapeHTML value}"
    end
  end.flatten.join("&")
end