├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.markdown ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── doc └── image.png ├── jekyll-gitlab-letsencrypt.gemspec ├── lib └── jekyll │ ├── commands │ └── gitlab │ │ └── letsencrypt.rb │ └── gitlab │ ├── letsencrypt.rb │ └── letsencrypt │ ├── acme.rb │ ├── configuration.rb │ ├── gitlab_client.rb │ ├── process.rb │ └── version.rb └── spec ├── fixtures └── vcr_cassettes │ ├── authorization.yml │ └── gitlab_commit.yml ├── lib └── jekyll │ ├── commands │ └── gitlab │ │ └── letsencrypt_spec.rb │ └── gitlab │ └── letsencrypt │ ├── acme_spec.rb │ ├── configuration_spec.rb │ ├── gitlab_client_spec.rb │ ├── process_spec.rb │ └── version_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | *.gem 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1 5 | - 2.2 6 | - 2.3 7 | - 2.4 8 | - 2.5 9 | env: 10 | - JEKYLL_VERSION=3.3.1 11 | - JEKYLL_VERSION=3.4.5 12 | - JEKYLL_VERSION=3.5.2 13 | - JEKYLL_VERSION=3.6.3 14 | - JEKYLL_VERSION=3.7.4 15 | - JEKYLL_VERSION=3.8.5 16 | matrix: 17 | allow_failures: 18 | - rvm: 2.1 # Jekyll doesn't support 2.1.x itself -> https://github.com/jekyll/jekyll/pull/6623 19 | before_install: 20 | - echo "gem \"jekyll\", \"$JEKYLL_VERSION\"" >> Gemfile 21 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | # 0.4.1 2 | - #23 - Add option to append arbitrary text to the challenge file 3 | 4 | # 0.4.0 5 | 6 | - Updated travis testing matrix 7 | - #21 - Add commit message configuration option 8 | - Thanks to @daehlith ! 9 | 10 | # 0.3.0 11 | 12 | - #17 - Check for repo id fetch request status 13 | - Update to Gitlab API V4 14 | - Mostly from #18 - thanks @ethernet-zero ! 15 | 16 | # 0.2.1 17 | 18 | - Add custom GitLab URL setting 19 | 20 | # 0.2.0 21 | 22 | - #8 - Fully automated! Use new gitlab API for the last step 23 | 24 | # 0.1.0 25 | 26 | - #6 - Added option to get secret gitlab token from env var 27 | 28 | # 0.0.2 29 | 30 | - #3 - Added option to specify scheme used for challenge url request 31 | 32 | # 0.0.1 33 | 34 | - Initial Release 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in jekyll-gitlab-letsencrypt.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT), with added prohibitions loosely based on http://neveragain.tech/ 2 | 3 | Copyright (c) 2017 Justin Aiken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | - The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | - Use in any kind of the following use cases is expressly prohibited: 15 | - Creation of databases of identifying information for the United States government to target individuals based on race, religion, or national origin. 16 | - Collection and retention of data that would facilitate ethnic or religious targeting 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | - This gem uses the letsencypt ACME v1 API, which is now deprecated. A change to use the v2 API would be need to make it work. 4 | - It's probably not needed - gitlab now offers [automatic Letsencrypt integration](https://gitlab.com/help/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md) 5 | - I switched my own page to ^^ from this gem and it worked okay... 6 | 7 | [![Gem Version](http://img.shields.io/gem/v/jekyll-gitlab-letsencrypt.svg)](https://rubygems.org/gems/jekyll-gitlab-letsencrypt) [![Build Status](http://img.shields.io/travis/JustinAiken/jekyll-gitlab-letsencrypt/master.svg)](http://travis-ci.org/JustinAiken/jekyll-gitlab-letsencrypt) [![Coveralls branch](http://img.shields.io/coveralls/JustinAiken/jekyll-gitlab-letsencrypt/master.svg)](https://coveralls.io/r/JustinAiken/jjekyll-gitlab-letsencrypt?branch=master) [![Code Climate](http://img.shields.io/codeclimate/github/JustinAiken/jekyll-gitlab-letsencrypt.svg)](https://codeclimate.com/github/JustinAiken/jekyll-gitlab-letsencrypt) 8 | 9 | # jekyll-gitlab-letsencrypt 10 | 11 | ![img](doc/image.png) 12 | 13 | This plugin automagically does the entire the letsencrypt process for your gitlab-hosted jekyll blog! 14 | 15 | - *(automatic)* It registers your email to the letsencrypt server 16 | - *(automatic)* It generates a challenge file, and commits it directly via the gitlab API 17 | - *(automatic)* It sleeps until the challenge file is live on the internet 18 | - *(automatic)* It asks letsencrypt to verify it 19 | - *(automatic)* It spits out the certificate chain and private key 20 | - *(automatic)* It updates the gitlab pages domain settings to use the certificate 21 | 22 | ## Usage 23 | 24 | ### Prerequisites 25 | 26 | You must have: 27 | - A jekyll blog 28 | - Hosted on gitlab pages 29 | - With a domain name set up and working 30 | - Gitlab CI setup such that when you push to `master` (or your preferred branch), your changes are deployed live 31 | 32 | Versions supported: 33 | - Jekyll 3+ 34 | - 3.3.x - 3.8.x is tested against 35 | - Ruby 2.1+ 36 | - Although 2.2+ recommend, as Jekyll itself doesn't support 2.1 37 | 38 | ### Installation 39 | 40 | - Add to your Gemfile: 41 | 42 | ```ruby 43 | group :jekyll_plugins do 44 | gem 'jekyll-emojis' 45 | gem 'jekyll-more-emojis' 46 | ++ gem 'jekyll-gitlab-letsencrypt' 47 | end 48 | ``` 49 | 50 | and run `bundle install` 51 | 52 | ## First-time Configuration 53 | 54 | - Get a personal access token: https://gitlab.com/profile/personal_access_tokens 55 | 56 | Add a `gitlab-letsencrypt` to your `_config.yml`: 57 | 58 | ```yaml 59 | gitlab-letsencrypt: 60 | # Gitlab settings: 61 | personal_access_token: 'MUCH SECRET' # Gotten from the step above ^^ 62 | gitlab_repo: 'gitlab_user/gitlab_repo' # Namespaced repository identifier 63 | 64 | # Domain settings: 65 | email: 'example@example.com' # Let's Encrypt email address 66 | domain: 'example.com' # Domain that the cert will be issued for 67 | 68 | # Jekyll settings: 69 | base_path: './' # Where you want the file to go 70 | pretty_url: false # Add a "/" on the end of the URL... set to `true` if you use permalink_style: pretty 71 | append_str: '' # Append this string to the end of the challenge URL 72 | filename: 'letsencrypt.html' # What to call the generated challenge file 73 | 74 | # Delay settings: 75 | initial_delay: 120 # How long to wait for Gitlab CI to push your changes before it starts checking 76 | delay_time: 15 # How long to wait between each check once it starts looking for the file 77 | 78 | # Optional settings you probably don't need: 79 | gitlab_url: 'https://someurl' # Set if you need to use a self-hosted GitLab instance 80 | endpoint: 'https://somewhere' # if you're doing the ACME thing outside of letsencrypt 81 | branch: 'master' # Defaults to master, but you can use a different branch 82 | layout: 'null' # Layout to use for challenge file - defaults to null, but you can change if needed 83 | scheme: 'https' # Scheme to use for challenge request; default http 84 | commit_message: 'Renew certificate [ROBOT]' # Commit message to use; defaults to "Automated Let's Encrypt renewal" 85 | ``` 86 | 87 | ### Running 88 | 89 | - Just type `jekyll letsencrypt` 90 | 91 | ```shell 92 | $ jekyll letsencrypt 93 | Registering example@example.com to https://acme-v01.api.letsencrypt.org/... 94 | Pushing file to Gitlab 95 | Commiting challenge file as lets.html 96 | Done Commiting! Check https://gitlab.com/gitlab_user/gitlab_repo/commits/master 97 | Going to check http://example.com/.well-known/acme-challenge/lots_of_numbers/ for the challenge to be present... 98 | Waiting 120 seconds before we start checking for challenge.. 99 | Got response code 404, waiting 15 seconds... 100 | Got response code 404, waiting 15 seconds... 101 | Got response code 200, file is present! 102 | Requesting verification... 103 | Challenge status = valid 104 | Challenge is valid! 105 | Certificate retrieved! 106 | Updating domain example.com pages setting with new certificates.. 107 | Success! 108 | ``` 109 | 110 | ### Alternative token usage 111 | 112 | If you don't want to put your secret gitlab token in your `_config.yml`, you can pass it as an ENV var when you run the command: 113 | 114 | ```bash 115 | GITLAB_TOKEN="VERY_SECRET_NOT_IN_GIT_PLEASE" jekyll letsencrypt 116 | ``` 117 | 118 | # License 119 | 120 | MIT 121 | 122 | # Credits/thanks 123 | 124 | - :heart: Gitlab for free page hosting, free repos, and free CI! 125 | - :heart: the Jekyll team for the easy-to-use blogging engine! 126 | - Inspired by the excellent [gitlab-letsencrypt](https://github.com/rolodato/gitlab-letsencrypt) npm package. 127 | - Thanks to contributors: 128 | - @ethernet-zero 129 | - @daehlith 130 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new :spec 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /doc/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinAiken/jekyll-gitlab-letsencrypt/0143263207a4dcfcc4fec60b1d815be81fbd2934/doc/image.png -------------------------------------------------------------------------------- /jekyll-gitlab-letsencrypt.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'jekyll/gitlab/letsencrypt/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-gitlab-letsencrypt" 7 | spec.version = Jekyll::Gitlab::Letsencrypt::VERSION 8 | spec.authors = ["Justin Aiken"] 9 | spec.email = ["60tonangel@gmail.com"] 10 | 11 | spec.summary = %q{Automate letsencrypt renewals for gitlab pages.} 12 | spec.description = %q{Automate letsencrypt renewals for gitlab pages.} 13 | spec.homepage = "https://github.com/JustinAiken/jekyll-gitlab-letsencrypt" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match %r{^(spec)/} } 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_development_dependency "bundler", ">= 1.13" 20 | spec.add_development_dependency "rake", ">= 10.0" 21 | spec.add_development_dependency "rspec", "~> 3.0" 22 | spec.add_development_dependency "jekyll", ">= 3.0" 23 | spec.add_development_dependency "vcr", "~> 3.0.3" 24 | spec.add_development_dependency "coveralls" 25 | 26 | spec.add_dependency "activesupport", ">= 3.0.0" 27 | spec.add_dependency "acme-client", "~> 0.6" 28 | end 29 | -------------------------------------------------------------------------------- /lib/jekyll/commands/gitlab/letsencrypt.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Commands 3 | module Gitlab 4 | class Letsencrypt < ::Jekyll::Command 5 | def self.init_with_program(prog) 6 | prog.command(:letsencrypt) do |c| 7 | c.description "Setup/Renew letsencrypt certificate" 8 | 9 | c.action do |_args, _opts| 10 | Jekyll::Gitlab::Letsencrypt::Process.process! 11 | end 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/core_ext/module/delegation' 3 | 4 | require 'jekyll/gitlab/letsencrypt/version' 5 | require 'jekyll/gitlab/letsencrypt/configuration' 6 | require 'jekyll/gitlab/letsencrypt/acme' 7 | require 'jekyll/gitlab/letsencrypt/process' 8 | require 'jekyll/gitlab/letsencrypt/gitlab_client' 9 | 10 | module Jekyll 11 | module Gitlab 12 | module Letsencrypt 13 | end 14 | end 15 | end 16 | 17 | require 'jekyll/commands/gitlab/letsencrypt' 18 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt/acme.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'acme-client' 3 | 4 | module Jekyll 5 | module Gitlab 6 | module Letsencrypt 7 | class Acme 8 | 9 | delegate :email, :endpoint, :domain, to: Configuration 10 | 11 | attr_accessor :registration 12 | 13 | def register! 14 | Jekyll.logger.info "Registering #{email} to #{endpoint}..." 15 | @registration = client.register contact: "mailto:#{email}" 16 | @registration.agree_terms 17 | self 18 | end 19 | 20 | def authorized? 21 | authorization.status == 'valid' 22 | end 23 | 24 | def challenge 25 | @challenge ||= authorization.http01 26 | end 27 | 28 | def client 29 | @client ||= begin 30 | private_key = OpenSSL::PKey::RSA.new(4096) 31 | ::Acme::Client.new private_key: private_key, endpoint: endpoint, connection_options: { request: { open_timeout: 5, timeout: 5 } } 32 | end 33 | end 34 | 35 | private 36 | 37 | def authorization 38 | @authorization ||= client.authorize(domain: domain) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt/configuration.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/object/blank" 2 | 3 | module Jekyll 4 | module Gitlab 5 | module Letsencrypt 6 | class Configuration 7 | 8 | DEFAULT_FILENAME = 'letsencrypt_challenge.html' 9 | DEFAULT_ENDPOINT = 'https://acme-v01.api.letsencrypt.org/' 10 | DEFAULT_BRANCH = 'master' 11 | DEFAULT_LAYOUT = 'null' 12 | DEFAULT_INITIAL_DELAY = 120 13 | DEFAULT_DELAY_TIME = 15 14 | DEFAULT_SCHEME = 'http' 15 | DEFAULT_GITLAB_URL = 'https://gitlab.com' 16 | DEFAULT_COMMIT_MESSAGE = "Automated Let's Encrypt renewal" 17 | 18 | REQUIRED_KEYS = %w{gitlab_repo email domain} 19 | 20 | class << self 21 | 22 | def valid? 23 | REQUIRED_KEYS.all? { |key| jekyll_config.has_key? key } && personal_access_token 24 | end 25 | 26 | def endpoint 27 | jekyll_config['endpoint'] || DEFAULT_ENDPOINT 28 | end 29 | 30 | def gitlab_url 31 | jekyll_config['gitlab_url'] || DEFAULT_GITLAB_URL 32 | end 33 | 34 | def gitlab_repo 35 | jekyll_config['gitlab_repo'] 36 | end 37 | 38 | def base_path 39 | jekyll_config['base_path'] || '' 40 | end 41 | 42 | def pretty_url? 43 | !!jekyll_config['pretty_url'] 44 | end 45 | 46 | def append_str 47 | jekyll_config['append_str'] || '' 48 | end 49 | 50 | def layout 51 | jekyll_config['layout'] || DEFAULT_LAYOUT 52 | end 53 | 54 | def personal_access_token 55 | jekyll_config['personal_access_token'].presence || ENV['GITLAB_TOKEN'].presence 56 | end 57 | 58 | def email 59 | jekyll_config['email'] 60 | end 61 | 62 | def domain 63 | jekyll_config['domain'] 64 | end 65 | 66 | def branch 67 | jekyll_config['branch'] || DEFAULT_BRANCH 68 | end 69 | 70 | def filename 71 | jekyll_config['filename'] || DEFAULT_FILENAME 72 | end 73 | 74 | def initial_delay 75 | jekyll_config['initial_delay'] || DEFAULT_INITIAL_DELAY 76 | end 77 | 78 | def delay_time 79 | jekyll_config['delay_time'] || DEFAULT_DELAY_TIME 80 | end 81 | 82 | def scheme 83 | jekyll_config['scheme'] || DEFAULT_SCHEME 84 | end 85 | 86 | def reset! 87 | @jekyll_config = nil 88 | end 89 | 90 | def jekyll_config 91 | @jekyll_config ||= (Jekyll.configuration({})['gitlab-letsencrypt'] || {}) 92 | end 93 | 94 | def commit_message 95 | jekyll_config['commit_message'] || DEFAULT_COMMIT_MESSAGE 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt/gitlab_client.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | module Jekyll 4 | module Gitlab 5 | module Letsencrypt 6 | class GitlabClient 7 | 8 | attr_accessor :content 9 | 10 | delegate :filename, :personal_access_token, :gitlab_url, :gitlab_repo, :branch, :domain, :commit_message, to: Configuration 11 | 12 | def commit!(content) 13 | @content = content 14 | create_branch! unless branch_exists? 15 | commit_file! 16 | end 17 | 18 | def update_certificate!(certificate, key) 19 | Jekyll.logger.info "Updating domain #{domain} pages setting with new certificates.." 20 | response = connection.put do |req| 21 | req.url "projects/#{repo_id}/pages/domains/#{domain}" 22 | req.body = { 23 | certificate: certificate, 24 | key: key 25 | }.to_json 26 | end 27 | response.success? 28 | end 29 | 30 | def create_branch! 31 | Jekyll.logger.info "Creating branch #{branch}.." 32 | connection.post do |req| 33 | req.url "projects/#{repo_id}/repository/branches" 34 | req.body = { 35 | branch: branch, 36 | ref: 'master' 37 | }.to_json 38 | end 39 | end 40 | 41 | def commit_file! 42 | Jekyll.logger.info "Commiting challenge file as #{filename}" 43 | connection.run_request(request_method_for_commit, nil, nil, nil) do |req| 44 | req.url "projects/#{repo_id}/repository/files/#{enc_filename}" 45 | req.body = { 46 | commit_message: commit_message, 47 | branch: branch, 48 | content: content 49 | }.to_json 50 | end 51 | Jekyll.logger.info "Done Commiting! Check #{gitlab_url}/#{gitlab_repo}/commits/#{branch}" 52 | end 53 | 54 | private 55 | 56 | def branch_exists? 57 | response = connection.get "projects/#{repo_id}/repository/branches" 58 | JSON.parse(response.body).any? { |json| json['name'] == branch } 59 | end 60 | 61 | def request_method_for_commit 62 | response = connection.get "projects/#{repo_id}/repository/files/#{enc_filename}?ref=#{branch}" 63 | response.status == 404 ? :post : :put 64 | end 65 | 66 | def enc_filename 67 | filename.gsub "/", "%2f" 68 | end 69 | 70 | def repo_id 71 | @repo_id ||= begin 72 | repo_name = gitlab_repo.gsub "/", "%2f" 73 | response = connection.get "projects/#{repo_name}" 74 | unless response.success? 75 | fail StandardError, "Failed response for projects/#{repo_name}. Please check if personal token and repo name are correct" 76 | end 77 | JSON.parse(response.body)['id'] 78 | end 79 | end 80 | 81 | def connection 82 | @connection ||= Faraday.new(url: "#{gitlab_url}/api/v4/") do |faraday| 83 | faraday.adapter Faraday.default_adapter 84 | faraday.headers['Content-Type'] = 'application/json' 85 | faraday.headers['PRIVATE-TOKEN'] = personal_access_token 86 | end 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt/process.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | module Jekyll 4 | module Gitlab 5 | module Letsencrypt 6 | class Process 7 | 8 | attr_accessor :client 9 | 10 | def self.process! 11 | client = Acme.new.register! 12 | self.new(client).process! 13 | end 14 | 15 | delegate :base_path, :gitlab_url, :gitlab_repo, :pretty_url?, :append_str, :layout, :domain, :initial_delay, :delay_time, :scheme, to: Configuration 16 | 17 | def initialize(client) 18 | @client = client 19 | end 20 | 21 | def process! 22 | Jekyll.logger.abort_with "Client is already authorized." if client.authorized? 23 | 24 | commit_to_gitlab! 25 | wait_until_challenge_is_present 26 | request_verification! 27 | await_verification_confirmation 28 | if update_gitlab_pages 29 | Jekyll.logger.info "Success!" 30 | else 31 | Jekyll.logger.info "Updating certificate failed... manual steps:" 32 | display_certificate 33 | end 34 | 35 | Jekyll.logger.info "All finished! Don't forget to \`git pull\` in order to bring your local repo up to date with changes this plugin made." 36 | end 37 | 38 | private 39 | 40 | def commit_to_gitlab! 41 | Jekyll.logger.info "Pushing file to Gitlab" 42 | gitlab_client.commit!(challenge_content) 43 | end 44 | 45 | def wait_until_challenge_is_present 46 | Jekyll.logger.info "Going to check #{challenge_url} for the challenge to be present..." 47 | Jekyll.logger.info "Waiting #{initial_delay} seconds before we start checking for challenge.." 48 | sleep initial_delay 49 | 50 | loop do 51 | response = Faraday.get challenge_url 52 | if response.success? 53 | Jekyll.logger.info "Got response code #{response.status}, file is present!" 54 | return 55 | end 56 | Jekyll.logger.info "Got response code #{response.status}, waiting #{delay_time} seconds..." 57 | sleep delay_time 58 | end 59 | end 60 | 61 | def request_verification! 62 | Jekyll.logger.info "Requesting verification..." 63 | challenge.request_verification 64 | rescue ::Acme::Client::Error::BadNonce 65 | Jekyll.logger.info "bad nonce! trying again.." 66 | challenge.request_verification 67 | end 68 | 69 | def await_verification_confirmation 70 | tries = 0 71 | loop do 72 | tries = tries + 1 73 | if challenge.authorization.verify_status == 'valid' 74 | Jekyll.logger.info "Challenge is valid!" 75 | return 76 | end 77 | Jekyll.logger.info "Challenge status = #{challenge.authorization.verify_status}" 78 | Jekyll.logger.abort_with "Challenge failed to verify" if tries >= 3 79 | sleep delay_time 80 | end 81 | end 82 | 83 | def update_gitlab_pages 84 | gitlab_client.update_certificate! certificate.fullchain_to_pem, certificate.request.private_key.to_pem 85 | end 86 | 87 | def display_certificate 88 | Jekyll.logger.info "Certifcate retrieved!" 89 | Jekyll.logger.info "Go to #{gitlab_url}/#{gitlab_repo}/pages" 90 | Jekyll.logger.info " - If you already have an existing entry for #{domain}, remove it" 91 | Jekyll.logger.info " - Then click + New Domain and enter the following:" 92 | Jekyll.logger.info "" 93 | Jekyll.logger.info "Domain: #{domain}" 94 | Jekyll.logger.info "" 95 | Jekyll.logger.info "Certificate (PEM): " 96 | Jekyll.logger.info certificate.fullchain_to_pem 97 | Jekyll.logger.info "\n" 98 | Jekyll.logger.info "Key (PEM): " 99 | Jekyll.logger.info certificate.request.private_key.to_pem 100 | Jekyll.logger.info "" 101 | Jekyll.logger.info "" 102 | Jekyll.logger.info "... hit save, wait a bit, and your new SSL will be live!" 103 | end 104 | 105 | def challenge_content 106 | permalink = "" 107 | permalink += base_path if base_path 108 | permalink += challenge.filename 109 | permalink += "/" if pretty_url? 110 | permalink += append_str 111 | 112 | content = "---\n" 113 | content += "layout: #{layout}\n" 114 | content += "permalink: #{permalink}\n" 115 | content += "---\n" 116 | content += "\n" 117 | content += challenge.file_content 118 | content += "\n" 119 | 120 | content 121 | end 122 | 123 | def challenge_url 124 | @challenge_url ||= begin 125 | url = "#{scheme}://#{domain}/" 126 | url += challenge.filename 127 | url += "/" if pretty_url? 128 | url 129 | end 130 | end 131 | 132 | def gitlab_client 133 | @gitlab_client ||= GitlabClient.new 134 | end 135 | 136 | def challenge 137 | @challenge ||= client.challenge 138 | end 139 | 140 | def certificate 141 | @certificate ||= begin 142 | csr = ::Acme::Client::CertificateRequest.new names: Array(domain) 143 | client.client.new_certificate csr 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/jekyll/gitlab/letsencrypt/version.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Gitlab 3 | module Letsencrypt 4 | VERSION = "0.4.1" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/authorization.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: head 5 | uri: https://acme-v01.api.letsencrypt.org/acme/new-reg 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Acme::Client v0.5.0 (https://github.com/unixcharles/acme-client) 12 | response: 13 | status: 14 | code: 405 15 | message: 16 | headers: 17 | server: 18 | - nginx 19 | content-type: 20 | - application/problem+json 21 | content-length: 22 | - '91' 23 | allow: 24 | - POST 25 | boulder-request-id: 26 | - P0WGSrm70EcocVsP1j3siIu6TLpjerjua1VC8l-HXdQ 27 | replay-nonce: 28 | - DRkuNB_xplmzR9So-a9wHF4eQeyA80hnMDkVcsV-ZpI 29 | expires: 30 | - Tue, 07 Feb 2017 22:54:47 GMT 31 | cache-control: 32 | - max-age=0, no-cache, no-store 33 | pragma: 34 | - no-cache 35 | date: 36 | - Tue, 07 Feb 2017 22:54:47 GMT 37 | connection: 38 | - close 39 | body: 40 | encoding: UTF-8 41 | string: '' 42 | http_version: 43 | recorded_at: Tue, 07 Feb 2017 22:54:47 GMT 44 | - request: 45 | method: post 46 | uri: https://acme-v01.api.letsencrypt.org/acme/new-reg 47 | body: 48 | encoding: UTF-8 49 | string: '{"protected":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiI2aFJlSEpISG12TW1HdFJGMFQtUEdBSW54Yk1OVHZ2VGhPU2cwdmFSYzNEeGdrYnRPWU9fLUllQjV0UmRfSEk0eEE2NzBJVUk2VXRFaVRKZW92T3ctbWNTVlhuc1h0NzhiWXJ5dXNvVlNsY0RNVVhEdWloeDQyQ0x6TDV4VVlwS3Vaa1piUUZuaXZkeWhKZkFiTkF0SHFEZHdZZGQzMHFKMTlTTkQwekN1T1NvTWoyTzFVa2EzbmRGNmh0UDhxRFRCeDkybWkzQXhGWERBbnhWUzhYakRLTEtYVXlOdmJBY2hOZmtxNW9CTGlkYVRCX09sc3V6OWcxVDNub1dQa0JHVGlHNVh5LVlZMUNOQTFtWnFqV1BYYlh1aDczMS1oYllOT2FBVmZfeXQ5Nnk4TzZxcGJ1eWppb2tmQjF5SEg5WDlUeDBLZzZBRHFjX1BleVZHNG15a3ZUTlNXVUp2dzNGdjhPNVRLRkZfdWp0N0NoNEY1OFBReWQ4WmNvTEtQcnZtRmY5N2VFaFpWenV3VDBYN1hnVGhBa3RxM1BzWUZRa3YzdjVyNmw1UWY4b2s5eG1xaVFRYlNteGxNc0pFZFIyT3RocU5UcWpma3NWTEFmdmxlY1ZfbXAyYmlBNldUTU5abWszNnhjQnR3a0hzeEpCaWZJYkVRRzMxdzNLUnZFRjJCT21kNE55ZUJSQl9Ub3dWNEs2eExZYzh3cGVoZ2Rzb0w5T1ZQdGtxdnNyN1JMM3lpczM2TkpjQlAwWGV6aUE3SVFuSE1vZlBPWVFVWjJWalpIV2hNLWROMzRuUzNNYkdfVmdDblZlWnRXNWhxS1h5LWtFRzlMME5rYndYRUplcFBSOGxxeHowTG9uSnN2bmxkd3J1TzNOam92OU01OEFMdkJnYUNaMi1UOCJ9LCJub25jZSI6IkRSa3VOQl94cGxtelI5U28tYTl3SEY0ZVFleUE4MGhuTURrVmNzVi1acEkifQ","payload":"eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjpbIm1haWx0bzpleGFtcGxlQGV4YW1wbGUuY29tIl19","signature":"NSM6qWiO-CNOhMIJT0f2_XefEmSHK32h44Jr1Z7ubID1B_kr9sONUrL4Fd3eD1v3y2VY5eCm-nJtzlLQOro_5o930AWRSQrc9A-TM6jQHtgHu9Q8ocwoC8s1M_2GEh9dLx4WV1_VQFAzBItu1zQYpFqEf8LEfvz9exGwHj0w2Rn2SvrwBRfAiwvQGWFmE1iqNnZTTbkEUmlyLWea_Z1sOx8y4o7eDyxhlF4LomBBZiK81CHB-rIpgrMRqFUz0U5gGSCry4wLOVL3GAYW17Zxl0X8yowdCoxg_v5TgcoeagesxNbqCunm9GxD4xcbM50ZQGXhROtaNlQ7rwTJ-saGOTo790QbSxnRTIec7P8LMvesb-gXVcjYvIH7pojHgLLqLfb3jVy2OFwdynIXwTKCs93myRvggQTPlvQ-H-SGa2qRkCVH4dj8Jz8WCqqaqL0d9nMRIXrvSM7tlxz66EOxaexZ0yedJ5Vtq-6bEGcd6lS3ARqKQBfOsxKUp8xp6LbiWN0UUd72f6EIreOvQHSpcWZ7OZo7P5x0zcUw_9l8AkNmH-MqGQ43R83xPIiFtrRL8q0nASqotGSeiAIvD2sZkrULIllB4FShhCxwIivc4G74iz0WxicBPTdr3BoFzLpunwNhYZpfZLW0nvwshRstRGp9JNMG_DVmQYr1o3695xs"}' 50 | headers: 51 | User-Agent: 52 | - Acme::Client v0.5.0 (https://github.com/unixcharles/acme-client) 53 | response: 54 | status: 55 | code: 201 56 | message: 57 | headers: 58 | server: 59 | - nginx 60 | content-type: 61 | - application/json 62 | content-length: 63 | - '919' 64 | boulder-request-id: 65 | - WvlyL2rrgNQuTnKy2uhgJgtgO9yoCNcvat7oar0UKJ0 66 | boulder-requester: 67 | - '9310231' 68 | link: 69 | - ;rel="next", ;rel="terms-of-service" 70 | location: 71 | - https://acme-v01.api.letsencrypt.org/acme/reg/9310231 72 | replay-nonce: 73 | - 3_eWVs3wGeRlP9tUy69zBNCYqA_m-NbIOda64LxQlUI 74 | x-frame-options: 75 | - DENY 76 | strict-transport-security: 77 | - max-age=604800 78 | expires: 79 | - Tue, 07 Feb 2017 22:54:48 GMT 80 | cache-control: 81 | - max-age=0, no-cache, no-store 82 | pragma: 83 | - no-cache 84 | date: 85 | - Tue, 07 Feb 2017 22:54:48 GMT 86 | connection: 87 | - close 88 | body: 89 | encoding: UTF-8 90 | string: |- 91 | { 92 | "id": 9310231, 93 | "key": { 94 | "kty": "RSA", 95 | "n": "6hReHJHHmvMmGtRF0T-PGAInxbMNTvvThOSg0vaRc3DxgkbtOYO_-IeB5tRd_HI4xA670IUI6UtEiTJeovOw-mcSVXnsXt78bYryusoVSlcDMUXDuihx42CLzL5xUYpKuZkZbQFnivdyhJfAbNAtHqDdwYdd30qJ19SND0zCuOSoMj2O1Uka3ndF6htP8qDTBx92mi3AxFXDAnxVS8XjDKLKXUyNvbAchNfkq5oBLidaTB_Olsuz9g1T3noWPkBGTiG5Xy-YY1CNA1mZqjWPXbXuh731-hbYNOaAVf_yt96y8O6qpbuyjiokfB1yHH9X9Tx0Kg6ADqc_PeyVG4mykvTNSWUJvw3Fv8O5TKFF_ujt7Ch4F58PQyd8ZcoLKPrvmFf97eEhZVzuwT0X7XgThAktq3PsYFQkv3v5r6l5Qf8ok9xmqiQQbSmxlMsJEdR2OthqNTqjfksVLAfvlecV_mp2biA6WTMNZmk36xcBtwkHsxJBifIbEQG31w3KRvEF2BOmd4NyeBRB_TowV4K6xLYc8wpehgdsoL9OVPtkqvsr7RL3yis36NJcBP0XeziA7IQnHMofPOYQUZ2VjZHWhM-dN34nS3MbG_VgCnVeZtW5hqKXy-kEG9L0NkbwXEJepPR8lqxz0LonJsvnldwruO3Njov9M58ALvBgaCZ2-T8", 96 | "e": "AQAB" 97 | }, 98 | "contact": [ 99 | "mailto:example@example.com" 100 | ], 101 | "initialIp": "192.64.23.87", 102 | "createdAt": "2017-02-07T22:54:48.423630333Z", 103 | "Status": "valid" 104 | } 105 | http_version: 106 | recorded_at: Tue, 07 Feb 2017 22:54:48 GMT 107 | - request: 108 | method: post 109 | uri: https://acme-v01.api.letsencrypt.org/acme/reg/9310231 110 | body: 111 | encoding: UTF-8 112 | string: '{"protected":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiI2aFJlSEpISG12TW1HdFJGMFQtUEdBSW54Yk1OVHZ2VGhPU2cwdmFSYzNEeGdrYnRPWU9fLUllQjV0UmRfSEk0eEE2NzBJVUk2VXRFaVRKZW92T3ctbWNTVlhuc1h0NzhiWXJ5dXNvVlNsY0RNVVhEdWloeDQyQ0x6TDV4VVlwS3Vaa1piUUZuaXZkeWhKZkFiTkF0SHFEZHdZZGQzMHFKMTlTTkQwekN1T1NvTWoyTzFVa2EzbmRGNmh0UDhxRFRCeDkybWkzQXhGWERBbnhWUzhYakRLTEtYVXlOdmJBY2hOZmtxNW9CTGlkYVRCX09sc3V6OWcxVDNub1dQa0JHVGlHNVh5LVlZMUNOQTFtWnFqV1BYYlh1aDczMS1oYllOT2FBVmZfeXQ5Nnk4TzZxcGJ1eWppb2tmQjF5SEg5WDlUeDBLZzZBRHFjX1BleVZHNG15a3ZUTlNXVUp2dzNGdjhPNVRLRkZfdWp0N0NoNEY1OFBReWQ4WmNvTEtQcnZtRmY5N2VFaFpWenV3VDBYN1hnVGhBa3RxM1BzWUZRa3YzdjVyNmw1UWY4b2s5eG1xaVFRYlNteGxNc0pFZFIyT3RocU5UcWpma3NWTEFmdmxlY1ZfbXAyYmlBNldUTU5abWszNnhjQnR3a0hzeEpCaWZJYkVRRzMxdzNLUnZFRjJCT21kNE55ZUJSQl9Ub3dWNEs2eExZYzh3cGVoZ2Rzb0w5T1ZQdGtxdnNyN1JMM3lpczM2TkpjQlAwWGV6aUE3SVFuSE1vZlBPWVFVWjJWalpIV2hNLWROMzRuUzNNYkdfVmdDblZlWnRXNWhxS1h5LWtFRzlMME5rYndYRUplcFBSOGxxeHowTG9uSnN2bmxkd3J1TzNOam92OU01OEFMdkJnYUNaMi1UOCJ9LCJub25jZSI6IjNfZVdWczN3R2VSbFA5dFV5Njl6Qk5DWXFBX20tTmJJT2RhNjRMeFFsVUkifQ","payload":"eyJyZXNvdXJjZSI6InJlZyIsImFncmVlbWVudCI6Imh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL2RvY3VtZW50cy9MRS1TQS12MS4xLjEtQXVndXN0LTEtMjAxNi5wZGYifQ","signature":"5egGd0zvyqgLKd_R5bKeCTybJd3GTP3YTypdfW0OVCPMyfF3ZxGEsADD_dfKD76cA9uUvyEM5RHbIa-8OQk05MjpitNDEH0_byDupaRSq3durOx6fKa5yYIYXgu4YWAwe6nfS6Pyt6ILWkULI3S5m1kG6ct_NLdPl3ekBghVpYOGe51mpgFuhszvtweNXQiskUqLNajt3eS3OxG0Q36D-hwdWzeo4wzxB9xS9tnx5H9A-y_d6EJTqApbGhTqW6EI63t4v54i_CZAXICNBKjfDCsk6L2ife9CW1QQl5PPG3niAdhvcPPajemQY8_DEkv70h9B-rJDceWMnnxp4rwQ-wpunWC8A3YJ7_wxI0QuhGtsmj9L_DSmUmpl9AA5-iW1ECR7t8TPssYiAOgqtAVMn6u_MAjiAAgCrJQcCqvDJTAt0x5KiAKS0qfwqC7lbseXYAPdx-zKnD7MCHbjYi4-_C668DYpGBnJZB8tV-sdQ62_eDM1Og78PXKZTBj_3m179XrrKnjIEndbsSrA3BLkahjDPz3yUYWlpd8-H5Umz32K1cX3KGzw7byAwkOvbYU3hLRn2H4TwOeoEVTnohey6O71fEvQEZ6WzNNp2Z1BtIQK1xmdlpRX3FIzPpdUeG97LBim3_9_ir7n6a_O-ovGeMPlfv9Zxub8d4JHTPgfXM8"}' 113 | headers: 114 | User-Agent: 115 | - Acme::Client v0.5.0 (https://github.com/unixcharles/acme-client) 116 | response: 117 | status: 118 | code: 202 119 | message: 120 | headers: 121 | server: 122 | - nginx 123 | content-type: 124 | - application/json 125 | content-length: 126 | - '992' 127 | boulder-request-id: 128 | - 5RoAXBjUweXg3H9mRhCMEjg5_njPgd1x_uHKeYHT0jM 129 | boulder-requester: 130 | - '9310231' 131 | link: 132 | - ;rel="next", ;rel="terms-of-service" 133 | replay-nonce: 134 | - Ab_rP6-D9CkxMG2AoH5jx1xPxqf13dBT4-b3MBqhEM8 135 | expires: 136 | - Tue, 07 Feb 2017 22:54:48 GMT 137 | cache-control: 138 | - max-age=0, no-cache, no-store 139 | pragma: 140 | - no-cache 141 | date: 142 | - Tue, 07 Feb 2017 22:54:48 GMT 143 | connection: 144 | - close 145 | body: 146 | encoding: UTF-8 147 | string: |- 148 | { 149 | "id": 9310231, 150 | "key": { 151 | "kty": "RSA", 152 | "n": "6hReHJHHmvMmGtRF0T-PGAInxbMNTvvThOSg0vaRc3DxgkbtOYO_-IeB5tRd_HI4xA670IUI6UtEiTJeovOw-mcSVXnsXt78bYryusoVSlcDMUXDuihx42CLzL5xUYpKuZkZbQFnivdyhJfAbNAtHqDdwYdd30qJ19SND0zCuOSoMj2O1Uka3ndF6htP8qDTBx92mi3AxFXDAnxVS8XjDKLKXUyNvbAchNfkq5oBLidaTB_Olsuz9g1T3noWPkBGTiG5Xy-YY1CNA1mZqjWPXbXuh731-hbYNOaAVf_yt96y8O6qpbuyjiokfB1yHH9X9Tx0Kg6ADqc_PeyVG4mykvTNSWUJvw3Fv8O5TKFF_ujt7Ch4F58PQyd8ZcoLKPrvmFf97eEhZVzuwT0X7XgThAktq3PsYFQkv3v5r6l5Qf8ok9xmqiQQbSmxlMsJEdR2OthqNTqjfksVLAfvlecV_mp2biA6WTMNZmk36xcBtwkHsxJBifIbEQG31w3KRvEF2BOmd4NyeBRB_TowV4K6xLYc8wpehgdsoL9OVPtkqvsr7RL3yis36NJcBP0XeziA7IQnHMofPOYQUZ2VjZHWhM-dN34nS3MbG_VgCnVeZtW5hqKXy-kEG9L0NkbwXEJepPR8lqxz0LonJsvnldwruO3Njov9M58ALvBgaCZ2-T8", 153 | "e": "AQAB" 154 | }, 155 | "contact": [ 156 | "mailto:example@example.com" 157 | ], 158 | "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", 159 | "initialIp": "192.64.23.87", 160 | "createdAt": "2017-02-07T22:54:48Z", 161 | "Status": "valid" 162 | } 163 | http_version: 164 | recorded_at: Tue, 07 Feb 2017 22:54:48 GMT 165 | recorded_with: VCR 3.0.3 166 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/gitlab_commit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://gitlab.com/api/v4/projects/gitlab_user%2fgitlab_repo 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | PRIVATE-TOKEN: 13 | - SECRET_TOKEN 14 | User-Agent: 15 | - Faraday v0.11.0SE 16 | response: 17 | status: 18 | code: 200 19 | message: 20 | headers: 21 | server: 22 | - nginx 23 | date: 24 | - Tue, 07 Feb 2017 23:31:52 GMT 25 | content-type: 26 | - application/json 27 | content-length: 28 | - '1529' 29 | connection: 30 | - close 31 | cache-control: 32 | - max-age=0, private, must-revalidate 33 | etag: 34 | - W/"0e670d91c729a963bfeb10c0f98c3cfe" 35 | vary: 36 | - Origin 37 | x-request-id: 38 | - a61b7806-5d35-4eee-bb5e-e726fcd2fb81 39 | x-runtime: 40 | - '0.303968' 41 | content-security-policy-report-only: 42 | - object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' piwik.gitlab.com 43 | https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 44 | 'self' 'unsafe-inline'; img-src * data:; frame-src 'self' https://www.google.com/recaptcha/; 45 | frame-ancestors 'none'; connect-src 'self'; report-uri https://sentry-infra.gitlap.com/api/3/csp-report/?sentry_key=a664fdde83424b43a991f25fa7c78987 46 | body: 47 | encoding: UTF-8 48 | string: '{"id":1234}' 49 | http_version: 50 | recorded_at: Tue, 07 Feb 2017 23:31:52 GMT 51 | - request: 52 | method: get 53 | uri: https://gitlab.com/api/v4/projects/1234/repository/branches 54 | body: 55 | encoding: US-ASCII 56 | string: '' 57 | headers: 58 | Content-Type: 59 | - application/json 60 | PRIVATE-TOKEN: 61 | - SECRET_TOKEN 62 | User-Agent: 63 | - Faraday v0.11.0 64 | response: 65 | status: 66 | code: 200 67 | message: 68 | headers: 69 | server: 70 | - nginx 71 | date: 72 | - Tue, 07 Feb 2017 23:31:52 GMT 73 | content-type: 74 | - application/json 75 | content-length: 76 | - '1542' 77 | connection: 78 | - close 79 | cache-control: 80 | - max-age=0, private, must-revalidate 81 | etag: 82 | - W/"b5db4742e03745caf391836215995f14" 83 | vary: 84 | - Origin 85 | x-request-id: 86 | - c003a026-a337-4f4b-b43f-d9851d011c5e 87 | x-runtime: 88 | - '0.266958' 89 | content-security-policy-report-only: 90 | - object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' piwik.gitlab.com 91 | https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 92 | 'self' 'unsafe-inline'; img-src * data:; frame-src 'self' https://www.google.com/recaptcha/; 93 | frame-ancestors 'none'; connect-src 'self'; report-uri https://sentry-infra.gitlap.com/api/3/csp-report/?sentry_key=a664fdde83424b43a991f25fa7c78987 94 | body: 95 | encoding: UTF-8 96 | string: '[{"name":"master"}]' 97 | http_version: 98 | recorded_at: Tue, 07 Feb 2017 23:31:52 GMT 99 | - request: 100 | method: post 101 | uri: https://gitlab.com/api/v4/projects/1234/repository/branches 102 | body: 103 | encoding: UTF-8 104 | string: '{"branch":"test_branch","ref":"master"}' 105 | headers: 106 | Content-Type: 107 | - application/json 108 | PRIVATE-TOKEN: 109 | - SECRET_TOKEN 110 | User-Agent: 111 | - Faraday v0.11.0 112 | response: 113 | status: 114 | code: 201 115 | message: 116 | headers: 117 | server: 118 | - nginx 119 | date: 120 | - Tue, 07 Feb 2017 23:31:54 GMT 121 | content-type: 122 | - application/json 123 | content-length: 124 | - '510' 125 | connection: 126 | - close 127 | cache-control: 128 | - max-age=0, private, must-revalidate 129 | etag: 130 | - W/"774a285a1f374376b278a61d962fc424" 131 | vary: 132 | - Origin 133 | x-request-id: 134 | - 8b6bad5f-a6ef-4ceb-9c6f-605c9715f994 135 | x-runtime: 136 | - '0.868943' 137 | content-security-policy-report-only: 138 | - object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' piwik.gitlab.com 139 | https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 140 | 'self' 'unsafe-inline'; img-src * data:; frame-src 'self' https://www.google.com/recaptcha/; 141 | frame-ancestors 'none'; connect-src 'self'; report-uri https://sentry-infra.gitlap.com/api/3/csp-report/?sentry_key=a664fdde83424b43a991f25fa7c78987 142 | body: 143 | encoding: UTF-8 144 | string: '{"name":"test_branch"}' 145 | http_version: 146 | recorded_at: Tue, 07 Feb 2017 23:31:54 GMT 147 | - request: 148 | method: get 149 | uri: https://gitlab.com/api/v4/projects/1234/repository/files/test_file.html?ref=test_branch 150 | body: 151 | encoding: US-ASCII 152 | string: '' 153 | headers: 154 | Content-Type: 155 | - application/json 156 | PRIVATE-TOKEN: 157 | - SECRET_TOKEN 158 | User-Agent: 159 | - Faraday v0.11.0 160 | response: 161 | status: 162 | code: 404 163 | message: 164 | headers: 165 | server: 166 | - nginx 167 | date: 168 | - Tue, 07 Feb 2017 23:31:55 GMT 169 | content-type: 170 | - application/json 171 | content-length: 172 | - '32' 173 | connection: 174 | - close 175 | cache-control: 176 | - no-cache 177 | vary: 178 | - Origin 179 | x-request-id: 180 | - 880c6ec2-7085-4ca4-978d-da8c80318623 181 | x-runtime: 182 | - '0.070913' 183 | content-security-policy-report-only: 184 | - object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' piwik.gitlab.com 185 | https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 186 | 'self' 'unsafe-inline'; img-src * data:; frame-src 'self' https://www.google.com/recaptcha/; 187 | frame-ancestors 'none'; connect-src 'self'; report-uri https://sentry-infra.gitlap.com/api/3/csp-report/?sentry_key=a664fdde83424b43a991f25fa7c78987 188 | body: 189 | encoding: UTF-8 190 | string: '{"message":"404 File Not Found"}' 191 | http_version: 192 | recorded_at: Tue, 07 Feb 2017 23:31:55 GMT 193 | - request: 194 | method: post 195 | uri: https://gitlab.com/api/v4/projects/1234/repository/files/test_file.html 196 | body: 197 | encoding: UTF-8 198 | string: '{"commit_message":"Automated Let''s Encrypt 199 | renewal","branch":"test_branch","content":""}' 200 | headers: 201 | Content-Type: 202 | - application/json 203 | PRIVATE-TOKEN: 204 | - SECRET_TOKEN 205 | User-Agent: 206 | - Faraday v0.11.0 207 | response: 208 | status: 209 | code: 201 210 | message: 211 | headers: 212 | server: 213 | - nginx 214 | date: 215 | - Tue, 07 Feb 2017 23:31:57 GMT 216 | content-type: 217 | - application/json 218 | content-length: 219 | - '58' 220 | connection: 221 | - close 222 | cache-control: 223 | - max-age=0, private, must-revalidate 224 | etag: 225 | - W/"03582b5874b2395818c11423a79d16ca" 226 | vary: 227 | - Origin 228 | x-request-id: 229 | - ecddb838-72e4-453b-a786-75efcd641955 230 | x-runtime: 231 | - '1.434225' 232 | content-security-policy-report-only: 233 | - object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' piwik.gitlab.com 234 | https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 235 | 'self' 'unsafe-inline'; img-src * data:; frame-src 'self' https://www.google.com/recaptcha/; 236 | frame-ancestors 'none'; connect-src 'self'; report-uri https://sentry-infra.gitlap.com/api/3/csp-report/?sentry_key=a664fdde83424b43a991f25fa7c78987 237 | body: 238 | encoding: UTF-8 239 | string: '{"file_path":"test_file.html","branch":"test_branch"}' 240 | http_version: 241 | recorded_at: Tue, 07 Feb 2017 23:31:57 GMT 242 | recorded_with: VCR 3.0.3 243 | -------------------------------------------------------------------------------- /spec/lib/jekyll/commands/gitlab/letsencrypt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Commands::Gitlab::Letsencrypt do 4 | describe '#init_with_program' do 5 | let(:prog) { double 'prog' } 6 | let(:command) { double Jekyll::Command } 7 | 8 | it 'calls out to the class' do 9 | expect(prog).to receive(:command).with(:letsencrypt).and_yield command 10 | expect(command).to receive(:description) 11 | expect(command).to receive(:action).and_yield nil, nil 12 | expect(Jekyll::Gitlab::Letsencrypt::Process).to receive :process! 13 | 14 | described_class.init_with_program prog 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/jekyll/gitlab/letsencrypt/acme_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Gitlab::Letsencrypt::Acme do 4 | let(:acme) { described_class.new } 5 | 6 | let(:email) { 'example@example.com' } 7 | let(:domain) { 'example.com' } 8 | let(:endpoint) { 'https://acme-v01.api.letsencrypt.org/' } 9 | 10 | before do 11 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:email).and_return email 12 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:domain).and_return domain 13 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:endpoint).and_return endpoint 14 | end 15 | 16 | describe '#register!' do 17 | it 'logs, registers, and returns itself' do 18 | VCR.use_cassette 'authorization' do 19 | expect(Jekyll.logger).to receive(:info).with 'Registering example@example.com to https://acme-v01.api.letsencrypt.org/...' 20 | expect(acme.register!).to eq acme 21 | end 22 | end 23 | end 24 | 25 | describe '#authorized?' do 26 | pending 27 | end 28 | 29 | describe '#challenge' do 30 | pending 31 | end 32 | 33 | describe '#client' do 34 | it 'returns a new ACME client' do 35 | expect(acme.client.endpoint).to eq endpoint 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/jekyll/gitlab/letsencrypt/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Gitlab::Letsencrypt::Configuration do 4 | 5 | before { allow(Jekyll).to receive(:configuration).with({}).and_return config } 6 | 7 | let(:config) { {'gitlab-letsencrypt' => plugin_config} } 8 | let(:plugin_config) { {} } 9 | 10 | describe '#valid?' do 11 | subject { described_class } 12 | 13 | context "when missing the config section" do 14 | let(:plugin_config) { nil } 15 | it { should_not be_valid } 16 | end 17 | 18 | context "when missing required keys" do 19 | let(:plugin_config) { {'gitlab_repo' => "yay"} } 20 | it { should_not be_valid } 21 | end 22 | 23 | context "when all required keys are present" do 24 | let(:plugin_config) do 25 | { 26 | 'personal_access_token' => "secret", 27 | 'gitlab_repo' => 'yay', 28 | 'email' => 'foo@foo.com', 29 | 'domain' => 'foo.com' 30 | } 31 | end 32 | 33 | it { should be_valid } 34 | end 35 | end 36 | 37 | describe '#personal_access_token' do 38 | subject { described_class.personal_access_token } 39 | 40 | context 'in the env var' do 41 | before { stub_const 'ENV', {'GITLAB_TOKEN' => 'from_the_env'} } 42 | it { should eq 'from_the_env'} 43 | end 44 | 45 | context "from the jeykll config" do 46 | let(:plugin_config) { {'personal_access_token' => 'from_config'} } 47 | it { should eq 'from_config'} 48 | end 49 | end 50 | 51 | describe '#endpoint' do 52 | subject { described_class.endpoint } 53 | context "with no endpoint" do 54 | it { should eq 'https://acme-v01.api.letsencrypt.org/'} 55 | end 56 | 57 | context "with an endpoint" do 58 | let(:plugin_config) { {'endpoint' => "https://foo/"} } 59 | it { should eq 'https://foo/'} 60 | end 61 | end 62 | 63 | describe '#gitlab_url' do 64 | subject { described_class.gitlab_url } 65 | context "with no gitlab_url" do 66 | it { should eq 'https://gitlab.com'} 67 | end 68 | 69 | context "with an gitlab_url" do 70 | let(:plugin_config) { {'gitlab_url' => "https://gitlab"} } 71 | it { should eq 'https://gitlab'} 72 | end 73 | end 74 | 75 | describe '#commit_message' do 76 | subject { described_class.commit_message } 77 | context "with no commit_message" do 78 | it { should eq 'Automated Let\'s Encrypt renewal' } 79 | end 80 | 81 | context "with a commit_message" do 82 | let(:plugin_config) { {'commit_message' => "Renew Let's Encrypt Certificate" } } 83 | it { should eq "Renew Let's Encrypt Certificate" } 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/lib/jekyll/gitlab/letsencrypt/gitlab_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Gitlab::Letsencrypt::GitlabClient do 4 | let(:gitlab_client) { described_class.new } 5 | let(:content) { '' } 6 | 7 | let(:filename) { 'test_file.html' } 8 | let(:personal_access_token) { 'SECRET_TOKEN' } 9 | let(:gitlab_url) { 'https://gitlab.com'} 10 | let(:gitlab_repo) { 'gitlab_user/gitlab_repo' } 11 | let(:branch) { 'test_branch' } 12 | let(:domain) { 'example.com' } 13 | 14 | before do 15 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:filename).and_return filename 16 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:personal_access_token).and_return personal_access_token 17 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:gitlab_repo).and_return gitlab_repo 18 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:branch).and_return branch 19 | allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:domain).and_return domain 20 | end 21 | 22 | describe "#commit!" do 23 | it 'creates the branch and shit' do 24 | VCR.use_cassette 'gitlab_commit' do 25 | expect(Jekyll.logger).to receive(:info).with "Creating branch test_branch.." 26 | expect(Jekyll.logger).to receive(:info).with "Commiting challenge file as test_file.html" 27 | expect(Jekyll.logger).to receive(:info).with "Done Commiting! Check https://gitlab.com/gitlab_user/gitlab_repo/commits/test_branch" 28 | gitlab_client.commit! content 29 | end 30 | end 31 | end 32 | 33 | describe "#update_certificate!" do 34 | let(:mock_connection) { double Faraday::Connection, put: response } 35 | 36 | before do 37 | expect(Jekyll.logger).to receive(:info).with "Updating domain example.com pages setting with new certificates.." 38 | allow(gitlab_client).to receive(:connection).and_return mock_connection 39 | end 40 | 41 | context "successful" do 42 | let(:response) { double Faraday::Response, success?: true } 43 | 44 | it "returns true" do 45 | expect(gitlab_client.update_certificate! :foo, :bar).to eq true 46 | end 47 | end 48 | 49 | context "unsuccessful" do 50 | let(:response) { double Faraday::Response, success?: false } 51 | 52 | it "returns false" do 53 | expect(gitlab_client.update_certificate! :foo, :bar).to eq false 54 | end 55 | end 56 | end 57 | 58 | describe "#repo_id" do 59 | let(:mock_connection) { double Faraday::Connection, get: response} 60 | 61 | before do 62 | allow(gitlab_client).to receive(:connection).and_return mock_connection 63 | end 64 | 65 | context "unsuccessful" do 66 | let(:response) { double Faraday::Response, success?: false } 67 | 68 | it "raises" do 69 | expect { gitlab_client.send(:repo_id) } 70 | .to raise_error(StandardError, /Failed response for projects.*/) 71 | end 72 | end 73 | 74 | context 'successful' do 75 | let(:response) { double Faraday::Response, success?: true, body: '{ "id": 123 }' } 76 | 77 | it "returns repo_id" do 78 | expect(gitlab_client.send(:repo_id)).to eq 123 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/lib/jekyll/gitlab/letsencrypt/process_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Gitlab::Letsencrypt::Process do 4 | let(:process) { described_class.new client } 5 | let(:client) { Jekyll::Gitlab::Letsencrypt::Acme.new } 6 | 7 | describe '#process!' do 8 | before do 9 | allow(process).to receive :commit_to_gitlab! 10 | allow(process).to receive :wait_until_challenge_is_present 11 | allow(process).to receive :request_verification! 12 | allow(process).to receive :await_verification_confirmation 13 | allow(process).to receive :update_gitlab_pages 14 | allow(process).to receive :display_certificate 15 | end 16 | 17 | after { process.process! } 18 | 19 | context 'when already authorized' do 20 | before { allow(client).to receive(:authorized?).and_return true } 21 | 22 | it 'bails early' do 23 | expect(Jekyll.logger).to receive(:abort_with).with("Client is already authorized.") 24 | end 25 | end 26 | 27 | context "when not authorized" do 28 | before { expect(client).to receive(:authorized?).and_return false } 29 | 30 | pending 31 | end 32 | end 33 | 34 | describe '#challenge_url' do 35 | subject { process.send :challenge_url } 36 | let(:challenge) { double 'challenge', filename: 'foo' } 37 | before { allow(client).to receive(:challenge).and_return challenge } 38 | 39 | context "by default" do 40 | it { should match /http\:/ } 41 | end 42 | 43 | context 'if scheme is overriden' do 44 | before { allow(Jekyll::Gitlab::Letsencrypt::Configuration).to receive(:scheme).and_return 'https' } 45 | it { should match /https\:/ } 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/jekyll/gitlab/letsencrypt/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jekyll::Gitlab::Letsencrypt do 4 | it 'has a version number' do 5 | expect(Jekyll::Gitlab::Letsencrypt::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll' 2 | require 'vcr' 3 | 4 | unless ENV["NO_COVERALLS"] 5 | require 'coveralls' 6 | Coveralls.wear! 7 | end 8 | 9 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 10 | require "jekyll/gitlab/letsencrypt" 11 | 12 | VCR.configure do |config| 13 | config.cassette_library_dir = "spec/fixtures/vcr_cassettes" 14 | config.hook_into :faraday 15 | config.allow_http_connections_when_no_cassette = true 16 | end 17 | 18 | RSpec.configure do |config| 19 | config.filter_run :focus 20 | config.run_all_when_everything_filtered = true 21 | 22 | config.before { Jekyll::Gitlab::Letsencrypt::Configuration.reset! } 23 | end 24 | --------------------------------------------------------------------------------