1
# frozen_string_literal: true
2

3 5
require 'set'
4 5
require 'uri'
5 5
require 'net/http'
6

7 5
module Roadie
8
  # @api public
9
  # External asset provider that downloads stylesheets from some other server
10
  # using Ruby's built-in {Net::HTTP} library.
11
  #
12
  # You can pass a whitelist of hosts that downloads are allowed on.
13
  #
14
  # @example Allowing all downloads
15
  #   provider = Roadie::NetHttpProvider.new
16
  #
17
  # @example Only allowing your own app domains
18
  #   provider = Roadie::NetHttpProvider.new(
19
  #     whitelist: ["myapp.com", "assets.myapp.com", "www.myapp.com"]
20
  #   )
21 5
  class NetHttpProvider
22 5
    attr_reader :whitelist
23

24
    # @option options [Array<String>] :whitelist ([]) A list of host names that downloads are allowed from. Empty set means everything is allowed.
25 5
    def initialize(options = {})
26 5
      @whitelist = host_set(Array(options.fetch(:whitelist, [])))
27
    end
28

29 5
    def find_stylesheet(url)
30 5
      find_stylesheet!(url)
31
    rescue CssNotFound
32 5
      nil
33
    end
34

35 5
    def find_stylesheet!(url)
36 5
      response = download(url)
37 5
      if response.kind_of? Net::HTTPSuccess
38 5
        Stylesheet.new(url, response_body(response))
39
      else
40 5
        raise CssNotFound.new(url, "Server returned #{response.code}: #{truncate response.body}", self)
41
      end
42
    rescue Timeout::Error
43 5
      raise CssNotFound.new(url, "Timeout", self)
44
    end
45

46 5
    def to_s() inspect end
47 5
    def inspect() "#<#{self.class} whitelist: #{whitelist.inspect}>" end
48

49 5
    private
50 5
    def host_set(hosts)
51 5
      hosts.each { |host| validate_host(host) }.to_set
52
    end
53

54 5
    def validate_host(host)
55 5
      if host.nil? || host.empty? || host == "." || host.include?("/")
56 5
        raise ArgumentError, "#{host.inspect} is not a valid hostname"
57
      end
58
    end
59

60 5
    def download(url)
61 5
      url = "https:#{url}" if url.start_with?("//")
62 5
      uri = URI.parse(url)
63 5
      if access_granted_to?(uri.host)
64 5
        get_response(uri)
65
      else
66 5
        raise CssNotFound.new(url, "#{uri.host} is not part of whitelist!", self)
67
      end
68
    end
69

70 5
    def get_response(uri)
71 5
      if RUBY_VERSION >= "2.0.0"
72 5
        Net::HTTP.get_response(uri)
73
      else
74 0
        Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == 'https')) do |http|
75 0
          http.request(Net::HTTP::Get.new(uri.request_uri))
76
        end
77
      end
78
    end
79

80 5
    def access_granted_to?(host)
81 5
      whitelist.empty? || whitelist.include?(host)
82
    end
83

84 5
    def truncate(string)
85 5
      if string.length > 50
86 5
        string[0, 49] + "…"
87
      else
88 5
        string
89
      end
90
    end
91

92 5
    def response_body(response)
93
      # Make sure we respect encoding because Net:HTTP will encode body as ASCII by default
94
      # which will break if the response is not compatible.
95 5
      supplied_charset = response.type_params['charset']
96 5
      body = response.body
97

98 5
      if supplied_charset
99 5
        body.force_encoding(supplied_charset).encode!("UTF-8")
100
      else
101
        # Default to UTF-8 when server does not specify encoding as that is the
102
        # most common charset.
103 5
        body.force_encoding("UTF-8")
104
      end
105
    end
106
  end
107
end

Read our documentation on viewing source code .

Loading