remote_image_fetch 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98ff838124f739e14ba9688f2fc8ead8f789e576
4
+ data.tar.gz: 85638d680340fb1a9dc46ab26bbfd84084dabc78
5
+ SHA512:
6
+ metadata.gz: aeb135e6059806159abc2a7fa71afc0d63fbcaa8227f42f249c6bfdea59db74b2e9af23513e9cbbfa353f3794ceb1f56ea2a2bd097ec20f098a37e7f90401cf6
7
+ data.tar.gz: b513de536e94d762dad68eaa85d6a6e0836b71cf5600340c67c5554f2e26df329726ea2ea478b7ddf9026274808aec5cd86cb1a37af53943c3fc327bd6effa43
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.1.10
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in remote_image_fetch.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # RemoteImageFetch
2
+
3
+ [![Build Status](https://travis-ci.org/livelink/remote_image_fetch.svg?branch=master)](https://travis-ci.org/livelink/remote_image_fetch)
4
+
5
+ Use Curl::Multi with support for filterable Location: redirects.
6
+
7
+ The intended use is fetching user specified URIs with libcurl from an application server, while blocking access to local hosts.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'remote_image_fetch'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install remote_image_fetch
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ downloader = RemoteImageFetch.new(ip_blacklist: /^(127\.0\.0|10|192\.168)\./)
29
+
30
+ downloader.download(raw_url) do |result|
31
+ if result.ok?
32
+ # process download
33
+ else
34
+ # handle error?
35
+ end
36
+ end
37
+
38
+ downloader.run
39
+ ```
40
+
41
+
42
+ ## Development
43
+
44
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
45
+
46
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
47
+
48
+ ## Contributing
49
+
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/livelink/remote_image_fetch.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "remote_image_fetch"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,96 @@
1
+ require 'curl'
2
+
3
+ require 'remote_image_fetch/version'
4
+ require 'remote_image_fetch/download_result'
5
+ require 'remote_image_fetch/curl_error'
6
+ require 'remote_image_fetch/curl_wrap'
7
+ require 'remote_image_fetch/uri_restrictions'
8
+
9
+ # RemoteImageFetch
10
+ class RemoteImageFetch
11
+ attr_reader :success, :failures
12
+
13
+ def initialize(options = {})
14
+ @max_parallel = options[:max_parallel] || 16
15
+ @max_redirects = options[:max_redirects] || 4
16
+
17
+ @uri_restrictions = RemoteImageFetch::UriRestrictions.new(options)
18
+
19
+ @change_flagged = false
20
+ @core = Curl::Multi.new
21
+ @downloads = []
22
+
23
+ @success = 0
24
+ @failures = 0
25
+ end
26
+
27
+ def download(url, *args, &callback)
28
+ curl = CurlWrap.new(url)
29
+ curl.setup self
30
+ curl.args = args
31
+ curl.callback = callback
32
+ downloads.push(curl)
33
+ curl
34
+ end
35
+
36
+ def run
37
+ auto_queue!
38
+ core.perform
39
+ end
40
+
41
+ def report_done(curl, *_args)
42
+ @change_flagged = true
43
+ @success += 1
44
+ curl.report(DownloadResult::OK.new(curl))
45
+ auto_queue!
46
+ end
47
+
48
+ def report_failed(curl, error = nil)
49
+ @change_flagged = true
50
+ @failures += 1
51
+ error ||= CurlError.new(curl)
52
+ curl.report(DownloadResult::Fail.new(curl, error))
53
+ auto_queue!
54
+ end
55
+
56
+ def check_and_redirect(curl)
57
+ raise "Too many redirects for #{curl.url}" if curl.redirects > max_redirects
58
+ check_url(curl.redirect_url)
59
+ curl.url = curl.redirect_url
60
+ core.add(curl)
61
+ rescue => e
62
+ report_failed(curl, e)
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :uri_restrictions, :downloads, :core,
68
+ :max_parallel, :max_redirects
69
+
70
+ def auto_queue!
71
+ loop do
72
+ break if core.requests.size > max_parallel
73
+ break if downloads.empty?
74
+
75
+ queue_next!
76
+ end
77
+ @change_flagged = false
78
+ end
79
+
80
+ def queue_next!
81
+ get(downloads.pop)
82
+ rescue
83
+ nil
84
+ end
85
+
86
+ def check_url(url)
87
+ uri_restrictions.check(url)
88
+ end
89
+
90
+ def get(curl)
91
+ check_url(curl.url)
92
+ core.add(curl)
93
+ rescue => e
94
+ report_failed(curl, e)
95
+ end
96
+ end
@@ -0,0 +1,16 @@
1
+ class RemoteImageFetch
2
+ # Wrap cURL response as if it's an error
3
+ class CurlError
4
+ def initialize(curl)
5
+ @curl = curl
6
+ end
7
+
8
+ def message
9
+ @curl.status
10
+ end
11
+
12
+ def to_s
13
+ message
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ class RemoteImageFetch
2
+ # Wrap Curl::Easy with additional helper methods
3
+ # Can't override initialize() as curb doesn't call it properly.
4
+ class CurlWrap < Curl::Easy
5
+ attr_reader :redirects, :result
6
+ attr_accessor :args, :callback, :redirect, :fetcher
7
+
8
+ def setup(fetcher)
9
+ self.follow_location = false
10
+ @fetcher = fetcher
11
+ @redirects = 0
12
+
13
+ on_complete(&method(:handle_complete))
14
+ on_redirect(&method(:handle_redirect))
15
+ end
16
+
17
+ def body_io
18
+ @body_io ||= StringIO.new(body_str)
19
+ end
20
+
21
+ def report(result)
22
+ @result = result
23
+ callback.call(result, *args) if callback
24
+ end
25
+
26
+ private
27
+
28
+ def handle_complete(*_args)
29
+ case response_code
30
+ when 301, 302, 303
31
+ # handled by handle_redirect
32
+ when 200
33
+ fetcher.report_done(self)
34
+ else
35
+ fetcher.report_failed(self)
36
+ end
37
+ true
38
+ end
39
+
40
+ def handle_redirect(*_args)
41
+ @redirects += 1
42
+ fetcher.check_and_redirect(self)
43
+ true
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ class DownloadResult
2
+ # Remote download succeeded
3
+ class OK < DownloadResult
4
+ def initialize(curl)
5
+ @curl = curl
6
+ end
7
+
8
+ def io
9
+ curl.body_io
10
+ end
11
+ end
12
+
13
+ # Remote download failed
14
+ class Fail < DownloadResult
15
+ def initialize(curl, error)
16
+ @curl = curl
17
+ @error = error
18
+ end
19
+
20
+ def error_message
21
+ @error.to_s
22
+ end
23
+ end
24
+
25
+ attr_reader :curl
26
+
27
+ def ok?
28
+ is_a?(OK)
29
+ end
30
+
31
+ def error?
32
+ is_a?(Fail)
33
+ end
34
+
35
+ def http_status
36
+ curl.response_code
37
+ end
38
+ end
@@ -0,0 +1,53 @@
1
+ require 'resolv'
2
+ require 'uri'
3
+
4
+ class RemoteImageFetch
5
+ # Wrap cURL response as if it's an error
6
+ class UriRestrictions
7
+ def initialize(options = {})
8
+ @schemes = options[:schemes] || ['http', 'https']
9
+ @ports = options[:ports] || [80, 443, 8080]
10
+
11
+ @ip_blacklist = options[:ip_blacklist]
12
+ @host_blacklist = options[:host_blacklist]
13
+ end
14
+
15
+ def check(url)
16
+ uri = URI.parse(url)
17
+
18
+ check_scheme! uri
19
+ check_port! uri
20
+ check_host! uri
21
+
22
+ Resolv.each_address(uri.hostname) do |addr|
23
+ raise "IP blacklisted - #{addr} for #{url}" if ip_blacklist_matches?(addr)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :schemes, :ports, :host_blacklist, :ip_blacklist
30
+
31
+ def check_scheme!(uri)
32
+ raise "Scheme rejected: #{uri}" unless schemes.include?(uri.scheme)
33
+ end
34
+
35
+ def check_port!(uri)
36
+ raise "Port rejected: #{uri}" unless ports.include?(uri.port)
37
+ end
38
+
39
+ def check_host!(uri)
40
+ raise "Host blacklisted: #{uri}" if host_blacklist_matches?(uri)
41
+ end
42
+
43
+ def ip_blacklist_matches?(ip)
44
+ return false unless ip_blacklist
45
+ ip_blacklist === ip
46
+ end
47
+
48
+ def host_blacklist_matches?(uri)
49
+ return false unless host_blacklist
50
+ host_blacklist === uri.host
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ class RemoteImageFetch
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'remote_image_fetch/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'remote_image_fetch'
8
+ spec.version = RemoteImageFetch::VERSION
9
+ spec.authors = ['Geoff Youngs']
10
+ spec.email = ['git@intersect-uk.co.uk']
11
+
12
+ spec.summary = 'Safe, easy, parallel downloads via libcurl'
13
+ spec.description = 'Restrict the types of redirect that libcurl will follow when fetching untrusted URLs'
14
+ spec.homepage = "https://github.com/livelink/remote_image_fetch"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org/"
20
+ else
21
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
22
+ 'public gem pushes.'
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(test|spec|features)/})
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+ spec.add_dependency 'curb', '> 0'
32
+ spec.add_development_dependency 'bundler', '~> 1.14'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remote_image_fetch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Geoff Youngs
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-05-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: curb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Restrict the types of redirect that libcurl will follow when fetching
70
+ untrusted URLs
71
+ email:
72
+ - git@intersect-uk.co.uk
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/remote_image_fetch.rb
86
+ - lib/remote_image_fetch/curl_error.rb
87
+ - lib/remote_image_fetch/curl_wrap.rb
88
+ - lib/remote_image_fetch/download_result.rb
89
+ - lib/remote_image_fetch/uri_restrictions.rb
90
+ - lib/remote_image_fetch/version.rb
91
+ - remote_image_fetch.gemspec
92
+ homepage: https://github.com/livelink/remote_image_fetch
93
+ licenses: []
94
+ metadata:
95
+ allowed_push_host: https://rubygems.org/
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.6.10
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Safe, easy, parallel downloads via libcurl
116
+ test_files: []