diff --git a/app.rb b/app.rb
index dae4afed..6ffb430b 100644
--- a/app.rb
+++ b/app.rb
@@ -486,6 +486,60 @@ post '/settings/profile' do
redirect '/settings'
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
dashboard_if_signed_in
diff --git a/migrations/040_add_ssl.rb b/migrations/040_add_ssl.rb
new file mode 100644
index 00000000..87b010f8
--- /dev/null
+++ b/migrations/040_add_ssl.rb
@@ -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
diff --git a/migrations/041_add_intermediate_ssl.rb b/migrations/041_add_intermediate_ssl.rb
new file mode 100644
index 00000000..8f1d76a3
--- /dev/null
+++ b/migrations/041_add_intermediate_ssl.rb
@@ -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
\ No newline at end of file
diff --git a/models/site.rb b/models/site.rb
index 598c8ea9..42c4652d 100644
--- a/models/site.rb
+++ b/models/site.rb
@@ -798,6 +798,10 @@ class Site < Sequel::Model
"#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}"
end
+ def ssl_installed?
+ ssl_key && ssl_cert && ssl_cert_intermediate
+ end
+
def to_rss
RSS::Maker.make("atom") do |maker|
maker.channel.title = title
@@ -808,8 +812,8 @@ class Site < Sequel::Model
latest_events.each do |event|
if event.site_change_id
maker.items.new_item do |item|
- item.link = "http://#{username}.neocities.org"
- item.title = "#{username}.neocities.org has been updated"
+ item.link = "https://#{host}"
+ item.title = "#{title} has been updated"
item.updated = event.site_change.created_at
end
end
diff --git a/tests/acceptance/settings_tests.rb b/tests/acceptance/settings_tests.rb
index a425ee74..18b1d722 100644
--- a/tests/acceptance/settings_tests.rb
+++ b/tests/acceptance/settings_tests.rb
@@ -1,6 +1,160 @@
require_relative './environment.rb'
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
include Capybara::DSL
diff --git a/tests/files/ssl/derpie.com-encrypted.key b/tests/files/ssl/derpie.com-encrypted.key
new file mode 100644
index 00000000..79f52994
--- /dev/null
+++ b/tests/files/ssl/derpie.com-encrypted.key
@@ -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-----
diff --git a/views/custom_domain.erb b/views/custom_domain.erb
index cc6e34e5..4abaa438 100644
--- a/views/custom_domain.erb
+++ b/views/custom_domain.erb
@@ -9,17 +9,11 @@
- <% if !current_site.errors.empty? %>
-
- <% current_site.errors.each do |error| %>
-
<%= error.last.first %>
- <% end %>
-
- <% end %>
-
- <% if flash[:success] %>
-
- <%== flash[:success] %>
+ <% if flash.keys.length > 0 %>
+
+ <% flash.keys.each do |key| %>
+ <%== flash[key] %>
+ <% end %>
<% end %>
@@ -47,9 +41,45 @@
+
+
+ Add SSL Certificate
- NOTE: This is for advanced users, we cannot provide technical support for this feature. 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.
+
+ <% if current_site.domain.nil? || current_site.domain.empty? %>
+ Cannot upload SSL certificate until domain is added.
+ <% else %>
+
+
+ <% end %>
\ No newline at end of file