From 5f7257c27dace1dcb9d3eb4732caf68f061a8d68 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski <ayufan@ayufan.eu>
Date: Tue, 9 Feb 2016 18:06:55 +0100
Subject: [PATCH] Initial work on GitLab Pages update

---
 Gemfile                                       |  3 +
 Gemfile.lock                                  |  4 +
 app/controllers/projects/pages_controller.rb  | 94 +++++++++++++++++++
 app/controllers/projects_controller.rb        | 10 --
 app/helpers/projects_helper.rb                |  8 ++
 app/models/project.rb                         | 51 ++++++++--
 app/policies/project_policy.rb                |  1 +
 .../update_pages_configuration_service.rb     | 53 +++++++++++
 app/validators/certificate_key_validator.rb   | 24 +++++
 app/validators/certificate_validator.rb       | 30 ++++++
 .../layouts/nav/_project_settings.html.haml   |  4 +
 app/views/projects/pages/_access.html.haml    | 34 +++++++
 app/views/projects/pages/_destroy.haml        | 10 ++
 app/views/projects/pages/_disabled.html.haml  |  4 +
 app/views/projects/pages/_form.html.haml      | 35 +++++++
 .../pages/_remove_certificate.html.haml       | 16 ++++
 .../pages/_upload_certificate.html.haml       | 32 +++++++
 app/views/projects/pages/_use.html.haml       | 18 ++++
 app/views/projects/pages/show.html.haml       | 18 ++++
 app/workers/pages_worker.rb                   |  6 +-
 config/initializers/1_settings.rb             |  1 +
 config/routes/project.rb                      |  5 +-
 ...808_add_pages_custom_domain_to_projects.rb | 10 ++
 23 files changed, 451 insertions(+), 20 deletions(-)
 create mode 100644 app/controllers/projects/pages_controller.rb
 create mode 100644 app/services/projects/update_pages_configuration_service.rb
 create mode 100644 app/validators/certificate_key_validator.rb
 create mode 100644 app/validators/certificate_validator.rb
 create mode 100644 app/views/projects/pages/_access.html.haml
 create mode 100644 app/views/projects/pages/_destroy.haml
 create mode 100644 app/views/projects/pages/_disabled.html.haml
 create mode 100644 app/views/projects/pages/_form.html.haml
 create mode 100644 app/views/projects/pages/_remove_certificate.html.haml
 create mode 100644 app/views/projects/pages/_upload_certificate.html.haml
 create mode 100644 app/views/projects/pages/_use.html.haml
 create mode 100644 app/views/projects/pages/show.html.haml
 create mode 100644 db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb

diff --git a/Gemfile b/Gemfile
index dd7c93c5a75..bc1b13c7331 100644
--- a/Gemfile
+++ b/Gemfile
@@ -48,6 +48,9 @@ gem 'rqrcode-rails3', '~> 0.1.7'
 gem 'attr_encrypted', '~> 3.0.0'
 gem 'u2f', '~> 0.2.1'
 
+# GitLab Pages
+gem 'validates_hostname', '~> 1.0.0'
+
 # Browser detection
 gem 'browser', '~> 2.2'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 3b207d19d1f..6263b02b041 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -799,6 +799,9 @@ GEM
       get_process_mem (~> 0)
       unicorn (>= 4, < 6)
     uniform_notifier (1.10.0)
+    validates_hostname (1.0.5)
+      activerecord (>= 3.0)
+      activesupport (>= 3.0)
     version_sorter (2.1.0)
     virtus (1.0.5)
       axiom-types (~> 0.1)
@@ -1014,6 +1017,7 @@ DEPENDENCIES
   unf (~> 0.1.4)
   unicorn (~> 5.1.0)
   unicorn-worker-killer (~> 0.4.4)
