jekyll-remote-theme 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fd5c42945efed4017267b250e90d3a9dd3c5b07b1d7e4cd34ab38fb3b5a5f89
4
- data.tar.gz: 4997758808b903abf1ff1f2e185c9e408af4c0a94d2a6aabf25c2c5347194e91
3
+ metadata.gz: e8b60407212cb023740ee7d4258e9a02c862bed37246db406607af4d3e6228e8
4
+ data.tar.gz: 448d9d30e4f0e0a2efa300c1ab7bdd8199ff82fcc4afd97264227892d6ab5fe3
5
5
  SHA512:
6
- metadata.gz: 1a1bd64eab9afac4223799d871f17f9cfa23601a752879a0b6273e389b7d60301eaf9b963eaaff54c6790614661984cc06a0c466c4b7d4871e6679ade95ae74f
7
- data.tar.gz: dc3ebb66ed63e4722813b95f710815b79702245ff59b1431186cf0eb417b2fad5f3284eba10c9dea2b372da37dbb2de92057b0506a3c570223c435979312d37a
6
+ metadata.gz: 36eacae972f56890144e128ec284067fa33f03f528b4f70047b0c60e4f90e80f04426990a6abb6ea87d1d6cbecac1b12effec5dcafbe89c7eb6abd9d1160c357
7
+ data.tar.gz: '0871deb358244f78aa294b171af43415eb0bed1367c3224554992c1723dbae092d48d0bc321c116fd0297b1e22d7847e030599c748a6fef5b5192a9f1ccdfa93'
@@ -6,6 +6,9 @@ module Jekyll
6
6
  PROJECT_URL = "https://github.com/benbalter/jekyll-remote-theme"
7
7
  USER_AGENT = "Jekyll Remote Theme/#{VERSION} (+#{PROJECT_URL})"
8
8
  MAX_FILE_SIZE = 1 * (1024 * 1024 * 1024) # Size in bytes (1 GB)
9
+ # Limit on the theme's total size once extracted, to stop zip bombs
10
+ MAX_EXTRACTED_SIZE = 2 * (1024 * 1024 * 1024) # Size in bytes (2 GB)
11
+ EXTRACT_CHUNK_SIZE = 64 * 1024
9
12
  NET_HTTP_ERRORS = [
10
13
  Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::OpenTimeout,
11
14
  Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
@@ -45,23 +48,30 @@ module Jekyll
45
48
  @zip_file ||= Tempfile.new([TEMP_PREFIX, ".zip"], :binmode => true)
46
49
  end
47
50
 
48
- # rubocop:disable Metrics/AbcSize
49
51
  def download
50
52
  Jekyll.logger.debug LOG_KEY, "Downloading #{zip_url} to #{zip_file.path}"
51
- http_class.start(zip_url.host, zip_url.port, :use_ssl => true) do |http|
53
+ HTTP.start(zip_url, :use_ssl => true) do |http|
52
54
  http.request(request) do |response|
53
55
  raise_unless_success(response)
54
56
  enforce_max_file_size(response.content_length)
55
- response.read_body do |chunk|
56
- zip_file.write chunk
57
- end
57
+ write_body(response)
58
58
  end
59
59
  end
60
60
  @downloaded = true
61
61
  rescue *NET_HTTP_ERRORS => e
62
62
  raise DownloadError, e.message
63
63
  end
64
- # rubocop:enable Metrics/AbcSize
64
+
65
+ # Content-Length is optional (e.g. chunked responses), so also count the
66
+ # bytes as they arrive and stop once the limit is exceeded.
67
+ def write_body(response)
68
+ bytes = 0
69
+ response.read_body do |chunk|
70
+ bytes += chunk.bytesize
71
+ enforce_max_file_size(bytes)
72
+ zip_file.write chunk
73
+ end
74
+ end
65
75
 
66
76
  def request
67
77
  return @request if defined? @request
@@ -102,6 +112,7 @@ module Jekyll
102
112
  # is now resolved relative to a `destination_directory:` keyword,
103
113
  # mangling the absolute paths we pass). Reading each entry's stream and
104
114
  # writing it ourselves behaves identically across rubyzip 1.x–3.x.
115
+ @extracted_size = 0
105
116
  Zip::File.open(zip_file) do |archive|
106
117
  archive.each { |entry| extract_entry(entry) }
107
118
  end
@@ -116,64 +127,45 @@ module Jekyll
116
127
  def extract_entry(entry)
117
128
  return if entry.name.end_with?("/") # skip directory entries
118
129
 
130
+ enforce_max_extracted_size(@extracted_size + entry.size)
119
131
  dest = path_without_name_and_ref(entry.name)
120
132
  FileUtils.mkdir_p File.dirname(dest)
121
- entry.get_input_stream { |input| File.binwrite(dest, input.read) }
122
- end
123
-
124
- # Full URL to codeload zip download endpoint for the given theme
125
- def zip_url
126
- @zip_url ||= Addressable::URI.new(
127
- :scheme => theme.scheme,
128
- :host => "codeload.#{theme.host}",
129
- :path => [theme.owner, theme.name, "zip", theme.git_ref].join("/")
130
- ).normalize
131
- end
132
-
133
- # Returns an HTTP class that respects proxy environment variables
134
- def http_class
135
- @http_class ||= Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass)
136
- end
137
-
138
- # Extracts proxy settings from environment variables
139
- def proxy_uri
140
- return @proxy_uri if defined?(@proxy_uri)
141
-
142
- proxy_env = find_proxy_env_var
143
- @proxy_uri = parse_proxy_uri(proxy_env)
144
- end
145
-
146
- def find_proxy_env_var
147
- # Check for HTTPS proxy first if the URL uses HTTPS
148
- if theme.scheme == "https"
149
- ENV["https_proxy"] || ENV["HTTPS_PROXY"] || ENV["http_proxy"] || ENV["HTTP_PROXY"]
150
- else
151
- ENV["http_proxy"] || ENV["HTTP_PROXY"]
133
+ File.open(dest, "wb") do |output|
134
+ entry.get_input_stream { |input| copy_entry(entry, input, output) }
152
135
  end
