Read about our upcoming Code of Conduct on this issue

pages_domain.rb 4.79 KB
Newer Older
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1
class PagesDomain < ActiveRecord::Base
2
3
4
  VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze
  VERIFICATION_THRESHOLD = 3.days.freeze

Kamil Trzcinski's avatar
Kamil Trzcinski committed
5
6
  belongs_to :project

Drew Blessing's avatar
Drew Blessing committed
7
  validates :domain, hostname: { allow_numeric_hostname: true }
Douwe Maan's avatar
Douwe Maan committed
8
  validates :domain, uniqueness: { case_sensitive: false }
Rob Watson's avatar
Rob Watson committed
9
10
11
12
  validates :certificate, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
  validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
  validates :key, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
  validates :key, certificate_key: true, if: ->(domain) { domain.key.present? }
13
  validates :verification_code, presence: true, allow_blank: false
Kamil Trzcinski's avatar
Kamil Trzcinski committed
14

Kamil Trzcinski's avatar
Kamil Trzcinski committed
15
16
  validate :validate_pages_domain
  validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
17
18
  validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }

19
20
  attr_encrypted :key,
    mode: :per_attribute_iv_and_salt,
James Lopez's avatar
James Lopez committed
21
    insecure_mode: true,
22
23
    key: Gitlab::Application.secrets.db_key_base,
    algorithm: 'aes-256-cbc'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
24

25
  after_initialize :set_verification_code
26
  after_create :update_daemon
27
  after_update :update_daemon, if: :pages_config_changed?
28
  after_destroy :update_daemon
Kamil Trzcinski's avatar
Kamil Trzcinski committed
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  scope :enabled, -> { where('enabled_until >= ?', Time.now ) }
  scope :needs_verification, -> do
    verified_at = arel_table[:verified_at]
    enabled_until = arel_table[:enabled_until]
    threshold = Time.now + VERIFICATION_THRESHOLD

    where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
  end

  def verified?
    !!verified_at
  end

  def unverified?
    !verified?
  end

  def enabled?
    !Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present?
  end

Rob Watson's avatar
Rob Watson committed
51
52
53
54
  def https?
    certificate.present?
  end

55
56
57
58
  def to_param
    domain
  end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
59
60
61
  def url
    return unless domain

62
    if certificate.present?
63
      "https://#{domain}"
Kamil Trzcinski's avatar
Kamil Trzcinski committed
64
    else
65
      "http://#{domain}"
Kamil Trzcinski's avatar
Kamil Trzcinski committed
66
67
68
    end
  end

69
  def has_matching_key?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
70
71
    return false unless x509
    return false unless pkey
72
73
74
75
76
77
78
79

    # We compare the public key stored in certificate with public key from certificate key
    x509.check_private_key(pkey)
  end

  def has_intermediates?
    return false unless x509

Kamil Trzcinski's avatar
Kamil Trzcinski committed
80
81
82
    # self-signed certificates doesn't have the certificate chain
    return true if x509.verify(x509.public_key)

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
    store = OpenSSL::X509::Store.new
    store.set_default_paths

    # This forces to load all intermediate certificates stored in `certificate`
    Tempfile.open('certificate_chain') do |f|
      f.write(certificate)
      f.flush
      store.add_file(f.path)
    end

    store.verify(x509)
  rescue OpenSSL::X509::StoreError
    false
  end

  def expired?
    return false unless x509
100

101
    current = Time.new
Kamil Trzcinski's avatar
Kamil Trzcinski committed
102
    current < x509.not_before || x509.not_after < current
103
104
  end

105
106
107
108
  def expiration
    x509&.not_after
  end

109
110
  def subject
    return unless x509
111

Kamil Trzcinski's avatar
Kamil Trzcinski committed
112
    x509.subject.to_s
113
114
  end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
115
116
  def certificate_text
    @certificate_text ||= x509.try(:to_text)
117
118
  end

119
120
121
122
123
124
125
126
127
128
129
130
131
132
  # Verification codes may be TXT records for domain or verification_domain, to
  # support the use of CNAME records on domain.
  def verification_domain
    return unless domain.present?

    "_#{VERIFICATION_KEY}.#{domain}"
  end

  def keyed_verification_code
    return unless verification_code.present?

    "#{VERIFICATION_KEY}=#{verification_code}"
  end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
133
134
  private

135
136
137
138
139
140
  def set_verification_code
    return if self.verification_code.present?

    self.verification_code = SecureRandom.hex(16)
  end

141
  def update_daemon
142
143
144
    ::Projects::UpdatePagesConfigurationService.new(project).execute
  end

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
  def pages_config_changed?
    project_id_changed? ||
      domain_changed? ||
      certificate_changed? ||
      key_changed? ||
      became_enabled? ||
      became_disabled?
  end

  def became_enabled?
    enabled_until.present? && !enabled_until_was.present?
  end

  def became_disabled?
    !enabled_until.present? && enabled_until_was.present?
  end

162
163
164
165
166
167
168
169
170
171
  def validate_matching_key
    unless has_matching_key?
      self.errors.add(:key, "doesn't match the certificate")
    end
  end

  def validate_intermediates
    unless has_intermediates?
      self.errors.add(:certificate, 'misses intermediates')
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
172
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
173
174
175

  def validate_pages_domain
    return unless domain
176

Drew Blessing's avatar
Drew Blessing committed
177
    if domain.downcase.ends_with?(Settings.pages.host.downcase)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
178
179
180
181
182
183
      self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
    end
  end

  def x509
    return unless certificate
184

Kamil Trzcinski's avatar
Kamil Trzcinski committed
185
186
187
188
189
190
191
    @x509 ||= OpenSSL::X509::Certificate.new(certificate)
  rescue OpenSSL::X509::CertificateError
    nil
  end

  def pkey
    return unless key
192

Kamil Trzcinski's avatar
Kamil Trzcinski committed
193
194
195
196
    @pkey ||= OpenSSL::PKey::RSA.new(key)
  rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
    nil
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
197
end