Implement extra domains and save pages configuration
This commit is contained in:
parent
6e99226cca
commit
13b6bad17e
17 changed files with 249 additions and 208 deletions
|
@ -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:
|
||||
Congratulations! Your pages are served under:
|
||||
|
||||
%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.
|
||||
|
||||
- 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.
|
||||
- @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
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
- 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"
|
||||
|
|
16
app/views/projects/pages/_list.html.haml
Normal file
16
app/views/projects/pages/_list.html.haml
Normal 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
|
6
app/views/projects/pages/_no_domains.html.haml
Normal file
6
app/views/projects/pages/_no_domains.html.haml
Normal 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.
|
|
@ -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) }
|
|
@ -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"
|
25
app/views/projects/pages/index.html.haml
Normal file
25
app/views/projects/pages/index.html.haml
Normal 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'
|
6
app/views/projects/pages/new.html.haml
Normal file
6
app/views/projects/pages/new.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
- page_title 'Pages'
|
||||
%h3.page_title
|
||||
New Pages Domain
|
||||
%hr.clearfix
|
||||
%div
|
||||
= render 'form'
|
|
@ -1,18 +1,22 @@
|
|||
- 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
|
||||
|
|
Loading…
Reference in a new issue