paperclip-nginx-upload 0.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.
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ spec/tmp
16
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ rvm:
2
+ - 1.9.3
3
+
4
+ script: "bundle exec rspec"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in paperclip-nginx-upload.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Tim Fischbach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Paperclip Nginx Upload
2
+
3
+ [![Build Status](https://travis-ci.org/tf/paperclip-nginx-upload.png?branch=master)](https://travis-ci.org/tf/paperclip-nginx-upload)
4
+
5
+ A Paperclip IOAdapter to handle file upload requests which have been
6
+ rewritten by the
7
+ [nginx upload module](https://github.com/vkholodkov/nginx-upload-module).
8
+
9
+ The gem evolved out of the discussion in the following paperclip
10
+ issue:
11
+
12
+ https://github.com/thoughtbot/paperclip/issues/1396
13
+
14
+ ## Motivation
15
+
16
+ Nginx is must faster when it comes to parsing file uploads from the
17
+ body of HTTP requests. We do not want to occupy our Rails processes
18
+ with this tasks. Using the
19
+ [nginx upload module](https://github.com/vkholodkov/nginx-upload-module),
20
+ upload request can be rewritten to contain the path of a temp file
21
+ parsed from the request body before they are passed to our Rails app.
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'paperclip-nginx-upload'
28
+
29
+ ## Usage
30
+
31
+ Configure nginx to parse your upload requests. There is an example
32
+ nginx configuration snippet in `examples/nginx.conf`.
33
+
34
+ Add an initializer to configure the gem:
35
+
36
+ ```ruby
37
+ # config/intializers/paperclip_nginx_upload.rb
38
+ require 'paperclip/nginx/upload'
39
+
40
+ Paperclip::Nginx::Upload::IOAdapter.default_options.merge!(
41
+ # location where nginx places file uploads
42
+ tmp_file_whitelist: ['/tmp/nginx_uploads/**'],
43
+
44
+ # Change this option to true to move temp files created
45
+ # by nginx to the paperclip tmp file location. By default
46
+ # files are copied.
47
+ move_tempfile: false
48
+ )
49
+ ```
50
+
51
+ The adapter is a drop in replacement. When using strong parameters,
52
+ you only have to make sure the param containing the upload can also be
53
+ a hash of values:
54
+
55
+ ```ruby
56
+ class User
57
+ has_attached_file :avatar
58
+ end
59
+
60
+ class UsersController < ApplicationController
61
+ def update
62
+ @user.update_attributes(user_params)
63
+ end
64
+
65
+ def user_params
66
+ params.require(:user)
67
+ # in production avatar will be a hash generated by nginx
68
+ .permit(avatar: [:tmp_path, :original_name, :content_type])
69
+
70
+ # in development we want to permit the uploaded file itself
71
+ .merge(params.require(:user).permit(:avatar))
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Security Considerations
77
+
78
+ Assume an upload request contains a form field named
79
+ `user[avatar]`. Given the nginx configuration from
80
+ `examples/nginx.conf`, the request is rewritten to contain the
81
+ following three form fields instead:
82
+
83
+ * `user[avatar][original_name]`
84
+ * `user[avatar][conten_type]`
85
+ * `user[avatar][upload_tmp_path]`
86
+
87
+ By using this gem, you basically tell your app to accept paths to
88
+ local files in the `upload_tmp_path` param and move them around the
89
+ file system. Nginx ensures that these parameters can not be passed in
90
+ from the outside, preventing an attacker from passing `/etc/passwd` as
91
+ `upload_tmp_path` and having it delivered to him as his own upload
92
+ later on.
93
+
94
+ Still, if you forget to configure the nginx-upload-module correctly
95
+ for one of your upload end points, this could become a threat. This is
96
+ especially dangerous when not using strong parameters. While, as seen
97
+ above, the nested hash has to be permitted explicitly, methods
98
+ assigning attachments directly might be open to attacks:
99
+
100
+ ```ruby
101
+ @user.avatar = params[:avatar]
102
+ ```
103
+
104
+ Therefore the paperclip-nginx-upload adapter only accepts tmp files
105
+ from locations matching an entry in the `tmp_file_whitelist`. That way
106
+ an attacker will only be able to access running uploads of other
107
+ visitors of the site. He still would have to guess the random file
108
+ names chosen by nginx, which seems rather unfeasable.
109
+
110
+ ## Move vs Copy
111
+
112
+ By default, temp files created by ngninx are copied before passing
113
+ them to Paperclip. This ensures proper file ownership. When your nginx
114
+ runs as the same user as your rails processes, it might be sufficient
115
+ to simply move the file. Depending on your file system, this might be
116
+ a significant performance gain. In that case set `move_tempfile` to
117
+ `true`.
118
+
119
+ ## Contributing
120
+
121
+ 1. Fork it
122
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
123
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
124
+ 4. Push to the branch (`git push origin my-new-feature`)
125
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ location ~ ^/entries/[0-9]+/files$ {
2
+ upload_pass @app;
3
+
4
+ upload_store /tmp/nginx_uploads 1;
5
+ upload_store_access user:rw group:rw all:r;
6
+
7
+ upload_set_form_field $upload_field_name[original_name] "$upload_file_name";
8
+ upload_set_form_field $upload_field_name[content_type] "$upload_content_type";
9
+ upload_set_form_field $upload_field_name[tmp_path] "$upload_tmp_path";
10
+
11
+ upload_pass_form_field "^authenticity_token$|^format$";
12
+ upload_cleanup 400 404 500-505;
13
+
14
+ # Catch "Method not allowed" raised for non-POST requests and
15
+ # delegate to app
16
+ error_page 405 = @app;
17
+ }
18
+
19
+ location @app {
20
+ passenger_enabled on;
21
+
22
+ ...
23
+ }
@@ -0,0 +1,9 @@
1
+ require "paperclip/nginx/upload/io_adapter"
2
+ require "paperclip/nginx/upload/version"
3
+
4
+ module Paperclip
5
+ module Nginx
6
+ module Upload
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,75 @@
1
+ require 'fileutils'
2
+ require 'paperclip'
3
+
4
+ module Paperclip
5
+ module Nginx
6
+ module Upload
7
+ class TmpPathNotInWhitelistError < StandardError
8
+ def initialize(tmp_path, whitelist)
9
+ super("Paperclip nginx upload adapter received non whitelisted tmp file '#{tmp_path}'. " +
10
+ "Whitelist: #{whitelist.inspect}")
11
+ end
12
+ end
13
+
14
+ class IOAdapter < Paperclip::AbstractAdapter
15
+ def initialize(target, options = {})
16
+ @target = target
17
+ @options = self.class.default_options.merge(options)
18
+
19
+ require_whitelist_match!(@target[:tmp_path])
20
+ cache_current_values
21
+ end
22
+
23
+ def self.default_options
24
+ @default_options ||= {
25
+ :tmp_path_whitelist => [],
26
+ :move_tempfile => false
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def require_whitelist_match!(path)
33
+ unless matches_whitelist?(File.expand_path(path))
34
+ raise TmpPathNotInWhitelistError.new(path, tmp_path_whitelist)
35
+ end
36
+ end
37
+
38
+ def matches_whitelist?(path)
39
+ tmp_path_whitelist.any? do |glob|
40
+ File.fnmatch(glob, path)
41
+ end
42
+ end
43
+
44
+ def tmp_path_whitelist
45
+ @options[:tmp_path_whitelist]
46
+ end
47
+
48
+ def cache_current_values
49
+ self.original_filename = @target[:original_name]
50
+ @content_type = @target[:content_type].to_s.strip
51
+ @size = File.size(@target[:tmp_path])
52
+
53
+ copy_or_move(@target[:tmp_path], destination.path)
54
+ @tempfile = destination
55
+
56
+ # Required to reopen the tempfile that we have overwritten
57
+ @tempfile.open
58
+ @tempfile.binmode
59
+ end
60
+
61
+ def copy_or_move(source_path, destination_path)
62
+ if @options[:move_tempfile]
63
+ FileUtils.mv(@target[:tmp_path], destination.path)
64
+ else
65
+ FileUtils.cp(@target[:tmp_path], destination.path)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ Paperclip.io_adapters.register Paperclip::Nginx::Upload::IOAdapter do |target|
74
+ Hash === target && [:original_name, :content_type, :tmp_path].all? { |key| target.key?(key) }
75
+ end
@@ -0,0 +1,7 @@
1
+ module Paperclip
2
+ module Nginx
3
+ module Upload
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'paperclip/nginx/upload/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "paperclip-nginx-upload"
8
+ spec.version = Paperclip::Nginx::Upload::VERSION
9
+ spec.authors = ["Tim Fischbach"]
10
+ spec.email = ["tfischbach@codevise.de"]
11
+ spec.summary = "Paperclip IOAdapter for integration with nginx upload module"
12
+ spec.homepage = "https://github.com/tf/paperclip-nignx-upload"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency "paperclip"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec"
25
+ end
Binary file
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ describe Paperclip::Nginx::Upload::IOAdapter do
4
+ context 'for tmp file in whitelist' do
5
+ let :tempfile do
6
+ fixture_file('5k.png').binmode
7
+ end
8
+
9
+ subject do
10
+ nginx_upload_hash = {
11
+ :original_name => '5k.png',
12
+ :tmp_path => tempfile.path,
13
+ :content_type => "image/x-png-by-browser\r"
14
+ }
15
+
16
+ options = {
17
+ :tmp_path_whitelist => [File.join(PROJECT_ROOT, 'spec', 'tmp', '**')]
18
+ }
19
+
20
+ Paperclip::Nginx::Upload::IOAdapter.new(nginx_upload_hash, options)
21
+ end
22
+
23
+ it 'gets the right filename' do
24
+ expect(subject.original_filename).to eq('5k.png')
25
+ end
26
+
27
+ it 'gets content type' do
28
+ expect(subject.content_type).to eq('image/x-png-by-browser')
29
+ end
30
+
31
+ it 'gets the file\'s size' do
32
+ expect(subject.size).to eq(4456)
33
+ end
34
+
35
+ it 'returns false for a call to nil?' do
36
+ expect(subject.nil?).to be(false)
37
+ end
38
+
39
+ it 'generates a MD5 hash of the contents' do
40
+ hash = Digest::MD5.file(tempfile.path).to_s
41
+ expect(subject.fingerprint).to eq(hash)
42
+ end
43
+
44
+ it 'reads the contents of the file' do
45
+ content = tempfile.read
46
+ expect(content.length).to be > 0
47
+ expect(subject.read).to eq(content)
48
+ end
49
+
50
+ it 'copies the tempfile by default' do
51
+ subject
52
+ expect(File.exist?(tempfile.path)).to eq(true)
53
+ end
54
+ end
55
+
56
+ context 'for tmp file not in whitelist' do
57
+ let :tempfile do
58
+ fixture_file('5k.png').binmode
59
+ end
60
+
61
+ it 'raises an exception' do
62
+ nginx_upload_hash = {
63
+ :original_name => '5k.png',
64
+ :tmp_path => tempfile.path,
65
+ :content_type => "image/x-png-by-browser\r"
66
+ }
67
+
68
+ expect {
69
+ Paperclip::Nginx::Upload::IOAdapter.new(nginx_upload_hash, :tmp_path_whitelist => [])
70
+ }.to raise_error(Paperclip::Nginx::Upload::TmpPathNotInWhitelistError)
71
+ end
72
+ end
73
+
74
+ context 'with move_tempfile option set to true' do
75
+ let :tempfile do
76
+ fixture_file('5k.png').binmode
77
+ end
78
+
79
+ it 'moves the tempfile to the new location' do
80
+ nginx_upload_hash = {
81
+ :original_name => '5k.png',
82
+ :tmp_path => tempfile.path,
83
+ :content_type => "image/x-png-by-browser\r"
84
+ }
85
+
86
+ Paperclip::Nginx::Upload::IOAdapter.new(nginx_upload_hash,
87
+ :tmp_path_whitelist => [File.join(PROJECT_ROOT, 'spec', 'tmp', '**')],
88
+ :move_tempfile => true)
89
+
90
+ expect(File.exist?(tempfile.path)).to eq(false)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+
3
+ PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')).freeze
4
+ $LOAD_PATH << File.join(PROJECT_ROOT, 'lib')
5
+
6
+ require 'paperclip/nginx/upload'
7
+
8
+ Dir[File.join(PROJECT_ROOT, 'spec', 'support', '**', '*.rb')].each { |file| require(file) }
@@ -0,0 +1,10 @@
1
+ require 'fileutils'
2
+
3
+ FIXTURE_DIR = File.join(File.dirname(__FILE__), '..', 'fixtures')
4
+ TMP_DIR = File.join(File.dirname(__FILE__), '..', 'tmp')
5
+
6
+ def fixture_file(filename)
7
+ FileUtils.mkdir_p(TMP_DIR)
8
+ FileUtils.cp(File.join(FIXTURE_DIR, filename), File.join(TMP_DIR, filename))
9
+ File.new(File.join(TMP_DIR, filename))
10
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paperclip-nginx-upload
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Fischbach
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-02-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: paperclip
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description:
79
+ email:
80
+ - tfischbach@codevise.de
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - .travis.yml
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - examples/nginx.conf
92
+ - lib/paperclip/nginx/upload.rb
93
+ - lib/paperclip/nginx/upload/io_adapter.rb
94
+ - lib/paperclip/nginx/upload/version.rb
95
+ - paperclip-nginx-upload.gemspec
96
+ - spec/fixtures/5k.png
97
+ - spec/paperclip/nginx/upload/io_adapter_spec.rb
98
+ - spec/spec_helper.rb
99
+ - spec/support/fixture_file.rb
100
+ homepage: https://github.com/tf/paperclip-nignx-upload
101
+ licenses:
102
+ - MIT
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ segments:
114
+ - 0
115
+ hash: 1774978758898476396
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ segments:
123
+ - 0
124
+ hash: 1774978758898476396
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 1.8.25
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: Paperclip IOAdapter for integration with nginx upload module
131
+ test_files:
132
+ - spec/fixtures/5k.png
133
+ - spec/paperclip/nginx/upload/io_adapter_spec.rb
134
+ - spec/spec_helper.rb
135
+ - spec/support/fixture_file.rb