Commit 5f7257c2 authored by Kamil Trzcinski's avatar Kamil Trzcinski Committed by James Edwards-Jones

Initial work on GitLab Pages update

parent c4c8ca04
......@@ -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'
......
......@@ -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)
......
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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
can! :update_pages
end
def public_access!
......
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
# 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
# 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
......@@ -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
- 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.
- 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) }
.panel.panel-default
.nothing-here-block
GitLab Pages is disabled.
Ask your system's administrator to enable it.
- 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"
- 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"
- 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.
- 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'
......@@ -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)
......
......@@ -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
......
......@@ -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
......
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
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