File: static.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (192 lines) | stat: -rw-r--r-- 6,528 bytes parent folder | download | duplicates (2)
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
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# frozen_string_literal: true

# :markup: markdown

require "rack/utils"

module ActionDispatch
  # # Action Dispatch Static
  #
  # This middleware serves static files from disk, if available. If no file is
  # found, it hands off to the main app.
  #
  # In Rails apps, this middleware is configured to serve assets from the
  # `public/` directory.
  #
  # Only GET and HEAD requests are served. POST and other HTTP methods are handed
  # off to the main app.
  #
  # Only files in the root directory are served; path traversal is denied.
  class Static
    def initialize(app, path, index: "index", headers: {})
      @app = app
      @file_handler = FileHandler.new(path, index: index, headers: headers)
    end

    def call(env)
      @file_handler.attempt(env) || @app.call(env)
    end
  end

  # # Action Dispatch FileHandler
  #
  # This endpoint serves static files from disk using `Rack::Files`.
  #
  # URL paths are matched with static files according to expected conventions:
  # `path`, `path`.html, `path`/index.html.
  #
  # Precompressed versions of these files are checked first. Brotli (.br) and gzip
  # (.gz) files are supported. If `path`.br exists, this endpoint returns that
  # file with a `content-encoding: br` header.
  #
  # If no matching file is found, this endpoint responds `404 Not Found`.
  #
  # Pass the `root` directory to search for matching files, an optional `index:
  # "index"` to change the default `path`/index.html, and optional additional
  # response headers.
  class FileHandler
    # `Accept-Encoding` value -> file extension
    PRECOMPRESSED = {
      "br" => ".br",
      "gzip" => ".gz",
      "identity" => nil
    }

    def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript|image\/svg\+xml)/)
      @root = root.chomp("/").b
      @index = index

      @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
      @compressible_content_types = compressible_content_types

      @file_server = ::Rack::Files.new(@root, headers)
    end

    def call(env)
      attempt(env) || @file_server.call(env)
    end

    def attempt(env)
      request = Rack::Request.new env

      if request.get? || request.head?
        if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
          serve request, *found
        end
      end
    end

    private
      def serve(request, filepath, content_headers)
        original, request.path_info =
          request.path_info, ::Rack::Utils.escape_path(filepath).b

        @file_server.call(request.env).tap do |status, headers, body|
          # Omit content-encoding/type/etc headers for 304 Not Modified
          if status != 304
            headers.update(content_headers)
          end
        end
      ensure
        request.path_info = original
      end

      # Match a URI path to a static file to be served.
      #
      # Used by the `Static` class to negotiate a servable file in the `public/`
      # directory (see Static#call).
      #
      # Checks for `path`, `path`.html, and `path`/index.html files, in that order,
      # including .br and .gzip compressed extensions.
      #
      # If a matching file is found, the path and necessary response headers
      # (Content-Type, Content-Encoding) are returned.
      def find_file(path_info, accept_encoding:)
        each_candidate_filepath(path_info) do |filepath, content_type|
          if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
            return response
          end
        end
      end

      def try_files(filepath, content_type, accept_encoding:)
        headers = { Rack::CONTENT_TYPE => content_type }

        if compressible? content_type
          try_precompressed_files filepath, headers, accept_encoding: accept_encoding
        elsif file_readable? filepath
          [ filepath, headers ]
        end
      end

      def try_precompressed_files(filepath, headers, accept_encoding:)
        each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
          if file_readable? precompressed_filepath
            # Identity encoding is default, so we skip Accept-Encoding negotiation and
            # needn't set Content-Encoding.
            #
            # Vary header is expected when we've found other available encodings that
            # Accept-Encoding ruled out.
            if content_encoding == "identity"
              return precompressed_filepath, headers
            else
              headers[ActionDispatch::Constants::VARY] = "accept-encoding"

              if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
                headers[ActionDispatch::Constants::CONTENT_ENCODING] = content_encoding
                return precompressed_filepath, headers
              end
            end
          end
        end
      end

      def file_readable?(path)
        file_path = File.join(@root, path.b)
        File.file?(file_path) && File.readable?(file_path)
      end

      def compressible?(content_type)
        @compressible_content_types.match?(content_type)
      end

      def each_precompressed_filepath(filepath)
        @precompressed.each do |content_encoding|
          precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
          yield content_encoding, "#{filepath}#{precompressed_ext}"
        end

        nil
      end

      def each_candidate_filepath(path_info)
        return unless path = clean_path(path_info)

        ext = ::File.extname(path)
        content_type = ::Rack::Mime.mime_type(ext, nil)
        yield path, content_type || "text/plain"

        # Tack on .html and /index.html only for paths that don't have an explicit,
        # resolvable file extension. No need to check for foo.js.html and
        # foo.js/index.html.
        unless content_type
          default_ext = ::ActionController::Base.default_static_extension
          if ext != default_ext
            default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")

            yield "#{path}#{default_ext}", default_content_type
            yield "#{path}/#{@index}#{default_ext}", default_content_type
          end
        end

        nil
      end

      def clean_path(path_info)
        path = ::Rack::Utils.unescape_path path_info.chomp("/")
        if ::Rack::Utils.valid_path? path
          ::Rack::Utils.clean_path_info path
        end
      end
  end
end