geminabox 3.1.1 → 4.0.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rubygems/util'
4
+
3
5
  module Geminabox
4
6
 
5
7
  # This class represents a sorted collection of Geminabox::GemVersion objects.
@@ -7,6 +9,16 @@ module Geminabox
7
9
  class GemVersionCollection
8
10
  include Enumerable
9
11
 
12
+ # Loads the collection recorded in the legacy Marshal specs indexes
13
+ # (specs.4.8.gz and prerelease_specs.4.8.gz) under data_dir.
14
+ def self.from_specs_index(data_dir)
15
+ specs = %w[specs prerelease_specs].flat_map do |type|
16
+ path = File.join(data_dir, "#{type}.#{Gem.marshal_version}.gz")
17
+ File.exist?(path) ? Marshal.load(Gem::Util.gunzip(Gem.read_binary(path))) : []
18
+ end
19
+ new(specs.uniq)
20
+ end
21
+
10
22
  # Array of Geminabox::GemVersion objects, or an array of [name, version,
11
23
  # platform] triples.
12
24
  def initialize(initial_gems=[])
@@ -9,10 +9,7 @@ module Geminabox
9
9
 
10
10
  def serve
11
11
  headers["Cache-Control"] = 'no-transform'
12
- path = File.expand_path(File.join(Geminabox.data, *request.path_info))
13
- # Rack::Protection normally strips ".." first; this holds without it.
14
- halt 404 unless path.start_with?(File.expand_path(Geminabox.data) + File::SEPARATOR)
15
- send_file(path, :type => response['Content-Type'])
12
+ send_file(File.expand_path(File.join(Geminabox.data, *request.path_info)), :type => response['Content-Type'])
16
13
  end
17
14
 
