git_handler 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +3 -0
- data/README.md +229 -0
- data/Rakefile +10 -0
- data/git_handler.gemspec +22 -0
- data/lib/git_handler.rb +10 -0
- data/lib/git_handler/authorized_keys.rb +40 -0
- data/lib/git_handler/configuration.rb +23 -0
- data/lib/git_handler/core_ext/array.rb +9 -0
- data/lib/git_handler/core_ext/hash.rb +9 -0
- data/lib/git_handler/errors.rb +6 -0
- data/lib/git_handler/git_command.rb +46 -0
- data/lib/git_handler/public_key.rb +47 -0
- data/lib/git_handler/request.rb +5 -0
- data/lib/git_handler/session.rb +129 -0
- data/lib/git_handler/version.rb +3 -0
- data/spec/authorized_keys_spec.rb +45 -0
- data/spec/command_spec.rb +55 -0
- data/spec/configuration_spec.rb +11 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/public_key_spec.rb +28 -0
- data/spec/session_spec.rb +101 -0
- data/spec/spec_helper.rb +11 -0
- metadata +128 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
*.swp
|
4
|
+
*.tmproj
|
5
|
+
*~
|
6
|
+
.DS_Store
|
7
|
+
.\#*
|
8
|
+
.bundle
|
9
|
+
.config
|
10
|
+
.yardoc
|
11
|
+
Gemfile.lock
|
12
|
+
InstalledFiles
|
13
|
+
\#*
|
14
|
+
_yardoc
|
15
|
+
coverage
|
16
|
+
doc/
|
17
|
+
lib/bundler/man
|
18
|
+
pkg
|
19
|
+
rdoc
|
20
|
+
spec/reports
|
21
|
+
test/tmp
|
22
|
+
test/version_tmp
|
23
|
+
tmp
|
24
|
+
tmtags
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# GitHandler [![Build Status](https://secure.travis-ci.org/sosedoff/git-handler.png?branch=master)](http://travis-ci.org/sosedoff/git-handler)
|
2
|
+
|
3
|
+
A tool to simplify your git flow customizations. Its main purpose is to provide an
|
4
|
+
application-based control layer for Git request processing.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Install using rubygems:
|
9
|
+
|
10
|
+
```
|
11
|
+
gem install git_handler
|
12
|
+
```
|
13
|
+
|
14
|
+
Or using latest source code:
|
15
|
+
|
16
|
+
```
|
17
|
+
git clone git://github.com/sosedoff/git-handler.git
|
18
|
+
cd git-handler
|
19
|
+
bundle install
|
20
|
+
rake install
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
If you already have an operation system configured, make sure you have ```git```
|
26
|
+
user in your system. In order to use git_handler you'll need to generate a customized SSH public key and
|
27
|
+
add it to ```~/.ssh/authorized_keys``` on server. Generation should be something
|
28
|
+
that needs to be implemented in your application or script, there is functionality already
|
29
|
+
built for that:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require 'git_handler/public_key'
|
33
|
+
|
34
|
+
# Load your current pub key
|
35
|
+
content = File.read(File.expand_path('~/.ssh/id_rsa.pub'))
|
36
|
+
|
37
|
+
# Create a key
|
38
|
+
key = GitHandler::PublicKey.new(content)
|
39
|
+
```
|
40
|
+
|
41
|
+
Now, to convert loaded key into a system key just run:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
key.to_system_key('/usr/bin/git_proxy')
|
45
|
+
# => command="/usr/bin/git_proxy",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNjN3ZUOoosWeuJ7KczE5FAOzwZ+Z51KSQvqTCb7ccBi4u+pPYcGEYr2t0cx/BUcx/ZGE8ih+zxN1qM8KmM0uluuy54itHsKFdAwoibkbG22fQc2DY0RmktXXB/w6LxmFuQrmz0fkcbkE39pm5k6Nw6mqks5HjM7aDXRdwM8fSrq0PjfUNiESIrIAeEMGhtZFaj+WZVMfXaIlgzxZsAUpUULhN4j069v8VgxWyyOUT+gwcQB8lVc0BVYhptlFaJBtwhfWvOAviSuK7Cpjh60NdkZ3R2QYeh6wb6fF+KGCkM4iED4PZ1Ep8fRzrbCHky4VHSOyOvg9qKcgP1h+e+diD
|
46
|
+
```
|
47
|
+
|
48
|
+
SSH public key is now ready for usage on server side. Drop it into ```~/home/git/.ssh/authorized_keys``` file
|
49
|
+
if your user is ```git```. The whole purpose of key modifications is that we're
|
50
|
+
restricting SSH to a specific command or script on server, which gives us ability
|
51
|
+
to control permissions and other restrictions.
|
52
|
+
|
53
|
+
### Control script
|
54
|
+
|
55
|
+
In the example above as you can see we specify ```/usr/bin/git_proxy``` to be
|
56
|
+
executed once SSH connection is being established. GitHandler provides a simple
|
57
|
+
api to verify and execute git request that comes from client.
|
58
|
+
|
59
|
+
Example of ```/usr/bin/git_proxy``` file:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
#!/usr/bin/env ruby
|
63
|
+
require 'git_handler'
|
64
|
+
|
65
|
+
config = GitHandler::Configuration.new
|
66
|
+
|
67
|
+
# Configuration has a bunch of options:
|
68
|
+
# :user - Git user, default: git
|
69
|
+
# :home_path - Home path, default: /home/git
|
70
|
+
# :repos_path - Path to repositories, default: /home/git/repositories
|
71
|
+
# :log_path - Git requests logger, default: /var/log/git_handler.log
|
72
|
+
|
73
|
+
begin
|
74
|
+
session = GitHandler::Session.new(config)
|
75
|
+
session.execute(ARGV, ENV)
|
76
|
+
rescue Exception => ex
|
77
|
+
STDERR.puts "Error: #{ex.message}"
|
78
|
+
exit(1)
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
**NOTE:** Script must have permissions for execution.
|
83
|
+
|
84
|
+
Session instance will check if incoming git request has a valid environment and
|
85
|
+
valid git command. After check is complete it will shell out to ```git-shell -c COMMAND```
|
86
|
+
to perform an original git command. Providing block to ```session.execute``` will
|
87
|
+
override default and allow you to control the logic:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
session.execute(ARGV, ENV) do |request|
|
91
|
+
|
92
|
+
# Yields GitHandler::Request instance that
|
93
|
+
# contains all information about git request, env and repo
|
94
|
+
|
95
|
+
STDERR.puts "-----------------------------"
|
96
|
+
STDERR.puts "REMOTE IP: #{request.remote_ip}"
|
97
|
+
STDERR.puts "ARGS: #{request.args.inspect}"
|
98
|
+
STDERR.puts "ENV: #{request.env.inspect}"
|
99
|
+
STDERR.puts "REPO: #{request.repo}"
|
100
|
+
STDERR.puts "REPO PATH: #{request.repo_path}"
|
101
|
+
STDERR.puts "COMMAND: #{request.command}"
|
102
|
+
STDERR.puts "-----------------------------"
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
By default, if request has invalid environment attributes or not a git request,
|
107
|
+
session raises ```GitHandler::SessionError```. If you dont want to handle exceptions,
|
108
|
+
just use ```session.execute_safe``` method:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
session = GitHandler::Session.new(config)
|
112
|
+
session.execute_safe(ARGV, ENV)
|
113
|
+
```
|
114
|
+
|
115
|
+
To test if all that works try this:
|
116
|
+
|
117
|
+
```
|
118
|
+
ssh -vT git@YOUR_HOST.com
|
119
|
+
```
|
120
|
+
|
121
|
+
In the debug output you'll something similar:
|
122
|
+
|
123
|
+
```
|
124
|
+
debug1: Remote: Agent forwarding disabled.
|
125
|
+
debug1: Remote: Pty allocation disabled.
|
126
|
+
debug1: Remote: Forced command.
|
127
|
+
debug1: Remote: Port forwarding disabled.
|
128
|
+
debug1: Remote: X11 forwarding disabled.
|
129
|
+
debug1: Remote: Agent forwarding disabled.
|
130
|
+
debug1: Remote: Pty allocation disabled.
|
131
|
+
debug1: Sending environment.
|
132
|
+
debug1: Sending env LANG = en_US.UTF-8
|
133
|
+
|
134
|
+
>>> Error: Invalid git request <<<<
|
135
|
+
|
136
|
+
debug1: client_input_channel_req: channel 0 rtype exit-status reply 0
|
137
|
+
debug1: client_input_channel_req: channel 0 rtype eow@openssh.com reply 0
|
138
|
+
debug1: channel 0: free: client-session, nchannels 1
|
139
|
+
Transferred: sent 2384, received 2880 bytes, in 0.3 seconds
|
140
|
+
Bytes per second: sent 7308.1, received 8828.6
|
141
|
+
debug1: Exit status 1
|
142
|
+
```
|
143
|
+
|
144
|
+
This means that everything works. Script does not provide any shell access and
|
145
|
+
only allows git requests. To test that, create an empty repository:
|
146
|
+
|
147
|
+
```
|
148
|
+
mkdir /home/git/repositories
|
149
|
+
cd /home/git/repositories
|
150
|
+
git init --bare testrepo.git
|
151
|
+
```
|
152
|
+
|
153
|
+
And clone it (on local machine):
|
154
|
+
|
155
|
+
```
|
156
|
+
git clone git@YOUR_HOST.com:testrepo.git
|
157
|
+
```
|
158
|
+
|
159
|
+
### Server side configuration
|
160
|
+
|
161
|
+
In case you dont have a git user on your server, here is a quick manual
|
162
|
+
on how to get it rolling.
|
163
|
+
|
164
|
+
Create a git user:
|
165
|
+
|
166
|
+
```bash
|
167
|
+
adduser --home /home/git --disabled-password git
|
168
|
+
```
|
169
|
+
|
170
|
+
Restrict SSH authentication only via public keys. Open file ```/etc/ssh/sshd_config``` and
|
171
|
+
add this snippet to the end:
|
172
|
+
|
173
|
+
```
|
174
|
+
Match User !root
|
175
|
+
PasswordAuthentication no
|
176
|
+
```
|
177
|
+
|
178
|
+
This will disable password authentications for everyone except root, or other user
|
179
|
+
of your choice. You'll need to restart ssh daemon:
|
180
|
+
|
181
|
+
```
|
182
|
+
/etc/init.d/ssh restart
|
183
|
+
```
|
184
|
+
|
185
|
+
### Authorized Keys
|
186
|
+
|
187
|
+
GitHandler provides a simple api to manage your ```authorized_keys``` file content.
|
188
|
+
|
189
|
+
Each write operation issues a lock ```File::LOCK_EX``` on file.
|
190
|
+
|
191
|
+
Example:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
require 'git_handler/public_key'
|
195
|
+
require 'git_handler/authorized_keys'
|
196
|
+
|
197
|
+
# Read your local ssh public key content
|
198
|
+
content = File.read(File.expand_path('~/.ssh/id_rsa.pub'))
|
199
|
+
|
200
|
+
# Create a new key
|
201
|
+
key = GitHandler::PublicKey.new(content)
|
202
|
+
|
203
|
+
# Write formatted key to authorized_keys file
|
204
|
+
GitHandler::AuthorizedKeys.write_key('/path/to/file', key, 'my_command')
|
205
|
+
```
|
206
|
+
|
207
|
+
You can also write multiple keys:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
GitHandler::AuthorizedKeys.write_keys('/path/to/file', [k1, k2, k3], 'my_command')
|
211
|
+
```
|
212
|
+
|
213
|
+
## Testing
|
214
|
+
|
215
|
+
To run the test suite execute:
|
216
|
+
|
217
|
+
```
|
218
|
+
rake test
|
219
|
+
```
|
220
|
+
|
221
|
+
## License
|
222
|
+
|
223
|
+
Copyright (c) 2012 Dan Sosedoff.
|
224
|
+
|
225
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
226
|
+
|
227
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
228
|
+
|
229
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/git_handler.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../lib/git_handler/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "git_handler"
|
5
|
+
s.version = GitHandler::VERSION
|
6
|
+
s.summary = "Server-side git request handler"
|
7
|
+
s.description = "Set of tool to simplify custom git server setup"
|
8
|
+
s.homepage = "http://github.com/sosedoff/git_handler"
|
9
|
+
s.authors = ["Dan Sosedoff"]
|
10
|
+
s.email = ["dan.sosedoff@gmail.com"]
|
11
|
+
|
12
|
+
s.add_development_dependency 'rake', '~> 0.8'
|
13
|
+
s.add_development_dependency 'rspec', '~> 2.6'
|
14
|
+
s.add_development_dependency 'simplecov', '~> 0.4'
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'sshkey', '~> 1.3'
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
data/lib/git_handler.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'git_handler/core_ext/array'
|
2
|
+
require 'git_handler/core_ext/hash'
|
3
|
+
require 'git_handler/version'
|
4
|
+
require 'git_handler/errors'
|
5
|
+
require 'git_handler/configuration'
|
6
|
+
require 'git_handler/request'
|
7
|
+
require 'git_handler/git_command'
|
8
|
+
require 'git_handler/session'
|
9
|
+
|
10
|
+
module GitHandler ; end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module GitHandler
|
2
|
+
module AuthorizedKeys
|
3
|
+
# Write contents to file with lock
|
4
|
+
#
|
5
|
+
# path - Path to output file
|
6
|
+
# content - String buffer
|
7
|
+
#
|
8
|
+
def self.write(path, content)
|
9
|
+
raise ArgumentError, "File \"#{path}\" does not exist." if !File.exists?(path)
|
10
|
+
raise ArgumentError, "File \"#{path}\" is not writable." if !File.writable?(path)
|
11
|
+
|
12
|
+
File.open(path, 'w') do |f|
|
13
|
+
f.flock(File::LOCK_EX)
|
14
|
+
f.write(content)
|
15
|
+
f.flock(File::LOCK_UN)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Write formatted keys content to file
|
20
|
+
#
|
21
|
+
# path - Path to authorized_keys file
|
22
|
+
# keys - Array of GitHandler::PublicKey instances
|
23
|
+
# command - A custom command for the key
|
24
|
+
#
|
25
|
+
def self.write_keys(path, keys, command)
|
26
|
+
content = keys.map { |k| k.to_system_key(command) }.join("\n").strip
|
27
|
+
self.write(path, content)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Write a single key formatted content to file
|
31
|
+
#
|
32
|
+
# path - Path to the output file
|
33
|
+
# key - GitHandler::PublicKey instance
|
34
|
+
# command - A custom command for the key
|
35
|
+
#
|
36
|
+
def self.write_key(path, key, command)
|
37
|
+
self.write_keys(path, [key], command)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GitHandler
|
2
|
+
class Configuration
|
3
|
+
attr_reader :user
|
4
|
+
attr_reader :home_path
|
5
|
+
attr_reader :repos_path
|
6
|
+
attr_reader :log_path
|
7
|
+
|
8
|
+
# Initialize a new Configuration instance with options hash
|
9
|
+
#
|
10
|
+
# Valid options:
|
11
|
+
# :user - Git user
|
12
|
+
# :home_path - Git user home path
|
13
|
+
# :repos_path - Path to repositories
|
14
|
+
# :log_path - Git access log path
|
15
|
+
#
|
16
|
+
def initialize(options={})
|
17
|
+
@user = options[:user] || 'git'
|
18
|
+
@home_path = options[:home_path] || '/home/git'
|
19
|
+
@repos_path = options[:repos_path] || File.join(@home_path, 'repositories')
|
20
|
+
@log_path = options[:log_path] || File.join(@home_path, 'access.log')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module GitHandler
|
2
|
+
module GitCommand
|
3
|
+
GIT_COMMAND = /^(git-upload-pack|git upload-pack|git-upload-archive|git upload-archive|git-receive-pack|git receive-pack) '(.*)'$/
|
4
|
+
|
5
|
+
COMMANDS_READONLY = [
|
6
|
+
'git-upload-pack',
|
7
|
+
'git upload-pack',
|
8
|
+
'git-upload-archive',
|
9
|
+
'git upload-archive'
|
10
|
+
]
|
11
|
+
|
12
|
+
COMMANDS_WRITE = [
|
13
|
+
'git-receive-pack',
|
14
|
+
'git receive-pack'
|
15
|
+
]
|
16
|
+
|
17
|
+
def parse_command(cmd)
|
18
|
+
unless valid_command?(cmd)
|
19
|
+
raise ParseError, "Invalid command: #{cmd}"
|
20
|
+
end
|
21
|
+
|
22
|
+
match = cmd.scan(GIT_COMMAND).flatten
|
23
|
+
action = match.first
|
24
|
+
repo = match.last
|
25
|
+
|
26
|
+
{
|
27
|
+
:action => action,
|
28
|
+
:repo => repo,
|
29
|
+
:read => read_command?(action),
|
30
|
+
:write => write_command?(action)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_command?(cmd)
|
35
|
+
cmd =~ GIT_COMMAND ? true : false
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_command?(cmd)
|
39
|
+
COMMANDS_READONLY.include?(cmd)
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_command?(cmd)
|
43
|
+
COMMANDS_WRITE.include?(cmd)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'sshkey'
|
3
|
+
|
4
|
+
module GitHandler
|
5
|
+
class PublicKey
|
6
|
+
COMMAND_OPTIONS = [
|
7
|
+
'no-port-forwarding',
|
8
|
+
'no-X11-forwarding',
|
9
|
+
'no-agent-forwarding',
|
10
|
+
'no-pty'
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_reader :content
|
14
|
+
|
15
|
+
def initialize(content=nil)
|
16
|
+
@content = cleanup_content(content)
|
17
|
+
if @content.empty?
|
18
|
+
raise ArgumentError, 'Key content is empty!'
|
19
|
+
end
|
20
|
+
unless valid?
|
21
|
+
raise ArgumentError, "Is not a valid public key!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
SSHKey.valid_ssh_public_key?(@content)
|
27
|
+
end
|
28
|
+
|
29
|
+
def md5
|
30
|
+
Digest::MD5.hexdigest(@content)
|
31
|
+
end
|
32
|
+
|
33
|
+
def sha1
|
34
|
+
Digest::SHA1.hexdigest(@content)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_system_key(command)
|
38
|
+
"command=\"#{command}\",#{COMMAND_OPTIONS.join(",")} #{@content}"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def cleanup_content(str)
|
44
|
+
str.to_s.strip.gsub(/(\r|\n)*/m, "")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module GitHandler
|
4
|
+
class Session
|
5
|
+
include GitHandler::GitCommand
|
6
|
+
|
7
|
+
attr_reader :args, :env, :config
|
8
|
+
attr_reader :log
|
9
|
+
|
10
|
+
# Initialize a new Session
|
11
|
+
#
|
12
|
+
# config - GitHandler::Configuration instance
|
13
|
+
#
|
14
|
+
def initialize(config=nil)
|
15
|
+
unless config.kind_of?(GitHandler::Configuration)
|
16
|
+
raise SessionError, 'Configuration required!'
|
17
|
+
end
|
18
|
+
|
19
|
+
unless File.exists?(config.home_path)
|
20
|
+
raise ConfigurationError, "Home path does not exist!"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless File.exists?(config.repos_path)
|
24
|
+
raise ConfigurationError, "Repositories path does not exist!"
|
25
|
+
end
|
26
|
+
|
27
|
+
@config = config
|
28
|
+
@log = Logger.new(@config.log_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Execute session
|
32
|
+
#
|
33
|
+
# args - Command arguments
|
34
|
+
# env - Environment parameters
|
35
|
+
# run_git - Execute git shell if no block provided#
|
36
|
+
#
|
37
|
+
def execute(args, env, run_git=true)
|
38
|
+
@args = args
|
39
|
+
@env = env
|
40
|
+
|
41
|
+
raise SessionError, "Invalid environment" unless valid_environment?
|
42
|
+
raise SessionError, "Invalid git request" unless valid_request?
|
43
|
+
|
44
|
+
command = parse_command(env['SSH_ORIGINAL_COMMAND'])
|
45
|
+
repo_path = File.join(config.repos_path, command[:repo])
|
46
|
+
request = GitHandler::Request.new(
|
47
|
+
:remote_ip => env['SSH_CLIENT'].split(' ').first,
|
48
|
+
:args => args,
|
49
|
+
:env => env,
|
50
|
+
:repo => command[:repo],
|
51
|
+
:repo_path => repo_path,
|
52
|
+
:command => [command[:action], "'#{repo_path}'"].join(' '),
|
53
|
+
:read => command[:read],
|
54
|
+
:write => command[:write]
|
55
|
+
)
|
56
|
+
|
57
|
+
log_request(request)
|
58
|
+
|
59
|
+
unless File.exist?(request.repo_path)
|
60
|
+
raise SessionError, "Repository #{request.repo} does not exist!"
|
61
|
+
end
|
62
|
+
|
63
|
+
if block_given?
|
64
|
+
# Pass all request information for custom processing
|
65
|
+
# if no block is defined it will execute git-shell
|
66
|
+
# with parameters provided
|
67
|
+
yield request
|
68
|
+
else
|
69
|
+
if run_git == true
|
70
|
+
exec("git-shell", "-c", request.command)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Interesting part, inspired by github write-up
|
75
|
+
# if we need to pass this to another server
|
76
|
+
# the process should replace itself with another ssh call:
|
77
|
+
# exec("ssh", "git@TARGET", "#{args.join(' ')}")
|
78
|
+
end
|
79
|
+
|
80
|
+
# Execute session in safe manner, catch all exceptions
|
81
|
+
# and terminate session
|
82
|
+
#
|
83
|
+
def execute_safe(args, env, run_git=true)
|
84
|
+
begin
|
85
|
+
execute(args, env, run_git)
|
86
|
+
rescue GitHandler::SessionError => err
|
87
|
+
# TODO: Some additional logging here
|
88
|
+
terminate(err.message)
|
89
|
+
rescue Exception => err
|
90
|
+
# TODO: Needs some love here
|
91
|
+
terminate(err.message)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Terminate session execution
|
96
|
+
#
|
97
|
+
# reason - Process termination reason message
|
98
|
+
# exit_status - Exit code (default: 1)
|
99
|
+
#
|
100
|
+
def terminate(reason='', exit_status=1)
|
101
|
+
logger.error("Session terminated. Reason: #{reason}")
|
102
|
+
$stderr.puts("Request failed: #{reason}")
|
103
|
+
exit(exit_status)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check if session environment is valid
|
107
|
+
#
|
108
|
+
def valid_environment?
|
109
|
+
env['USER'] == config.user && env['HOME'] == config.home_path
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check if session request is valid
|
113
|
+
#
|
114
|
+
def valid_request?
|
115
|
+
if env.keys_all?(['SSH_CLIENT', 'SSH_CONNECTION', 'SSH_ORIGINAL_COMMAND'])
|
116
|
+
if valid_command?(env['SSH_ORIGINAL_COMMAND'])
|
117
|
+
return true
|
118
|
+
end
|
119
|
+
end
|
120
|
+
false
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def log_request(req)
|
126
|
+
log.info("Request \"#{req.command}\" from #{req.remote_ip}. Args: #{req.args.join(' ')}")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'git_handler/authorized_keys'
|
4
|
+
|
5
|
+
describe GitHandler::AuthorizedKeys do
|
6
|
+
before :each do
|
7
|
+
@path = '/tmp/authorized_keys'
|
8
|
+
File.delete(@path) if File.exists?(@path)
|
9
|
+
end
|
10
|
+
|
11
|
+
after :each do
|
12
|
+
File.delete(@path) if File.exists?(@path)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.write' do
|
16
|
+
it 'raises error if output file does not exist' do
|
17
|
+
proc { GitHandler::AuthorizedKeys.write(@path, 'data') }.
|
18
|
+
should raise_error ArgumentError, "File \"#{@path}\" does not exist."
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises error if output file is not writable' do
|
22
|
+
FileUtils.touch(@path)
|
23
|
+
FileUtils.chmod(0400, @path)
|
24
|
+
|
25
|
+
proc { GitHandler::AuthorizedKeys.write(@path, 'data') }.
|
26
|
+
should raise_error ArgumentError, "File \"#{@path}\" is not writable."
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'writes data to the output file' do
|
30
|
+
FileUtils.touch(@path)
|
31
|
+
proc { GitHandler::AuthorizedKeys.write(@path, 'data') }.should_not raise_error
|
32
|
+
File.read(@path).should eq("data")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.write_keys' do
|
37
|
+
it 'writes formatted keys content into the output file' do
|
38
|
+
FileUtils.touch(@path)
|
39
|
+
k = SSHKey.generate
|
40
|
+
key = GitHandler::PublicKey.new(k.ssh_public_key)
|
41
|
+
GitHandler::AuthorizedKeys.write_keys(@path, [key], 'custom_command')
|
42
|
+
File.read(@path).should eq('command="custom_command",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ' + k.ssh_public_key)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestInstance
|
4
|
+
include GitHandler::GitCommand
|
5
|
+
end
|
6
|
+
|
7
|
+
describe GitHandler::GitCommand do
|
8
|
+
before do
|
9
|
+
@obj = TestInstance.new
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'detects a valid git command' do
|
13
|
+
@obj.valid_command?("invalid command").should be_false
|
14
|
+
@obj.valid_command?("git-receive-pack").should be_false
|
15
|
+
@obj.valid_command?("git-receive-pack repo.git").should be_false
|
16
|
+
@obj.valid_command?("git-receive-pack 'repo'").should be_true
|
17
|
+
@obj.valid_command?("git-receive-pack 'repo.git'").should be_true
|
18
|
+
end
|
19
|
+
|
20
|
+
context '.parse_command' do
|
21
|
+
it 'raises error on invalid git command' do
|
22
|
+
proc { @obj.parse_command("invalid command") }.
|
23
|
+
should raise_error GitHandler::ParseError
|
24
|
+
|
25
|
+
proc { @obj.parse_command("git-receive-pack 'repo.git'") }.
|
26
|
+
should_not raise_error GitHandler::ParseError
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns a proper action and repo' do
|
30
|
+
result = @obj.parse_command("git-receive-pack 'repo.git'")
|
31
|
+
result.should be_a Hash
|
32
|
+
result.should eql(
|
33
|
+
:action => 'git-receive-pack',
|
34
|
+
:repo => 'repo.git',
|
35
|
+
:read => false,
|
36
|
+
:write => true
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'detects read command' do
|
42
|
+
@obj.read_command?('git-receive-pack').should be_false
|
43
|
+
@obj.read_command?('git-upload-pack').should be_true
|
44
|
+
@obj.read_command?('git upload-pack').should be_true
|
45
|
+
@obj.read_command?('git-upload-archive').should be_true
|
46
|
+
@obj.read_command?('git upload-archive').should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'detects write command' do
|
50
|
+
@obj.write_command?("git-upload-pack").should be_false
|
51
|
+
@obj.write_command?("git-upload-archive").should be_false
|
52
|
+
@obj.write_command?("git receive-pack").should be_true
|
53
|
+
@obj.write_command?("git-receive-pack").should be_true
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GitHandler::Configuration do
|
4
|
+
it 'has default settings' do
|
5
|
+
config = GitHandler::Configuration.new
|
6
|
+
config.user.should eq('git')
|
7
|
+
config.home_path.should eq('/home/git')
|
8
|
+
config.repos_path.should eq('/home/git/repositories')
|
9
|
+
config.log_path.should eq('/home/git/access.log')
|
10
|
+
end
|
11
|
+
end
|
File without changes
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'git_handler/public_key'
|
3
|
+
|
4
|
+
describe GitHandler::PublicKey do
|
5
|
+
it 'required content' do
|
6
|
+
proc { GitHandler::PublicKey.new }.
|
7
|
+
should raise_error ArgumentError, 'Key content is empty!'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should be valid' do
|
11
|
+
proc { GitHandler::PublicKey.new('some data') }.
|
12
|
+
should raise_error ArgumentError, 'Is not a valid public key!'
|
13
|
+
|
14
|
+
k = SSHKey.generate
|
15
|
+
|
16
|
+
proc { GitHandler::PublicKey.new(k.ssh_public_key) }.
|
17
|
+
should_not raise_error ArgumentError, 'Is not a valid public key!'
|
18
|
+
end
|
19
|
+
|
20
|
+
context '.to_system_key' do
|
21
|
+
it 'returns a customized key content' do
|
22
|
+
k = SSHKey.generate
|
23
|
+
key = GitHandler::PublicKey.new(k.ssh_public_key)
|
24
|
+
custom = key.to_system_key('foobar')
|
25
|
+
custom.should eq('command="foobar",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ' + k.ssh_public_key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
describe GitHandler::Session do
|
5
|
+
before do
|
6
|
+
FileUtils.mkdir_p('/tmp/valid-repo.git')
|
7
|
+
end
|
8
|
+
|
9
|
+
context '.new' do
|
10
|
+
it 'requires configuration' do
|
11
|
+
proc { GitHandler::Session.new }.
|
12
|
+
should raise_error GitHandler::SessionError, 'Configuration required!'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'raises error if home path does not exist' do
|
16
|
+
config = GitHandler::Configuration.new(:home_path => '/var/foo')
|
17
|
+
proc { GitHandler::Session.new(config) }.
|
18
|
+
should raise_error GitHandler::ConfigurationError, "Home path does not exist!"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises error if repos path does not exist' do
|
22
|
+
config = GitHandler::Configuration.new(:home_path => '/tmp', :repos_path => '/var/foo')
|
23
|
+
proc { GitHandler::Session.new(config) }.
|
24
|
+
should raise_error GitHandler::ConfigurationError, "Repositories path does not exist!"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context '.execute' do
|
29
|
+
before :each do
|
30
|
+
@config = GitHandler::Configuration.new(
|
31
|
+
:home_path => '/tmp',
|
32
|
+
:repos_path => '/tmp'
|
33
|
+
)
|
34
|
+
@session = GitHandler::Session.new(@config)
|
35
|
+
@env = {
|
36
|
+
'USER' => 'git',
|
37
|
+
'HOME' => '/tmp',
|
38
|
+
'SSH_CLIENT' => '127.0.0.1',
|
39
|
+
'SSH_CONNECTION' => '127.0.0.1 64039 127.0.0.2 22',
|
40
|
+
'SSH_ORIGINAL_COMMAND' => "git-upload-pack 'valid-repo.git'"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
subject do
|
45
|
+
GitHandler::Session.new(@config)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'validates environment' do
|
49
|
+
proc { subject.execute([], {}) }.
|
50
|
+
should raise_error GitHandler::SessionError, 'Invalid environment'
|
51
|
+
|
52
|
+
proc { subject.execute([], {'USER' => 'git', 'HOME' => '/invalid/path'}) }.
|
53
|
+
should raise_error GitHandler::SessionError, 'Invalid environment'
|
54
|
+
|
55
|
+
proc { subject.execute([], {'USER' => 'git', 'HOME' => '/tmp'}) }.
|
56
|
+
should_not raise_error GitHandler::SessionError, 'Invalid environment'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'validates git request' do
|
60
|
+
env = {'USER' => 'git', 'HOME' => '/tmp'}
|
61
|
+
|
62
|
+
proc { subject.execute([], env) }.
|
63
|
+
should raise_error GitHandler::SessionError, 'Invalid git request'
|
64
|
+
|
65
|
+
env.merge!(
|
66
|
+
'SSH_CLIENT' => '127.0.0.1',
|
67
|
+
'SSH_CONNECTION' => '127.0.0.1 64039 127.0.0.2 22',
|
68
|
+
'SSH_ORIGINAL_COMMAND' => 'invalid command'
|
69
|
+
)
|
70
|
+
|
71
|
+
proc { subject.execute([], env, false) }.
|
72
|
+
should raise_error GitHandler::SessionError, 'Invalid git request'
|
73
|
+
|
74
|
+
env['SSH_ORIGINAL_COMMAND'] = "git-upload-pack 'foobar.git'"
|
75
|
+
|
76
|
+
proc { subject.execute([], env, false) }.
|
77
|
+
should_not raise_error GitHandler::SessionError, 'Invalid git request'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'validates repository existense' do
|
81
|
+
@env['SSH_ORIGINAL_COMMAND'] = "git-upload-pack 'invalid-repo.git'"
|
82
|
+
proc { subject.execute([], @env, false) }.
|
83
|
+
should raise_error GitHandler::SessionError, 'Repository invalid-repo.git does not exist!'
|
84
|
+
|
85
|
+
@env['SSH_ORIGINAL_COMMAND'] = "git-upload-pack 'valid-repo.git'"
|
86
|
+
proc { subject.execute([], @env, false) }.
|
87
|
+
should_not raise_error GitHandler::SessionError, 'Repository valid-repo.git does not exist!'
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'yields request payload if block provided' do
|
91
|
+
payload = nil
|
92
|
+
subject.execute([], @env, false) { |req| payload = req }
|
93
|
+
payload.should_not be_nil
|
94
|
+
payload.should be_a GitHandler::Request
|
95
|
+
payload.env.should eq(@env)
|
96
|
+
payload.repo.should eq('valid-repo.git')
|
97
|
+
payload.repo_path.should eq('/tmp/valid-repo.git')
|
98
|
+
payload.command.should eq("git-upload-pack '/tmp/valid-repo.git'")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git_handler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.2.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dan Sosedoff
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-05-23 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.8"
|
24
|
+
type: :development
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "2.6"
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: simplecov
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0.4"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id003
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: sshkey
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "1.3"
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id004
|
59
|
+
description: Set of tool to simplify custom git server setup
|
60
|
+
email:
|
61
|
+
- dan.sosedoff@gmail.com
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files: []
|
67
|
+
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- .travis.yml
|
72
|
+
- Gemfile
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- git_handler.gemspec
|
76
|
+
- lib/git_handler.rb
|
77
|
+
- lib/git_handler/authorized_keys.rb
|
78
|
+
- lib/git_handler/configuration.rb
|
79
|
+
- lib/git_handler/core_ext/array.rb
|
80
|
+
- lib/git_handler/core_ext/hash.rb
|
81
|
+
- lib/git_handler/errors.rb
|
82
|
+
- lib/git_handler/git_command.rb
|
83
|
+
- lib/git_handler/public_key.rb
|
84
|
+
- lib/git_handler/request.rb
|
85
|
+
- lib/git_handler/session.rb
|
86
|
+
- lib/git_handler/version.rb
|
87
|
+
- spec/authorized_keys_spec.rb
|
88
|
+
- spec/command_spec.rb
|
89
|
+
- spec/configuration_spec.rb
|
90
|
+
- spec/fixtures/.gitkeep
|
91
|
+
- spec/public_key_spec.rb
|
92
|
+
- spec/session_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
homepage: http://github.com/sosedoff/git_handler
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: "0"
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: "0"
|
114
|
+
requirements: []
|
115
|
+
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 1.8.24
|
118
|
+
signing_key:
|
119
|
+
specification_version: 3
|
120
|
+
summary: Server-side git request handler
|
121
|
+
test_files:
|
122
|
+
- spec/authorized_keys_spec.rb
|
123
|
+
- spec/command_spec.rb
|
124
|
+
- spec/configuration_spec.rb
|
125
|
+
- spec/fixtures/.gitkeep
|
126
|
+
- spec/public_key_spec.rb
|
127
|
+
- spec/session_spec.rb
|
128
|
+
- spec/spec_helper.rb
|