System notice: In light of the Debian OpenSSL security issue we've regenerated the server keys. See this thread for instructions and the new key fingerprints.

Commit 37e1c41b4f693c787096e3fb7858f8b8c68c966b

Merge branch 'ruby-git-daemon'

Commit diff

app/models/cloner.rb

 
1class Cloner < ActiveRecord::Base
2 belongs_to :repository
3end
toggle raw diff

app/models/repository.rb

 
99 :order => "status, id desc", :dependent => :destroy
1010 has_many :proposed_merge_requests, :foreign_key => 'source_repository_id',
1111 :class_name => 'MergeRequest', :order => "id desc", :dependent => :destroy
12 has_many :cloners, :dependent => :destroy
1213
1314 validates_presence_of :user_id, :project_id, :name
1415 validates_format_of :name, :with => /^[a-z0-9_\-]+$/i,
2929 find_by_name(name) || raise(ActiveRecord::RecordNotFound)
3030 end
3131
32 def self.find_by_path(path)
33 base_path = path.gsub(/^#{Regexp.escape(GitoriousConfig['repository_base_path'])}/, "")
34 repo_name, project_name = base_path.split("/").reverse
35
36 project = Project.find_by_slug!(project_name)
37 project.repositories.find_by_name(repo_name.sub(/\.git/, ""))
38 end
39
3240 def self.create_git_repository(path)
3341 git_backend.create(full_path_from_partial_path(path))
3442 end
230230 users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash }
231231 users_by_email
232232 end
233
234 def cloned_from(ip, country_code = "--", country_name = nil)
235 cloners.create(:ip => ip, :date => Time.now.utc, :country_code => country_code, :country => country_name)
236 end
233237
234238 protected
235239 def set_as_mainline_if_first
toggle raw diff

app/views/repositories/_infobox.html.erb

 
4141 </div>
4242 </li>
4343 <% end -%>
44 <li>The repository has been cloned <%= @repository.cloners.size %> times.</li>
4445</ul>
toggle raw diff

data/GeoIP.dat

 
toggle raw diff

db/migrate/023_create_cloners.rb

 
1class CreateCloners < ActiveRecord::Migration
2 def self.up
3 create_table :cloners do |t|
4 t.string :ip
5 t.string :country_code, :length => 2
6 t.string :country
7 t.datetime :date
8 t.integer :repository_id
9 end
10 end
11
12 def self.down
13 drop_table :cloners
14 end
15end
16
toggle raw diff