153
136
  end
154
137
 
155
- def parse_proxy_uri(proxy_env)
156
- return nil unless proxy_env
157
-
158
- Addressable::URI.parse(proxy_env)
159
- rescue Addressable::URI::InvalidURIError
160
- nil
161
- end
138
+ # Copies an entry in chunks, counting the bytes actually inflated rather
139
+ # than trusting the sizes in the archive. Zip.validate_entry_sizes only
140
+ # applies to Zip::Entry#extract, so the same check is made here: an entry
141
+ # may not inflate to more than its declared size.
142
+ def copy_entry(entry, input, output)
143
+ written = 0
144
+ while (chunk = input.read(EXTRACT_CHUNK_SIZE))
145
+ written += chunk.bytesize
146
+ if written > entry.size
147
+ raise DownloadError, "Zip entry #{entry.name} is larger than its declared size"
148
+ end
162
149
 
163
- def proxy_host
164
- proxy_uri&.host
150
+ @extracted_size += chunk.bytesize
151
+ enforce_max_extracted_size(@extracted_size)
152
+ output.write(chunk)
153
+ end
165
154
  end
166
155
 
167
- def proxy_port
168
- proxy_uri&.port
169
- end
156
+ def enforce_max_extracted_size(size)
157
+ return unless size > MAX_EXTRACTED_SIZE
170
158
 
171
- def proxy_user
172
- proxy_uri&.user
159
+ raise DownloadError, "Maximum extracted theme size of #{MAX_EXTRACTED_SIZE} bytes exceeded"
173
160
  end
174
161
 
175
- def proxy_pass
176
- proxy_uri&.password
162
+ # Full URL to codeload zip download endpoint for the given theme
163
+ def zip_url
164
+ @zip_url ||= Addressable::URI.new(
165
+ :scheme => theme.scheme,
166
+ :host => "codeload.#{theme.host}",
167
+ :path => [theme.owner, theme.name, "zip", theme.git_ref].join("/")
168
+ ).normalize
177
169
  end
178
170
 
179
171
  def theme_dir_exists?
@@ -181,7 +173,7 @@ module Jekyll
181
173
  end
182
174
 
183
175
  def theme_dir_empty?
184
- Dir["#{theme.root}/*"].empty?
176
+ Dir.empty?(theme.root)
185
177
  end
186
178
 
