1aba11c by David A. Cuadrado at 2008-04-14 1
#!/usr/bin/env ruby
2
3
require 'rubygems'
4
require 'daemons'
9fea730 by David A. Cuadrado at 2008-04-14 5
require 'geoip'
1aba11c by David A. Cuadrado at 2008-04-14 6
require 'socket'
ddae231 by Johan Sørensen at 2009-01-09 7
require 'fcntl'
4ee78e5 by Johan Sørensen at 2008-04-16 8
require "optparse"
1aba11c by David A. Cuadrado at 2008-04-14 9
9fea730 by David A. Cuadrado at 2008-04-14 10
ENV["RAILS_ENV"] ||= "production"
1aba11c by David A. Cuadrado at 2008-04-14 11
require File.dirname(__FILE__)+'/../config/environment'
12
9fea730 by David A. Cuadrado at 2008-04-14 13
Rails.configuration.log_level = :info # Disable debug
4ee78e5 by Johan Sørensen at 2008-04-16 14
ActiveRecord::Base.allow_concurrency = true
9fea730 by David A. Cuadrado at 2008-04-14 15
899296a by Simon Hausmann at 2008-11-28 16
ENV["PATH"] = "/usr/local/bin/:/opt/local/bin:#{ENV["PATH"]}"
17
1aba11c by David A. Cuadrado at 2008-04-14 18
BASE_PATH = File.expand_path(GitoriousConfig['repository_base_path'])
19
d9a4e80 by Johan Sørensen at 2008-04-19 20
TIMEOUT = 30
3ec9c37 by David A. Cuadrado at 2008-04-16 21
MAX_CHILDREN = 30
00ae132 by David A. Cuadrado at 2008-04-14 22
$children_reaped = 0
23
$children_active = 0
24
09d0924 by Johan Sørensen at 2009-06-03 25
class GeoIP
26
  def close
27
    @file.close
28
  end
29
end
30
1aba11c by David A. Cuadrado at 2008-04-14 31
module Git
4ee78e5 by Johan Sørensen at 2008-04-16 32
  class Daemon
33
    include Daemonize
1aba11c by David A. Cuadrado at 2008-04-14 34
    
3a811a9 by Johan Sørensen at 2009-06-03 35
    SERVICE_READ_REGEXP = /^(git\-upload\-pack|git\ upload\-pack)\s(.+)\x00host=([\w\.\-]+)/.freeze
36
    SERVICE_WRITE_REGEXP = /^(git\-receive\-pack|git\ receive\-pack)\s(.+)\x00host=([\w\.\-]+)/.freeze
4ee78e5 by Johan Sørensen at 2008-04-16 37
    
38
    def initialize(options)
39
      @options = options
1aba11c by David A. Cuadrado at 2008-04-14 40
    end
41
    
4ee78e5 by Johan Sørensen at 2008-04-16 42
    def start
43
      if @options[:daemonize]
44
        daemonize(@options[:logfile])
00ae132 by David A. Cuadrado at 2008-04-14 45
      end
9b62e40 by Johan Sørensen at 2009-06-30 46
      Dir.chdir(Rails.root) # So Logger don't get confused
4ee78e5 by Johan Sørensen at 2008-04-16 47
      @socket = TCPServer.new(@options[:host], @options[:port])
c6c6cfe by Johan Sørensen at 2009-04-22 48
      @socket.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, !!@options[:reuseaddr])
ddae231 by Johan Sørensen at 2009-01-09 49
      @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
4ee78e5 by Johan Sørensen at 2008-04-16 50
      log(Process.pid, "Listening on #{@options[:host]}:#{@options[:port]}...")
5f6dc61 by Johan Sørensen at 2009-02-04 51
      ActiveRecord::Base.verify_active_connections! if @options[:daemonize]
4ee78e5 by Johan Sørensen at 2008-04-16 52
      run
53
    end
00ae132 by David A. Cuadrado at 2008-04-14 54
      
4ee78e5 by Johan Sørensen at 2008-04-16 55
    def run
06c39c4 by Johan Sørensen at 2009-05-15 56
      Dir.chdir(GitoriousConfig["repository_base_path"])
57
      if @options[:pidfile]
58
        File.open(@options[:pidfile], "w") do |f|
59
          f.write(Process.pid)
60
        end
61
      end
d341dce by Johan Sørensen at 2009-04-22 62
      while session = accept_socket
d3001d5 by Johan Sørensen at 2008-04-19 63
        connections = $children_active - $children_reaped
64
        if connections > MAX_CHILDREN
65
          log(Process.pid, "too many active children #{connections}/#{MAX_CHILDREN}")
66
          session.close
67
          next
fa39ead by David A. Cuadrado at 2008-04-17 68
        end
