summaryrefslogtreecommitdiff
path: root/lib/gitlab/middleware/compressed_json.rb
blob: 80916eab5ac9ebd20e3c4be719838af407d99f71 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# frozen_string_literal: true

module Gitlab
  module Middleware
    class CompressedJson
      COLLECTOR_PATH = '/api/v4/error_tracking/collector'
      PACKAGES_PATH = %r{
        \A/api/v4/ (?# prefix)
        (?:projects/
          (?<project_id>
            .+ (?# at least one character)
          )/
        )? (?# projects segment)
       packages/npm/-/npm/v1/security/
       (?:(?:advisories/bulk)|(?:audits/quick))\z (?# end)
      }xi.freeze
      MAXIMUM_BODY_SIZE = 200.kilobytes.to_i
      UNSAFE_CHARACTERS = %r{[!"#&'()*+,./:;<>=?@\[\]^`{}|~$]}xi.freeze

      def initialize(app)
        @app = app
      end

      def call(env)
        if compressed_et_request?(env)
          input = extract(env['rack.input'])

          if input.length > MAXIMUM_BODY_SIZE
            return too_large
          end

          env.delete('HTTP_CONTENT_ENCODING')
          env['CONTENT_LENGTH'] = input.length
          env['rack.input'] = StringIO.new(input)
        end

        @app.call(env)
      end

      def compressed_et_request?(env)
        post_request?(env) &&
          gzip_encoding?(env) &&
          match_content_type?(env) &&
          match_path?(env)
      end

      def too_large
        [413, { 'Content-Type' => 'text/plain' }, ['Payload Too Large']]
      end

      def relative_url
        File.join('', Gitlab.config.gitlab.relative_url_root).chomp('/')
      end

      def extract(input)
        Zlib::GzipReader.new(input).read(MAXIMUM_BODY_SIZE + 1)
      end

      def post_request?(env)
        env['REQUEST_METHOD'] == 'POST'
      end

      def gzip_encoding?(env)
        env['HTTP_CONTENT_ENCODING'] == 'gzip'
      end

      def match_content_type?(env)
        env['CONTENT_TYPE'].nil? ||
          env['CONTENT_TYPE'] == 'application/json' ||
          env['CONTENT_TYPE'] == 'application/x-sentry-envelope'
      end

      def match_path?(env)
        env['PATH_INFO'].start_with?((File.join(relative_url, COLLECTOR_PATH))) ||
          match_packages_path?(env)
      end

      def match_packages_path?(env)
        match_data = env['PATH_INFO'].delete_prefix(relative_url).match(PACKAGES_PATH)
        return false unless match_data

        return true unless match_data[:project_id] # instance level endpoint was matched

        url_encoded?(match_data[:project_id])
      end

      def url_encoded?(project_id)
        project_id !~ UNSAFE_CHARACTERS
      end
    end
  end
end