Skip to content

Commit 882cb35

Browse files
Merge pull request #8624 from Edouard-chin/ec-ssl-diagnostic
Add SSL troubleshooting to `bundle doctor` (cherry picked from commit 1b12c9c)
1 parent 1f79f79 commit 882cb35

File tree

8 files changed

+831
-177
lines changed

8 files changed

+831
-177
lines changed

Manifest.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ bundler/lib/bundler/cli/common.rb
2626
bundler/lib/bundler/cli/config.rb
2727
bundler/lib/bundler/cli/console.rb
2828
bundler/lib/bundler/cli/doctor.rb
29+
bundler/lib/bundler/cli/doctor/diagnose.rb
30+
bundler/lib/bundler/cli/doctor/ssl.rb
2931
bundler/lib/bundler/cli/exec.rb
3032
bundler/lib/bundler/cli/fund.rb
3133
bundler/lib/bundler/cli/gem.rb

bundler/lib/bundler/cli.rb

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -610,17 +610,8 @@ def env
610610
end
611611

612612
desc "doctor [OPTIONS]", "Checks the bundle for common problems"
613-
long_desc <<-D
614-
Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If
615-
missing dependencies are detected, Bundler prints them and exits status 1.
616-
Otherwise, Bundler prints a success message and exits with a status of 0.
617-
D
618-
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
619-
method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
620-
def doctor
621-
require_relative "cli/doctor"
622-
Doctor.new(options).run
623-
end
613+
require_relative "cli/doctor"
614+
subcommand("doctor", Doctor)
624615

625616
desc "issue", "Learn how to report an issue in Bundler"
626617
def issue

bundler/lib/bundler/cli/doctor.rb

Lines changed: 27 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,161 +1,33 @@
11
# frozen_string_literal: true
22