d3001d5 by Johan Sørensen at 2008-04-19 69
        
70
        run_service(session)
fa39ead by David A. Cuadrado at 2008-04-17 71
      end
72
    end
73
    
74
    def run_service(session)
75
      $children_active += 1
73d5092 by Johan Sørensen at 2008-04-19 76
      ip_family, port, name, ip = session.peeraddr
fa39ead by David A. Cuadrado at 2008-04-17 77
      
d341dce by Johan Sørensen at 2009-04-22 78
      line = receive_data(session)
d9a4e80 by Johan Sørensen at 2008-04-19 79
      
3a811a9 by Johan Sørensen at 2009-06-03 80
      if line =~ SERVICE_READ_REGEXP
7d56991 by Johan Sørensen at 2008-04-19 81
        start_time = Time.now
bb57ea9 by Johan Sørensen at 2009-04-22 82
        service = $1
83
        base_path = $2
84
        host = $3
3a811a9 by Johan Sørensen at 2009-06-03 85
4d0b3d5 by Johan Sørensen at 2009-04-22 86
        path = File.expand_path("#{BASE_PATH}/#{base_path}")
87
        log(Process.pid, "Connection from #{ip} for #{base_path.inspect}")
4710ebb by Johan Sørensen at 2009-04-22 88
        
4d0b3d5 by Johan Sørensen at 2009-04-22 89
        repository = nil        
4710ebb by Johan Sørensen at 2009-04-22 90
        begin
14132d8 by Johan Sørensen at 2009-05-09 91
          ActiveRecord::Base.verify_active_connections!
4710ebb by Johan Sørensen at 2009-04-22 92
          repository = ::Repository.find_by_path(path)
93
        rescue => e
94
          log(Process.pid, "AR error: #{e.class.name} #{e.message}:\n #{e.backtrace.join("\n  ")}")
95
        end
96
        
4d0b3d5 by Johan Sørensen at 2009-04-22 97
        unless repository
4710ebb by Johan Sørensen at 2009-04-22 98
          log(Process.pid, "Cannot find repository: #{path}")
9ad08cc by Johan Sørensen at 2009-04-22 99
          write_error_message(session, "Cannot find repository: #{base_path}")
100
          $children_active -= 1
4710ebb by Johan Sørensen at 2009-04-22 101
          session.close
102
          return
103
        end
4d0b3d5 by Johan Sørensen at 2009-04-22 104
        
7c2483a by Johan Sørensen at 2009-04-22 105
        real_path = File.expand_path(repository.full_repository_path)
4710ebb by Johan Sørensen at 2009-04-22 106
        log(Process.pid, "#{ip} wants #{path.inspect} => #{real_path.inspect}")
107
        
b1f805e by Johan Sørensen at 2009-04-22 108
        if real_path.index(BASE_PATH) != 0 || !File.directory?(real_path)
4710ebb by Johan Sørensen at 2009-04-22 109
          log(Process.pid, "Invalid path: #{real_path}")
9ad08cc by Johan Sørensen at 2009-04-22 110
          write_error_message(session, "Cannot find repository: #{base_path}")
1aba11c by David A. Cuadrado at 2008-04-14 111
          session.close
cdf2711 by Johan Sørensen at 2008-04-15 112
          $children_active -= 1
fa39ead by David A. Cuadrado at 2008-04-17 113
          return
1aba11c by David A. Cuadrado at 2008-04-14 114
        end
4ee78e5 by Johan Sørensen at 2008-04-16 115
      
4710ebb by Johan Sørensen at 2009-04-22 116
        if !File.exist?(File.join(real_path, "git-daemon-export-ok"))
fa39ead by David A. Cuadrado at 2008-04-17 117
          session.close
118
          $children_active -= 1
119
          return
120
        end
2efb027 by Johan Sørensen at 2009-02-04 121
        
4d0b3d5 by Johan Sørensen at 2009-04-22 122
        if ip_family == "AF_INET6"
123
          repository.cloned_from(ip)
124
        else
125
          geoip = GeoIP.new(File.join(RAILS_ROOT, "data", "GeoIP.dat"))
126
          localization = geoip.country(ip)
09d0924 by Johan Sørensen at 2009-06-03 127
          geoip.close
52049c7 by Marius Mathiesen at 2009-04-22 128
          repository.cloned_from(ip, localization[3], localization[5], 'git')
2efb027 by Johan Sørensen at 2009-02-04 129
        end
fa39ead by David A. Cuadrado at 2008-04-17 130
      
4710ebb by Johan Sørensen at 2009-04-22 131
        Dir.chdir(real_path) do
d9a4e80 by Johan Sørensen at 2008-04-19 132
          cmd = "git-upload-pack --strict --timeout=#{TIMEOUT} ."
