Skip to content

Commit

Permalink
Initial work on GitLab Pages update
Browse files Browse the repository at this point in the history
  • Loading branch information
ayufan authored and James Edwards-Jones committed Jan 31, 2017
1 parent c4c8ca0 commit 5f7257c
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 20 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -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'

Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions 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
10 changes: 0 additions & 10 deletions app/controllers/projects_controller.rb
Expand Up @@ -151,16 +151,6 @@ def unarchive
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

Expand Down
8 changes: 8 additions & 0 deletions app/helpers/projects_helper.rb
Expand Up @@ -81,6 +81,14 @@ def remove_fork_project_message(project)
"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
Expand Down
51 changes: 43 additions & 8 deletions app/models/project.rb
Expand Up @@ -76,6 +76,8 @@ def update_forks_visibility_level
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
Expand Down Expand Up @@ -205,6 +207,11 @@ def update_forks_visibility_level
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

Expand Down Expand Up @@ -1164,16 +1171,27 @@ def runners_token
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

Expand All @@ -1185,6 +1203,15 @@ def public_pages_path
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
Expand All @@ -1194,6 +1221,14 @@ def remove_pages
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
Expand Down
1 change: 1 addition & 0 deletions app/policies/project_policy.rb
Expand Up @@ -110,6 +110,7 @@ def master_access!
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
can! :update_pages
end

def public_access!
Expand Down
53 changes: 53 additions & 0 deletions 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
24 changes: 24 additions & 0 deletions 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
30 changes: 30 additions & 0 deletions 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
4 changes: 4 additions & 0 deletions app/views/layouts/nav/_project_settings.html.haml
Expand Up @@ -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

0 comments on commit 5f7257c

Please sign in to comment.