codeclimate-services 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 77816c3d07a72abbd79d831e8852592b4b30b654
4
- data.tar.gz: f8696a4c43abd9b325dc9d8baadf5033c4c8246f
3
+ metadata.gz: 4df2ee48a6422a56813233250049c38979fc3cb8
4
+ data.tar.gz: 7e58584424589e2c0b2ca337a853a4ea37e7b0fa
5
5
  SHA512:
6
- metadata.gz: d6ae01e70ac2bdc798333a52500b11778b47c256c444c7cceb76316c348c532fca4bd405864d4ba12310aac91f81d1a3ae4498a3a986ceb2ef4e0959f888f1e0
7
- data.tar.gz: 1237a7918f78505f03e166fdd34f0efe21596dc39837d41a25abe1d8b0867e5a1531205f757cf179b20efba0153f7144064604eccbff01252d1a6118b6885ff3
6
+ metadata.gz: ebb9f1f93b763a3858d6c66283ad1b27ff6507e57612301b71eeaf2077a1a034e808f98e787bbd9d9a9b2091f7f2e0bb397f38b3710c726bcd6907d5e37d51ad
7
+ data.tar.gz: db694bb54c853bf76dab733883350b38e1c3123706104e3b2a7730ebb41618b65badd9b6c7afc4ccade4ecc26db5997935cd8c5392a052987a06c156f75ab541
data/.codeclimate.yml CHANGED
@@ -10,10 +10,8 @@ engines:
10
10
  rubocop:
11
11
  enabled: true
12
12
  exclude_fingerprints:
13
- # Using #=== intentionally in SafeWebhook
13
+ # Using #=== intentionally to do subnet masking
14
14
  - d1afe90be49c43e85a76bfa58f637804
15
- # High complexity in http method due to SafeWebhook check
16
- - f05cea2d219c0f8119eb826067a18eda
17
15
  ratings:
18
16
  paths:
19
17
  - "**.rb"
data/Gemfile CHANGED
@@ -4,3 +4,8 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  gem "pry"
7
+
8
+ group :test do
9
+ gem "simplecov"
10
+ gem "codeclimate-test-reporter", "1.0.0.pre.rc2"
11
+ end
data/circle.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ override:
3
+ - bundle exec rake
4
+ - bundle exec codeclimate-test-reporter
@@ -23,7 +23,6 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency "activemodel", ">= 3.0"
24
24
  spec.add_dependency "activesupport", ">= 3.0"
25
25
  spec.add_development_dependency "bundler", ">= 1.6.2"
26
- spec.add_development_dependency "codeclimate-test-reporter"
27
26
  spec.add_development_dependency "rake"
28
27
  spec.add_development_dependency "rspec"
29
28
  end
@@ -0,0 +1,29 @@
1
+ require "resolv-replace"
2
+
3
+ module CC
4
+ class FixedResolv < Resolv::DNS
5
+ def self.enable!
6
+ new.tap do |instance|
7
+ Resolv::DefaultResolver.replace_resolvers([instance])
8
+ end
9
+ end
10
+
11
+ def initialize
12
+ @addresses = {}
13
+ end
14
+
15
+ def setaddress(name, address)
16
+ addresses[name] = address
17
+ end
18
+
19
+ def each_address(name)
20
+ if addresses.key?(name)
21
+ yield addresses.fetch(name)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :addresses
28
+ end
29
+ end
@@ -53,16 +53,13 @@ module CC::Service::HTTP
53
53
  def http_method(method, url = nil, body = nil, headers = nil)
54
54
  block = Proc.new if block_given?
55
55
 
56
+ CC::Service::SafeWebhook.ensure_safe!(url)
57
+
56
58
  http.send(method) do |req|
57
59
  req.url(url) if url
58
60
  req.headers.update(headers) if headers
59
61
  req.body = body if body
60
62
  block.call req if block
61
-
62
- unless allow_internal_webhooks?
63
- safe_webhook = CC::Service::SafeWebhook.new(url)
64
- safe_webhook.validate!(req)
65
- end
66
63
  end
67
64
  end
68
65
 
