paperclip-nginx-upload 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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