+  validates_hostname (~> 1.0.0)
   version_sorter (~> 2.1.0)
   virtus (~> 1.0.1)
   vmstat (~> 2.3.0)
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
new file mode 100644
index 00000000000..ef0ed505142
--- /dev/null
+++ b/app/controllers/projects/pages_controller.rb
@@ -0,0 +1,94 @@
+class Projects::PagesController < Projects::ApplicationController
+  layout 'project_settings'
+
+  before_action :authorize_update_pages!, except: [:show]
+  before_action :authorize_remove_pages!, only: :destroy
+
+  helper_method :valid_certificate?, :valid_certificate_key?
+  helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates?
+  helper_method :certificate, :certificate_key
+
+  def show
+  end
+
+  def update
+    if @project.update_attributes(pages_params)
+      redirect_to namespace_project_pages_path(@project.namespace, @project)
+    else
+      render 'show'
+    end
+  end
+
+  def certificate
+    @project.remove_pages_certificate
+  end
+
+  def destroy
+    @project.remove_pages
+
+    respond_to do |format|
+      format.html { redirect_to project_path(@project) }
+    end
+  end
+
+  private
+
+  def pages_params
+    params.require(:project).permit(
+                              :pages_custom_certificate,
+                              :pages_custom_certificate_key,
+                              :pages_custom_domain,
+                              :pages_redirect_http,
+    )
+  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
+
+    certificate.verify(certificate_key)
+  rescue OpenSSL::X509::CertificateError
+    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
+    nil
+  end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 123dc179e73..444ff837bb3 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -151,16 +151,6 @@ class ProjectsController < Projects::ApplicationController
     end
   end
 
-  def remove_pages
-    return access_denied! unless can?(current_user, :remove_pages, @project)
-
-    @project.remove_pages
-
-    respond_to do |format|
-      format.html { redirect_to project_path(@project) }
-    end
-  end
-
   def housekeeping
     ::Projects::HousekeepingService.new(@project).execute
 
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb98204285d..63aa182502d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -81,6 +81,14 @@ module ProjectsHelper
     "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}.  Are you ABSOLUTELY sure?"
   end
 
+  def remove_pages_message(project)
+    "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/project.rb b/app/models/project.rb
index 8a8aca44945..34618817fb6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -76,6 +76,8 @@ class Project < ActiveRecord::Base
   attr_accessor :new_default_branch
   attr_accessor :old_path_with_namespace
 
+  attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
+
   alias_attribute :title, :name
 
   # Relations
@@ -205,6 +207,11 @@ class Project < ActiveRecord::Base
     presence: true,
     inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
 
+  validates :pages_custom_domain, hostname: true, allow_blank: true, allow_nil: true
+  validates_uniqueness_of :pages_custom_domain, allow_nil: true, allow_blank: true
+  validates :pages_custom_certificate, certificate: { intermediate: true }
+  validates :pages_custom_certificate_key, certificate_key: true
+
   add_authentication_token_field :runners_token
   before_save :ensure_runners_token
 
@@ -1164,16 +1171,27 @@ class Project < ActiveRecord::Base
   end
 
   def pages_url
-    if Dir.exist?(public_pages_path)
-      host = "#{namespace.path}.#{Settings.pages.host}"
-      url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
-        "#{prefix}#{namespace.path}."
-      end
+    return unless Dir.exist?(public_pages_path)
+
+    host = "#{namespace.path}.#{Settings.pages.host}"
+    url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+      "#{prefix}#{namespace.path}."
+    end
+
+    # If the project path is the same as host, leave the short version
+    return url if host == path
+
+    "#{url}/#{path}"
+  end
 
-      # If the project path is the same as host, leave the short version
-      return url if host == path
+  def pages_custom_url
+    return unless pages_custom_domain
+    return unless Dir.exist?(public_pages_path)
 
-      "#{url}/#{path}"
+    if Gitlab.config.pages.https
+      return "https://#{pages_custom_domain}"
+    else
+      return "http://#{pages_custom_domain}"
     end
   end
 
@@ -1185,6 +1203,15 @@ class Project < ActiveRecord::Base
     File.join(pages_path, 'public')
   end
 