18
15
  %w[/specs.4.8.gz
@@ -26,7 +23,10 @@ module Geminabox
26
23
  end
27
24
 
28
25
  get '/quick/Marshal.4.8/*.gemspec.rz' do
29
- content_type('application/x-deflate')
26
+ # rubygems.org serves the deflated quick specs as octet-stream, and the
27
+ # compact-index conformance suite asserts it; clients inflate the body
28
+ # regardless of this header.
29
+ content_type('application/octet-stream')
30
30
  serve
31
31
  end
32
32
 
@@ -26,12 +26,8 @@ module Geminabox
26
26
  File.open(@tempfile.path, "rb")
27
27
  end
28
28
 
29
- # spec comes from the uploaded archive and RubyGems does not validate it on
30
- # read, so the name must be checked before it becomes part of a path.
31
29
  def valid?
32
- spec && spec.name && spec.version &&
33
- spec.name.match?(Gem::Specification::VALID_NAME_PATTERN) &&
34
- File.basename(name) == name
30
+ spec && spec.name && spec.version
35
31
  rescue Gem::Package::Error
36
32
  false
37
33
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'reentrant_flock'
4
4
  require 'rubygems/util'
5
+ require 'rss'
5
6
 
6
7
  module Geminabox
7
8
 
@@ -11,12 +12,7 @@ module Geminabox
11
12
  set :views, Geminabox.views
12
13
  set :host_authorization, { permitted_hosts: [] }
13
14
 
14
- if Geminabox.rubygems_proxy
15
- Geminabox.warn_rubygems_proxy_deprecation
16
- use Proxy::Hostess
17
- else
18
- use Hostess
19
- end
15
+ use Hostess
20
16
 
21
17
  class << self
22
18
  def disallow_replace?
@@ -43,6 +39,7 @@ module Geminabox
43
39
  if force_rebuild
44
40
  indexer.generate_index
45
41
  dependency_cache.flush
42
+ compact_indexer.reindex
46
43
  else
47
44
  begin
48
45
  require 'geminabox/indexer'
@@ -50,6 +47,7 @@ module Geminabox
50
47
  return if updated_gemspecs.empty?
51
48
  indexer.update_index
52
49
  updated_gemspecs.each { |gem| dependency_cache.flush_key(gem.name) }
50
+ compact_indexer.reindex
53
51
  rescue Errno::ENOENT
54
52
  with_rlock { reindex(:force_rebuild) }
55
53
  rescue => e
@@ -69,6 +67,10 @@ module Geminabox
69
67
  @dependency_cache ||= Geminabox::DiskCache.new(File.join(Geminabox.data, "_cache"))
70
68
  end
71
69
 
70
+ def compact_indexer
71
+ Geminabox::CompactIndexer.new(Geminabox.data)
72
+ end
73
+
72
74
  def with_rlock(&block)
73
75
  file_class.open(Geminabox.lockfile, File::RDWR | File::CREAT) do |f|
74
76
  ReentrantFlock.synchronize(f, File::LOCK_EX | File::LOCK_NB, &block)
@@ -91,6 +93,7 @@ module Geminabox
91
93
  end
92
94
 
93
95
  get '/' do
96
+ content_type :html
94
97
  @gems = load_gems
95
98
  @index_gems = index_gems(@gems)
96
99
  @allow_upload = self.class.allow_upload?
@@ -99,15 +102,40 @@ module Geminabox
99
102
  end
100
103
 
101
104
  get '/atom.xml' do
105
+ content_type 'application/atom+xml'
102
106
  @gems = load_gems
103
107
  erb :atom, :layout => false
104
108
  end
105
109
 
110
+ helpers Geminabox::CompactIndexApi
111
+
112
+ get '/versions' do
113
+ bootstrap_compact_index(self.class.compact_indexer.versions_path)
114
+ serve_compact_file(self.class.compact_indexer.versions_path)
115
+ end
116
+
117
+ get '/names' do
118
+ bootstrap_compact_index(self.class.compact_indexer.names_path)
119
+ serve_compact_file(self.class.compact_indexer.names_path)
120
+ end
121
+
122
+ get '/info/:name' do
123
+ # Reject anything that is not a gem-name shape before touching the
124
+ # filesystem. This also blocks NUL bytes and empty names, which would
125
+ # otherwise raise ArgumentError from File.file? and 500.
126
+ halt 404 unless params[:name] =~ /\A[a-zA-Z0-9_.-]+\z/
127
+ path = self.class.compact_indexer.info_path(params[:name])
128
+ heal_missing_info(params[:name], path) unless File.file?(path)
129
+ serve_compact_file(path)
130
+ end
131
+
106
132
  get '/api/v1/dependencies' do
133
+ content_type 'application/octet-stream'
107
134
  query_gems.any? ? Marshal.dump(gem_list) : 200
108
135
  end
109
136
 
110
137
  get '/api/v1/dependencies.json' do
138
+ content_type :json
111
139
  query_gems.any? ? gem_list.to_json : {}
112
140
  end
113
141
 
@@ -116,6 +144,7 @@ module Geminabox
116
144
  error_response(403, 'Gem uploading is disabled')
117
145
  end
118
146
 
147
+ content_type :html
119
148
  erb :upload
120
149
  end
121
150
 
@@ -136,6 +165,7 @@ module Geminabox
136
165
  @gem = gems[params[:gemname]]
137
166
  @allow_delete = self.class.allow_delete?
138
167
  halt 404 unless @gem
168
+ content_type :html
139
169
  erb :gem
140
170
  end
141
171
 
@@ -144,9 +174,8 @@ module Geminabox
144
174
  error_response(403, 'Gem deletion is disabled - see https://github.com/geminabox/geminabox/issues/115')
145
175
  end
146
176
 
147
- path = file_path
148
177
  serialize_update do
149
- File.delete path if File.exist? path
178
+ File.delete file_path if File.exist? file_path
150
179
  self.class.reindex(:force_rebuild)
151
180
  redirect url("/")
152
181
  end
@@ -161,8 +190,16 @@ module Geminabox
161
190
  halt 400 unless request.form_data?
162
191
 
163
192
  serialize_update do
164
- gems = load_gems.select { |gem| params['gem_name'] == gem.name and
165
- params['version'] == gem.number.version }
193
+ # A yank targets one platform. The client omits the platform param for
194
+ # a plain-ruby gem, so an absent/blank value means the ruby platform;
195
+ # without this a `gem yank foo -v 1.0.0` would also destroy foo's java
196
+ # and mingw builds sharing that version.
197
+ requested_platform = params['platform'].to_s.empty? ? 'ruby' : params['platform']
198
+ gems = load_gems.select do |gem|
199
+ params['gem_name'] == gem.name &&
200
+ params['version'] == gem.number.version &&
201
+ (gem.ruby? ? 'ruby' : gem.platform) == requested_platform
202
+ end
166
203
  halt 404, 'Gem not found' if gems.size == 0
167
204
  gems.each do |gem|
168
205
  gem_path = File.expand_path(File.join(Geminabox.data, 'gems',
@@ -217,6 +254,22 @@ module Geminabox
217
254
  self.class.with_rlock(&block)
218
255
  end
219
256
 
257
+ def bootstrap_compact_index(path)
258
+ return if File.exist?(path)
259
+ serialize_update { self.class.compact_indexer.reindex }
260
+ end
261
+
262
+ # Rebuild info/NAME on the read path when it is missing but versions.list
263
+ # still lists the gem, so a partially corrupted index does not 404 a gem
264
+ # Bundler was told exists. The ledger-membership check reads only
265
+ # versions.list and takes no lock, so an unknown name stays a cheap 404
266
+ # with no per-gem stats -- arbitrary /info probes cannot force writes.
267
+ def heal_missing_info(name, path)
268
+ indexer = self.class.compact_indexer
269
+ return unless indexer.ledger_lists?(name)
270
+ serialize_update { indexer.heal_info(name) unless File.file?(path) }
271
+ end
272
+
220
273
  def handle_incoming_gem(gem)
221
274
  begin
222
275
  GemStore.create(gem, params[:overwrite])
@@ -256,46 +309,15 @@ HTML
256
309
  end
257
310
 
258
311
  def file_path
259
- path = File.expand_path(File.join(Geminabox.data, *request.path_info))
260
- # Rack::Protection normally strips ".." first; this holds without it.
261
- halt 404 unless path.start_with?(File.expand_path(Geminabox.data) + File::SEPARATOR)
262
- path
312
+ File.expand_path(File.join(Geminabox.data, *request.path_info))
263
313
  end
264
314
 
265
315
  def dependency_cache
266
316
  self.class.dependency_cache
267
317
  end
268
318
 
269
- def all_gems
270
- all_gems_with_duplicates.inject(:|)
271
- end
272
-
273
- def all_gems_with_duplicates
274
- specs_files_paths.map do |specs_file_path|
275
- if File.exist?(specs_file_path)
276
- Marshal.load(Gem::Util.gunzip(Gem.read_binary(specs_file_path)))
277
- else
278
- []
279
- end
280
- end
281
- end
282
-
283
- def specs_file_types
284
- [:specs, :prerelease_specs]
285
- end
286
-
287
- def specs_files_paths
288
- specs_file_types.map do |specs_file_type|
289
- File.join(Geminabox.data, spec_file_name(specs_file_type))
290
- end
291
- end
292
-
293
- def spec_file_name(specs_file_type)
294
- [specs_file_type, Gem.marshal_version, 'gz'].join('.')
295
- end
296
-
297
319
  def load_gems
298
- @loaded_gems ||= Geminabox::GemVersionCollection.new(all_gems)
320
+ @loaded_gems ||= Geminabox::GemVersionCollection.from_specs_index(Geminabox.data)
299
321
  end
300
322
 
301
323
  def index_gems(gems)
@@ -303,25 +325,13 @@ HTML
303
325
  end
304
326
 
305
327
  def gem_list
306
- Geminabox.rubygems_proxy ? combined_gem_list : local_gem_list
328
+ query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
307
329
  end
308
330
 
309
331
  def query_gems
310
332
  params[:gems].to_s.split(',')
311
333
  end
312
334
 
313
- def local_gem_list
314
- query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
315
- end
316
-
317
- def remote_gem_list
318
- RubygemsDependency.for(*query_gems)
319
- end
320
-
321
- def combined_gem_list
322
- GemListMerge.merge(local_gem_list, remote_gem_list, strategy: Geminabox.rubygems_proxy_merge_strategy)
323
- end
324
-
325
335
  helpers do
326
336
  def href(text)
327
337
  if text && (text.start_with?('http://') || text.start_with?('https://'))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geminabox
4
- VERSION = '3.1.1' unless defined? VERSION
4
+ VERSION = '4.0.0' unless defined? VERSION
5
5
  end
data/lib/geminabox.rb CHANGED
@@ -17,7 +17,6 @@ module Geminabox
17
17
  class Error < StandardError ; end
18
18
 
19
19
  require_relative 'geminabox/version'
20
- require_relative 'geminabox/proxy'
21
20
  require_relative 'geminabox/http_adapter'
22
21
 
23
22
  def self.geminabox_path(file)
@@ -27,10 +26,10 @@ module Geminabox
27
26
  autoload :Hostess, geminabox_path('hostess')
28
27
  autoload :GemStore, geminabox_path('gem_store')
29
28
  autoload :GemStoreError, geminabox_path('gem_store_error')
30
- autoload :RubygemsDependency, geminabox_path('rubygems_dependency')
31
- autoload :GemListMerge, geminabox_path('gem_list_merge')
32
29
  autoload :GemVersion, geminabox_path('gem_version')
33
30
  autoload :GemVersionCollection, geminabox_path('gem_version_collection')
31
+ autoload :CompactIndexer, geminabox_path('compact_indexer')
32
+ autoload :CompactIndexApi, geminabox_path('compact_index_api')
34
33
  autoload :Server, geminabox_path('server')
35
34
  autoload :DiskCache, geminabox_path('disk_cache')
36
35
  autoload :IncomingGem, geminabox_path('incoming_gem')
@@ -45,14 +44,9 @@ module Geminabox
45
44
  :allow_replace,
46
45
  :gem_permissions,
47
46
  :allow_delete,
48
- :rubygems_proxy,
49
- :rubygems_proxy_merge_strategy,
50
47
  :http_adapter,
51
48
  :lockfile,
52
49
  :retry_interval,
53
- :allow_remote_failure,
54
- :ruby_gems_url,
55
- :bundler_ruby_gems_url,
56
50
  :allow_upload,
57
51
  :on_gem_received
58
52
  )
@@ -68,17 +62,6 @@ module Geminabox
68
62
  Server.settings
69
63
  end
70
64
 
71
- # Deprecated: the RubyGems proxy is unmaintained and will be removed in
72
- # Geminabox 4.0. It depends on the RubyGems.org Dependency API
73
- # (/api/v1/dependencies), which was sunset on 2023-05-24, so proxy mode no
74
- # longer functions.
75
- def warn_rubygems_proxy_deprecation
76
- warn '[DEPRECATION] Geminabox RubyGems proxy is deprecated and will be ' \
77
- 'removed in Geminabox 4.0. It depends on the RubyGems.org ' \
78
- 'Dependency API (/api/v1/dependencies), which was sunset on ' \
79
- '2023-05-24, so proxy mode no longer functions.'
80
- end
81
-
82
65
  def call(env)
83
66
  Server.call env
84
67
  end
@@ -91,17 +74,38 @@ module Geminabox
91
74
  views: File.join(File.dirname(__FILE__), *%w[.. views]),
92
75
  allow_replace: false,
93
76
  gem_permissions: 0644,
94
- rubygems_proxy: (ENV['RUBYGEMS_PROXY'] == 'true'),
95
- rubygems_proxy_merge_strategy: ENV.fetch('RUBYGEMS_PROXY_MERGE_STRATEGY') { :local_gems_take_precedence_over_remote_gems }.to_sym,
96
77
  allow_delete: true,
97
78
  http_adapter: HttpClientAdapter.new,
98
79
  lockfile: File.join(ENV.fetch('TMPDIR', Dir.tmpdir), 'geminabox.lockfile'),
99
80
  retry_interval: 60,
100
- allow_remote_failure: false,
101
- ruby_gems_url: 'https://rubygems.org/',
102
- bundler_ruby_gems_url: 'https://bundler.rubygems.org/',
103
81
  allow_upload: true,
104
82
  on_gem_received: nil
105
83
  )
106
84
 
85
+ # Removed in 4.0 with the RubyGems proxy. Warn-and-ignore (rather than
86
+ # NoMethodError at boot) so stale config.ru lines don't break upgrades.
87
+ # Drop these shims in 5.0.
88
+ [
89
+ :rubygems_proxy,
90
+ :rubygems_proxy_merge_strategy,
91
+ :allow_remote_failure,
92
+ :ruby_gems_url,
93
+ :bundler_ruby_gems_url
94
+ ].each do |setting|
95
+ define_singleton_method("#{setting}=") do |_value|
96
+ warn "[REMOVED] Geminabox.#{setting} was removed in Geminabox 4.0 " \
97
+ 'along with the RubyGems proxy and has no effect. Remove this ' \
98
+ 'line from your config.ru. Migration notes: ' \
99
+ 'https://github.com/geminabox/geminabox/pull/735'
100
+ end
101
+ define_singleton_method(setting) { nil }
102
+ end
103
+
104
+ if ENV['RUBYGEMS_PROXY'] == 'true' || ENV.key?('RUBYGEMS_PROXY_MERGE_STRATEGY')
105
+ warn '[REMOVED] The RUBYGEMS_PROXY and RUBYGEMS_PROXY_MERGE_STRATEGY ' \
106
+ 'environment variables were removed in Geminabox 4.0; Geminabox ' \
107
+ 'now always serves only local gems. Migration notes: ' \
108
+ 'https://github.com/geminabox/geminabox/pull/735'
109
+ end
110
+
107
111
  end