From 13b6bad17ec46eb78878f6972da1e7e34be86bb5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 10 Feb 2016 15:06:31 +0100 Subject: [PATCH] Implement extra domains and save pages configuration --- app/controllers/projects/pages_controller.rb | 94 +++++++------------ app/helpers/projects_helper.rb | 4 - app/models/pages_domain.rb | 84 ++++++++++++++++- .../update_pages_configuration_service.rb | 32 ++++--- app/views/projects/pages/_access.html.haml | 29 +----- app/views/projects/pages/_destroy.haml | 2 +- app/views/projects/pages/_form.html.haml | 64 ++++++------- app/views/projects/pages/_list.html.haml | 16 ++++ .../projects/pages/_no_domains.html.haml | 6 ++ .../pages/_remove_certificate.html.haml | 16 ---- .../pages/_upload_certificate.html.haml | 32 ------- app/views/projects/pages/index.html.haml | 25 +++++ app/views/projects/pages/new.html.haml | 6 ++ app/views/projects/pages/show.html.haml | 38 ++++---- config/gitlab.yml.example | 2 + config/initializers/1_settings.rb | 3 +- config/routes/project.rb | 4 +- 17 files changed, 249 insertions(+), 208 deletions(-) create mode 100644 app/views/projects/pages/_list.html.haml create mode 100644 app/views/projects/pages/_no_domains.html.haml delete mode 100644 app/views/projects/pages/_remove_certificate.html.haml delete mode 100644 app/views/projects/pages/_upload_certificate.html.haml create mode 100644 app/views/projects/pages/index.html.haml create mode 100644 app/views/projects/pages/new.html.haml diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 055f182ae00..82814afe196 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -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 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 63aa182502d..054cc849839 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -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 diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index eebdf7501de..810af4e832a 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -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 diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index be4c2fbef8c..5afb0582ca6 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -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) diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml index d64f99fd22b..9740877b214 100644 --- a/app/views/projects/pages/_access.html.haml +++ b/app/views/projects/pages/_access.html.haml @@ -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 diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 61b995a5934..dd493a6d312 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -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 diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml index a7b03d552db..c69b76c6697 100644 --- a/app/views/projects/pages/_form.html.haml +++ b/app/views/projects/pages/_form.html.haml @@ -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" diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml new file mode 100644 index 00000000000..7dfeb0e6e12 --- /dev/null +++ b/app/views/projects/pages/_list.html.haml @@ -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 diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml new file mode 100644 index 00000000000..5a18740346a --- /dev/null +++ b/app/views/projects/pages/_no_domains.html.haml @@ -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. diff --git a/app/views/projects/pages/_remove_certificate.html.haml b/app/views/projects/pages/_remove_certificate.html.haml deleted file mode 100644 index e8c0d03adfa..00000000000 --- a/app/views/projects/pages/_remove_certificate.html.haml +++ /dev/null @@ -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) } diff --git a/app/views/projects/pages/_upload_certificate.html.haml b/app/views/projects/pages/_upload_certificate.html.haml deleted file mode 100644 index 30873fcf395..00000000000 --- a/app/views/projects/pages/_upload_certificate.html.haml +++ /dev/null @@ -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" diff --git a/app/views/projects/pages/index.html.haml b/app/views/projects/pages/index.html.haml new file mode 100644 index 00000000000..fea34c113ba --- /dev/null +++ b/app/views/projects/pages/index.html.haml @@ -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' diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml new file mode 100644 index 00000000000..2609df62aac --- /dev/null +++ b/app/views/projects/pages/new.html.haml @@ -0,0 +1,6 @@ +- page_title 'Pages' +%h3.page_title + New Pages Domain +%hr.clearfix +%div + = render 'form' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 5f689800da8..98c4e890968 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -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 diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c6f06d43d07..f2bde602795 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -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 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 239aa662d9f..0015ddf902d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -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 diff --git a/config/routes/project.rb b/config/routes/project.rb index 956a2d3186f..ac1e3fce16a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -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