jekyll-remote-theme 0.5.1 → 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: 25fe811cc5335e5f2932bc6db5e74c83b2f7984229d745d13a6ba850396f3e44
4
- data.tar.gz: 617b6d962ddea1194a7f0f801124756deada8b6a0960ba9ecabf11bdebb2ef4c
3
+ metadata.gz: e8b60407212cb023740ee7d4258e9a02c862bed37246db406607af4d3e6228e8
4
+ data.tar.gz: 448d9d30e4f0e0a2efa300c1ab7bdd8199ff82fcc4afd97264227892d6ab5fe3
5
5
  SHA512:
6
- metadata.gz: f36daa0ed584f6c5c25f0006b0eab5075c35fc252e7bba19fcbd72a8b225217545c11da7d38dd00c82cbff666cb42fdce3b1d440b4e610cfbc3f74c69f127a80
7
- data.tar.gz: a78d981b482904e7f673235a18c5e9a058f33fb0e0714dbeee2788abf64e60552b44b05550634538555daf0fbbd7c95350590f1a2a5d278d5a2630de1f27c234
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
@@ -97,67 +107,65 @@ module Jekyll
97
107
  # File IO is already open, rewind pointer to start of file to read
98
108
  zip_file.rewind
99
109
 
110
+ # Extract each entry by hand rather than calling Zip::Entry#extract,
111
+ # whose signature changed incompatibly in rubyzip 3.0 (the destination
112
+ # is now resolved relative to a `destination_directory:` keyword,
113
+ # mangling the absolute paths we pass). Reading each entry's stream and
114
+ # writing it ourselves behaves identically across rubyzip 1.x–3.x.
115
+ @extracted_size = 0
100
116
  Zip::File.open(zip_file) do |archive|
101
- archive.each { |file| file.extract path_without_name_and_ref(file.name) }
117
+ archive.each { |entry| extract_entry(entry) }
102
118
  end
103
119
  ensure
104
120
  zip_file.close
105
121
  zip_file.unlink
106
122
  end
107
123
 
108
- # Full URL to codeload zip download endpoint for the given theme
109
- def zip_url
110
- @zip_url ||= Addressable::URI.new(
111
- :scheme => theme.scheme,
112
- :host => "codeload.#{theme.host}",
113
- :path => [theme.owner, theme.name, "zip", theme.git_ref].join("/")
114
- ).normalize
115
- end
124
+ # Writes a single zip entry to its destination within theme.root.
125
+ # path_without_name_and_ref sandboxes the destination, guarding against
126
+ # Zip Slip.
127
+ def extract_entry(entry)
128
+ return if entry.name.end_with?("/") # skip directory entries
116
129
 
117
- # Returns an HTTP class that respects proxy environment variables
118
- def http_class
119
- @http_class ||= Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass)
120
- end
121
-
122
- # Extracts proxy settings from environment variables
123
- def proxy_uri
124
- return @proxy_uri if defined?(@proxy_uri)
125
-
126
- proxy_env = find_proxy_env_var
127
- @proxy_uri = parse_proxy_uri(proxy_env)
128
- end
129
-
130
- def find_proxy_env_var
131
- # Check for HTTPS proxy first if the URL uses HTTPS
132
- if theme.scheme == "https"
133
- ENV["https_proxy"] || ENV["HTTPS_PROXY"] || ENV["http_proxy"] || ENV["HTTP_PROXY"]
134
- else
135
- ENV["http_proxy"] || ENV["HTTP_PROXY"]
130
+ enforce_max_extracted_size(@extracted_size + entry.size)
131
+ dest = path_without_name_and_ref(entry.name)
132
+ FileUtils.mkdir_p File.dirname(dest)
133
+ File.open(dest, "wb") do |output|
134
+ entry.get_input_stream { |input| copy_entry(entry, input, output) }
136
135
  end
137
136
  end
138
137
 
139
- def parse_proxy_uri(proxy_env)
140
- return nil unless proxy_env
141
-
142
- Addressable::URI.parse(proxy_env)
143
- rescue Addressable::URI::InvalidURIError
144
- nil
145
- 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
146
149
 
147
- def proxy_host
148
- proxy_uri&.host
150
+ @extracted_size += chunk.bytesize
151
+ enforce_max_extracted_size(@extracted_size)
152
+ output.write(chunk)
153
+ end
149
154
  end
150
155
 
151
- def proxy_port
152
- proxy_uri&.port
153
- end
156
+ def enforce_max_extracted_size(size)
157
+ return unless size > MAX_EXTRACTED_SIZE
154
158
 
155
- def proxy_user
156
- proxy_uri&.user
159
+ raise DownloadError, "Maximum extracted theme size of #{MAX_EXTRACTED_SIZE} bytes exceeded"
157
160
  end
158
161
 
159
- def proxy_pass
160
- 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
161
169
  end
162
170
 
163
171
  def theme_dir_exists?
@@ -165,7 +173,7 @@ module Jekyll
165
173
  end
166
174
 
167
175
  def theme_dir_empty?
168
- Dir["#{theme.root}/*"].empty?
176
+ Dir.empty?(theme.root)
169
177
  end
170
178
 
171
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
@@ -63,13 +67,22 @@ module Jekyll
63
67
  end
64
68
 
65
69
  # Initialize GitHub metadata munger if it was loaded after :after_init hook
70
+ # or if the existing munger needs to be updated for the current site
66
71
  def initialize_github_metadata
67
72
  return unless defined?(Jekyll::GitHubMetadata::SiteGitHubMunger)
68
- return if Jekyll::GitHubMetadata::SiteGitHubMunger.global_munger
69
73
 
70
- munger = Jekyll::GitHubMetadata::SiteGitHubMunger.new(site)
71
- munger.munge!
72
- Jekyll::GitHubMetadata::SiteGitHubMunger.global_munger = munger
74
+ # Create a new munger if one doesn't exist or if it's for a different site
75
+ current_munger = Jekyll::GitHubMetadata::SiteGitHubMunger.global_munger
76
+ needs_init = current_munger.nil? ||
77
+ (Jekyll::GitHubMetadata.respond_to?(:site) &&
78
+ Jekyll::GitHubMetadata.site != site)
79
+
80
+ if needs_init
81
+ Jekyll.logger.debug LOG_KEY, "Initializing GitHub metadata munger"
82
+ munger = Jekyll::GitHubMetadata::SiteGitHubMunger.new(site)
83
+ munger.munge!
84
+ Jekyll::GitHubMetadata::SiteGitHubMunger.global_munger = munger
85
+ end
73
86
  end
74
87
 
75
88
  def enqueue_theme_cleanup
@@ -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.1"
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.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Balter
@@ -92,7 +92,7 @@ dependencies:
92
92
  version: 1.3.0
93
93
  - - "<"
94
94
  - !ruby/object:Gem::Version
95
- version: '3.0'
95
+ version: '4.0'
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
@@ -102,7 +102,7 @@ dependencies:
102
102
  version: 1.3.0
103
103
  - - "<"
104
104
  - !ruby/object:Gem::Version
105
- version: '3.0'
105
+ version: '4.0'
106
106
  - !ruby/object:Gem::Dependency
107
107
  name: jekyll_test_plugin_malicious
108
108
  requirement: !ruby/object:Gem::Requirement
@@ -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.10
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: []