+  def remove_pages_certificate
+    update(
+      pages_custom_certificate: nil,
+      pages_custom_certificate_key: nil
+    )
+
+    UpdatePagesConfigurationService.new(self).execute
+  end
+
   def remove_pages
     # 1. We rename pages to temporary directory
     # 2. We wait 5 minutes, due to NFS caching
@@ -1194,6 +1221,14 @@ class Project < ActiveRecord::Base
     if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path)
       PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path)
     end
+
+    update(
+      pages_custom_certificate: nil,
+      pages_custom_certificate_key: nil,
+      pages_custom_domain: nil
+    )
+
+    UpdatePagesConfigurationService.new(self).execute
   end
 
   def wiki
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 63bc639688d..ca5b39a001f 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy
     can! :admin_pipeline
     can! :admin_environment
     can! :admin_deployment
+    can! :update_pages
   end
 
   def public_access!
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
new file mode 100644
index 00000000000..be4c2fbef8c
--- /dev/null
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -0,0 +1,53 @@
+module Projects
+  class UpdatePagesConfigurationService < BaseService
+    attr_reader :project
+
+    def initialize(project)
+      @project = project
+    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)
+      reload_daemon
+      success
+    rescue => e
+      error(e.message)
+    end
+
+    private
+
+    def reload_daemon
+      # GitLab Pages daemon constantly watches for modification time of `pages.path`
+      # It reloads configuration when `pages.path` is modified
+      File.touch(Settings.pages.path)
+    end
+
+    def pages_path
+      @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')
+    end
+
+    def update_file(file, data)
+      if data
+        File.open(file, 'w') do |file|
+          file.write(data)
+        end
+      else
+        File.rm_r(file)
+      end
+    end
+  end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
new file mode 100644
index 00000000000..3b5bd30db1a
--- /dev/null
+++ b/app/validators/certificate_key_validator.rb
@@ -0,0 +1,24 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+#   class Project < ActiveRecord::Base
+#     validates :certificate_key, certificate_key: true
+#   end
+#
+class CertificateKeyValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless valid_private_key_pem?(value)
+      record.errors.add(attribute, "must be a valid PEM private key")
+    end
+  end
+
+  private
+
+  def valid_private_key_pem?(value)
+    pkey = OpenSSL::PKey::RSA.new(value)
+    pkey.private?
+  rescue OpenSSL::PKey::PKeyError
+    false
+  end
+end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
new file mode 100644
index 00000000000..2cba5a435b7
--- /dev/null
+++ b/app/validators/certificate_validator.rb
@@ -0,0 +1,30 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+#   class Project < ActiveRecord::Base
+#     validates :certificate_key, certificate_key: true
+#   end
+#
+class CertificateValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    certificate = parse_certificate(value)
+    unless certificate
+      record.errors.add(attribute, "must be a valid PEM certificate")
+    end
+
+    if options[:intermediates]
+      unless certificate
+        record.errors.add(attribute, "certificate verification failed: missing intermediate certificates")
+      end
+    end
+  end
+
+  private
+
+  def parse_certificate(value)
+    OpenSSL::X509::Certificate.new(value)
+  rescue OpenSSL::X509::CertificateError
+    nil
+  end
+end
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index c6df66d2c3c..d6c158b6de3 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -34,3 +34,7 @@
       = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
         %span
           CI/CD Pipelines
+  = nav_link(controller: :pages) do
+    = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
+      %span
+        Pages
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
new file mode 100644
index 00000000000..d64f99fd22b
--- /dev/null
+++ b/app/views/projects/pages/_access.html.haml
@@ -0,0 +1,34 @@
+- if @project.pages_url
+  .panel.panel-default
+    .panel-heading
+      Access pages
+    .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.
+
+        - 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.
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
new file mode 100644
index 00000000000..61b995a5934
--- /dev/null
+++ b/app/views/projects/pages/_destroy.haml
@@ -0,0 +1,10 @@
+- if can?(current_user, :remove_pages, @project) && @project.pages_url
+  .panel.panel-default.panel.panel-danger
+    .panel-heading Remove pages
+    .errors-holder
+    .panel-body
+      = form_tag(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
+          = button_to 'Remove pages', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_message(@project) }
diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml
new file mode 100644
index 00000000000..cf9ef5b4d6f
--- /dev/null
+++ b/app/views/projects/pages/_disabled.html.haml
@@ -0,0 +1,4 @@
+.panel.panel-default
+  .nothing-here-block
+    GitLab Pages is disabled.
+    Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml
new file mode 100644
index 00000000000..a7b03d552db
--- /dev/null
+++ b/app/views/projects/pages/_form.html.haml
@@ -0,0 +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-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.
+
+        - 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
+
+        .form-actions
+          = f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/pages/_remove_certificate.html.haml b/app/views/projects/pages/_remove_certificate.html.haml
new file mode 100644
index 00000000000..e8c0d03adfa
--- /dev/null
+++ b/app/views/projects/pages/_remove_certificate.html.haml
@@ -0,0 +1,16 @@
+- 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
new file mode 100644
index 00000000000..30873fcf395
--- /dev/null
+++ b/app/views/projects/pages/_upload_certificate.html.haml
@@ -0,0 +1,32 @@
+- 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/_use.html.haml b/app/views/projects/pages/_use.html.haml
new file mode 100644
index 00000000000..5542bbe670b
--- /dev/null
+++ b/app/views/projects/pages/_use.html.haml
@@ -0,0 +1,18 @@
+- unless @project.pages_url
+  .panel.panel-info
+    .panel-heading
+      Configure pages
+    .panel-body
+      %p
+        Learn how to upload your static site and have it served by
+        GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
+      %p
+        In the example below we define a special job named
+        %code pages
+        which is using Jekyll to build a static site. The generated
+        HTML will be stored in the
+        %code public/
+        directory which will then be archived and uploaded to GitLab.
+        The name of the directory should not be different than
+        %code public/
+        in order for the pages to work.
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
new file mode 100644
index 00000000000..5f689800da8
--- /dev/null
+++ b/app/views/projects/pages/show.html.haml
@@ -0,0 +1,18 @@
+- 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
+
+- 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'
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 8c99e8dbe76..4eeb9666bb0 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -9,7 +9,11 @@ class PagesWorker
 
   def deploy(build_id)
     build = Ci::Build.find_by(id: build_id)
-    Projects::UpdatePagesService.new(build.project, build).execute
+    result = Projects::UpdatePagesService.new(build.project, build).execute
+    if result[:status] == :success
+      result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+    end
+    result
   end
 
   def remove(namespace_path, project_path)
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 860cafad325..239aa662d9f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -273,6 +273,7 @@ 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?
 
 #
 # Git LFS
diff --git a/config/routes/project.rb b/config/routes/project.rb
index cd56f6281f5..956a2d3186f 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -39,6 +39,10 @@ constraints(ProjectUrlConstrainer.new) do
         end
       end
 
+      resource :pages, only: [:show, :update, :destroy] do
+        delete :certificates
+      end
+
       resources :compare, only: [:index, :create] do
         collection do
           get :diff_for_path
@@ -329,7 +333,6 @@ constraints(ProjectUrlConstrainer.new) do
         post :archive
         post :unarchive
         post :housekeeping
-        post :remove_pages
         post :toggle_star
         post :preview_markdown
         post :export
diff --git a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb
new file mode 100644
index 00000000000..6472199fc4a
--- /dev/null
+++ b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb
@@ -0,0 +1,10 @@
+class AddPagesCustomDomainToProjects < ActiveRecord::Migration
+  def change
+    add_column :projects, :pages_custom_certificate, :text
+    add_column :projects, :pages_custom_certificate_key, :text
+    add_column :projects, :pages_custom_certificate_key_iv, :string
+    add_column :projects, :pages_custom_certificate_key_salt, :string
+    add_column :projects, :pages_custom_domain, :string, unique: true
+    add_column :projects, :pages_redirect_http, :boolean, default: false, null: false
+  end
+end
-- 
GitLab