ddae231 by Johan Sørensen at 2009-01-09 133
          
134
          child_pid = fork do
135
            log(Process.pid, "Deferred in #{'%0.5f' % (Time.now - start_time)}s")
4fc2d16 by Johan Sørensen at 2009-01-16 136
            
137
            $stdout.reopen(session)
138
            $stdin.reopen(session)
139
            $stderr.reopen("/dev/null")
140
            
fa39ead by David A. Cuadrado at 2008-04-17 141
            exec(cmd)
d9a4e80 by Johan Sørensen at 2008-04-19 142
            # FIXME; we don't ever get here since we exec(), so reaped count may be incorrect 
fa39ead by David A. Cuadrado at 2008-04-17 143
            $children_reaped += 1
144
            exit!
145
          end
146
        end rescue Errno::EAGAIN
3a811a9 by Johan Sørensen at 2009-06-03 147
      elsif line =~ SERVICE_WRITE_REGEXP
148
        service, base_path, host = $1, $2, $3
149
        log(Process.pid, "Not accepting #{service.inspect} for #{base_path.inspect}")
150
        write_error_message(session, "The git:// url is read-only. Please see " +
151
          "http://#{GitoriousConfig['gitorious_host']}#{base_path.sub(/\.git$/, '')} " +
152
          "for the push url, if you're a committer.")
153
        $children_active -= 1
154
        session.close
155
        return
fa39ead by David A. Cuadrado at 2008-04-17 156
      else
3d0b8bd by Johan Sørensen at 2009-04-22 157
        # $stderr.puts "Invalid request from #{ip}: #{line.inspect}"
fa39ead by David A. Cuadrado at 2008-04-17 158
        $children_active -= 1
1aba11c by David A. Cuadrado at 2008-04-14 159
      end
ddae231 by Johan Sørensen at 2009-01-09 160
      session.close
1aba11c by David A. Cuadrado at 2008-04-14 161
    end
162
  
4ee78e5 by Johan Sørensen at 2008-04-16 163
    def handle_stop(signal)
ddae231 by Johan Sørensen at 2009-01-09 164
      @socket.close
4ee78e5 by Johan Sørensen at 2008-04-16 165
      log(Process.pid, "Received #{signal}, exiting..")
166
      exit 0
167
    end
168
  
169
    def handle_cld
3ec9c37 by David A. Cuadrado at 2008-04-16 170
      loop do
171
        pid = nil
172
        begin
173
          pid = Process.wait(-1, Process::WNOHANG)
174
        rescue Errno::ECHILD
175
          break
176
        end
177
        
fa39ead by David A. Cuadrado at 2008-04-17 178
        if pid && $?
3ec9c37 by David A. Cuadrado at 2008-04-16 179
          $children_reaped += 1
180
          log(pid, "Disconnected. (status=#{$?.exitstatus})") if pid > 0
181
          if $children_reaped == $children_active
182
            $children_reaped = 0
183
            $children_active = 0 
184
          end
185
          
186
          next
187
        end
188
        break
4ee78e5 by Johan Sørensen at 2008-04-16 189
      end
190
    end
191
  
192
    def log(pid, msg)
