/lib/gitlab/middleware/go.rb

https://gitlab.com/mehlah/gitlab-ce · Ruby · 122 lines · 77 code · 25 blank · 20 comment · 6 complexity · 7ec06503dec04e96e4ebc425fb24d9e8 MD5 · raw file

  1. # A dumb middleware that returns a Go HTML document if the go-get=1 query string
  2. # is used irrespective if the namespace/project exists
  3. module Gitlab
  4. module Middleware
  5. class Go
  6. include ActionView::Helpers::TagHelper
  7. include Gitlab::CurrentSettings
  8. PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
  9. def initialize(app)
  10. @app = app
  11. end
  12. def call(env)
  13. request = Rack::Request.new(env)
  14. render_go_doc(request) || @app.call(env)
  15. end
  16. private
  17. def render_go_doc(request)
  18. return unless go_request?(request)
  19. path = project_path(request)
  20. return unless path
  21. body = go_body(path)
  22. return unless body
  23. response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
  24. response.finish
  25. end
  26. def go_request?(request)
  27. request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
  28. end
  29. def go_body(path)
  30. config = Gitlab.config
  31. project_url = URI.join(config.gitlab.url, path)
  32. import_prefix = strip_url(project_url.to_s)
  33. repository_url = case current_application_settings.enabled_git_access_protocol
  34. when 'ssh'
  35. shell = config.gitlab_shell
  36. port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
  37. "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
  38. when 'http', nil
  39. "#{project_url}.git"
  40. end
  41. meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
  42. head_tag = content_tag :head, meta_tag
  43. content_tag :html, head_tag
  44. end
  45. def strip_url(url)
  46. url.gsub(/\Ahttps?:\/\//, '')
  47. end
  48. def project_path(request)
  49. path_info = request.env["PATH_INFO"]
  50. path_info.sub!(/^\//, '')
  51. project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
  52. return unless project_path_match
  53. path = project_path_match[1]
  54. # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
  55. # In a traditional project with a single namespace, this would denote repo
  56. # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
  57. # groups, this could also be `namespace/project/path1` with subpath
  58. # `path2/../pathN`, for example.
  59. # We find all potential project paths out of the path segments
  60. path_segments = path.split('/')
  61. simple_project_path = path_segments.first(2).join('/')
  62. # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
  63. return simple_project_path if path_segments.length <= 2
  64. project_paths = []
  65. begin
  66. project_paths << path_segments.join('/')
  67. path_segments.pop
  68. end while path_segments.length >= 2
  69. # We see if a project exists with any of these potential paths
  70. project = project_for_paths(project_paths, request)
  71. if project
  72. # If a project is found and the user has access, we return the full project path
  73. project.full_path
  74. else
  75. # If not, we return the first two components as if it were a simple `namespace/project` path,
  76. # so that we don't reveal the existence of a nested project the user doesn't have access to.
  77. # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
  78. # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
  79. # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
  80. # Since `go get` doesn't authenticate by default, this means that
  81. # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
  82. # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
  83. # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
  84. simple_project_path
  85. end
  86. end
  87. def project_for_paths(paths, request)
  88. project = Project.where_full_path_in(paths).first
  89. return unless Ability.allowed?(current_user(request), :read_project, project)
  90. project
  91. end
  92. def current_user(request)
  93. request.env['warden']&.authenticate
  94. end
  95. end
  96. end
  97. end