gemstash 1.0.0.pre.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +13 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +139 -0
  6. data/Rakefile +35 -0
  7. data/bin/console +14 -0
  8. data/bin/gemstash +3 -0
  9. data/bin/setup +5 -0
  10. data/docs/config.md +136 -0
  11. data/docs/debug.md +24 -0
  12. data/docs/deploy.md +30 -0
  13. data/docs/mirror.md +30 -0
  14. data/docs/multiple_sources.md +68 -0
  15. data/docs/private_gems.md +140 -0
  16. data/docs/reference.md +308 -0
  17. data/exe/gemstash +3 -0
  18. data/gemstash.gemspec +47 -0
  19. data/gemstash.png +0 -0
  20. data/lib/gemstash.rb +26 -0
  21. data/lib/gemstash/authorization.rb +87 -0
  22. data/lib/gemstash/cache.rb +79 -0
  23. data/lib/gemstash/cli.rb +71 -0
  24. data/lib/gemstash/cli/authorize.rb +69 -0
  25. data/lib/gemstash/cli/base.rb +46 -0
  26. data/lib/gemstash/cli/setup.rb +173 -0
  27. data/lib/gemstash/cli/start.rb +52 -0
  28. data/lib/gemstash/cli/status.rb +21 -0
  29. data/lib/gemstash/cli/stop.rb +21 -0
  30. data/lib/gemstash/config.ru +13 -0
  31. data/lib/gemstash/configuration.rb +41 -0
  32. data/lib/gemstash/db.rb +15 -0
  33. data/lib/gemstash/db/authorization.rb +20 -0
  34. data/lib/gemstash/db/dependency.rb +50 -0
  35. data/lib/gemstash/db/rubygem.rb +14 -0
  36. data/lib/gemstash/db/version.rb +51 -0
  37. data/lib/gemstash/dependencies.rb +93 -0
  38. data/lib/gemstash/env.rb +150 -0
  39. data/lib/gemstash/gem_fetcher.rb +50 -0
  40. data/lib/gemstash/gem_pusher.rb +125 -0
  41. data/lib/gemstash/gem_source.rb +37 -0
  42. data/lib/gemstash/gem_source/dependency_caching.rb +40 -0
  43. data/lib/gemstash/gem_source/private_source.rb +139 -0
  44. data/lib/gemstash/gem_source/rack_middleware.rb +22 -0
  45. data/lib/gemstash/gem_source/upstream_source.rb +183 -0
  46. data/lib/gemstash/gem_unyanker.rb +61 -0
  47. data/lib/gemstash/gem_yanker.rb +61 -0
  48. data/lib/gemstash/http_client.rb +77 -0
  49. data/lib/gemstash/logging.rb +93 -0
  50. data/lib/gemstash/migrations/01_gem_dependencies.rb +41 -0
  51. data/lib/gemstash/migrations/02_authorizations.rb +12 -0
  52. data/lib/gemstash/puma.rb +6 -0
  53. data/lib/gemstash/rack_env_rewriter.rb +66 -0
  54. data/lib/gemstash/specs_builder.rb +93 -0
  55. data/lib/gemstash/storage.rb +207 -0
  56. data/lib/gemstash/upstream.rb +65 -0
  57. data/lib/gemstash/version.rb +4 -0
  58. data/lib/gemstash/web.rb +97 -0
  59. metadata +304 -0
