├── .rspec ├── lib ├── ssh2http │ └── version.rb └── ssh2http.rb ├── .travis.yml ├── Rakefile ├── .gitignore ├── spec ├── ssh2http_spec.rb └── spec_helper.rb ├── Gemfile ├── exe └── ssh2http ├── ssh2http.gemspec ├── LICENSE └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/ssh2http/version.rb: -------------------------------------------------------------------------------- 1 | class Ssh2http 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.3.0 5 | 6 | before_install: 7 | - gem install bundler -v 1.11.2 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.gem 11 | -------------------------------------------------------------------------------- /spec/ssh2http_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ssh2http do 4 | it 'has a version number' do 5 | expect(Ssh2http::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ssh2http.gemspec 4 | gemspec 5 | 6 | gem 'codecov', :require => false, :group => :test 7 | gem 'pry' 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | if ENV['CI']=='true' 4 | require 'codecov' 5 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 6 | end 7 | 8 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 9 | require 'ssh2http' 10 | -------------------------------------------------------------------------------- /exe/ssh2http: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path('../../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'ssh2http' 6 | 7 | Ssh2http.die "Only ssh allowed." unless ENV['SSH_CONNECTION'] 8 | Ssh2http.die "Please provide a destination url." unless ARGV[0] && ARGV[0].length > 0 9 | 10 | Ssh2http.new(ARGV[0], *ENV['SSH_ORIGINAL_COMMAND'].to_s.split).run! 11 | exit 0 12 | -------------------------------------------------------------------------------- /ssh2http.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ssh2http/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ssh2http" 8 | spec.version = Ssh2http::VERSION 9 | spec.authors = ["P.S.V.R"] 10 | spec.email = ["pmq2001@gmail.com"] 11 | 12 | spec.summary = %q{delegating git ssh requests to git http backends} 13 | spec.description = %q{A restricted login shell for Git-only SSH access, which delegates git pull/push requests to a git http backend.} 14 | spec.homepage = 'https://github.com/pmq20/ssh2http' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", "~> 1.11" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | spec.add_development_dependency "rspec", "~> 3.0" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Minqi Pan (P.S.V.R) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh2http 2 | 3 | A restricted login shell for Git-only SSH access, 4 | which delegates git pull/push requests to a git http backend. 5 | 6 | [![Gem Version](https://badge.fury.io/rb/ssh2http.svg)](https://badge.fury.io/rb/ssh2http) 7 | [![Build Status](https://travis-ci.org/pmq20/ssh2http.svg)](https://travis-ci.org/pmq20/ssh2http) 8 | [![Code Climate](https://codeclimate.com/github/pmq20/ssh2http/badges/gpa.svg)](https://codeclimate.com/github/pmq20/ssh2http) 9 | [![codecov.io](https://codecov.io/github/pmq20/ssh2http/coverage.svg?branch=master)](https://codecov.io/github/pmq20/ssh2http?branch=master) 10 | [![](http://inch-ci.org/github/pmq20/ssh2http.svg?branch=master)](http://inch-ci.org/github/pmq20/ssh2http?branch=master) 11 | 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'ssh2http' 19 | ``` 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install ssh2http 28 | 29 | ## Usage 30 | 31 | Add this line to your ~/.ssh/authorized_keys: 32 | 33 | command="source $HOME/.profile && cd && bundle exec ssh2http ",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty 34 | 35 | Or if you prefer to install the gem globally: 36 | 37 | command="source $HOME/.profile && ssh2http ",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty 38 | 39 | For example, 40 | 41 | command="source $HOME/.profile && cd /Users/pmq20/ssh2http && bundle exec ssh2http http://localhost",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa XXXXXXXXXXXXXXXXXXXX pmq2001@gmail.com 42 | 43 | Then: 44 | 45 | git clone pmq20@localhost:/path/to/repo.git 46 | 47 | and the request will be delegated to `http://localhost/path/to/repo.git`. 48 | -------------------------------------------------------------------------------- /lib/ssh2http.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require "ssh2http/version" 3 | 4 | class Ssh2http 5 | def initialize(destination, *cmds) 6 | @destination = destination 7 | @cmds = cmds 8 | end 9 | 10 | def run! 11 | case cmd 12 | when 'git-upload-pack' 13 | upload! 14 | when 'git-receive-pack' 15 | receive! 16 | else 17 | die 'Unknown command' 18 | end 19 | end 20 | 21 | def upload! 22 | open(url('/info/refs?service=git-upload-pack')) do |f| 23 | f.each_line do |line| 24 | if "001e# service=git-upload-pack\n" == line 25 | f.read(4) # skip "0000" 26 | next 27 | end 28 | print line 29 | end 30 | end 31 | STDOUT.flush 32 | 33 | input = '' 34 | while line = STDIN.gets 35 | input += line 36 | break if line =~ /0009done\n$/ 37 | end 38 | 39 | url = URI.parse(url('/git-upload-pack')) 40 | Net::HTTP.start(url.host, url.port) do |http| 41 | request = Net::HTTP::Post.new url.path 42 | request.body = input 43 | request['Content-Type'] = 'application/x-git-upload-pack-request' 44 | http.request request do |response| 45 | response.read_body do |chunk| 46 | STDOUT.write chunk 47 | STDOUT.flush 48 | end 49 | end 50 | end 51 | end 52 | 53 | def receive! 54 | open(url('/info/refs?service=git-receive-pack')) do |f| 55 | f.each_line do |line| 56 | if "001f# service=git-receive-pack\n" == line 57 | f.read(4) # skip "0000" 58 | next 59 | end 60 | print line 61 | end 62 | end 63 | STDOUT.flush 64 | 65 | input = STDIN.read 66 | url = URI.parse(url('/git-receive-pack')) 67 | http = Net::HTTP.new(url.host, url.port) 68 | request = Net::HTTP::Post.new(url.path) 69 | request.body = input 70 | request['Content-Type'] = 'application/x-git-receive-pack-request' 71 | response = http.request(request) 72 | print response.body 73 | STDOUT.flush 74 | end 75 | 76 | def cmd 77 | @cmds[0] 78 | end 79 | 80 | def key 81 | @cmds[1].gsub(/['"]/, '') 82 | end 83 | 84 | def url(part) 85 | "#{@destination}#{key}#{part}" 86 | end 87 | 88 | def debug 89 | var('@cmds') 90 | var('@destination') 91 | var('RUBY_VERSION') 92 | var('::Ssh2http::VERSION') 93 | ENV.each do |k,v| 94 | var("ENV[#{k.inspect}]") 95 | end 96 | end 97 | 98 | def var(var) 99 | STDERR.puts "#{var}=#{eval(var).inspect}" 100 | end 101 | 102 | def die(msg) 103 | self.class.die("#{@destination} #{@cmds} #{msg}") 104 | end 105 | 106 | def self.die(msg) 107 | STDERR.puts msg 108 | exit 1 109 | end 110 | end 111 | --------------------------------------------------------------------------------