Commit 13b6bad1 authored by Kamil Trzcinski's avatar Kamil Trzcinski Committed by James Edwards-Jones

Implement extra domains and save pages configuration

parent 6e99226c
......@@ -2,25 +2,45 @@ class Projects::PagesController < Projects::ApplicationController
layout 'project_settings'
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_key_for_certificiate?, :valid_certificate_intermediates?
helper_method :certificate, :certificate_key
def index
@domains = @project.pages_domains.order(:domain)
end
def show
end
def update
if @project.update_attributes(pages_params)
def new
@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)
else
render 'show'
render 'new'
end
end
def certificate
@project.remove_pages_certificate
def destroy
@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
def destroy
......@@ -33,63 +53,15 @@ class Projects::PagesController < Projects::ApplicationController
private
def pages_params
params.require(:project).permit(
:pages_custom_certificate,
:pages_custom_certificate_key,
:pages_custom_domain,
:pages_redirect_http,
def pages_domain_params
params.require(:pages_domain).permit(
:certificate,
:key,
:domain
)
end
def valid_certificate?
certificate.present?
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
def domain
@domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
end
end
......@@ -85,10 +85,6 @@ module ProjectsHelper
"You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?"
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
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
......
......@@ -2,19 +2,25 @@ class PagesDomain < ActiveRecord::Base
belongs_to :project
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 :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_save :update
after_destroy :update
def to_param
domain
end
def url
return unless domain
return unless Dir.exist?(project.public_pages_path)
if certificate
return "https://#{domain}"
......@@ -23,7 +29,77 @@ class PagesDomain < ActiveRecord::Base
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
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
......@@ -7,9 +7,7 @@ module Projects
end
def execute
update_file(pages_cname_file, project.pages_custom_domain)
update_file(pages_certificate_file, project.pages_custom_certificate)
update_file(pages_certificate_file_key, project.pages_custom_certificate_key)
update_file(pages_config_file, pages_config)
reload_daemon
success
rescue => e
......@@ -18,6 +16,22 @@ module Projects
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
# GitLab Pages daemon constantly watches for modification time of `pages.path`
# It reloads configuration when `pages.path` is modified
......@@ -28,16 +42,8 @@ module Projects
@pages_path ||= project.pages_path
end
def pages_cname_file
File.join(pages_path, 'CNAME')
end
def pages_certificate_file
File.join(pages_path, 'domain.crt')
end
def pages_certificate_key_file
File.join(pages_path, 'domain.key')
def pages_config_file
File.join(pages_path, 'config.jso')
end
def update_file(file, data)
......
......@@ -5,30 +5,9 @@
.panel-body
%p
%strong
Congratulations! Your pages are served at:
%p= link_to @project.pages_url, @project.pages_url
- if Settings.pages.custom_domain && @project.pages_custom_url
%p= link_to @project.pages_custom_url, @project.pages_custom_url
- if @project.pages_custom_certificate
- unless valid_certificate?
#error_explanation
.alert.alert-warning
Your certificate is invalid.
Congratulations! Your pages are served under:
- 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.
%p= link_to @project.pages_url, @project.pages_url
- unless valid_certificate_intermediates?
#error_explanation
.alert.alert-warning
Your certificate doesn't have intermediates.
Your page may not work properly.
- @project.pages_domains.each do |domain|
%p= link_to domain.url, domain.url
......@@ -3,7 +3,7 @@
.panel-heading Remove pages
.errors-holder
.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
Removing the pages will prevent from exposing them to outside world.
.form-actions
......
- if can?(current_user, :update_pages, @project)
.panel.panel-default
.panel-heading
Settings
.panel-body
= 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_for [@domain], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f|
- if @domain.errors.any?
#error_explanation
.alert.alert-danger
- @domain.errors.full_messages.each do |msg|
%p= msg
.form-group
= f.label :pages_domain, class: 'control-label' do
Custom domain
.col-sm-10
- if Settings.pages.custom_domain
= f.text_field :pages_custom_domain, required: false, autocomplete: 'off', class: 'form-control'
%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.
.form-group
= f.label :domain, class: 'control-label' do
Domain
.col-sm-10
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
- if Settings.pages.https
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :pages_redirect_http do
= f.check_box :pages_redirect_http
%span.descr Force HTTPS
.help-block Redirect the HTTP to HTTPS forcing to always use the secure connection
- if Settings.pages.external_https
.form-group
= f.label :certificate, class: 'control-label' do
Certificate (PEM)
.col-sm-10
= f.text_area :certificate, rows: 5, class: 'form-control', value: ''
%span.help-inline Upload a certificate for your domain with all intermediates
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
.form-group
= 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"
.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
.panel.panel-default
.panel-heading
Domains
.nothing-here-block
Support for domains and certificates is disabled.
Ask your system's administrator to enable it.
- 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) }
- 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"
- 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'
- page_title 'Pages'
%h3.page_title
New Pages Domain
%hr.clearfix
%div
= render 'form'
- page_title "Pages"
%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
- page_title "#{@domain.domain}", "Pages Domain"
- if Settings.pages.enabled
= render 'access'
= render 'use'
- if @project.pages_url
= render 'form'
= render 'upload_certificate'
= render 'remove_certificate'
= render 'destroy'
- else
= render 'disabled'
%h3.page-title
#{@domain.domain}
.table-holder
%table.table
%tr
%td
Domain
%td
= link_to @domain.domain, @domain.url
%tr
%td
Certificate
%td
- if @domain.certificate
%pre
= @domain.certificate.to_text
- else
.light
missing
......@@ -165,6 +165,8 @@ production: &base
host: example.com
port: 80 # Set to 443 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
## For enabling Add to Mattermost button
......
......@@ -273,7 +273,8 @@ Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
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
......
......@@ -39,8 +39,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
resource :pages, only: [:show, :update, :destroy] do
delete :certificates
resources :pages, except: [:edit, :update] do
delete :remove_pages
end
resources :compare, only: [:index, :create] do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment