gemirro 2.0.0 → 3.0.1
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 +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +73 -61
- data/MANIFEST +1 -0
- data/bin/gemirro +1 -8
- data/gemirro.gemspec +4 -6
- data/lib/gemirro/cli/index.rb +48 -28
- data/lib/gemirro/cli/init.rb +48 -24
- data/lib/gemirro/cli/list.rb +37 -17
- data/lib/gemirro/cli/server.rb +173 -114
- data/lib/gemirro/cli/update.rb +42 -17
- data/lib/gemirro/cli.rb +62 -21
- data/lib/gemirro/configuration.rb +23 -9
- data/lib/gemirro/gem_version.rb +1 -0
- data/lib/gemirro/gem_version_collection.rb +4 -4
- data/lib/gemirro/gems_fetcher.rb +7 -9
- data/lib/gemirro/http.rb +115 -41
- data/lib/gemirro/indexer.rb +1 -1
- data/lib/gemirro/mirror_file.rb +3 -10
- data/lib/gemirro/server.rb +2 -1
- data/lib/gemirro/utils.rb +27 -0
- data/lib/gemirro/version.rb +1 -1
- data/lib/gemirro.rb +3 -7
- data/spec/gemirro/cli_spec.rb +49 -15
- data/spec/gemirro/gems_fetcher_spec.rb +1 -1
- data/spec/gemirro/http_spec.rb +58 -54
- data/spec/gemirro/server_spec.rb +35 -1
- data/spec/gemirro/source_spec.rb +1 -0
- data/spec/gemirro/utils_spec.rb +43 -0
- data/views/gem.erb +6 -1
- metadata +30 -66
data/lib/gemirro/http.rb
CHANGED
|
@@ -2,61 +2,135 @@
|
|
|
2
2
|
|
|
3
3
|
module Gemirro
|
|
4
4
|
##
|
|
5
|
-
# The Http class is responsible for executing GET
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# @!attribute [r] client
|
|
9
|
-
# @return [HTTPClient]
|
|
5
|
+
# The Http class is responsible for executing GET requests against a
|
|
6
|
+
# given URL, following redirects and reusing keep-alive connections.
|
|
10
7
|
#
|
|
11
8
|
class Http
|
|
12
|
-
attr_accessor :client
|
|
13
|
-
|
|
14
9
|
##
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# @param [String] url
|
|
18
|
-
# @return [HTTP::Message]
|
|
10
|
+
# Raised when a request comes back with a non-successful,
|
|
11
|
+
# non-redirect HTTP status.
|
|
19
12
|
#
|
|
20
|
-
|
|
21
|
-
response = client.get(url, follow_redirect: true)
|
|
13
|
+
class Error < StandardError; end
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
MAX_REDIRECTS = 10
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
class << self
|
|
18
|
+
##
|
|
19
|
+
# Requests the given HTTP resource, following redirects.
|
|
20
|
+
#
|
|
21
|
+
# @param [String] url
|
|
22
|
+
# @return [Net::HTTPResponse]
|
|
23
|
+
#
|
|
24
|
+
def get(url)
|
|
25
|
+
fetch(URI.parse(url), MAX_REDIRECTS)
|
|
26
|
+
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
##
|
|
29
|
+
# Closes and forgets every connection cached for the current
|
|
30
|
+
# Thread. Mostly useful for tests.
|
|
31
|
+
#
|
|
32
|
+
def reset!
|
|
33
|
+
pool.each_value { |connection| connection.finish if connection.started? }
|
|
34
|
+
pool.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @param [URI] uri
|
|
41
|
+
# @param [Integer] redirects_left
|
|
42
|
+
# @return [Net::HTTPResponse]
|
|
43
|
+
#
|
|
44
|
+
def fetch(uri, redirects_left)
|
|
45
|
+
response = connection_for(uri).request(build_request(uri))
|
|
46
|
+
|
|
47
|
+
case response
|
|
48
|
+
when Net::HTTPRedirection
|
|
49
|
+
raise Error, "Too many redirects for #{uri}" if redirects_left <= 0
|
|
50
|
+
|
|
51
|
+
fetch(URI.parse(response['location']), redirects_left - 1)
|
|
52
|
+
when Net::HTTPSuccess
|
|
53
|
+
response
|
|
54
|
+
else
|
|
55
|
+
raise Error, "#{response.code} #{response.message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# @param [URI] uri
|
|
61
|
+
# @return [Net::HTTP::Get]
|
|
62
|
+
#
|
|
63
|
+
def build_request(uri)
|
|
64
|
+
request = Net::HTTP::Get.new(uri)
|
|
65
|
+
config = Utils.configuration
|
|
66
|
+
request.basic_auth(config.upstream_user, config.upstream_password) if defined?(config.upstream_user)
|
|
67
|
+
|
|
68
|
+
request
|
|
39
69
|
end
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
##
|
|
72
|
+
# Returns a started, keep-alive connection for the given URI,
|
|
73
|
+
# reusing one already opened by the current Thread when possible.
|
|
74
|
+
#
|
|
75
|
+
# @param [URI] uri
|
|
76
|
+
# @return [Net::HTTP]
|
|
77
|
+
#
|
|
78
|
+
def connection_for(uri)
|
|
79
|
+
key = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
80
|
+
pool[key] ||= build_connection(uri)
|
|
44
81
|
end
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
##
|
|
84
|
+
# Connection pool for the current Thread. Kept per-Thread so
|
|
85
|
+
# sockets are never shared/interleaved across concurrent requests.
|
|
86
|
+
#
|
|
87
|
+
# @return [Hash]
|
|
88
|
+
#
|
|
89
|
+
def pool
|
|
90
|
+
Thread.current[:gemirro_http_pool] ||= {}
|
|
53
91
|
end
|
|
54
92
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
##
|
|
94
|
+
# @param [URI] uri
|
|
95
|
+
# @return [Net::HTTP]
|
|
96
|
+
#
|
|
97
|
+
def build_connection(uri)
|
|
98
|
+
config = Utils.configuration
|
|
99
|
+
http = Net::HTTP.new(uri.host, uri.port, *proxy_args(config))
|
|
100
|
+
http.use_ssl = uri.scheme == 'https'
|
|
101
|
+
configure_ssl(http, config)
|
|
102
|
+
|
|
103
|
+
http.start
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# @param [Net::HTTP] http
|
|
108
|
+
# @param [Gemirro::Configuration] config
|
|
109
|
+
#
|
|
110
|
+
def configure_ssl(http, config)
|
|
111
|
+
if defined?(config.rootca)
|
|
112
|
+
abort "The configuration file #{config.rootca} does not exist" unless File.file?(config.rootca)
|
|
113
|
+
|
|
114
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
115
|
+
http.ca_file = config.rootca
|
|
116
|
+
elsif defined?(config.verify_mode) && !config.verify_mode
|
|
117
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Splits a `proxy 'http://user:pass@host:port'` configuration
|
|
123
|
+
# value into the args expected by `Net::HTTP.new`.
|
|
124
|
+
#
|
|
125
|
+
# @param [Gemirro::Configuration] config
|
|
126
|
+
# @return [Array]
|
|
127
|
+
#
|
|
128
|
+
def proxy_args(config)
|
|
129
|
+
return [] unless defined?(config.proxy) && config.proxy
|
|
130
|
+
|
|
131
|
+
proxy_uri = URI.parse(config.proxy)
|
|
132
|
+
[proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
|
|
58
133
|
end
|
|
59
|
-
@client = client
|
|
60
134
|
end
|
|
61
135
|
end
|
|
62
136
|
end
|
data/lib/gemirro/indexer.rb
CHANGED
|
@@ -119,7 +119,7 @@ module Gemirro
|
|
|
119
119
|
source_host = Gemirro.configuration.source.host
|
|
120
120
|
Utils.logger.info("Download from source #{source_host}/#{file}")
|
|
121
121
|
resp = Http.get("#{source_host}/#{File.basename(file)}")
|
|
122
|
-
return unless resp.code == 200
|
|
122
|
+
return unless resp.code.to_i == 200
|
|
123
123
|
|
|
124
124
|
resp.body
|
|
125
125
|
end
|
data/lib/gemirro/mirror_file.rb
CHANGED
|
@@ -27,10 +27,8 @@ module Gemirro
|
|
|
27
27
|
#
|
|
28
28
|
def write(content)
|
|
29
29
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
handle.write(content)
|
|
33
|
-
handle.close
|
|
30
|
+
File.write(@path, content)
|
|
31
|
+
nil
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
##
|
|
@@ -39,12 +37,7 @@ module Gemirro
|
|
|
39
37
|
# @return [String]
|
|
40
38
|
#
|
|
41
39
|
def read
|
|
42
|
-
|
|
43
|
-
content = handle.read
|
|
44
|
-
|
|
45
|
-
handle.close
|
|
46
|
-
|
|
47
|
-
content
|
|
40
|
+
File.read(@path)
|
|
48
41
|
end
|
|
49
42
|
end
|
|
50
43
|
end
|
data/lib/gemirro/server.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'sinatra/base'
|
|
4
|
-
require '
|
|
4
|
+
require 'puma'
|
|
5
5
|
require 'uri'
|
|
6
6
|
require 'addressable/uri'
|
|
7
7
|
require 'base64'
|
|
@@ -33,6 +33,7 @@ module Gemirro
|
|
|
33
33
|
config.server.port = '2000' if config.server.port.nil?
|
|
34
34
|
|
|
35
35
|
set :static, true
|
|
36
|
+
set :server, 'puma'
|
|
36
37
|
|
|
37
38
|
set :views, Gemirro::Configuration.views_directory
|
|
38
39
|
set :port, config.server.port
|
data/lib/gemirro/utils.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'addressable/uri'
|
|
3
4
|
require 'gemirro/gem_version'
|
|
4
5
|
|
|
5
6
|
module Gemirro
|
|
@@ -25,6 +26,7 @@ module Gemirro
|
|
|
25
26
|
URI_REGEXP = /^(.*)-(\d+(?:\.\d+){1,4}.*?)(?:-(x86-(?:(?:mswin|mingw)(?:32|64)).*?|java))?\.(gem(?:spec\.rz)?)$/
|
|
26
27
|
GEMSPEC_TYPE = 'gemspec.rz'
|
|
27
28
|
GEM_TYPE = 'gem'
|
|
29
|
+
SAFE_HOMEPAGE_SCHEMES = %w[http https].freeze
|
|
28
30
|
|
|
29
31
|
##
|
|
30
32
|
# Generate Gems collection from Marshal dump - always the .local file
|
|
@@ -156,6 +158,31 @@ module Gemirro
|
|
|
156
158
|
end
|
|
157
159
|
end
|
|
158
160
|
|
|
161
|
+
##
|
|
162
|
+
# Return a normalized homepage URL that is safe to use as a link target,
|
|
163
|
+
# or nil when it is missing, unparsable or not an http(s) URL with a host.
|
|
164
|
+
# Gem metadata comes from upstream sources and must not be trusted.
|
|
165
|
+
#
|
|
166
|
+
# @param [String] homepage
|
|
167
|
+
# @return [String, nil]
|
|
168
|
+
#
|
|
169
|
+
def self.safe_homepage(homepage)
|
|
170
|
+
return if homepage.nil?
|
|
171
|
+
|
|
172
|
+
# Browsers ignore control characters and whitespace inside a scheme
|
|
173
|
+
# (e.g. "java\tscript:"), so drop them before validating.
|
|
174
|
+
value = homepage.to_s.gsub(/[[:cntrl:][:space:]]/, '')
|
|
175
|
+
return if value.empty?
|
|
176
|
+
|
|
177
|
+
uri = Addressable::URI.parse(value).normalize
|
|
178
|
+
return unless SAFE_HOMEPAGE_SCHEMES.include?(uri.scheme&.downcase)
|
|
179
|
+
return if uri.host.nil? || uri.host.empty?
|
|
180
|
+
|
|
181
|
+
uri.to_s
|
|
182
|
+
rescue Addressable::URI::InvalidURIError, ArgumentError, TypeError
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
159
186
|
##
|
|
160
187
|
# Try to fetch gem and download its if it's possible, and
|
|
161
188
|
# build and install indicies.
|
data/lib/gemirro/version.rb
CHANGED
data/lib/gemirro.rb
CHANGED
|
@@ -4,14 +4,15 @@ require 'builder'
|
|
|
4
4
|
require 'confstruct'
|
|
5
5
|
require 'digest/sha2'
|
|
6
6
|
require 'fileutils'
|
|
7
|
-
require 'httpclient'
|
|
8
7
|
require 'json'
|
|
9
8
|
require 'logger'
|
|
9
|
+
require 'net/http'
|
|
10
|
+
require 'openssl'
|
|
11
|
+
require 'optparse'
|
|
10
12
|
require 'parallel'
|
|
11
13
|
require 'rubygems'
|
|
12
14
|
require 'rubygems/indexer'
|
|
13
15
|
require 'rubygems/user_interaction'
|
|
14
|
-
require 'slop'
|
|
15
16
|
require 'stringio'
|
|
16
17
|
require 'tempfile'
|
|
17
18
|
require 'zlib'
|
|
@@ -34,8 +35,3 @@ require 'gemirro/versions_fetcher'
|
|
|
34
35
|
require 'gemirro/gems_fetcher'
|
|
35
36
|
|
|
36
37
|
require 'gemirro/cli'
|
|
37
|
-
require 'gemirro/cli/index'
|
|
38
|
-
require 'gemirro/cli/init'
|
|
39
|
-
require 'gemirro/cli/list'
|
|
40
|
-
require 'gemirro/cli/server'
|
|
41
|
-
require 'gemirro/cli/update'
|
data/spec/gemirro/cli_spec.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
require 'gemirro/cli'
|
|
3
3
|
require 'gemirro/mirror_file'
|
|
4
|
-
require 'slop'
|
|
5
4
|
|
|
6
5
|
# Gemirro tests
|
|
7
6
|
module Gemirro
|
|
@@ -10,21 +9,47 @@ module Gemirro
|
|
|
10
9
|
describe 'CLI' do
|
|
11
10
|
include FakeFS::SpecHelpers
|
|
12
11
|
|
|
13
|
-
it 'should
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
it 'should print version information for -v' do
|
|
13
|
+
expect { CLI.run(['-v']) }.to output(/gemirro v.* on ruby/).to_stdout
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'should print version information for --version' do
|
|
17
|
+
expect { CLI.run(['--version']) }.to output(/gemirro v.* on ruby/).to_stdout
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'should print usage for -h' do
|
|
21
|
+
expect { CLI.run(['-h']) }.to output(/Usage: gemirro \[COMMAND\] \[OPTIONS\]/).to_stdout
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'should print usage for --help' do
|
|
25
|
+
expect { CLI.run(['--help']) }.to output(/Usage: gemirro \[COMMAND\] \[OPTIONS\]/).to_stdout
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'should print usage when no argument is given' do
|
|
29
|
+
expect { CLI.run([]) }.to output(/Usage: gemirro \[COMMAND\] \[OPTIONS\]/).to_stdout
|
|
30
|
+
end
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
it 'should list the available commands in the usage banner' do
|
|
33
|
+
output = capture_stdout { CLI.run(['-h']) }
|
|
34
|
+
|
|
35
|
+
CLI::COMMANDS.each do |name, description|
|
|
36
|
+
expect(output).to match(/#{name}(\s+)#{Regexp.escape(description)}/)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should set $PROGRAM_NAME' do
|
|
41
|
+
$PROGRAM_NAME = 'rspec'
|
|
42
|
+
CLI.run(['-v'])
|
|
43
|
+
expect($PROGRAM_NAME).to eq('gemirro')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'should print an error and exit for an unknown command' do
|
|
47
|
+
expect { CLI.run(['nope']) }.to output(/Unknown command: nope/).to_stdout.and raise_error(SystemExit)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'should print an option error for an unknown flag' do
|
|
51
|
+
expect { CLI.run(['list', '--nope']) }
|
|
52
|
+
.to output(/invalid option: --nope/).to_stdout.and raise_error(SystemExit)
|
|
28
53
|
end
|
|
29
54
|
|
|
30
55
|
it 'should retrieve version information' do
|
|
@@ -45,6 +70,15 @@ module Gemirro
|
|
|
45
70
|
file.write('test')
|
|
46
71
|
expect { CLI.load_configuration('config.rb') }.to raise_error LoadError
|
|
47
72
|
end
|
|
73
|
+
|
|
74
|
+
def capture_stdout
|
|
75
|
+
original = $stdout
|
|
76
|
+
$stdout = StringIO.new
|
|
77
|
+
yield
|
|
78
|
+
$stdout.string
|
|
79
|
+
ensure
|
|
80
|
+
$stdout = original
|
|
81
|
+
end
|
|
48
82
|
end
|
|
49
83
|
end
|
|
50
84
|
end
|
|
@@ -47,7 +47,7 @@ module Gemirro
|
|
|
47
47
|
version = ::Gem::Version.new('0.0.1')
|
|
48
48
|
Utils.configuration.ignore_gem('gemirro', '0.0.1', 'ruby')
|
|
49
49
|
allow(@source).to receive(:fetch_gem)
|
|
50
|
-
.once.with('gemirro'
|
|
50
|
+
.once.with('gemirro-0.0.1.gem').and_raise(ArgumentError)
|
|
51
51
|
allow(Utils.logger).to receive(:error)
|
|
52
52
|
.once.with(/Failed to retrieve/)
|
|
53
53
|
allow(Utils.logger).to receive(:debug)
|
data/spec/gemirro/http_spec.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
-
require '
|
|
2
|
+
require 'webmock/rspec'
|
|
3
|
+
require 'gemirro/utils'
|
|
3
4
|
require 'gemirro/http'
|
|
4
5
|
|
|
5
6
|
module Gemirro
|
|
@@ -7,88 +8,91 @@ module Gemirro
|
|
|
7
8
|
let(:config) { double('Configuration') }
|
|
8
9
|
|
|
9
10
|
before do
|
|
10
|
-
Http.
|
|
11
|
+
Http.reset!
|
|
11
12
|
allow(Utils).to receive(:configuration).and_return(config)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
it 'initializes a new HTTPClient' do
|
|
16
|
-
expect(Http.client).to be_a(HTTPClient)
|
|
17
|
-
end
|
|
15
|
+
after { Http.reset! }
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
describe '.get' do
|
|
18
|
+
context 'with successful response' do
|
|
19
|
+
it 'returns the response' do
|
|
20
|
+
stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'content')
|
|
21
|
+
|
|
22
|
+
response = Http.get('http://example.com/')
|
|
23
|
+
expect(response).to be_a(Net::HTTPSuccess)
|
|
24
|
+
expect(response.body).to eq('content')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'reuses the connection for a second request on the same host' do
|
|
28
|
+
stub_request(:get, 'http://example.com/one').to_return(status: 200, body: 'one')
|
|
29
|
+
stub_request(:get, 'http://example.com/two').to_return(status: 200, body: 'two')
|
|
30
|
+
|
|
31
|
+
Http.get('http://example.com/one')
|
|
32
|
+
Http.get('http://example.com/two')
|
|
22
33
|
|
|
23
|
-
expect(
|
|
34
|
+
expect(a_request(:get, 'http://example.com/one')).to have_been_made.once
|
|
35
|
+
expect(a_request(:get, 'http://example.com/two')).to have_been_made.once
|
|
24
36
|
end
|
|
25
37
|
end
|
|
26
38
|
|
|
27
|
-
context 'with
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
39
|
+
context 'with a redirect' do
|
|
40
|
+
it 'follows it and returns the final response' do
|
|
41
|
+
stub_request(:get, 'http://example.com/old')
|
|
42
|
+
.to_return(status: 302, headers: { 'Location' => 'http://example.com/new' })
|
|
43
|
+
stub_request(:get, 'http://example.com/new').to_return(status: 200, body: 'moved')
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
expect { Http.client }.to raise_error(SystemExit)
|
|
36
|
-
end
|
|
45
|
+
expect(Http.get('http://example.com/old').body).to eq('moved')
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
48
|
+
it 'raises after too many redirects' do
|
|
49
|
+
stub_request(:get, 'http://example.com/loop')
|
|
50
|
+
.to_return(status: 302, headers: { 'Location' => 'http://example.com/loop' })
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
52
|
+
expect { Http.get('http://example.com/loop') }
|
|
53
|
+
.to raise_error(Http::Error, /Too many redirects/)
|
|
47
54
|
end
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
context 'with
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
57
|
+
context 'with an error response' do
|
|
58
|
+
it 'raises Http::Error for a failed request' do
|
|
59
|
+
stub_request(:get, 'http://example.com/missing').to_return(status: [404, 'Not Found'])
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
expect { Http.get('http://example.com/missing') }
|
|
62
|
+
.to raise_error(Http::Error, '404 Not Found')
|
|
57
63
|
end
|
|
58
64
|
end
|
|
59
|
-
end
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
context 'with upstream authentication configured' do
|
|
67
|
+
it 'sends a basic auth header on every request' do
|
|
68
|
+
allow(config).to receive(:upstream_user).and_return('user')
|
|
69
|
+
allow(config).to receive(:upstream_password).and_return('pass')
|
|
70
|
+
stub_request(:get, 'http://example.com/private')
|
|
71
|
+
.with(basic_auth: %w[user pass])
|
|
72
|
+
.to_return(status: 200, body: 'ok')
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
expect(Http.get('http://example.com/private').body).to eq('ok')
|
|
75
|
+
end
|
|
66
76
|
end
|
|
67
77
|
|
|
68
|
-
context 'with
|
|
69
|
-
|
|
78
|
+
context 'with proxy configuration' do
|
|
79
|
+
it 'routes the request through the configured proxy' do
|
|
80
|
+
allow(config).to receive(:proxy).and_return('http://proxy.example.com:8080')
|
|
81
|
+
stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'content')
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
allow(client).to receive(:get)
|
|
73
|
-
.with('http://example.com', follow_redirect: true)
|
|
74
|
-
.and_return(response)
|
|
83
|
+
Http.get('http://example.com/')
|
|
75
84
|
|
|
76
|
-
expect(
|
|
85
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/')
|
|
77
86
|
end
|
|
78
87
|
end
|
|
79
88
|
|
|
80
|
-
context 'with
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
it 'raises BadResponseError for failed request' do
|
|
84
|
-
allow(client).to receive(:get)
|
|
85
|
-
.with('http://example.com', follow_redirect: true)
|
|
86
|
-
.and_return(response)
|
|
89
|
+
context 'with an invalid rootca path' do
|
|
90
|
+
it 'aborts with an error message' do
|
|
91
|
+
allow(config).to receive(:rootca).and_return('/nonexistent/ca.crt')
|
|
87
92
|
|
|
88
|
-
expect { Http.get('
|
|
89
|
-
.to raise_error(HTTPClient::BadResponseError, 'Not Found')
|
|
93
|
+
expect { Http.get('https://example.com/') }.to raise_error(SystemExit)
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
96
|
end
|
|
93
97
|
end
|
|
94
|
-
end
|
|
98
|
+
end
|
data/spec/gemirro/server_spec.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
1
2
|
require 'rack/test'
|
|
2
3
|
require 'json'
|
|
3
4
|
require 'parallel'
|
|
4
5
|
require 'sinatra/base'
|
|
5
|
-
require '
|
|
6
|
+
require 'puma'
|
|
6
7
|
require 'base64'
|
|
7
8
|
require 'gemirro/utils'
|
|
8
9
|
require 'gemirro/mirror_directory'
|
|
@@ -80,6 +81,39 @@ module Gemirro
|
|
|
80
81
|
expect(last_response.status).to eq(200)
|
|
81
82
|
expect(last_response).to be_ok
|
|
82
83
|
end
|
|
84
|
+
|
|
85
|
+
it 'should not link a javascript: homepage from gem metadata' do
|
|
86
|
+
MirrorFile.new('/var/www/gemirro/versions.md5.aaa256.list').write(%(created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0 checksum))
|
|
87
|
+
spec = ::Gem::Specification.new do |s|
|
|
88
|
+
s.name = 'volay'
|
|
89
|
+
s.version = '0.1.0'
|
|
90
|
+
s.authors = ['Mallory']
|
|
91
|
+
s.homepage = 'javascript:alert(1)'
|
|
92
|
+
end
|
|
93
|
+
allow(Utils).to receive(:spec_for).and_return(spec)
|
|
94
|
+
|
|
95
|
+
get '/gem/volay'
|
|
96
|
+
|
|
97
|
+
expect(last_response).to be_ok
|
|
98
|
+
expect(last_response.body).to include('Mallory')
|
|
99
|
+
expect(last_response.body).not_to include('javascript:')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'should link an https homepage from gem metadata' do
|
|
103
|
+
MirrorFile.new('/var/www/gemirro/versions.md5.aaa256.list').write(%(created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0 checksum))
|
|
104
|
+
spec = ::Gem::Specification.new do |s|
|
|
105
|
+
s.name = 'volay'
|
|
106
|
+
s.version = '0.1.0'
|
|
107
|
+
s.authors = ['Alice']
|
|
108
|
+
s.homepage = 'https://example.com/volay'
|
|
109
|
+
end
|
|
110
|
+
allow(Utils).to receive(:spec_for).and_return(spec)
|
|
111
|
+
|
|
112
|
+
get '/gem/volay'
|
|
113
|
+
|
|
114
|
+
expect(last_response).to be_ok
|
|
115
|
+
expect(last_response.body).to include('href="https://example.com/volay"')
|
|
116
|
+
end
|
|
83
117
|
|
|
84
118
|
it 'responds to compact_index /names' do
|
|
85
119
|
MirrorFile.new('/var/www/gemirro/names.md5.aaa256.list').write(%(---\nvolay))
|
data/spec/gemirro/source_spec.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'gemirro/utils'
|
|
5
|
+
|
|
6
|
+
# Utils tests
|
|
7
|
+
module Gemirro
|
|
8
|
+
describe Utils do
|
|
9
|
+
describe '.safe_homepage' do
|
|
10
|
+
it 'keeps http and https URLs' do
|
|
11
|
+
expect(Utils.safe_homepage('http://example.com')).to eq('http://example.com/')
|
|
12
|
+
expect(Utils.safe_homepage('https://example.com/foo?a=1')).to eq('https://example.com/foo?a=1')
|
|
13
|
+
expect(Utils.safe_homepage('HTTPS://Example.com/x')).to eq('https://example.com/x')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'rejects missing or empty values' do
|
|
17
|
+
expect(Utils.safe_homepage(nil)).to be_nil
|
|
18
|
+
expect(Utils.safe_homepage('')).to be_nil
|
|
19
|
+
expect(Utils.safe_homepage(" \n")).to be_nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
[
|
|
23
|
+
'javascript:alert(1)',
|
|
24
|
+
'JavaScript:alert(1)',
|
|
25
|
+
' javascript:alert(1)',
|
|
26
|
+
"java\tscript:alert(1)",
|
|
27
|
+
"java\nscript:alert(1)",
|
|
28
|
+
"\u0000javascript:alert(1)",
|
|
29
|
+
'data:text/html,<script>alert(1)</script>',
|
|
30
|
+
'vbscript:msgbox(1)',
|
|
31
|
+
'file:///etc/passwd',
|
|
32
|
+
'javascript%3Aalert(1)',
|
|
33
|
+
'//evil.example.com',
|
|
34
|
+
'/relative/path',
|
|
35
|
+
'https://'
|
|
36
|
+
].each do |value|
|
|
37
|
+
it "rejects #{value.inspect}" do
|
|
38
|
+
expect(Utils.safe_homepage(value)).to be_nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|