@@ -103,9 +100,4 @@ module CC::Service::HTTP
103
100
  message: "Success",
104
101
  }
105
102
  end
106
-
107
- def allow_internal_webhooks?
108
- var = ENV["CODECLIMATE_ALLOW_INTERNAL_WEBHOOKS"] || ""
109
- var == "1" || var == "true"
110
- end
111
103
  end
@@ -1,13 +1,13 @@
1
1
  require "ipaddr"
2
- require "resolv"
2
+ require "uri"
3
+
4
+ require "cc/fixed_resolv"
3
5
 
4
6
  module CC
5
7
  class Service
6
8
  class SafeWebhook
7
- InvalidWebhookURL = Class.new(StandardError)
9
+ InternalWebhookError = Class.new(StandardError)
8
10
 
9
- # https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
10
- # https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
11
11
  PRIVATE_ADDRESS_SUBNETS = [
12
12
  IPAddr.new("10.0.0.0/8"),
13
13
  IPAddr.new("172.16.0.0/12"),
@@ -17,54 +17,52 @@ module CC
17
17
  IPAddr.new("0:0:0:0:0:0:0:1"),
18
18
  ].freeze
19
19
 
20
+ def self.ensure_safe!(url)
21
+ instance = new(url)
22
+ instance.ensure_safe!
23
+ end
24
+
20
25
  def self.getaddress(host)
21
- @resolv ||= Resolv::DNS.new
22
- @resolv.getaddress(host).to_s
26
+ @dns ||= Resolv::DNS.new
27
+ @dns.getaddress(host)
28
+ end
29
+
30
+ def self.setaddress(host, address)
31
+ @fixed_resolv ||= CC::FixedResolv.enable!
32
+ @fixed_resolv.setaddress(host, address)
23
33
  end
24
34
 
25
35
  def initialize(url)
26
36
  @url = url
27
37
  end
28
38
 
29
- # Resolve the Host to an IP address, validate that it doesn't point to
30
- # anything internal, then alter the request to be for the IP directly with
31
- # an explicit Host header given.
32
- #
33
- # See http://blog.fanout.io/2014/01/27/how-to-safely-invoke-webhooks/#ip-address-blacklisting
34
- def validate!(request)
39
+ def ensure_safe!
35
40
  uri = URI.parse(url)
36
- address = self.class.getaddress(uri.host)
37
41
 
38
- if internal?(address)
39
- raise_invalid("resolves to a private IP address")
42
+ if !allow_internal_webhooks? && internal?(uri.host)
43
+ raise InternalWebhookError, "#{url.inspect} maps to an internal address"
40
44
  end
41
-
42
- alter_request(request, uri, address)
43
- rescue URI::InvalidURIError, Resolv::ResolvError, Resolv::ResolvTimeout => ex
44
- raise_invalid(ex.message)
45
45
  end
46
46
 
47
47
  private
48
48
 
49
49
  attr_reader :url
50
50
 
51
- def internal?(address)
52
- ip_addr = IPAddr.new(address)
51
+ def internal?(host)
52
+ address = self.class.getaddress(host)
53
+
54
+ self.class.setaddress(host, address)
53
55
 
54
56
  PRIVATE_ADDRESS_SUBNETS.any? do |subnet|
55
- subnet === ip_addr
57
+ subnet === IPAddr.new(address.to_s)
56
58
  end
59
+ rescue Resolv::ResolvError
60
+ true # localhost
57
61
  end
58
62
 
59
- def alter_request(request, uri, address)
60
- address_uri = uri.dup
61
- address_uri.host = address
62
- request.url(address_uri.to_s)
63
- request.headers.update(Host: uri.host)
64
- end
65
-
66
- def raise_invalid(message)
67
- raise InvalidWebhookURL, "The Webhook URL #{url} is invalid: #{message}"
63
+ def allow_internal_webhooks?
64
+ var = ENV["CODECLIMATE_ALLOW_INTERNAL_WEBHOOKS"] || ""
65
+ var == "1" || var == "true"
68
66
  end
69
67
  end
70
68
  end
@@ -1,5 +1,5 @@
1
1
  module CC
2
2
  module Services
