Welcome to mirror list, hosted at ThFree Co, Russian Federation.

validate_remote_git_endpoint_service.rb « import « services « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 2177238fddfd7fa7787e90bae07cbb14e38566e8 (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
# frozen_string_literal: true

module Import
  class ValidateRemoteGitEndpointService
    # Validates if the remote endpoint is a valid GIT repository
    # Only smart protocol is supported
    # Validation rules are taken from https://git-scm.com/docs/http-protocol#_smart_clients

    GIT_SERVICE_NAME = "git-upload-pack"
    GIT_EXPECTED_FIRST_PACKET_LINE = "# service=#{GIT_SERVICE_NAME}"
    GIT_BODY_MESSAGE_REGEXP = /^[0-9a-f]{4}#{GIT_EXPECTED_FIRST_PACKET_LINE}/
    # https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L56-L59
    GIT_PROTOCOL_PKT_LEN = 4
    GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
    EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
    INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body'
    INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type'

    def initialize(params)
      @params = params
    end

    def execute
      uri = Gitlab::Utils.parse_url(@params[:url])

      if !uri || !uri.hostname || Project::VALID_IMPORT_PROTOCOLS.exclude?(uri.scheme)
        return ServiceResponse.error(message: "#{@params[:url]} is not a valid URL")
      end

      return ServiceResponse.success if uri.scheme == 'git'

      uri.fragment = nil
      url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")

      response, response_body = http_get_and_extract_first_chunks(url)

      validate(uri, response, response_body)
    rescue *Gitlab::HTTP::HTTP_ERRORS => err
      error_result("HTTP #{err.class.name.underscore} error: #{err.message}")
    rescue StandardError => err
      ServiceResponse.error(
        message: "Internal #{err.class.name.underscore} error: #{err.message}",
        reason: 500
      )
    end

    private

    def http_get_and_extract_first_chunks(url)
      # We are interested only in the first chunks of the response
      # So we're using stream_body: true and breaking when receive enough body
      response = nil
      response_body = ''

      Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk|
        response = response_chunk
        response_body += response_chunk
        break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length
      end

      [response, response_body]
    end

    def auth
      unless @params[:user].to_s.blank?
        {
          username: @params[:user],
          password: @params[:password]
        }
      end
    end

    def validate(uri, response, response_body)
      return status_code_error(uri, response) unless status_code_is_valid?(response)
      return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response)
      return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body)

      ServiceResponse.success
    end

    def status_code_error(uri, response)
      http_code = response.http_response.code.to_i
      message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code]

      error_result(
        "#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}",
        http_code
      )
    end

    def error_result(message, reason = nil)
      ServiceResponse.error(message: message, reason: reason)
    end

    def status_code_is_valid?(response)
      response.http_response.code == '200'
    end

    def content_type_is_valid?(response)
      response.http_response['content-type'] == EXPECTED_CONTENT_TYPE
    end

    def response_body_is_valid?(response_body)
      response_body.length <= GIT_MINIMUM_RESPONSE_LENGTH && response_body.match?(GIT_BODY_MESSAGE_REGEXP)
    end
  end
end