Rudimentary protection of private repositories in HTTP pulling
[gitorious:mainline.git] / app / metal / git_http_cloner.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2012 Gitorious AS and/or its subsidiary(-ies)
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #
6 #   This program is free software: you can redistribute it and/or modify
7 #   it under the terms of the GNU Affero General Public License as published by
8 #   the Free Software Foundation, either version 3 of the License, or
9 #   (at your option) any later version.
10 #
11 #   This program is distributed in the hope that it will be useful,
12 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #   GNU Affero General Public License for more details.
15 #
16 #   You should have received a copy of the GNU Affero General Public License
17 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #++
19
20 # Middleware that handles HTTP cloning
21 # This piece of code is performed before the Rails stack kicks in.
22 # If we return a 404 status code, control is passed on to Rails
23 #
24 # What it does is:
25 # - Check if the hostname begins with +http+ (this will be reserved in
26 #   the site model)
27 # - As longs as we're sure we are in our own context, rip out the repo
28 #   path and rest from the URI
29 # - Return a X-Sendfile header in the response containing the full
30 #   path to the object requested
31 # - This will be picked up by Apache (given mod-x_sendfile is
32 #   installed) and then delivered to the client
33 require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
34
35 class GitHttpCloner
36   TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
37   NOT_FOUND_RESPONSE = [404, {"Content-Type" => "text/html"},[]]
38   NOT_ALLOWED_RESPONSE = [403, {"Content-Type" => "text/html"}, []]
39
40   def self.call(env)
41     perform_http_cloning = env["HTTP_HOST"] =~ /^#{Site::HTTP_CLONING_SUBDOMAIN}\..*/
42     if perform_http_cloning && !GitoriousConfig["hide_http_clone_urls"]
43       if env["PATH_INFO"] =~ /^\/robots.txt$/
44         body = ["User-Agent: *\nDisallow: /\n"]
45         return [200, {"Content-Type" => "text/plain"}, body]
46       elsif match = /(.*\.git)(.*)/.match(env["PATH_INFO"])
47         path = match[1]
48         rest = match[2]
49         begin
50           repo = Repository.find_by_path(path)
51           return NOT_ALLOWED_RESPONSE if !can_read?(nil, repo)
52           return NOT_FOUND_RESPONSE unless repo
53           repo.cloned_from(remote_ip(env), nil, nil, "http") if rest == "/HEAD"
54           full_path = File.join(repo.full_repository_path, rest)
55           headers = {
56             "X-Sendfile" => full_path,
57             "Content-Type" => "application/octet-stream"
58           }
59           env["rack.session.options"] = {}
60           return [200, headers, []]
61         rescue ActiveRecord::RecordNotFound
62           # Repo not found
63           return NOT_FOUND_RESPONSE
64         end
65       end
66     end
67     return NOT_FOUND_RESPONSE
68   end
69
70   protected
71   def self.can_read?(user, repository)
72     return true if !GitoriousConfig["enable_private_repositories"]
73     CommittershipAuthorization.new.can_read_repository?(user, repository)
74   end
75
76   # Borrowed from ActionController::Request. Extract proxy addresses and stuff (except our own)
77   # Does not do ip spoofing checks
78   def self.remote_ip(env)
79     remote_addr_list = env["REMOTE_ADDR"] && env["REMOTE_ADDR"].scan(/[^,\s]+/)
80     unless remote_addr_list.blank?
81       not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
82       return not_trusted_addrs.first unless not_trusted_addrs.empty?
83     end
84
85     remote_ips = env["HTTP_X_FORWARDED_FOR"] && env["HTTP_X_FORWARDED_FOR"].split(",")
86
87     if env.include? "HTTP_CLIENT_IP"
88       return env["HTTP_CLIENT_IP"]
89     end
90
91     if remote_ips
92       while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
93         remote_ips.pop
94       end
95       return remote_ips.last.strip
96     end
97
98     env["REMOTE_ADDR"]
99   end
100 end