shellex 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
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
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shellex.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'popen4'
8
+
9
+ group :test do
10
+ gem 'shoulda-context'
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Dima Sabanin
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,102 @@
1
+ # Shellex [![Build Status](https://secure.travis-ci.org/dsabanin/shellex.png)](http://travis-ci.org/dsabanin/shellex)
2
+
3
+ Shellex allows you to run shell commands from your ruby scripts in a more robust and secure way than the built-in options.
4
+ We had a security audit in http://beanstalkapp.com recently which showed many problems caused by shell injections. This code
5
+ is the result of our attempt to fix these issues once and for all.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'shellex'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install shellex
20
+
21
+ ## Usage
22
+
23
+ Grabbing STDOUT output:
24
+
25
+ ```ruby
26
+ stdout, stderr = shellex("echo hello, world!")
27
+ # stdout => "hello, world!\n"
28
+ # stderr => ""
29
+ ```
30
+
31
+ Grabbing STDERR output:
32
+
33
+ ```ruby
34
+ stdout, stderr = shellex("echo error here 1>&2")
35
+ # stdout => ""
36
+ # stderr => "error here\n"
37
+ ```
38
+
39
+ Convenience methods:
40
+
41
+ ```ruby
42
+ shellex("echo hello, world").to_s # => "hello, world\n"
43
+ shellex("echo hello, world").stdout # => "hello, world\n"
44
+ shellex("echo error here 1>&2").stderr # => "error here\n"
45
+ ```
46
+
47
+ Providing STDIN input:
48
+
49
+ ```ruby
50
+ shellex("cat -", :input => "hello").stdout # => "hello"
51
+ ```
52
+
53
+ By default if you don't provide input we close the STDIN stream, but if you want you can leave it open:
54
+
55
+ ```ruby
56
+ shellex("cat /dev/stdin", :close_stdin => false)
57
+ ```
58
+
59
+ Timeouts (default timeout is set to 5 minutes):
60
+
61
+ ```ruby
62
+ shellex("sleep 10", :timeout => 1) # raises ShellExecutionTimeout exception
63
+ ```
64
+
65
+ Interpolation of arguments:
66
+
67
+ ```ruby
68
+ # to_s gets called on all arguments
69
+ shellex("echo ? ? ?", 1, "blah", :symbol)
70
+ # executes: echo '1' 'blah' 'symbol'
71
+
72
+ # ?& interpolates each array element separately
73
+ shellex("? ?& ?", "echo", [1,2,3,4], "abc")
74
+ # executes: 'echo' '1' '2' '3' '4' 'abc'
75
+
76
+ # ?& requires array to be present in the respective position
77
+ shellex("? ?& ?", "echo", 1) # raises ShellArgumentMissing
78
+
79
+ # ?~ escapes question mark
80
+ shellex("? ?~", "echo", "ello")
81
+ # executes: 'echo' ?
82
+
83
+ # ? by default will turn nil into empty string
84
+ shellex("echo ?", nil)
85
+ # executes: echo ''
86
+
87
+ # ?? will skip the argument if it's nil
88
+ shellex("echo ??", nil)
89
+ # executes: echo
90
+
91
+ # Shell injection protection
92
+ shellex("echo ?", "'; rm -Rf /; '")
93
+ # executes harmless: echo ''\''; rm -Rf /; '\'''
94
+ ```
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'test'
6
+ test.pattern = 'test/*_test.rb'
7
+ end
8
+
9
+ task :default => [:test]
data/lib/lib.iml ADDED
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
7
+ </content>
8
+ <orderEntry type="inheritedJdk" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ </component>
11
+ </module>
12
+
data/lib/shellex.rb ADDED
@@ -0,0 +1,144 @@
1
+ require "shellex/version"
2
+ require 'popen4'
3
+
4
+ class ShellExecutionError < StandardError; end
5
+ class ShellExecutionFailed < ShellExecutionError
6
+ attr_accessor :output_without_command
7
+ end
8
+ class ShellExecutionTimeout < ShellExecutionError; end
9
+ class ShellArgumentMissing < ShellExecutionError; end
10
+
11
+ module Kernel
12
+ SHELLEX_IO_BUFFER = 32*1024
13
+ DEFAULT_TIMEOUT = 5*60
14
+
15
+ def _shellex_retvalue(ret)
16
+ def ret.stdout; self[0] end
17
+ def ret.stderr; self[1] end
18
+ def ret.to_s; stdout end
19
+ def ret.to_str; stdout end
20
+ ret.freeze
21
+ end
22
+
23
+ def _shellex_extend_status(status)
24
+ def status.command=(cmd)
25
+ @command = cmd
26
+ end
27
+ def status.command
28
+ @command
29
+ end
30
+ end
31
+
32
+ def shellex(cmd, *args)
33
+ stdout, stderr, status = silent_shellex(cmd, *args)
34
+ if status.success?
35
+ ret = [stdout, stderr]
36
+ return _shellex_retvalue(ret)
37
+ else
38
+ exc = ShellExecutionFailed.new("#{status.command} (exit code: #{status.exitstatus})\nOutput: #{stderr}")
39
+ exc.output_without_command = stderr
40
+ raise exc
41
+ end
42
+ end
43
+
44
+ def silent_shellex(cmd, *args)
45
+ opts = {:timeout => DEFAULT_TIMEOUT, :close_stdin => true}
46
+ if args.last.is_a?(Hash)
47
+ opts = opts.merge(args.pop)
48
+ end
49
+
50
+ cmd = cmd.with_args(*args)
51
+ out, err, pid, status = '', '', nil, nil
52
+
53
+ begin
54
+ Timeout.timeout(opts[:timeout].to_i) do
55
+ status = Open4.open4(cmd) do |pid, stdin, stdout, stderr|
56
+ stdin.write(opts[:input]) if opts[:input]
57
+ stdin.close if opts[:close_stdin]
58
+
59
+ while tmp = stdout.read(SHELLEX_IO_BUFFER)
60
+ out << tmp
61
+ end
62
+
63
+ while tmp = stderr.read(SHELLEX_IO_BUFFER)
64
+ err << tmp
65
+ end
66
+ end
67
+ end
68
+ rescue Exception => e
69
+ if pid
70
+ Process.kill 9, pid
71
+ Process.wait pid
72
+ end
73
+
74
+ # Exceptions coming from popen4 are deserialized from forked process, so they are not caught by listing
75
+ # class name in rescue clause
76
+ case e.class.to_s
77
+ when /Timeout::Error/
78
+ raise ShellExecutionTimeout, "Timeout after #{opts[:timeout]} secs while running '#{cmd}'"
79
+ else
80
+ raise ShellExecutionError, e.message
81
+ end
82
+ end
83
+
84
+ _shellex_debug(cmd, err, opts, out)
85
+
86
+ _shellex_extend_status(status)
87
+ status.command = cmd
88
+
89
+ return out, err, status
90
+ end
91
+
92
+ def _shellex_debug(cmd, err, opts, out)
93
+ if defined?(Rails) and Rails.env.development?
94
+ Rails.logger.info { "Executed: #{cmd}\n STDIN: #{opts[:input].inspect} STDOUT: #{out.inspect}\nSTDERR: #{err.inspect}" }
95
+ end
96
+ end
97
+ end
98
+
99
+ class String
100
+ def with_args(*args)
101
+ escape = proc { |val| val.to_s._shellex_escape }
102
+ ignore_nil = proc { |val| escape.call(val) unless val.nil? }
103
+
104
+ gsub(/(\?\&|\?\?|\?\!|\?~|\?)/) do |match|
105
+ val = args.shift
106
+
107
+ case match
108
+ when "?~" # Escape question mark
109
+ "?"
110
+ when "?!" # Argument can't be nil
111
+ if val.nil?
112
+ raise ShellArgumentMissing, "Argument marked as required with ?! is nil"
113
+ else
114
+ escape.call(val)
115
+ end
116
+ when "??" # Argument will be omitted if nil
117
+ ignore_nil.call(val)
118
+ when "?" # Argument will be escaped even if nil
119
+ escape.call(val)
120
+ when "?&" # Argument has to be array and each element will be escaped or ommitted if nil
121
+ if val.is_a?(Array)
122
+ val.map(&ignore_nil).compact.join(" ")
123
+ else
124
+ raise ShellArgumentMissing, "If ?& is present in this position, #{val.inspect} should be an Array"
125
+ end
126
+ end
127
+ end.strip
128
+ end
129
+
130
+ def _shellex_escape
131
+ if self.empty? or self.strip == ""
132
+ return "''"
133
+ end
134
+ self.split(/'/, -1).map{|e| "'#{e}'"}.join("\\'")
135
+ end
136
+
137
+ def shellex(*args)
138
+ shellex(self, *args)
139
+ end
140
+
141
+ def silent_shellex(*args)
142
+ silent_shellex(self, *args)
143
+ end
144
+ end
@@ -0,0 +1,3 @@
1
+ module Shellex
2
+ VERSION = "1.0.0"
3
+ end
data/shellex.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shellex/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "shellex"
8
+ spec.version = Shellex::VERSION
9
+ spec.authors = ["Dima Sabanin"]
10
+ spec.email = ["sdmitry@gmail.com"]
11
+ spec.description = %q{Shell execution made easy and secure}
12
+ spec.summary = %q{Allows you to securely execute shell code with exceptions on errors and timeouts}
13
+ spec.homepage = "https://github.com/dsabanin/shellex"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,142 @@
1
+ require 'shellex'
2
+ require 'test/unit'
3
+ require 'shoulda/context'
4
+
5
+ class ShellexTest < Test::Unit::TestCase
6
+
7
+ context "ShellEx" do
8
+
9
+ context "capturing" do
10
+ should "stdout capturing" do
11
+ stdout, stderr = shellex("echo stdout")
12
+ assert_equal "stdout\n", stdout
13
+ assert_equal "", stderr
14
+ end
15
+
16
+ should "stderr capturing" do
17
+ stdout, stderr = shellex("echo stderr 1>&2")
18
+ assert_equal "", stdout
19
+ assert_equal "stderr\n", stderr
20
+ end
21
+
22
+ should "stdout and stderr capturing" do
23
+ stdout, stderr = shellex("echo stdout; echo stderr 1>&2")
24
+ assert_equal "stdout\n", stdout
25
+ assert_equal "stderr\n", stderr
26
+ end
27
+ end
28
+
29
+ should "eat stdin input" do
30
+ stdout, stderr = shellex("cat -", :input => "hello")
31
+ assert_equal "hello", stdout
32
+ end
33
+
34
+ should "timeout" do
35
+ assert_raises(ShellExecutionTimeout) do
36
+ shellex("sleep 10", :timeout => 1)
37
+ end
38
+ end
39
+
40
+ should "timeout on blocking io" do
41
+ assert_raises(ShellExecutionTimeout) do
42
+ shellex("cat /dev/stdin", :timeout => 1, :close_stdin => false)
43
+ end
44
+ end
45
+
46
+ context "interpolation" do
47
+ should "interpolate and escape the args" do
48
+ real = "echo ? ? ?".with_args(1, "blah", :symbol)
49
+ assert_equal "echo '1' 'blah' 'symbol'", real
50
+ end
51
+
52
+ should "interpolate ?& as series of args" do
53
+ real = "? ?&".with_args("echo", [1,2,3,4])
54
+ assert_equal "'echo' '1' '2' '3' '4'", real
55
+ end
56
+
57
+ should "interpolate ?& as series of args in the middle" do
58
+ real = "? ?& ?".with_args("echo", [1,2,3,4], "abc")
59
+ assert_equal "'echo' '1' '2' '3' '4' 'abc'", real
60
+ end
61
+
62
+ should "raise error if array for ?& is not given" do
63
+ assert_raises(ShellArgumentMissing) do
64
+ "? ?& ?".with_args("echo", 1)
65
+ end
66
+ end
67
+
68
+ should "interpolate empty array to empty space" do
69
+ real = "? ?& ?".with_args("echo", [], "abc")
70
+ assert_equal "'echo' 'abc'", real
71
+ end
72
+
73
+ should "interpolate nil in array to empty space" do
74
+ real = "? ?& ?".with_args("echo", [1, nil, 3], "abc")
75
+ assert_equal "'echo' '1' '3' 'abc'", real
76
+ end
77
+
78
+ should "interpolate on shellex call" do
79
+ stdout, stderr = shellex("? ?", "echo", "stdout")
80
+ assert_equal "stdout\n", stdout
81
+ end
82
+
83
+ should "interpolate nil value as empty string with ?" do
84
+ real = "? ?".with_args("echo", nil)
85
+ assert_equal "'echo' ''", real
86
+
87
+ real = "? ?".with_args("echo", "blah")
88
+ assert_equal "'echo' 'blah'", real
89
+ end
90
+
91
+ should "ignore nil values with ??" do
92
+ real = "? ??".with_args("echo", nil)
93
+ assert_equal "'echo'", real
94
+
95
+ real = "? ??".with_args("echo", "blah")
96
+ assert_equal "'echo' 'blah'", real
97
+ end
98
+
99
+ should "raise on required arguments" do
100
+ assert_raises(ShellArgumentMissing) do
101
+ real = "? ?!".with_args("echo", nil)
102
+ end
103
+
104
+ real = "? ??".with_args("echo", "blah")
105
+ assert_equal "'echo' 'blah'", real
106
+ end
107
+
108
+ should "be able to escape the question mark" do
109
+ real = "? ?~".with_args("echo", "ello")
110
+ assert_equal "'echo' ?", real
111
+ end
112
+ end
113
+
114
+ context "error handling" do
115
+ should "raise error if exit value is not zero" do
116
+ assert_raises(ShellExecutionFailed) do
117
+ shellex("test a = b")
118
+ end
119
+ end
120
+
121
+ should "have silent version that eats errors" do
122
+ cmd = "echo ? 1>&2; test a = ?"
123
+ stdout, stderr, status = silent_shellex(cmd, "stderr", "b")
124
+ assert_equal "", stdout
125
+ assert stderr.include?("stderr"), "Got: #{stderr}"
126
+ assert_equal false, status.success?
127
+ assert_equal "echo 'stderr' 1>&2; test a = 'b'", status.command
128
+ end
129
+ end
130
+
131
+ context "should have singleton-array api" do
132
+ should "provide stdout/stderr methods" do
133
+ api = shellex("echo test")
134
+ assert_equal "test\n", api.stdout
135
+ assert_equal "test\n", api.to_s
136
+ assert_equal "test\n", api.to_str
137
+ assert_equal "", api.stderr
138
+ assert api.frozen?
139
+ end
140
+ end
141
+ end
142
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shellex
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Dima Sabanin
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-03-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bundler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 9
29
+ segments:
30
+ - 1
31
+ - 3
32
+ version: "1.3"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: Shell execution made easy and secure
50
+ email:
51
+ - sdmitry@gmail.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - LICENSE.txt
62
+ - README.md
63
+ - Rakefile
64
+ - lib/lib.iml
65
+ - lib/shellex.rb
66
+ - lib/shellex/version.rb
67
+ - shellex.gemspec
68
+ - test/shellex_test.rb
69
+ homepage: https://github.com/dsabanin/shellex
70
+ licenses:
71
+ - MIT
72
+ post_install_message:
73
+ rdoc_options: []
74
+
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.24
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Allows you to securely execute shell code with exceptions on errors and timeouts
102
+ test_files:
103
+ - test/shellex_test.rb