Implement extra domains and save pages configuration

This commit is contained in:
Kamil Trzcinski 2016-02-10 15:06:31 +01:00 committed by James Edwards-Jones
parent 6e99226cca
commit 13b6bad17e
17 changed files with 249 additions and 208 deletions

View file

@ -2,25 +2,45 @@ class Projects::PagesController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :authorize_remove_pages!, only: :destroy before_action :authorize_remove_pages!, only: [:remove_pages]
before_action :label, only: [:destroy]
before_action :domain, only: [:show]
helper_method :valid_certificate?, :valid_certificate_key? helper_method :valid_certificate?, :valid_certificate_key?
helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates? helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates?
helper_method :certificate, :certificate_key helper_method :certificate, :certificate_key
def index
@domains = @project.pages_domains.order(:domain)
end
def show def show
end end
def update def new
if @project.update_attributes(pages_params) @domain = @project.pages_domains.new
end
def create
@domain = @project.pages_domains.create(pages_domain_params)
if @domain.valid?
redirect_to namespace_project_pages_path(@project.namespace, @project) redirect_to namespace_project_pages_path(@project.namespace, @project)
else else
render 'show' render 'new'
end end
end end
def certificate def destroy
@project.remove_pages_certificate @domain.destroy
respond_to do |format|
format.html do
redirect_to(namespace_project_pages_path(@project.namespace, @project),
notice: 'Domain was removed')
end
format.js
end
end end
def destroy def destroy
@ -33,63 +53,15 @@ class Projects::PagesController < Projects::ApplicationController
private private
def pages_params def pages_domain_params
params.require(:project).permit( params.require(:pages_domain).permit(
:pages_custom_certificate, :certificate,
:pages_custom_certificate_key, :key,
:pages_custom_domain, :domain
:pages_redirect_http,
) )
end end
def valid_certificate? def domain
certificate.present? @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
end
def valid_certificate_key?
certificate_key.present?
end
def valid_key_for_certificiate?
return false unless certificate
return false unless certificate_key
# We compare the public key stored in certificate with public key from certificate key
certificate.public_key.to_pem == certificate_key.public_key.to_pem
rescue OpenSSL::X509::CertificateError, OpenSSL::PKey::PKeyError
false
end
def valid_certificate_intermediates?
return false unless certificate
store = OpenSSL::X509::Store.new
store.set_default_paths
# This forces to load all intermediate certificates stored in `pages_custom_certificate`
Tempfile.open('project_certificate') do |f|
f.write(@project.pages_custom_certificate)
f.flush
store.add_file(f.path)
end
store.verify(certificate)
rescue OpenSSL::X509::StoreError
false
end
def certificate
return unless @project.pages_custom_certificate
@certificate ||= OpenSSL::X509::Certificate.new(@project.pages_custom_certificate)
rescue OpenSSL::X509::CertificateError
nil
end
def certificate_key
return unless @project.pages_custom_certificate_key
@certificate_key ||= OpenSSL::PKey::RSA.new(@project.pages_custom_certificate_key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
end end
end end

View file

@ -85,10 +85,6 @@ module ProjectsHelper
"You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" "You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?"
end end
def remove_pages_certificate_message(project)
"You are going to remove a certificates for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?"
end
def project_nav_tabs def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user) @nav_tabs ||= get_project_nav_tabs(@project, current_user)
end end

View file