@@ -0,0 +1,50 @@
1
+ require "gemstash"
2
+ require "set"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ class GemFetcher
7
+ def initialize(http_client)
8
+ @http_client = http_client
9
+ @valid_headers = Set.new(["etag", "content-type", "content-length", "last-modified"])
10
+ end
11
+
12
+ def fetch(gem_id, type, &block)
13
+ @http_client.get(path_for(gem_id, type)) do |body, headers|
14
+ properties = filter_headers(headers)
15
+ validate_download(body, properties)
16
+ yield body, properties
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def path_for(gem_id, type)
23
+ case type
24
+ when :gem
25
+ "gems/#{gem_id}"
26
+ when :spec
27
+ "quick/Marshal.4.8/#{gem_id}"
28
+ else
29
+ raise "Invalid type #{type.inspect}"
30
+ end
31
+ end
32
+
33
+ def filter_headers(headers)
34
+ headers.inject({}) do|properties, (key, value)|
35
+ properties[key.downcase] = value if @valid_headers.include?(key.downcase)
36
+ properties
37
+ end
38
+ end
39
+
40
+ def validate_download(content, headers)
41
+ expected_size = content_length(headers)
42
+ raise "Incomplete download, only #{body.length} was downloaded out of #{expected_size}" \
43
+ if content.length < expected_size
44
+ end
45
+
46
+ def content_length(headers)
47
+ headers["content-length"].to_i
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,125 @@
1
+ require "gemstash"
2
+ require "rubygems/package"
3
+ require "stringio"
4
+
5
+ #:nodoc:
6
+ module Gemstash
7
+ # Class that supports pushing a new gem to the private repository of gems.
8
+ class GemPusher
9
+ include Gemstash::Env::Helper
10
+
11
+ # This error is thrown when pushing to an existing version.
12
+ class ExistingVersionError < StandardError
13
+ end
14
+
15
+ # This error is thrown when pushing to a yanked version.
16
+ class YankedVersionError < ExistingVersionError
17
+ end
18
+
19
+ def initialize(auth_key, content)
20
+ @auth_key = auth_key
21
+ @content = content
22
+ end
23
+
24
+ def push
25
+ check_auth
26
+ store_gem
27
+ store_gemspec
28
+ save_to_database
29
+ invalidate_cache
30
+ end
31
+
32
+ private
33
+
34
+ def gem
35
+ @gem ||= Gem::Package.new(StringIO.new(@content))
36
+ end
37
+
38
+ def storage
39
+ @storage ||= Gemstash::Storage.for("private").for("gems")
40
+ end
41
+
42
+ def full_name
43
+ @full_name ||= gem.spec.full_name
44
+ end
45
+
46
+ def check_auth
47
+ Gemstash::Authorization.check(@auth_key, "push")
48
+ end
49
+
50
+ def store_gem
51
+ storage.resource(full_name).save({ gem: @content }, indexed: true)
52
+ end
53
+
54
+ def store_gemspec
55
+ spec = gem.spec
56
+ spec = Marshal.dump(spec)
57
+ spec = Zlib::Deflate.deflate(spec)
58
+ storage.resource(full_name).save(spec: spec)
59
+ end
60
+
61
+ def save_to_database
62
+ spec = gem.spec
63
+
64
+ gemstash_env.db.transaction do
65
+ gem_id = Gemstash::DB::Rubygem.find_or_insert(spec)
66
+ existing = Gemstash::DB::Version.find_by_spec(gem_id, spec)
67
+
68
+ if existing
69
+ if existing.indexed
70
+ raise ExistingVersionError, "Cannot push to an existing version!"
71
+ else
72
+ raise YankedVersionError, "Cannot push to a yanked version!"
73
+ end
74
+ end
75
+
76
+ version_id = Gemstash::DB::Version.insert_by_spec(gem_id, spec)
77
+ Gemstash::DB::Dependency.insert_by_spec(version_id, spec)
78
+ end
79
+ end
80
+
81
+ def invalidate_cache
82
+ gemstash_env.cache.invalidate_gem("private", gem.spec.name)
83
+ end
84
+ end
85
+
86
+ unless Gem::Requirement.new(">= 2.4").satisfied_by?(Gem::Version.new(Gem::VERSION))
87
+ require "tempfile"
88
+
89
+ # Adds support for legacy versions of RubyGems
90
+ module LegacyRubyGemsSupport
91
+ def self.included(base)
92
+ base.class_eval do
93
+ alias_method :push_without_cleanup, :push
94
+ remove_method :push
95
+ remove_method :gem
96
+ end
97
+ end
98
+
99
+ def push
100
+ push_without_cleanup
101
+ ensure
102
+ cleanup
103
+ end
104
+
105
+ private
106
+
107
+ def gem
108
+ @gem ||= begin
109
+ @tempfile = Tempfile.new("gemstash-gem")
110
+ @tempfile.write(@content)
111
+ @tempfile.flush
112
+ Gem::Package.new(@tempfile.path)
113
+ end
114
+ end
115
+
116
+ def cleanup
117
+ return unless @tempfile
118
+ @tempfile.close
119
+ @tempfile.unlink
120
+ end
121
+ end
122
+
123
+ GemPusher.send(:include, LegacyRubyGemsSupport)
124
+ end
125
+ end
@@ -0,0 +1,37 @@
1
+ require "gemstash"
2
+ require "forwardable"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ module GemSource
7
+ autoload :DependencyCaching, "gemstash/gem_source/dependency_caching"
8
+ autoload :PrivateSource, "gemstash/gem_source/private_source"
9
+ autoload :RackMiddleware, "gemstash/gem_source/rack_middleware"
10
+ autoload :RedirectSource, "gemstash/gem_source/upstream_source"
11
+ autoload :RubygemsSource, "gemstash/gem_source/upstream_source"
12
+ autoload :UpstreamSource, "gemstash/gem_source/upstream_source"
13
+
14
+ def self.sources
15
+ @sources ||= [
16
+ Gemstash::GemSource::PrivateSource,
17
+ Gemstash::GemSource::RedirectSource,
18
+ Gemstash::GemSource::UpstreamSource,
19
+ Gemstash::GemSource::RubygemsSource
20
+ ]
21
+ end
22
+
23
+ # Base GemSource for some common utilities.
24
+ class Base
25
+ extend Forwardable
26
+ extend Gemstash::Logging
27
+ include Gemstash::Logging
28
+
29
+ def_delegators :@app, :cache_control, :content_type, :env, :halt,
30
+ :headers, :http_client_for, :params, :redirect, :request
31
+
32
+ def initialize(app)
33
+ @app = app
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ module Gemstash
2
+ module GemSource
3
+ # Module for caching dependencies in a GemSource.
4
+ module DependencyCaching
5
+ API_REQUEST_LIMIT = 200
6
+
7
+ def serve_dependencies
8
+ gems = gems_from_params
9
+
10
+ if gems.length > API_REQUEST_LIMIT
11
+ halt 422, "Too many gems (use --full-index instead)"
12
+ end
13
+
14
+ content_type "application/octet-stream"
15
+ Marshal.dump dependencies.fetch(gems)
16
+ end
17
+
18
+ def serve_dependencies_json
19
+ gems = gems_from_params
20
+
21
+ if gems.length > API_REQUEST_LIMIT
22
+ halt 422, {
23
+ "error" => "Too many gems (use --full-index instead)",
24
+ "code" => 422
25
+ }.to_json
26
+ end
27
+
28
+ content_type "application/json;charset=UTF-8"
29
+ dependencies.fetch(gems).to_json
30
+ end
31
+
32
+ private
33
+
34
+ def gems_from_params
35
+ halt(200) if params[:gems].nil? || params[:gems].empty?
36
+ params[:gems].split(",").uniq
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,139 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module GemSource
5
+ # GemSource for privately stored gems.
6
+ class PrivateSource < Gemstash::GemSource::Base
7
+ include Gemstash::GemSource::DependencyCaching
8
+ include Gemstash::Env::Helper
9
+
10
+ def self.rack_env_rewriter
11
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/private})
12
+ end
13
+
14
+ def self.matches?(env)
15
+ rewriter = rack_env_rewriter.for(env)
16
+ return false unless rewriter.matches?
17
+ rewriter.rewrite
18
+ true
19
+ end
20
+
21
+ def serve_root
22
+ halt 403, "Not yet supported"
23
+ end
24
+
25
+ def serve_add_gem
26
+ authenticated("Gemstash Private Gems") do
27
+ auth = request.env["HTTP_AUTHORIZATION"]
28
+ gem = request.body.read
29
+ Gemstash::GemPusher.new(auth, gem).push
30
+ end
31
+ end
32
+
33
+ def serve_yank
34
+ authenticated("Gemstash Private Gems") do
35
+ auth = request.env["HTTP_AUTHORIZATION"]
36
+ gem_name = params[:gem_name]
37
+ Gemstash::GemYanker.new(auth, gem_name, slug_param).yank
38
+ end
39
+ end
40
+
41
+ def serve_unyank
42
+ authenticated("Gemstash Private Gems") do
43
+ auth = request.env["HTTP_AUTHORIZATION"]
44
+ gem_name = params[:gem_name]
45
+ Gemstash::GemUnyanker.new(auth, gem_name, slug_param).unyank
46
+ end
47
+ end
48
+
49
+ def serve_add_spec_json
50
+ halt 403, "Not yet supported"
51
+ end
52
+
53
+ def serve_remove_spec_json
54
+ halt 403, "Not yet supported"
55
+ end
56
+
57
+ def serve_names
58
+ halt 403, "Not yet supported"
59
+ end
60
+
61
+ def serve_versions
62
+ halt 403, "Not yet supported"
63
+ end
64
+
65
+ def serve_info(name)
66
+ halt 403, "Not yet supported"
67
+ end
68
+
69
+ def serve_marshal(id)
70
+ gem_full_name = id.sub(/\.gemspec\.rz\z/, "")
71
+ gem = fetch_gem(gem_full_name)
72
+ halt 404 unless gem.exist?(:spec)
73
+ content_type "application/octet-stream"
74
+ gem.load(:spec).content(:spec)
75
+ end
76
+
77
+ def serve_actual_gem(id)
78
+ halt 403, "Not yet supported"
79
+ end
80
+
81
+ def serve_gem(id)
82
+ gem_full_name = id.sub(/\.gem\z/, "")
83
+ gem = fetch_gem(gem_full_name)
84
+ content_type "application/octet-stream"
85
+ gem.content(:gem)
86
+ end
87
+
88
+ def serve_latest_specs
89
+ halt 403, "Not yet supported"
90
+ end
91
+
92
+ def serve_specs
93
+ content_type "application/octet-stream"
94
+ Gemstash::SpecsBuilder.all
95
+ end
96
+
97
+ def serve_prerelease_specs
98
+ content_type "application/octet-stream"
99
+ Gemstash::SpecsBuilder.prerelease
100
+ end
101
+
102
+ private
103
+
104
+ def slug_param
105
+ version = params[:version]
106
+ platform = params[:platform]
107
+
108
+ if platform.to_s.empty?
109
+ version
110
+ else
111
+ "#{version}-#{platform}"
112
+ end
113
+ end
114
+
115
+ def authenticated(realm)
116
+ yield
117
+ rescue Gemstash::NotAuthorizedError => e
118
+ headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
119
+ halt 401, e.message
120
+ end
121
+
122
+ def dependencies
123
+ @dependencies ||= Gemstash::Dependencies.for_private
124
+ end
125
+
126
+ def storage
127
+ @storage ||= Gemstash::Storage.for("private").for("gems")
128
+ end
129
+
130
+ def fetch_gem(gem_full_name)
131
+ gem = storage.resource(gem_full_name)
132
+ halt 404 unless gem.exist?(:gem)
133
+ gem.load(:gem)
134
+ halt 403, "That gem has been yanked" unless gem.properties[:indexed]
135
+ gem
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,22 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module GemSource
5
+ # Rack middleware to detect the gem source from the URL.
6
+ class RackMiddleware
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ Gemstash::GemSource.sources.each do |source|
13
+ next unless source.matches?(env)
14
+ env["gemstash.gem_source"] = source
15
+ break
16
+ end
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,183 @@
1
+ require "gemstash"
2
+ require "cgi"
3
+
4
+ module Gemstash
5
+ module GemSource
6
+ # GemSource that purely redirects to the upstream server.
7
+ class RedirectSource < Gemstash::GemSource::Base
8
+ def self.rack_env_rewriter
9
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/redirect/(?<upstream_url>[^/]+)})
10
+ end
11
+
12
+ def self.matches?(env)
13
+ rewriter = rack_env_rewriter.for(env)
14
+ return false unless rewriter.matches?
15
+ rewriter.rewrite
16
+ env["gemstash.upstream"] = rewriter.captures["upstream_url"]
17
+ capture_user_agent(env)
18
+ true
19
+ end
20
+
21
+ def self.capture_user_agent(env)
22
+ env["gemstash.user-agent"] = env["HTTP_USER_AGENT"]
23
+ end
24
+
25
+ def serve_root
26
+ cache_control :public, :max_age => 31_536_000
27
+ redirect upstream.url(nil, request.query_string)
28
+ end
29
+
30
+ def serve_add_gem
31
+ halt 403, "Cannot add gem to an upstream server!"
32
+ end
33
+
34
+ def serve_yank
35
+ halt 403, "Cannot yank from an upstream server!"
36
+ end
37
+
38
+ def serve_unyank
39
+ halt 403, "Cannot unyank from an upstream server!"
40
+ end
41
+
42
+ def serve_add_spec_json
43
+ halt 403, "Cannot add spec to an upstream server!"
44
+ end
45
+
46
+ def serve_remove_spec_json
47
+ halt 403, "Cannot remove spec from an upstream server!"
48
+ end
49
+
50
+ def serve_dependencies
51
+ redirect upstream.url("api/v1/dependencies", request.query_string)
52
+ end
53
+
54
+ def serve_dependencies_json
55
+ redirect upstream.url("api/v1/dependencies.json", request.query_string)
56
+ end
57
+
58
+ def serve_names
59
+ redirect upstream.url("names", request.query_string)
60
+ end
61
+
62
+ def serve_versions
63
+ redirect upstream.url("versions", request.query_string)
64
+ end
65
+
66
+ def serve_info(name)
67
+ redirect upstream.url("info/#{name}", request.query_string)
68
+ end
69
+
70
+ def serve_marshal(id)
71
+ redirect upstream.url("quick/Marshal.4.8/#{id}", request.query_string)
72
+ end
73
+
74
+ def serve_actual_gem(id)
75
+ redirect upstream.url("fetch/actual/gem/#{id}", request.query_string)
76
+ end
77
+
78
+ def serve_gem(id)
79
+ redirect upstream.url("gems/#{id}", request.query_string)
80
+ end
81
+
82
+ def serve_latest_specs
83
+ redirect upstream.url("latest_specs.4.8.gz", request.query_string)
84
+ end
85
+
86
+ def serve_specs
87
+ redirect upstream.url("specs.4.8.gz", request.query_string)
88
+ end
89
+
90
+ def serve_prerelease_specs
91
+ redirect upstream.url("prerelease_specs.4.8.gz", request.query_string)
92
+ end
93
+
94
+ private
95
+
96
+ def upstream
97
+ @upstream ||= Gemstash::Upstream.new(env["gemstash.upstream"],
98
+ user_agent: env["gemstash.user-agent"])
99
+ end
100
+ end
101
+
102
+ # GemSource for gems in an upstream server.
103
+ class UpstreamSource < Gemstash::GemSource::RedirectSource
104
+ include Gemstash::GemSource::DependencyCaching
105
+ include Gemstash::Env::Helper
106
+
107
+ def self.rack_env_rewriter
108
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/upstream/(?<upstream_url>[^/]+)})
109
+ end
110
+
111
+ def serve_marshal(id)
112
+ serve_cached(id, :spec)
113
+ end
114
+
115
+ def serve_gem(id)
116
+ serve_cached(id, :gem)
117
+ end
118
+
119
+ private
120
+
121
+ def serve_cached(id, key)
122
+ gem = fetch_gem(id, key)
123
+ headers.update(gem.properties[:headers][key]) if gem.properties[:headers] && gem.properties[:headers][key]
124
+ gem.content(key)
125
+ rescue Gemstash::WebError => e
126
+ halt e.code
127
+ end
128
+
129
+ def dependencies
130
+ @dependencies ||= begin
131
+ http_client = http_client_for(upstream)
132
+ Gemstash::Dependencies.for_upstream(upstream, http_client)
133
+ end
134
+ end
135
+
136
+ def storage
137
+ @storage ||= Gemstash::Storage.for("gem_cache")
138
+ @storage.for(upstream.host_id)
139
+ end
140
+
141
+ def gem_fetcher
142
+ @gem_fetcher ||= Gemstash::GemFetcher.new(http_client_for(upstream))
143
+ end
144
+
145
+ def fetch_gem(id, key)
146
+ gem_name = Gemstash::Upstream::GemName.new(upstream, id)
147
+ gem_resource = storage.resource(gem_name.name)
148
+ if gem_resource.exist?(key)
149
+ fetch_local_gem(gem_name, gem_resource, key)
150
+ else
151
+ fetch_remote_gem(gem_name, gem_resource, key)
152
+ end
153
+ end
154
+
155
+ def fetch_local_gem(gem_name, gem_resource, key)
156
+ log.info "Gem #{gem_name.name} exists, returning cached #{key}"
157
+ gem_resource.load(key)
158
+ end
159
+
160
+ def fetch_remote_gem(gem_name, gem_resource, key)
161
+ log.info "Gem #{gem_name.name} is not cached, fetching #{key}"
162
+ gem_fetcher.fetch(gem_name.id, key) do |content, properties|
163
+ gem_resource.save({ key => content }, headers: { key => properties })
164
+ end
165
+ end
166
+ end
167
+
168
+ # GemSource for https://rubygems.org (specifically when defined by using the
169
+ # default upstream).
170
+ class RubygemsSource < Gemstash::GemSource::UpstreamSource
171
+ def self.matches?(env)
172
+ if env["HTTP_X_GEMFILE_SOURCE"].to_s.empty?
173
+ env["gemstash.upstream"] = env["gemstash.env"].config[:rubygems_url]
174
+ else
175
+ env["gemstash.upstream"] = env["HTTP_X_GEMFILE_SOURCE"]
176
+ end
177
+ capture_user_agent(env)
178
+
179
+ true
180
+ end
181
+ end
182
+ end
183
+ end