187
179
  # Codeload generated zip files contain a top level folder in the form of
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module RemoteTheme
5
+ # Net::HTTP setup shared by the theme download and the GitHub API lookup
6
+ # for @latest, so both use the same proxy settings and timeouts.
7
+ module HTTP
8
+ OPEN_TIMEOUT = 10 # seconds
9
+ READ_TIMEOUT = 60 # seconds
10
+ # Proxy environment variables to try, by request scheme
11
+ PROXY_SCHEMES = {
12
+ "https" => %w(https http).freeze,
13
+ "http" => %w(http).freeze,
14
+ }.freeze
15
+
16
+ module_function
17
+
18
+ # Opens a connection to the host of `uri` (an Addressable::URI) and
19
+ # yields the Net::HTTP session.
20
+ def start(uri, use_ssl: uri.scheme == "https", env: ENV, &block)
21
+ proxy = proxy_uri(uri, env)
22
+ Net::HTTP.start(
23
+ uri.host, uri.port,
24
+ proxy&.host, proxy&.port, proxy&.user, proxy&.password,
25
+ :use_ssl => use_ssl,
26
+ :open_timeout => OPEN_TIMEOUT,
27
+ :read_timeout => READ_TIMEOUT,
28
+ &block
29
+ )
30
+ end
31
+
32
+ # The proxy to use for `uri`, from the environment, or nil. HTTPS
33
+ # requests use https_proxy, falling back to http_proxy. Hosts listed in
34
+ # no_proxy/NO_PROXY, and loopback addresses, bypass the proxy.
35
+ def proxy_uri(uri, env = ENV)
36
+ PROXY_SCHEMES.fetch(uri.scheme, PROXY_SCHEMES["http"]).each do |scheme|
37
+ target = URI::Generic.build(:scheme => scheme, :host => uri.host,
38
+ :port => uri.inferred_port)
39
+ proxy = target.find_proxy(env)
40
+ return proxy if proxy
41
+ end
42
+ nil
43
+ rescue URI::Error
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ end
@@ -20,11 +20,13 @@ module Jekyll
20
20
  end
21
21
 
22
22
  Jekyll.logger.info LOG_KEY, "Using theme #{theme.name_with_owner}"
23
+ # after_reset fires on every rebuild (e.g. `jekyll serve`). Only the
24
+ # first run downloads the theme, so only it registers the cleanup.
23
25
  unless munged?
24
26
  downloader.run
25
27
  configure_theme
28
+ enqueue_theme_cleanup
26
29
  end
27
- enqueue_theme_cleanup
28
30
 
29
31
  theme
30
32
  end
@@ -35,8 +37,10 @@ module Jekyll
35
37
  site.theme&.is_a?(Jekyll::RemoteTheme::Theme)
36
38
  end
37
39
 
40
+ # On a rebuild, reuse the theme configured by the first run rather than
41
+ # building a new one, which would create another temp directory.
38
42
  def theme
39
- @theme ||= Theme.new(raw_theme)
43
+ @theme ||= munged? ? site.theme : Theme.new(raw_theme)
40
44
  end
41
45
 
42
46
  def raw_theme
@@ -171,11 +171,7 @@ module Jekyll
171
171
  end
172
172
 
173
173
  def make_api_request(api_url)
174
- Net::HTTP.start(
175
- api_url.host,
176
- api_url.port,
177
- :use_ssl => api_url.scheme == "https"
178
- ) do |http|
174
+ HTTP.start(api_url) do |http|
179
175
  request = Net::HTTP::Get.new(api_url.request_uri)
180
176
  request["Accept"] = "application/vnd.github.v3+json"
181
177
  request["User-Agent"] = Downloader::USER_AGENT
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module RemoteTheme
5
- VERSION = "0.5.2"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -16,6 +16,7 @@ module Jekyll
16
16
  class DownloadError < StandardError; end
17
17
 
18
18
  autoload :Downloader, "jekyll-remote-theme/downloader"
19
+ autoload :HTTP, "jekyll-remote-theme/http"
19
20
  autoload :MockGemspec, "jekyll-remote-theme/mock_gemspec"
20
21
  autoload :Munger, "jekyll-remote-theme/munger"
21
22
  autoload :Theme, "jekyll-remote-theme/theme"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-remote-theme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Balter
@@ -279,6 +279,7 @@ extra_rdoc_files: []
279
279
  files:
280
280
  - lib/jekyll-remote-theme.rb
281
281
  - lib/jekyll-remote-theme/downloader.rb
282
+ - lib/jekyll-remote-theme/http.rb
282
283
  - lib/jekyll-remote-theme/mock_gemspec.rb
283
284
  - lib/jekyll-remote-theme/munger.rb
284
285
  - lib/jekyll-remote-theme/theme.rb
@@ -294,14 +295,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
294
295
  requirements:
295
296
  - - ">="
296
297
  - !ruby/object:Gem::Version
297
- version: 2.7.0
298
+ version: '3.0'
298
299
  required_rubygems_version: !ruby/object:Gem::Requirement
299
300
  requirements:
300
301
  - - ">="
301
302
  - !ruby/object:Gem::Version
302
303
  version: '0'
303
304
  requirements: []
304
- rubygems_version: 4.0.16
305
+ rubygems_version: 3.6.9
305
306
  specification_version: 4
306
307
  summary: Jekyll plugin for building Jekyll sites with any GitHub-hosted theme
307
308
  test_files: []