3-
require "rbconfig"
4-
require "shellwords"
5-
63
module Bundler
7-
class CLI::Doctor
8-
DARWIN_REGEX = /\s+(.+) \(compatibility /
9-
LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/
10-
11-
attr_reader :options
12-
13-
def initialize(options)
14-
@options = options
15-
end
16-
17-
def otool_available?
18-
Bundler.which("otool")
19-
end
20-
21-
def ldd_available?
22-
Bundler.which("ldd")
23-
end
24-
25-
def dylibs_darwin(path)
26-
output = `/usr/bin/otool -L #{path.shellescape}`.chomp
27-
dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq
28-
# ignore @rpath and friends
29-
dylibs.reject {|dylib| dylib.start_with? "@" }
30-
end
31-
32-
def dylibs_ldd(path)
33-
output = `/usr/bin/ldd #{path.shellescape}`.chomp
34-
output.split("\n").filter_map do |l|
35-
match = l.match(LDD_REGEX)
36-
next if match.nil?
37-
match.captures[0]
38-
end
39-
end
40-
41-
def dylibs(path)
42-
case RbConfig::CONFIG["host_os"]
43-
when /darwin/
44-
return [] unless otool_available?
45-
dylibs_darwin(path)
46-
when /(linux|solaris|bsd)/
47-
return [] unless ldd_available?
48-
dylibs_ldd(path)
49-
else # Windows, etc.
50-
Bundler.ui.warn("Dynamic library check not supported on this platform.")
51-
[]
52-
end
53-
end
54-
55-
def bundles_for_gem(spec)
56-
Dir.glob("#{spec.full_gem_path}/**/*.bundle")
57-
end
58-
59-
def lookup_with_fiddle(path)
60-
require "fiddle"
61-
Fiddle.dlopen(path)
62-
false
63-
rescue Fiddle::DLError
64-
true
65-
end
66-
67-
def check!
68-
require_relative "check"
69-
Bundler::CLI::Check.new({}).run
70-
end
71-
72-
def run
73-
Bundler.ui.level = "warn" if options[:quiet]
74-
Bundler.settings.validate!
75-
check!
76-
77-
definition = Bundler.definition
78-
broken_links = {}
79-
80-
definition.specs.each do |spec|
81-
bundles_for_gem(spec).each do |bundle|
82-
bad_paths = dylibs(bundle).select do |f|
83-
lookup_with_fiddle(f)
84-
end
85-
if bad_paths.any?
86-
broken_links[spec] ||= []
87-
broken_links[spec].concat(bad_paths)
88-
end
89-
end
90-
end
91-
92-
permissions_valid = check_home_permissions
93-
94-
if broken_links.any?
95-
message = "The following gems are missing OS dependencies:"
96-
broken_links.flat_map do |spec, paths|
97-
paths.uniq.map do |path|
98-
"\n * #{spec.name}: #{path}"
99-
end
100-
end.sort.each {|m| message += m }
101-
raise ProductionError, message
102-
elsif permissions_valid
103-
Bundler.ui.info "No issues found with the installed bundle"
104-
end
105-
end
106-
107-
private
108-
109-
def check_home_permissions
110-
require "find"
111-
files_not_readable = []
112-
files_not_readable_and_owned_by_different_user = []
113-
files_not_owned_by_current_user_but_still_readable = []
114-
broken_symlinks = []
115-
Find.find(Bundler.bundle_path.to_s).each do |f|
116-
if !File.exist?(f)
117-
broken_symlinks << f
118-
elsif !File.readable?(f)
119-
if File.stat(f).uid != Process.uid
120-
files_not_readable_and_owned_by_different_user << f
121-
else
122-
files_not_readable << f
123-
end
124-
elsif File.stat(f).uid != Process.uid
125-
files_not_owned_by_current_user_but_still_readable << f
126-
end
127-
end
128-
129-
ok = true
130-
131-
if broken_symlinks.any?
132-
Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}"
133-
134-
ok = false
135-
end
136-
137-
if files_not_owned_by_current_user_but_still_readable.any?
138-
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
139-
"user, but are still readable. These files are:\n - #{files_not_owned_by_current_user_but_still_readable.join("\n - ")}"
140-
141-
ok = false
142-
end
143-
144-
if files_not_readable_and_owned_by_different_user.any?
145-
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
146-
"user, and are not readable. These files are:\n - #{files_not_readable_and_owned_by_different_user.join("\n - ")}"
147-
148-
ok = false
149-
end
150-
151-
if files_not_readable.any?
152-
Bundler.ui.warn "Files exist in the Bundler home that are not " \
153-
"readable by the current user. These files are:\n - #{files_not_readable.join("\n - ")}"
154-
155-
ok = false
156-
end
157-
158-
ok
4+
class CLI::Doctor < Thor
5+
default_command(:diagnose)
6+
7+
desc "diagnose [OPTIONS]", "Checks the bundle for common problems"
8+
long_desc <<-D
9+
Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If
10+
missing dependencies are detected, Bundler prints them and exits status 1.
11+
Otherwise, Bundler prints a success message and exits with a status of 0.
12+
D
13+
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
14+
method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
15+
method_option "ssl", type: :boolean, default: false, banner: "Diagnose SSL problems."
16+
def diagnose
17+
require_relative "doctor/diagnose"
18+
Diagnose.new(options).run
19+
end
20+
21+
desc "ssl [OPTIONS]", "Diagnose SSL problems"
22+
long_desc <<-D
23+
Diagnose SSL problems, especially related to certificates or TLS version while connecting to https://rubygems.org.
24+
D
25+
method_option "host", type: :string, banner: "The host to diagnose."
26+
method_option "tls-version", type: :string, banner: "Specify the SSL/TLS version when running the diagnostic. Accepts either <1.1> or <1.2>"
27+
method_option "verify-mode", type: :string, banner: "Specify the mode used for certification verification. Accepts either <peer> or <none>"
28+
def ssl
29+
require_relative "doctor/ssl"
30+
SSL.new(options).run
15931
end
16032
end
16133
end
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# frozen_string_literal: true
2+
3+
require "rbconfig"
4+
require "shellwords"
5+
6+
module Bundler
7+
class CLI::Doctor::Diagnose
8+
DARWIN_REGEX = /\s+(.+) \(compatibility /
9+
LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/
10+
11+
attr_reader :options
12+
13+
def initialize(options)
14+
@options = options
15+
end
16+
17+
def otool_available?
18+
Bundler.which("otool")
19+
end
20+
21+
def ldd_available?
22+
Bundler.which("ldd")
23+
end
24+
25+
def dylibs_darwin(path)
26+
output = `/usr/bin/otool -L #{path.shellescape}`.chomp
27+
dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq
28+
# ignore @rpath and friends
29+
dylibs.reject {|dylib| dylib.start_with? "@" }
30+
end
31+
32+
def dylibs_ldd(path)
33+
output = `/usr/bin/ldd #{path.shellescape}`.chomp
34+
output.split("\n").filter_map do |l|
35+
match = l.match(LDD_REGEX)
36+
next if match.nil?
37+
match.captures[0]
38+
end
39+
end
40+
41+
def dylibs(path)
42+
case RbConfig::CONFIG["host_os"]
43+
when /darwin/
44+
return [] unless otool_available?
45+
dylibs_darwin(path)
46+
when /(linux|solaris|bsd)/
47+
return [] unless ldd_available?
48+
dylibs_ldd(path)
49+
else # Windows, etc.
50+
Bundler.ui.warn("Dynamic library check not supported on this platform.")
51+
[]
52+
end
53+
end
54+
55+
def bundles_for_gem(spec)
56+
Dir.glob("#{spec.full_gem_path}/**/*.bundle")
57+
end
58+
59+
def lookup_with_fiddle(path)
60+
require "fiddle"
61+
Fiddle.dlopen(path)
62+
false
63+
rescue Fiddle::DLError
64+
true
65+
end
66+
67+
def check!
68+
require_relative "../check"
69+
Bundler::CLI::Check.new({}).run
70+
end
71+
72+
def diagnose_ssl
73+
require_relative "ssl"
74+
Bundler::CLI::Doctor::SSL.new({}).run
75+
end
76+
77+
def run
78+
Bundler.ui.level = "warn" if options[:quiet]
79+
Bundler.settings.validate!
80+
check!
81+
diagnose_ssl if options[:ssl]
82+
83+
definition = Bundler.definition
84+
broken_links = {}
85+
86+
definition.specs.each do |spec|
87+
bundles_for_gem(spec).each do |bundle|
88+
bad_paths = dylibs(bundle).select do |f|
89+
lookup_with_fiddle(f)
90+
end
91+
if bad_paths.any?
92+
broken_links[spec] ||= []
93+
broken_links[spec].concat(bad_paths)
94+
end
95+
end
96+
end
97+
98+
permissions_valid = check_home_permissions
99+
100+
if broken_links.any?
101+
message = "The following gems are missing OS dependencies:"
102+
broken_links.flat_map do |spec, paths|
103+
paths.uniq.map do |path|
104+
"\n * #{spec.name}: #{path}"
105+
end
106+
end.sort.each {|m| message += m }
107+
raise ProductionError, message
108+
elsif permissions_valid
109+
Bundler.ui.info "No issues found with the installed bundle"
110+
end
111+
end
112+
113+
private
114+
115+
def check_home_permissions
116+
require "find"
117+
files_not_readable = []
118+
files_not_readable_and_owned_by_different_user = []
119+
files_not_owned_by_current_user_but_still_readable = []
120+
broken_symlinks = []
121+
Find.find(Bundler.bundle_path.to_s).each do |f|
122+
if !File.exist?(f)
123+
broken_symlinks << f
124+
elsif !File.readable?(f)
125+
if File.stat(f).uid != Process.uid
126+
files_not_readable_and_owned_by_different_user << f
127+
else
128+
files_not_readable << f
129+
end
130+
elsif File.stat(f).uid != Process.uid
131+
files_not_owned_by_current_user_but_still_readable << f
132+
end
133+
end
134+
135+
ok = true
136+
137+
if broken_symlinks.any?
138+
Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}"
139+
140+
ok = false
141+
end
142+
143+
if files_not_owned_by_current_user_but_still_readable.any?
144+
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
145+
"user, but are still readable. These files are:\n - #{files_not_owned_by_current_user_but_still_readable.join("\n - ")}"
146+
147+
ok = false
148+
end
149+
150+
if files_not_readable_and_owned_by_different_user.any?
151+
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
152+
"user, and are not readable. These files are:\n - #{files_not_readable_and_owned_by_different_user.join("\n - ")}"
153+
154+
ok = false
155+
end
156+
157+
if files_not_readable.any?
158+
Bundler.ui.warn "Files exist in the Bundler home that are not " \
159+
"readable by the current user. These files are:\n - #{files_not_readable.join("\n - ")}"
160+
161+
ok = false
162+
end
163+
164+
ok
165+
end
166+
end
167+
end

0 commit comments

Comments
 (0)