mirror of
https://github.com/neocities/neocities.git
synced 2025-04-24 17:22:35 +02:00
allow adding of ssl certs for domain
This commit is contained in:
parent
92fcee0aea
commit
af605f352e
7 changed files with 307 additions and 14 deletions
54
app.rb
54
app.rb
|
@ -486,6 +486,60 @@ post '/settings/profile' do
|
||||||
redirect '/settings'
|
redirect '/settings'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post '/settings/ssl' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
unless params[:key] && params[:cert] && params[:cert_intermediate]
|
||||||
|
flash[:error] = 'SSL key, certificate, and intermediate certificate are required to continue.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
key = OpenSSL::PKey::RSA.new params[:key][:tempfile].read, ''
|
||||||
|
rescue => e
|
||||||
|
flash[:error] = 'Could not process SSL key, file may be incorrect, damaged, or passworded (you need to remove the password).'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !key.private?
|
||||||
|
flash[:error] = 'SSL Key file does not have private key data.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
cert = OpenSSL::X509::Certificate.new params[:cert][:tempfile].read
|
||||||
|
rescue => e
|
||||||
|
flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
if cert.not_after < Time.now
|
||||||
|
flash[:error] = 'SSL Certificate is expired, please create a new one.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1]
|
||||||
|
if !cert_cn.match(current_site.domain)
|
||||||
|
flash[:error] = "The certificate CN (common name) #{cert_cn} does not match your domain: #{current_site.domain}"
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
cert_intermediate = OpenSSL::X509::Certificate.new params[:cert_intermediate][:tempfile].read
|
||||||
|
rescue => e
|
||||||
|
flash[:error] = 'Could not process intermediate SSL certificate, file may be incorrect or damaged.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
current_site.ssl_key = key.to_pem
|
||||||
|
current_site.ssl_cert = cert.to_pem
|
||||||
|
current_site.ssl_cert_intermediate = cert_intermediate.to_pem
|
||||||
|
current_site.save
|
||||||
|
|
||||||
|
flash[:success] = 'Updated SSL key/certificate.'
|
||||||
|
redirect '/custom_domain'
|
||||||
|
end
|
||||||
|
|
||||||
post '/signin' do
|
post '/signin' do
|
||||||
dashboard_if_signed_in
|
dashboard_if_signed_in
|
||||||
|
|
||||||
|
|
12
migrations/040_add_ssl.rb
Normal file
12
migrations/040_add_ssl.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :ssl_key, :text
|
||||||
|
DB.add_column :sites, :ssl_cert, :text
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :ssl_key
|
||||||
|
DB.drop_column :sites, :ssl_cert
|
||||||
|
}
|
||||||
|
end
|
9
migrations/041_add_intermediate_ssl.rb
Normal file
9
migrations/041_add_intermediate_ssl.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :ssl_cert_intermediate, :text
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :ssl_cert_intermediate
|
||||||
|
}
|
||||||
|
end
|
|
@ -798,6 +798,10 @@ class Site < Sequel::Model
|
||||||
"#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}"
|
"#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ssl_installed?
|
||||||
|
ssl_key && ssl_cert && ssl_cert_intermediate
|
||||||
|
end
|
||||||
|
|
||||||
def to_rss
|
def to_rss
|
||||||
RSS::Maker.make("atom") do |maker|
|
RSS::Maker.make("atom") do |maker|
|
||||||
maker.channel.title = title
|
maker.channel.title = title
|
||||||
|
@ -808,8 +812,8 @@ class Site < Sequel::Model
|
||||||
latest_events.each do |event|
|
latest_events.each do |event|
|
||||||
if event.site_change_id
|
if event.site_change_id
|
||||||
maker.items.new_item do |item|
|
maker.items.new_item do |item|
|
||||||
item.link = "http://#{username}.neocities.org"
|
item.link = "https://#{host}"
|
||||||
item.title = "#{username}.neocities.org has been updated"
|
item.title = "#{title} has been updated"
|
||||||
item.updated = event.site_change.created_at
|
item.updated = event.site_change.created_at
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,160 @@
|
||||||
require_relative './environment.rb'
|
require_relative './environment.rb'
|
||||||
|
|
||||||
describe 'site/settings' do
|
describe 'site/settings' do
|
||||||
|
describe 'ssl' do
|
||||||
|
include Capybara::DSL
|
||||||
|
|
||||||
|
before do
|
||||||
|
# https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb
|
||||||
|
# TODO make ca generation run only once
|
||||||
|
ca_keypair = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
ca_cert = OpenSSL::X509::Certificate.new
|
||||||
|
ca_cert.not_before = Time.now
|
||||||
|
ca_cert.subject = OpenSSL::X509::Name.new([
|
||||||
|
["C", "US"],
|
||||||
|
["ST", "Oregon"],
|
||||||
|
["L", "Portland"],
|
||||||
|
["CN", "Neocities CA"]
|
||||||
|
])
|
||||||
|
ca_cert.issuer = ca_cert.subject
|
||||||
|
# All issued certs will be unusuable after this time.
|
||||||
|
ca_cert.not_after = Time.now + 1000000000 # 40 or so years
|
||||||
|
ca_cert.serial = 1
|
||||||
|
ca_cert.public_key = ca_keypair.public_key
|
||||||
|
ef = OpenSSL::X509::ExtensionFactory.new
|
||||||
|
ef.subject_certificate = ca_cert
|
||||||
|
ef.issuer_certificate = ca_cert
|
||||||
|
# Read more about the various extensions here: http://www.openssl.org/docs/apps/x509v3_config.html
|
||||||
|
ca_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
|
||||||
|
ca_cert.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true))
|
||||||
|
ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
|
||||||
|
ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
|
||||||
|
ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new)
|
||||||
|
@ca_cert = ca_cert
|
||||||
|
@ca_keypair = ca_keypair
|
||||||
|
|
||||||
|
@domain = SecureRandom.uuid.gsub('-', '')+'.com'
|
||||||
|
@site = Fabricate :site, domain: @domain
|
||||||
|
page.set_rack_session id: @site.id
|
||||||
|
|
||||||
|
ca_cert = OpenSSL::X509::Certificate.new(@ca_cert.to_pem)
|
||||||
|
our_cert_keypair = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
our_cert_req = OpenSSL::X509::Request.new
|
||||||
|
our_cert_req.subject = OpenSSL::X509::Name.new([
|
||||||
|
["C", "US"],
|
||||||
|
["ST", "Oregon"],
|
||||||
|
["L", "Portland"],
|
||||||
|
["O", "Neocities User"],
|
||||||
|
["CN", "*.#{@domain}"]
|
||||||
|
])
|
||||||
|
our_cert_req.public_key = our_cert_keypair.public_key
|
||||||
|
our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new
|
||||||
|
our_cert = OpenSSL::X509::Certificate.new
|
||||||
|
our_cert.subject = our_cert_req.subject
|
||||||
|
our_cert.issuer = ca_cert.subject
|
||||||
|
our_cert.not_before = Time.now
|
||||||
|
our_cert.not_after = Time.now + 100000000 # 3 or so years.
|
||||||
|
our_cert.serial = 123 # Should be an unique number, the CA probably has a database.
|
||||||
|
our_cert.public_key = our_cert_req.public_key
|
||||||
|
# To make the certificate valid for both wildcard and top level domain name, we need an extension.
|
||||||
|
ef = OpenSSL::X509::ExtensionFactory.new
|
||||||
|
ef.subject_certificate = our_cert
|
||||||
|
ef.issuer_certificate = ca_cert
|
||||||
|
our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false))
|
||||||
|
our_cert.sign @ca_keypair, OpenSSL::Digest::SHA1.new
|
||||||
|
|
||||||
|
our_cert_tmpfile = Tempfile.new 'our_cert'
|
||||||
|
our_cert_tmpfile.write our_cert.to_pem
|
||||||
|
our_cert_tmpfile.close
|
||||||
|
@cert_path = our_cert_tmpfile.path
|
||||||
|
|
||||||
|
our_cert_keypair_tmpfile = Tempfile.new 'our_cert_keypair'
|
||||||
|
our_cert_keypair_tmpfile.write our_cert_keypair.to_pem
|
||||||
|
our_cert_keypair_tmpfile.close
|
||||||
|
@key_path = our_cert_keypair_tmpfile.path
|
||||||
|
|
||||||
|
ca_cert_tmpfile = Tempfile.new 'ca_cert'
|
||||||
|
ca_cert_tmpfile.write @ca_cert.to_pem
|
||||||
|
ca_cert_tmpfile.close
|
||||||
|
@cert_intermediate_path = ca_cert_tmpfile.path
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails without domain set' do
|
||||||
|
@site = Fabricate :site
|
||||||
|
page.set_rack_session id: @site.id
|
||||||
|
visit '/custom_domain'
|
||||||
|
page.must_have_content /Cannot upload SSL certificate until domain is added/i
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'works with valid key, cert and intermediate cert' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
page.must_have_content /status: inactive/i
|
||||||
|
attach_file 'key', @key_path
|
||||||
|
attach_file 'cert', @cert_path
|
||||||
|
attach_file 'cert_intermediate', @cert_intermediate_path
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /Updated SSL/
|
||||||
|
page.must_have_content /status: installed/i
|
||||||
|
@site.reload
|
||||||
|
@site.ssl_key.must_equal File.read(@key_path)
|
||||||
|
@site.ssl_cert.must_equal File.read(@cert_path)
|
||||||
|
@site.ssl_cert_intermediate.must_equal File.read(@cert_intermediate_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with no uploads' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /ssl key.+certificate.+intermediate.+required to continue/i
|
||||||
|
@site.reload
|
||||||
|
@site.ssl_key.must_equal nil
|
||||||
|
@site.ssl_cert.must_equal nil
|
||||||
|
@site.ssl_cert_intermediate.must_equal nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with encrypted key' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
attach_file 'key', './tests/files/ssl/derpie.com-encrypted.key'
|
||||||
|
attach_file 'cert', @cert_path
|
||||||
|
attach_file 'cert_intermediate', @cert_intermediate_path
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /could not process ssl key/i
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with junk key' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
attach_file 'key', './tests/files/index.html'
|
||||||
|
attach_file 'cert', @cert_path
|
||||||
|
attach_file 'cert_intermediate', @cert_intermediate_path
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /could not process ssl key/i
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with junk cert' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
attach_file 'key', @key_path
|
||||||
|
attach_file 'cert', './tests/files/index.html'
|
||||||
|
attach_file 'cert_intermediate', @cert_intermediate_path
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /could not process ssl certificate/i
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with junk intermediate cert' do
|
||||||
|
visit '/custom_domain'
|
||||||
|
attach_file 'key', @key_path
|
||||||
|
attach_file 'cert', @cert_path
|
||||||
|
attach_file 'cert_intermediate', './tests/files/index.html'
|
||||||
|
click_button 'Upload SSL Key and Certificate'
|
||||||
|
page.current_path.must_equal '/custom_domain'
|
||||||
|
page.must_have_content /could not process intermediate ssl certificate/i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'change username' do
|
describe 'change username' do
|
||||||
include Capybara::DSL
|
include Capybara::DSL
|
||||||
|
|
||||||
|
|
30
tests/files/ssl/derpie.com-encrypted.key
Normal file
30
tests/files/ssl/derpie.com-encrypted.key
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
Proc-Type: 4,ENCRYPTED
|
||||||
|
DEK-Info: DES-EDE3-CBC,966995A71814CBB6
|
||||||
|
|
||||||
|
cPhqT0MucfSfZJYnfwOfS9Cqa+Ad5if4sluHaG1LlbCOl9GOWDjh/modLqvFEa1L
|
||||||
|
c2WCf6z00Haw9HZoD+kSmTzYcwX5b5eOpRPhgcaxHl7QOvLZtdPDQhZEmAREw2Ey
|
||||||
|
7oM9hKsqfFvi0fC0OnW6vOK2a3qAsLXLrehdR4SdHjMXr9WJuj80aqXZPkOjC8gH
|
||||||
|
/MMfzb6ZTeRzNQrz7vYW+K8Qs08k7/pEneLpqC9Bdmus05VRGQixI5plQzJ3+w+S
|
||||||
|
xE2sr9n885AojC7BN6IXouxbmbZv5q+L1DI5gtHrme6tA5l4ziB3s14drHZnmA2A
|
||||||
|
MTH0tD3URWK0gY8uuf0HE8LuMqxe8HngyMoxoLM3ck4sEPJ+/ZxlPHO2HDZ/S+m3
|
||||||
|
z+R1hmIRs9XryRXUqiIF0zORrc0+3t2a6UOfdja3PZ8mNN1QrsuIpwTfP69lKe0u
|
||||||
|
prgdV0AkbTW3aC4/RfNI5uJ60uWLjcoWfiDK8fchUleOWYLHMUQt8YiVEWizpLFo
|
||||||
|
f3dP2kh4pB2R69zjMkvV6XlN1YOh0uVKWNPgrFXuKTvec9xLWSccW9XQoxtxN18k
|
||||||
|
sDSFTc2gxf+rTfJ7PTEDwPH1gIZjLQwGknPWPUlfySsbDOaZzaVZEj7f7NUu6BWm
|
||||||
|
L/r1jvW8EOjzSN+XL5Aw5mpIMVxb2CHezq2qF6HSW7SBxARjDn07rcHnRD5ayjqU
|
||||||
|
8AdsXFBYPbOY5ujpsIA5/d2VThY+JDfYom8iUbjXmuiXpqZxBJ+CBh46T2sRKxtU
|
||||||
|
S90WkujEFBAayuFE2b8h6HJsSg3RTTcDf2SbDWzSoH0ZCyf00/xwgcRZZ5bSbAfC
|
||||||
|
YhaTPtmENeHkA4ofMsBkTO9UCzqJAC+inp41PFptTIn6nDx/TwT4N3nsOlIDJVYw
|
||||||
|
edFhdpPlISJREI6g1SdzUhWb1YKhCFkkt8bKEbElOfUnF+bppHIEk6nwMkFkj4SV
|
||||||
|
504YTGA+cWLta0zxLWj/zVClodAtuaa9F5jjRezYlDbs9s1awqp/yLPXMD6d90Oa
|
||||||
|
vYWJclNT37y1PXdBHBrKummoMf3R0LoS45lekkPWazGRl0O7p0+tQUmcyOEVbPBj
|
||||||
|
fWZndBVi+3yw/ewImeZOX/PYPCuWSdYY/fFw3VMMq2UwetFwdM71k5tUAX695qhI
|
||||||
|
FMWVkTxonQOr6zgvl+5kdKV+cSgCXt+p/Fa70qGXbjhEgBysrh4uLuTV7hQs5Ext
|
||||||
|
aIkGm83cnqtQbyvpSNxzVQOj1PqegPi/Z4TKM/saWzBX77qEuSfUjo/+Sg6nMG9O
|
||||||
|
Tml9ZLXpkFVfeVx7rvpZNzXMk8TeEKVNo/lO5bKnvEUfKsY+FFwJl7ehe+V/Ig6z
|
||||||
|
2ugqtv/LSoVrwNNiIUHvUcx2b+Woa4iX1WJs4303U1cGhFOPtLSM32XNbHrRBk0u
|
||||||
|
SKLHTJgV5hMpXkq/I6UMnanD/bGNvK8Tx/XvvQY62sObAwSlKpBFfgOmSXcUU2PK
|
||||||
|
AE4ih0/4UE8edRY2VOW99JRfcLaJofRr6LLmsYzFSgo4R4d4SNDvzMV58zxXSLN4
|
||||||
|
bwBq9FNvW/LErbE844U0FG1zwTKLpPr3SRFKk/pM1yful1yziWyijg==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -9,17 +9,11 @@
|
||||||
<h3></h3>
|
<h3></h3>
|
||||||
<article>
|
<article>
|
||||||
|
|
||||||
<% if !current_site.errors.empty? %>
|
<% if flash.keys.length > 0 %>
|
||||||
<div class="alert alert-block alert-error">
|
<div class="alert alert-block alert-success">
|
||||||
<% current_site.errors.each do |error| %>
|
<% flash.keys.each do |key| %>
|
||||||
<p><%= error.last.first %></p>
|
<%== flash[key] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if flash[:success] %>
|
|
||||||
<div class="alert alert-block alert-success" style="margin-top: 20px">
|
|
||||||
<%== flash[:success] %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -47,9 +41,45 @@
|
||||||
<br>
|
<br>
|
||||||
<input class="btn-Action" type="submit" value="Update Domain">
|
<input class="btn-Action" type="submit" value="Update Domain">
|
||||||
</form>
|
</form>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Add SSL Certificate</h2>
|
||||||
<p>
|
<p>
|
||||||
<strong>NOTE: This is for advanced users, we cannot provide technical support for this feature.</strong> If you cannot make this work, please contact your domain registrar.
|
This allows you to add an SSL private key and certificate for your domain, enabling encryption for your site (https). It can take up to 5-30 minutes for the changes to go live, so please be patient.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<% if current_site.domain.nil? || current_site.domain.empty? %>
|
||||||
|
<p><strong>Cannot upload SSL certificate until domain is added.</strong></p>
|
||||||
|
<% else %>
|
||||||
|
|
||||||
|
<form method="POST" action="/settings/ssl" enctype="multipart/form-data">
|
||||||
|
<%== csrf_token_input_html %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
Status: <%= current_site.ssl_installed? ? 'Installed' : 'Inactive' %>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Private key (yourdomain.com.key):
|
||||||
|
<input name="key" type="file">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Primary Certificate (yourdomain.com.crt):
|
||||||
|
<input name="cert" type="file">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Intermediate Certificate (ca.crt/yourca.crt):
|
||||||
|
<input name="cert_intermediate" type="file">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input class="btn-Action" type="submit" value="Upload SSL Key and Certificate">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
<% end %>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
Loading…
Add table
Reference in a new issue