@ -2,19 +2,25 @@ class PagesDomain < ActiveRecord::Base
belongs_to :project belongs_to :project
validates :domain, hostname: true validates :domain, hostname: true
validates_uniqueness_of :domain, allow_nil: true, allow_blank: true validates_uniqueness_of :domain, case_sensitive: false
validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true
attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base validate :validate_matching_key, if: ->(domain) { domain.certificate.present? && domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
attr_encrypted :key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
after_create :update after_create :update
after_save :update after_save :update
after_destroy :update after_destroy :update
def to_param
domain
end
def url def url
return unless domain return unless domain
return unless Dir.exist?(project.public_pages_path)
if certificate if certificate
return "https://#{domain}" return "https://#{domain}"
@ -23,7 +29,77 @@ class PagesDomain < ActiveRecord::Base
end end
end end
def has_matching_key?
return unless x509
return unless pkey
# 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
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
current = Time.new
return current < x509.not_before || x509.not_after < current
end
def subject
return unless x509
return x509.subject.to_s
end
def fingerprint
return unless x509
@fingeprint ||= OpenSSL::Digest::SHA256.new(x509.to_der).to_s
end
private
def x509
return unless certificate
@x509 ||= OpenSSL::X509::Certificate.new(certificate)
rescue OpenSSL::X509::CertificateError
nil
end
def pkey
return unless key
@pkey ||= OpenSSL::PKey::RSA.new(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
end
def update def update
UpdatePagesConfigurationService.new(project).execute ::Projects::UpdatePagesConfigurationService.new(project).execute
end
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
end end
end end

View file

@ -7,9 +7,7 @@ module Projects
end end
def execute def execute
update_file(pages_cname_file, project.pages_custom_domain) update_file(pages_config_file, pages_config)
update_file(pages_certificate_file, project.pages_custom_certificate)
update_file(pages_certificate_file_key, project.pages_custom_certificate_key)
reload_daemon reload_daemon
success success
rescue => e rescue => e
@ -18,6 +16,22 @@ module Projects
private private
def pages_config
{
domains: pages_domains_config
}
end
def pages_domains_config
project.pages_domains.map do |domain|
{
domain: domain.domain,
certificate: domain.certificate,
key: domain.key,
}
end
end
def reload_daemon def reload_daemon
# GitLab Pages daemon constantly watches for modification time of `pages.path` # GitLab Pages daemon constantly watches for modification time of `pages.path`
# It reloads configuration when `pages.path` is modified # It reloads configuration when `pages.path` is modified
@ -28,16 +42,8 @@ module Projects
@pages_path ||= project.pages_path @pages_path ||= project.pages_path
end end
def pages_cname_file def pages_config_file
File.join(pages_path, 'CNAME') File.join(pages_path, 'config.jso')
end
def pages_certificate_file
File.join(pages_path, 'domain.crt')
end
def pages_certificate_key_file
File.join(pages_path, 'domain.key')
end end
def update_file(file, data) def update_file(file, data)

View file

@ -5,30 +5,9 @@
.panel-body .panel-body
%p %p
%strong %strong
Congratulations! Your pages are served at: Congratulations! Your pages are served under:
%p= link_to @project.pages_url, @project.pages_url %p= link_to @project.pages_url, @project.pages_url
- if Settings.pages.custom_domain && @project.pages_custom_url - @project.pages_domains.each do |domain|
%p= link_to @project.pages_custom_url, @project.pages_custom_url %p= link_to domain.url, domain.url
- if @project.pages_custom_certificate
- unless valid_certificate?
#error_explanation
.alert.alert-warning
Your certificate is invalid.
- unless valid_certificate_key?
#error_explanation
.alert.alert-warning
Your private key is invalid.
- unless valid_key_for_certificiate?
#error_explanation
.alert.alert-warning
Your private key can't be used with your certificate.
- unless valid_certificate_intermediates?
#error_explanation
.alert.alert-warning
Your certificate doesn't have intermediates.
Your page may not work properly.

View file

@ -3,7 +3,7 @@
.panel-heading Remove pages .panel-heading Remove pages
.errors-holder .errors-holder
.panel-body .panel-body
= form_tag(namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do = form_tag(remove_pages_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do
%p %p
Removing the pages will prevent from exposing them to outside world. Removing the pages will prevent from exposing them to outside world.
.form-actions .form-actions

View file

@ -1,35 +1,35 @@
- if can?(current_user, :update_pages, @project) = form_for [@domain], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f|
.panel.panel-default - if @domain.errors.any?
.panel-heading #error_explanation
Settings .alert.alert-danger
.panel-body - @domain.errors.full_messages.each do |msg|
= form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| %p= msg
- if @project.errors.any?
#error_explanation
.alert.alert-danger
- @project.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :pages_domain, class: 'control-label' do = f.label :domain, class: 'control-label' do
Custom domain Domain
.col-sm-10 .col-sm-10
- if Settings.pages.custom_domain = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
= f.text_field :pages_custom_domain, required: false, autocomplete: 'off', class: 'form-control' %span.help-inline * required
%span.help-inline Allows you to serve the pages under your domain
- else
.nothing-here-block
Support for custom domains and certificates is disabled.
Ask your system's administrator to enable it.
- if Settings.pages.https - if Settings.pages.external_https
.form-group .form-group
.col-sm-offset-2.col-sm-10 = f.label :certificate, class: 'control-label' do
.checkbox Certificate (PEM)
= f.label :pages_redirect_http do .col-sm-10
= f.check_box :pages_redirect_http = f.text_area :certificate, rows: 5, class: 'form-control', value: ''
%span.descr Force HTTPS %span.help-inline Upload a certificate for your domain with all intermediates
.help-block Redirect the HTTP to HTTPS forcing to always use the secure connection
.form-actions .form-group
= f.submit 'Save changes', class: "btn btn-save" = f.label :key, class: 'control-label' do
Key (PEM)
.col-sm-10
= f.text_area :key, rows: 5, class: 'form-control', value: ''
%span.help-inline Upload a certificate for your domain with all intermediates
- else
.nothing-here-block
Support for custom certificates is disabled.
Ask your system's administrator to enable it.
.form-actions
= f.submit 'Create New Domain', class: "btn btn-save"

View file

@ -0,0 +1,16 @@
.panel.panel-default
.panel-heading
Domains (#{@domains.count})
%ul.well-list
- @domains.each do |domain|
%li
.pull-right
= link_to 'Details', namespace_project_page_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped"
= link_to 'Remove', namespace_project_page_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
.clearfix
%span= link_to domain.domain, domain.url
%p
- if domain.subject
%span.label.label-gray Certificate: #{domain.subject}
- if domain.expired?
%span.label.label-danger Expired

View file

@ -0,0 +1,6 @@
.panel.panel-default
.panel-heading
Domains
.nothing-here-block
Support for domains and certificates is disabled.
Ask your system's administrator to enable it.

View file

@ -1,16 +0,0 @@
- if can?(current_user, :update_pages, @project) && @project.pages_custom_certificate
.panel.panel-default.panel.panel-danger
.panel-heading
Remove certificate
.errors-holder
.panel-body
= form_tag(certificates_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do
%p
Removing the certificate will stop serving the page under HTTPS.
- if certificate
%p
%pre
= certificate.to_text
.form-actions
= button_to 'Remove certificate', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_certificate_message(@project) }

View file

@ -1,32 +0,0 @@
- if can?(current_user, :update_pages, @project) && Settings.pages.https && Settings.pages.custom_domain
.panel.panel-default
.panel-heading
Certificate
.panel-body
%p
Allows you to upload your certificate which will be used to serve pages under your domain.
%br
= form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f|
- if @project.errors.any?
#error_explanation
.alert.alert-danger
- @project.errors.full_messages.each do |msg|
%p= msg
.form-group
= f.label :pages_custom_certificate, class: 'control-label' do
Certificate (PEM)
.col-sm-10
= f.text_area :pages_custom_certificate, required: true, rows: 5, class: 'form-control', value: ''
%span.help-inline Upload a certificate for your domain with all intermediates
.form-group
= f.label :pages_custom_certificate_key, class: 'control-label' do
Key (PEM)
.col-sm-10
= f.text_area :pages_custom_certificate_key, required: true, rows: 5, class: 'form-control', value: ''
%span.help-inline Upload a certificate for your domain with all intermediates
.form-actions
= f.submit 'Update certificate', class: "btn btn-save"

View file

@ -0,0 +1,25 @@
- page_title "Pages"
%h3.page_title
Pages
= link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do
%i.fa.fa-plus
New Domain
%p.light
With GitLab Pages you can host for free your static websites on GitLab.
Combined with the power of GitLab CI and the help of GitLab Runner
you can deploy static pages for your individual projects, your user or your group.
%hr.clearfix
- if Settings.pages.enabled
= render 'access'
= render 'use'
- if Settings.pages.external_http || Settings.pages.external_https
= render 'list'
- else
= render 'no_domains'
= render 'destroy'
- else
= render 'disabled'

View file

@ -0,0 +1,6 @@
- page_title 'Pages'
%h3.page_title
New Pages Domain
%hr.clearfix
%div
= render 'form'

View file

@ -1,18 +1,22 @@
- page_title "Pages" - page_title "#{@domain.domain}", "Pages Domain"
%h3.page_title Pages
%p.light
With GitLab Pages you can host for free your static websites on GitLab.
Combined with the power of GitLab CI and the help of GitLab Runner
you can deploy static pages for your individual projects, your user or your group.
%hr
- if Settings.pages.enabled %h3.page-title
= render 'access' #{@domain.domain}
= render 'use'
- if @project.pages_url .table-holder
= render 'form' %table.table
= render 'upload_certificate' %tr
= render 'remove_certificate' %td
= render 'destroy' Domain
- else %td
= render 'disabled' = link_to @domain.domain, @domain.url
%tr
%td
Certificate
%td
- if @domain.certificate
%pre
= @domain.certificate.to_text
- else
.light
missing

View file

@ -165,6 +165,8 @@ production: &base
host: example.com host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS
# external_http: "1.1.1.1:80" # if defined notifies the GitLab pages do support Custom Domains
# external_https: "1.1.1.1:443" # if defined notifies the GitLab pages do support Custom Domains with Certificates
## Mattermost ## Mattermost
## For enabling Add to Mattermost button ## For enabling Add to Mattermost button

View file

@ -273,7 +273,8 @@ Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
Settings.pages['url'] ||= Settings.send(:build_pages_url) Settings.pages['url'] ||= Settings.send(:build_pages_url)
Settings.pages['custom_domain'] ||= false if Settings.pages['custom_domain'].nil? Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
# #
# Git LFS # Git LFS

View file

@ -39,8 +39,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resource :pages, only: [:show, :update, :destroy] do resources :pages, except: [:edit, :update] do
delete :certificates delete :remove_pages
end end
resources :compare, only: [:index, :create] do resources :compare, only: [:index, :create] do