summaryrefslogtreecommitdiff
path: root/lib/gitlab/middleware/go.rb
blob: 72a788022ef58840018bf35dccd9b9cb8b69de1e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# frozen_string_literal: true

# A dumb middleware that returns a Go HTML document if the go-get=1 query string
# is used irrespective if the namespace/project exists
module Gitlab
  module Middleware
    class Go
      include ActionView::Helpers::TagHelper
      include ActionController::HttpAuthentication::Basic

      PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze

      def initialize(app)
        @app = app
      end

      def call(env)
        request = ActionDispatch::Request.new(env)

        render_go_doc(request) || @app.call(env)
      end

      private

      def render_go_doc(request)
        return unless go_request?(request)

        path = project_path(request)
        return unless path

        body = go_body(path)
        return unless body

        response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
        response.finish
      end

      def go_request?(request)
        request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
      end

      def go_body(path)
        config = Gitlab.config
        project_url = Gitlab::Utils.append_path(config.gitlab.url, path)
        import_prefix = strip_url(project_url.to_s)

        repository_url = if Gitlab::CurrentSettings.enabled_git_access_protocol == 'ssh'
                           shell = config.gitlab_shell
                           port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
                           "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
                         else
                           "#{project_url}.git"
                         end

        meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
        head_tag = content_tag :head, meta_tag
        content_tag :html, head_tag
      end

      def strip_url(url)
        url.gsub(%r{\Ahttps?://}, '')
      end

      def project_path(request)
        path_info = request.env["PATH_INFO"]
        path_info.sub!(%r{^/}, '')

        project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
        return unless project_path_match

        path = project_path_match[1]

        # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
        # In a traditional project with a single namespace, this would denote repo
        # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
        # groups, this could also be `namespace/project/path1` with subpath
        # `path2/../pathN`, for example.

        # We find all potential project paths out of the path segments
        path_segments = path.split('/')
        simple_project_path = path_segments.first(2).join('/')

        # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
        return simple_project_path if path_segments.length <= 2

        project_paths = []
        begin
          project_paths << path_segments.join('/')
          path_segments.pop
        end while path_segments.length >= 2

        # We see if a project exists with any of these potential paths
        project = project_for_paths(project_paths, request)

        if project
          # If a project is found and the user has access, we return the full project path
          project.full_path
        else
          # If not, we return the first two components as if it were a simple `namespace/project` path,
          # so that we don't reveal the existence of a nested project the user doesn't have access to.
          # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
          # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
          # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
          # Since `go get` doesn't authenticate by default, this means that
          # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
          # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
          # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
          simple_project_path
        end
      end

      def project_for_paths(paths, request)
        project = Project.where_full_path_in(paths).first
        return unless  Ability.allowed?(current_user(request, project), :read_project, project)

        project
      end

      def current_user(request, project)
        return unless has_basic_credentials?(request)

        login, password = user_name_and_password(request)
        auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
        return unless auth_result.success?

        return unless auth_result.actor&.can?(:access_git)

        return unless auth_result.authentication_abilities.include?(:read_project)

        auth_result.actor
      end
    end
  end
end