script/git-daemon

 
1#!/usr/bin/env ruby
2
3require 'rubygems'
4require 'daemons'
5require 'geoip'
6require 'socket'
7require "optparse"
8
9ENV["RAILS_ENV"] ||= "production"
10require File.dirname(__FILE__)+'/../config/environment'
11
12Rails.configuration.log_level = :info # Disable debug
13ActiveRecord::Base.allow_concurrency = true
14
15BASE_PATH = File.expand_path(GitoriousConfig['repository_base_path'])
16
17TIMEOUT = 30
18MAX_CHILDREN = 30
19$children_reaped = 0
20$children_active = 0
21
22module Git
23 class Daemon
24 include Daemonize
25
26 SERVICE_REGEXP = /(\w{4})(git\-upload\-pack)\s(.+)\x0host=([\w\.\-]+)/.freeze
27
28 def initialize(options)
29 @options = options
30 @geoip = GeoIP.new(File.join(RAILS_ROOT, "data", "GeoIP.dat"))
31 end
32
33 def start
34 if @options[:daemonize]
35 daemonize(@options[:logfile])
36 File.open(@options[:pidfile], "w") do |f|
37 f.write(Process.pid)
38 end
39 end
40 @socket = TCPServer.new(@options[:host], @options[:port])
41 log(Process.pid, "Listening on #{@options[:host]}:#{@options[:port]}...")
42 run
43 end
44
45 def run
46 while session = @socket.accept
47 connections = $children_active - $children_reaped
48 if connections > MAX_CHILDREN
49 log(Process.pid, "too many active children #{connections}/#{MAX_CHILDREN}")
50 session.close
51 next
52 end
53
54 run_service(session)
55 end
56 end
57
58 def run_service(session)
59 $children_active += 1
60
61 line = session.recv(1000)
62
63 if line =~ SERVICE_REGEXP
64 start_time = Time.now
65 code = $1
66 service = $2
67 base_path = $3
68 host = $4
69
70 path = File.expand_path("#{BASE_PATH}/#{base_path}")
71 if !path.index(BASE_PATH) == 0 || !File.directory?(path)
72 log(Process.pid, "Invalid path: #{base_path}")
73 session.close
74 $children_active -= 1
75 return
76 end
77
78 if !File.exist?(File.join(path, "git-daemon-export-ok"))
79 session.close
80 $children_active -= 1
81 return
82 end
83
84 Dir.chdir(path) do
85 cmd = "git-upload-pack --strict --timeout=#{TIMEOUT} ."
86
87 fork do
88 repository = nil
89
90 begin
91 repository = ::Repository.find_by_path(path)
92 rescue => e
93 log(Process.pid, "AR error: #{e.class.name} #{e.message}:\n #{e.backtrace.join("\n ")}")
94 end
95
96 pid = Process.pid
97 ip_family, port, name, ip = session.peeraddr
98 log(pid, "Connection from #{ip} for #{path.inspect}")
99
100 $stdout.reopen(session)
101 $stdin.reopen(session)
102 session.close
103
104 if repository
105 if ip_family == "AF_INET6"
106 repository.cloned_from(ip)
107 else
108 localization = @geoip.country(ip)
109 repository.cloned_from(ip, localization[3], localization[5])
110 end
111 else
112 log(pid, "Cannot find repository: #{path}")
113 end
114 log(Process.pid, "Deferred in #{'%0.5f' % (Time.now - start_time)}s")
115
116 exec(cmd)
117 # FIXME; we don't ever get here since we exec(), so reaped count may be incorrect
118 $children_reaped += 1
119 exit!
120 end
121 end rescue Errno::EAGAIN
122 else
123 $stderr.puts "Invalid request: #{line}"
124 session.close
125 $children_active -= 1
126 end
127 end
128
129 def handle_stop(signal)
130 log(Process.pid, "Received #{signal}, exiting..")
131 exit 0
132 end
133
134 def handle_cld
135 loop do
136 pid = nil
137 begin
138 pid = Process.wait(-1, Process::WNOHANG)
139 rescue Errno::ECHILD
140 break
141 end
142
143 if pid && $?
144 $children_reaped += 1
145 log(pid, "Disconnected. (status=#{$?.exitstatus})") if pid > 0
146 if $children_reaped == $children_active
147 $children_reaped = 0
148 $children_active = 0
149 end
150
151 next
152 end
153 break
154 end
155 end
156
157 def log(pid, msg)
158 $stderr.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} [#{pid}] #{msg}"
159 end
160
161 end
162end
163
164options = {
165 :port => 9418,
166 :host => "0.0.0.0",
167 :logfile => File.join(RAILS_ROOT, "log", "git-daemon.log"),
168 :pidfile => File.join(RAILS_ROOT, "log", "git-daemon.pid"),
169 :daemonize => false
170
171}
172
173OptionParser.new do |opts|
174 opts.banner = "Usage: #{$0} start|stop [options]"
175
176 opts.on("-p", "--port", Integer, "Port to listen on") do |o|
177 options[:port] = o
178 end
179
180 opts.on("-h", "--host", "Host to listen on") do |o|
181 options[:host] = o
182 end
183
184 opts.on("-l", "--logfile", "File to log to") do |o|
185 options[:logfile] = o
186 end
187
188 opts.on("-l", "--logfile", "PID file to use (if daemonized)") do |o|
189 options[:pidfile] = o
190 end
191
192 opts.on("-d", "--daemonize", "Daemonize or run in foreground (default)") do |o|
193 options[:daemonize] = o
194 end
195
196 # opts.on("-e", "--environment", "RAILS_ENV to use") do |o|
197 # options[:port] = o
198 # end
199end.parse!
200
201@git_daemon = Git::Daemon.new(options)
202
203trap("SIGKILL") { @git_daemon.handle_stop("SIGKILL") }
204trap("TERM") { @git_daemon.handle_stop("TERM") }
205trap("SIGINT") { @git_daemon.handle_stop("SIGINT") }
206trap("CLD") { @git_daemon.handle_cld }
207
208@git_daemon.start
209
toggle raw diff

spec/fixtures/cloners.yml

 
1# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
3argentina:
4 ip: 200.45.34.21
5 country: Argentina
6 country_code: AR
7 date: 2008-04-13 23:40:30
8
9mexico:
10 ip: 200.53.12.21
11 country: Mexico
12 country_code: MX
13 date: 2008-04-13 23:40:30
toggle raw diff

spec/models/cloner_spec.rb

 
1require File.dirname(__FILE__) + '/../spec_helper'
2require 'geoip'
3
4describe Cloner do
5 before(:all) do
6 @geoip = GeoIP.new(File.join(RAILS_ROOT, "data", "GeoIP.dat"))
7 end
8
9 before(:each) do
10 @cloner = Cloner.new
11 end
12
13 it "should has a valid country" do
14 localization = @geoip.country(cloners(:argentina).ip)
15 localization[3].should == cloners(:argentina).country_code
16 localization[5].should == cloners(:argentina).country
17 end
18end
toggle raw diff