3
- VERSION = "1.8.0".freeze
3
+ VERSION = "1.9.0".freeze
4
4
  end
5
5
  end
@@ -67,10 +67,8 @@ describe CC::Service::GitHubIssues, type: :service do
67
67
  end
68
68
 
69
69
  it "different base url" do
70
- stub_resolv("example.com", "1.1.1.2")
71
-
72
70
  http_stubs.post request_url do |env|
73
- expect(env[:url].to_s).to eq("http://1.1.1.2/#{request_url}")
71
+ expect(env[:url].to_s).to eq("http://example.com/#{request_url}")
74
72
  [200, {}, '{"number": 2, "html_url": "http://foo.bar"}']
75
73
  end
76
74
 
@@ -107,10 +107,8 @@ describe CC::Service::GitHubPullRequests, type: :service do
107
107
  end
108
108
 
109
109
  it "different base url" do
110
- stub_resolv("example.com", "1.1.1.2")
111
-
112
110
  http_stubs.post("/repos/pbrisbin/foo/statuses/#{"0" * 40}") do |env|
113
- expect(env[:url].to_s).to eq("http://1.1.1.2/repos/pbrisbin/foo/statuses/#{"0" * 40}")
111
+ expect(env[:url].to_s).to eq("http://example.com/repos/pbrisbin/foo/statuses/#{"0" * 40}")
114
112
  [422, { "x-oauth-scopes" => "gist, user, repo" }, ""]
115
113
  end
116
114
 
@@ -134,44 +134,14 @@ describe CC::Service::GitlabMergeRequests, type: :service do
134
134
  end
135
135
 
136
136
  it "different base url" do
137
- stub_resolv("gitlab.hal.org", "1.1.1.2")
138
-
139
137
  http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