193
      $stderr.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} [#{pid}] #{msg}"
194
    end
d341dce by Johan Sørensen at 2009-04-22 195
    
9ad08cc by Johan Sørensen at 2009-04-22 196
    def write_error_message(session, msg)
197
      message = ["\n----------------------------------------------"]
198
      message << msg
199
      message << "----------------------------------------------\n"
200
      write_into_sideband(session, message.join("\n"), 2)
201
    end
202
    
203
    def write_into_sideband(session, message, channel)
d231737 by Johan Sørensen at 2009-04-22 204
      msg = "%s%s" % [channel.chr, message]
205
      session.write("%04x%s" % [msg.length+4, msg])
9ad08cc by Johan Sørensen at 2009-04-22 206
    end
207
    
d341dce by Johan Sørensen at 2009-04-22 208
    def accept_socket
209
      if RUBY_VERSION < '1.9'
210
        @socket.accept
211
      else
212
        begin
213
          @socket.accept_nonblock
214
        rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR => e
215
          if IO.select([@socket])
216
            retry
217
          else
218
            raise e
219
          end
220
        end
221
      end
222
    end
223
    
224
    def receive_data(session)
225
      if RUBY_VERSION < '1.9'
bb57ea9 by Johan Sørensen at 2009-04-22 226
        read_data(session)
d341dce by Johan Sørensen at 2009-04-22 227
      else
bb57ea9 by Johan Sørensen at 2009-04-22 228
        read_data_nonblock(session)
229
      end
230
    end
231
    
232
    def read_data(session)
233
      size_string = session.recv(4)
c36bbad by Johan Sørensen at 2009-04-22 234
      return "" if !size_string
235
      size = size_string.to_i(16)
236
      return "" unless size > 4
237
      session.recv(size - 4)
238
    rescue Errno::ECONNRESET
239
      return ""
bb57ea9 by Johan Sørensen at 2009-04-22 240
    end
241
    
242
    def read_data_nonblock(session)
243
      begin
244
        size_string = session.recv_nonblock(4)
c36bbad by Johan Sørensen at 2009-04-22 245
        return "" if !size_string
246
        size = size_string.to_i(16)
247
        return "" unless size > 4
248
        session.recv_nonblock(size - 4)
bb57ea9 by Johan Sørensen at 2009-04-22 249
      rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
250
        if IO.select([@socket])
251
          retry
252
        else
253
          return ""
d341dce by Johan Sørensen at 2009-04-22 254
        end
255
      end
256
    end
4ee78e5 by Johan Sørensen at 2008-04-16 257
  
1aba11c by David A. Cuadrado at 2008-04-14 258
  end
259
end
260
4ee78e5 by Johan Sørensen at 2008-04-16 261
options = {
262
  :port => 9418,
3ec9c37 by David A. Cuadrado at 2008-04-16 263
  :host => "0.0.0.0",
4ee78e5 by Johan Sørensen at 2008-04-16 264
  :logfile => File.join(RAILS_ROOT, "log", "git-daemon.log"),
05e3db0 by Johan Sørensen at 2008-04-19 265
  :pidfile => File.join(RAILS_ROOT, "log", "git-daemon.pid"),
c6c6cfe by Johan Sørensen at 2009-04-22 266
  :daemonize => false,
267
  :reuseaddr => true,
4ee78e5 by Johan Sørensen at 2008-04-16 268
}
1aba11c by David A. Cuadrado at 2008-04-14 269
4ee78e5 by Johan Sørensen at 2008-04-16 270
OptionParser.new do |opts|
c5070c6 by Johan Sørensen at 2008-04-19 271
  opts.banner = "Usage: #{$0} [options]"
4ee78e5 by Johan Sørensen at 2008-04-16 272
c5070c6 by Johan Sørensen at 2008-04-19 273
  opts.on("-p", "--port=[port]", Integer, "Port to listen on", "Default: #{options[:port]}") do |o|
4ee78e5 by Johan Sørensen at 2008-04-16 274
    options[:port] = o
275
  end
276
c5070c6 by Johan Sørensen at 2008-04-19 277
  opts.on("-a", "--address=[host]", "Host to listen on", "Default: #{options[:host]}") do |o|
4ee78e5 by Johan Sørensen at 2008-04-16 278
    options[:host] = o
279
  end
280
  
c5070c6 by Johan Sørensen at 2008-04-19 281
  opts.on("-l", "--logfile=[file]", "File to log to", "Default: #{options[:logfile]}") do |o|
4ee78e5 by Johan Sørensen at 2008-04-16 282
    options[:logfile] = o
283
  end
284
  
c5070c6 by Johan Sørensen at 2008-04-19 285
  opts.on("-P", "--pidfile=[file]", "PID file to use (if daemonized)", "Default: #{options[:pidfile]}") do |o|
05e3db0 by Johan Sørensen at 2008-04-19 286
    options[:pidfile] = o
287
  end
288
  
c5070c6 by Johan Sørensen at 2008-04-19 289
  opts.on("-d", "--daemonize", "Daemonize or run in foreground", "Default: #{options[:daemonize]}") do |o|
4ee78e5 by Johan Sørensen at 2008-04-16 290
    options[:daemonize] = o
291
  end
292
  
c6c6cfe by Johan Sørensen at 2009-04-22 293
  opts.on("-r", "--reuseaddr", "Re-use addresses", "Default: #{options[:reuseaddr].inspect}") do |o|
294
    options[:reuseaddr] = o
295
  end
296
  
c5070c6 by Johan Sørensen at 2008-04-19 297
  opts.on_tail("-h", "--help", "Show this help message.") do
298
    puts opts
299
    exit
300
  end
301
  
4ee78e5 by Johan Sørensen at 2008-04-16 302
  # opts.on("-e", "--environment", "RAILS_ENV to use") do |o|
303
  #   options[:port] = o
304
  # end
305
end.parse!
306
307
@git_daemon = Git::Daemon.new(options)
308
309
trap("SIGKILL")  { @git_daemon.handle_stop("SIGKILL") }
310
trap("TERM")     { @git_daemon.handle_stop("TERM")    }
311
trap("SIGINT")   { @git_daemon.handle_stop("SIGINT")  }
312
trap("CLD")      { @git_daemon.handle_cld  }
313
314
@git_daemon.start
1aba11c by David A. Cuadrado at 2008-04-14 315