140
- expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
138
+ expect(env[:url].to_s).to eq("https://gitlab.hal.org/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
141
139
  [404, {}, ""]
142
140
  end
143
141
 
144
142
  expect(receive_test({ base_url: "https://gitlab.hal.org" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
145
143
  end
146
144
 
147
- context "SafeWebhook" do
148
- it "rewrites the request to be for the resolved IP" do
149
- stub_resolv("my.gitlab.com", "1.1.1.2")
150
-
151
- http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
152
- expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
153
- expect(env[:request_headers][:Host]).to eq("my.gitlab.com")
154
- [404, {}, ""]
155
- end
156
-
157
- expect(receive_test({ base_url: "https://my.gitlab.com" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
158
- end
159
-
160
- it "validates that the host doesn't resolve to something internal" do
161
- stub_resolv("my.gitlab.com", "127.0.0.1")
162
-
163
- expect do
164
- receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
165
- end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
166
-
167
- stub_resolv("my.gitlab.com", "10.0.0.9")
168
-
169
- expect do
170
- receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
171
- end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
172
- end
173
- end
174
-
175
145
  private
176
146
 
177
147
  def expect_status_update(repo, commit_sha, params)
@@ -2,59 +2,39 @@ require "spec_helper"
2
2
 
3
3
  class CC::Service
4
4
  describe SafeWebhook do
5
- describe ".getaddress" do
6
- it "resolves the dns name to a string" do
7
- address = SafeWebhook.getaddress("codeclimate.com")
8
-
9
- expect(address).to be_present
10
- expect(address).to respond_to(:start_with?)
11
- end
12
- end
13
-
14
- describe "#validate!" do
15
- context "valid webhook URLs" do
16
- it "rewrites the request to be safe" do
17
- stub_resolv("example.com", "2.2.2.2")
18
-
19
- request = double(headers: double)
20
- expect(request).to receive(:url).with("https://2.2.2.2:3000/foo")
21
- expect(request.headers).to receive(:update).with(Host: "example.com")
22
-
23
- safe_webhook = SafeWebhook.new("https://example.com:3000/foo")
24
- safe_webhook.validate!(request)
5
+ describe ".ensure_safe!" do
6
+ it "does not allow internal URLs" do
7
+ %w[ 127.0.0.1 192.168.0.1 10.0.1.18 ].each do |address|
8
+ stub_resolv("github.com", address)
9
+
10
+ expect do
11
+ SafeWebhook.ensure_safe!("https://github.com/api/v1/user")
12
+ end.to raise_error(SafeWebhook::InternalWebhookError)
25
13
  end
26
14
  end
27
15
 
28
- context "invalid Webhook URLs" do
29
- it "raises for invalid URL" do
30
- allow(URI).to receive(:parse).and_raise(URI::InvalidURIError)
31
-
32
- expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
33
- end
16
+ it "allows internal URLs when configured to do so" do
17
+ allow(ENV).to receive(:[]).
18
+ with("CODECLIMATE_ALLOW_INTERNAL_WEBHOOKS").
19
+ and_return("1")
34
20
 
35
- it "raises for un-resolvable URL" do
36
- allow(SafeWebhook).to receive(:getaddress).and_raise(Resolv::ResolvError)
21
+ stub_resolv("github.com", "10.0.1.18")
37
22
 
38
- expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
39
- end
23
+ SafeWebhook.ensure_safe!("https://github.com/api/v1/user")
24
+ end
40
25
 
41
- it "raises for localhost URLs" do
42
- stub_resolv("example.com", "127.0.0.1")
26
+ it "allows non-internal URLs" do
27
+ stub_resolv("github.com", "1.1.1.2")
43
28
 
44
- expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
45
- end
29
+ SafeWebhook.ensure_safe!("https://github.com/api/v1/user")
30
+ end
46
31
 
47
- it "raises for internal URLs" do
48
- stub_resolv("example.com", "10.0.0.1")
32
+ it "ensures future dns queries get the same answer" do
33
+ stub_resolv("github.com", "1.1.1.3")
49
34
 
50
- expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
51
- end
35
+ SafeWebhook.ensure_safe!("https://github.com/api/v1/user")
52
36
 
53
- def validate(url)
54
- request = double
55
- safe_webhook = SafeWebhook.new(url)
56
- safe_webhook.validate!(request)
57
- end
37
+ expect(Resolv.getaddress("github.com").to_s).to eq "1.1.1.3"
58
38
  end
59
39
  end
60
40
  end
@@ -32,42 +32,42 @@ describe CC::Service, type: :service do
32
32
  it "post success" do
33
33
  stub_http("/my/test/url", [200, {}, '{"ok": true, "thing": "123"}'])
34
34
 
35
- response = service_post("/my/test/url", { token: "1234" }.to_json, {}) do |inner_response|
35
+ response = service_post("http://example.com/my/test/url", { token: "1234" }.to_json, {}) do |inner_response|
36
36
  body = JSON.parse(inner_response.body)
37
37
  { thing: body["thing"] }
38
38
  end
39
39
 
40
40
  expect(response[:ok]).to eq(true)
41
41
  expect(response[:params]).to eq('{"token":"1234"}')
42
- expect(response[:endpoint_url]).to eq("/my/test/url")
42
+ expect(response[:endpoint_url]).to eq("http://example.com/my/test/url")
43
43
  expect(response[:status]).to eq(200)
44
44
  end
45
45
 
46
46
  it "post redirect success" do
47
- stub_http("/my/test/url", [307, { "Location" => "/my/redirect/url" }, '{"ok": false, "redirect": true}'])
47
+ stub_http("/my/test/url", [307, { "Location" => "http://example.com/my/redirect/url" }, '{"ok": false, "redirect": true}'])
48
48
  stub_http("/my/redirect/url", [200, {}, '{"ok": true, "thing": "123"}'])
49
49
 
50
- response = service_post_with_redirects("/my/test/url", { token: "1234" }.to_json, {}) do |inner_response|
50
+ response = service_post_with_redirects("http://example.com/my/test/url", { token: "1234" }.to_json, {}) do |inner_response|
51
51
  body = JSON.parse(inner_response.body)
52
52
  { thing: body["thing"] }
53
53
  end
54
54
 
55
55
  expect(response[:ok]).to eq(true)
56
56
  expect(response[:params]).to eq('{"token":"1234"}')
57
- expect(response[:endpoint_url]).to eq("/my/test/url")
57
+ expect(response[:endpoint_url]).to eq("http://example.com/my/test/url")
58
58
  expect(response[:status]).to eq(200)
59
59
  end
60
60
 
61
61
  it "post http failure" do
62
62
  stub_http("/my/wrong/url", [404, {}, ""])
63
63
 
64
- expect { service_post("/my/wrong/url", { token: "1234" }.to_json, {}) }.to raise_error(CC::Service::HTTPError)
64
+ expect { service_post("http://example.com/my/wrong/url", { token: "1234" }.to_json, {}) }.to raise_error(CC::Service::HTTPError)
65
65
  end
66
66
 
67
67
  it "post some other failure" do
68
68
  stub_http("/my/wrong/url") { raise ArgumentError, "lol" }
69
69
 
70
- expect { service_post("/my/wrong/url", { token: "1234" }.to_json, {}) }.to raise_error(ArgumentError)
70
+ expect { service_post("http://example.com/my/wrong/url", { token: "1234" }.to_json, {}) }.to raise_error(ArgumentError)
71
71
  end
72
72
 
73
73
  it "services" do
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
- require "codeclimate-test-reporter"
2
- CodeClimate::TestReporter.start
1
+ require "simplecov"
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
3
5
 
4
6
  cwd = File.expand_path(File.dirname(__FILE__))
5
7
  require "#{cwd}/../config/load"
@@ -34,8 +36,8 @@ RSpec.configure do |config|
34
36
  # This setting enables warnings. It's recommended, but in some cases may
35
37
  # be too noisy due to issues in dependencies.
36
38
  config.warnings = true
37
-
38
- config.before(:each) do
39
+ config.before do
40
+ # Disable actual DNS resolution during specs by default
39
41
  stub_resolv(anything, "1.1.1.1")
40
42
  end
41
43
  end
@@ -0,0 +1,10 @@
1
+ module ResolvHelpers
2
+ def stub_resolv(name, address)
3
+ allow(CC::Service::SafeWebhook).to receive(:getaddress).
4
+ with(name).and_return(Resolv::IPv4.create(address))
5
+ end
6
+ end
7
+
8
+ RSpec.configure do |conf|
9
+ conf.include(ResolvHelpers)
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codeclimate-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Helmkamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-26 00:00:00.000000000 Z
11
+ date: 2016-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -94,20 +94,6 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 1.6.2
97
- - !ruby/object:Gem::Dependency
98
- name: codeclimate-test-reporter
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: rake
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -158,10 +144,12 @@ files:
158
144
  - bin/nokogiri
159
145
  - bin/pry
160
146
  - bin/rake
147
+ - circle.yml
161
148
  - codeclimate-services.gemspec
162
149
  - config/cacert.pem
163
150
  - config/load.rb
164
151
  - lib/axiom/types/password.rb
152
+ - lib/cc/fixed_resolv.rb
165
153
  - lib/cc/formatters/linked_formatter.rb
166
154
  - lib/cc/formatters/plain_formatter.rb
167
155
  - lib/cc/formatters/snapshot_formatter.rb
@@ -225,7 +213,7 @@ files:
225
213
  - spec/fixtures.rb
226
214
  - spec/spec_helper.rb
227
215
  - spec/support/fake_logger.rb
228
- - spec/support/resolv_helper.rb
216
+ - spec/support/resolv_helpers.rb
229
217
  - spec/support/service_context.rb
230
218
  homepage: ''
231
219
  licenses:
@@ -276,5 +264,5 @@ test_files:
276
264
  - spec/fixtures.rb
277
265
  - spec/spec_helper.rb
278
266
  - spec/support/fake_logger.rb
279
- - spec/support/resolv_helper.rb
267
+ - spec/support/resolv_helpers.rb
280
268
  - spec/support/service_context.rb
@@ -1,10 +0,0 @@
1
- module ResolvHelper
2
- def stub_resolv(host, address)
3
- allow(CC::Service::SafeWebhook).to receive(:getaddress).
4
- with(host).and_return(address)
5
- end
6
- end
7
-
8
- RSpec.configure do |conf|
9
- conf.include ResolvHelper
10
- end