diff --git a/.gitignore b/.gitignore index 071dbf6d..6a1cc1be 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ files/sslsites.zip public/banned_sites public/deleted_sites files/disposable_email_blacklist.conf +files/letsencrypt.key +files/tor.txt +.bundle diff --git a/Gemfile b/Gemfile index 209cde9c..b57c9b33 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'simpleidn' gem 'gandi' gem 'hoe', '3.14.2', require: nil gem 'msgpack' +gem 'acme-client' platform :mri, :rbx do gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic diff --git a/Gemfile.lock b/Gemfile.lock index 3975320e..315b2bd3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,9 @@ GEM remote: https://rubygems.org/ specs: + acme-client (0.3.1) + faraday (~> 0.9, >= 0.9.1) + json-jwt (~> 1.2, >= 1.2.3) activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) @@ -14,6 +17,7 @@ GEM ansi (1.5.0) base32 (0.3.2) bcrypt (3.1.11) + bindata (2.3.0) blankslate (3.1.3) builder (3.2.2) byebug (8.2.4) @@ -58,6 +62,8 @@ GEM fabrication (2.15.0) faker (1.6.3) i18n (~> 0.5) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) ffi (1.9.10) ffi-compiler (0.1.3) ffi (>= 1.0.0) @@ -82,6 +88,12 @@ GEM rack (~> 1.4) rest-client (~> 1.0) json (1.8.3) + json-jwt (1.5.2) + activesupport + bindata + multi_json (>= 1.3) + securecompare + url_safe_base64 m (1.4.2) method_source (>= 0.6.7) rake (>= 0.9.2.2) @@ -104,6 +116,7 @@ GEM mock_redis (0.16.1) msgpack (0.7.5) multi_json (1.11.2) + multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (3.1.1) @@ -166,6 +179,7 @@ GEM scrypt (2.1.1) ffi-compiler (>= 0.0.2) rake + securecompare (1.0.0) sequel (4.8.0) sequel_pg (1.6.16) pg (>= 0.8.0) @@ -216,6 +230,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) + url_safe_base64 (0.2.2) uuidtools (2.1.5) webmock (1.24.2) addressable (>= 2.3.6) @@ -233,6 +248,7 @@ PLATFORMS ruby DEPENDENCIES + acme-client addressable ago base32 diff --git a/app/settings.rb b/app/settings.rb index a89ef315..5de3642b 100644 --- a/app/settings.rb +++ b/app/settings.rb @@ -66,6 +66,7 @@ post '/settings/:username/profile' do redirect "/settings/#{@site.username}#profile" end +=begin post '/settings/:username/ssl' do require_login require_ownership_for_settings @@ -167,6 +168,7 @@ post '/settings/:username/ssl' do flash[:success] = 'Updated SSL key/certificate.' redirect "/settings/#{@site.username}#custom_domain" end +=end post '/settings/:username/change_name' do require_login @@ -241,6 +243,7 @@ post '/settings/:username/custom_domain' do if @site.valid? @site.save_changes + RequestSSLAuthWorker.perform_async @site.id flash[:success] = 'The domain has been successfully updated.' redirect "/settings/#{@site.username}#custom_domain" else diff --git a/config.yml.template b/config.yml.template index 00045723..0238d383 100644 --- a/config.yml.template +++ b/config.yml.template @@ -15,6 +15,8 @@ development: paypal_api_username: derp paypal_api_password: ing paypal_api_signature: tonz + letsencrypt_key: ./tests/files/letsencrypt.key + letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/ test: database: 'postgres://neocities@localhost/neocities_test' database_pool: 1 @@ -31,3 +33,5 @@ test: paypal_api_username: derp paypal_api_password: ing paypal_api_signature: tonz + letsencrypt_key: ./tests/files/letsencrypt.key + letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/ \ No newline at end of file diff --git a/config.yml.travis b/config.yml.travis index e9cdfb70..1f832c7f 100644 --- a/config.yml.travis +++ b/config.yml.travis @@ -11,3 +11,5 @@ paypal_api_username: derp paypal_api_password: ing paypal_api_signature: tonz logs_path: "/tmp/neocitiestestlogs" +letsencrypt_key: ./tests/files/letsencrypt.key +letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/ \ No newline at end of file diff --git a/environment.rb b/environment.rb index 387685a6..ddb6f4b1 100644 --- a/environment.rb +++ b/environment.rb @@ -152,3 +152,10 @@ end gandi_opts = {} gandi_opts[:env] = :test unless ENV['RACK_ENV'] == 'production' $gandi = Gandi::Session.new $config['gandi_api_key'], gandi_opts + +# Let's Encrypt + +$letsencrypt = Acme::Client.new( + private_key: OpenSSL::PKey::RSA.new(File.read($config['letsencrypt_key'])), + endpoint: $config['letsencrypt_endpoint'] +) \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index 6b5603cd..d0d48903 100644 --- a/models/site.rb +++ b/models/site.rb @@ -1330,6 +1330,28 @@ class Site < Sequel::Model sanitized.gsub(/(http|https):\/\//, '').gsub(/[^\w\s]/, '').downcase.split.uniq.select{|v| v.length < SiteFile::CLASSIFIER_WORD_LIMIT}.join(' ') end + def request_ssl_authorization + auth = $letsencrypt.authorize domain: domain + challenge = auth.http01 + FileUtils.mkdir_p File.join(base_files_path, File.dirname(challenge.filename)) + + File.write File.join(base_files_path, challenge.filename), challenge.file_content + + challenge.request_verification + + challenge + end + + # request_ssl_authorization must be run first! + def obtain_ssl_certificate + csr = Acme::Client::CertificateRequest.new names: [domain, "www.#{domain}"] + certificate = $letsencrypt.new_certificate csr + self.ssl_key = certificate.request.private_key.to_pem + self.ssl_cert = certificate.fullchain_to_pem + save_changes + FileUtils.rm_rf File.join(base_files_path, '.well-known') + end + # array of hashes: filename, tempfile, opts. def store_files(files, opts={}) results = [] diff --git a/tests/files/letsencrypt.key b/tests/files/letsencrypt.key new file mode 100644 index 00000000..fc2ef03b --- /dev/null +++ b/tests/files/letsencrypt.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAwsY6rlj09XsfnxbzatQKEqjN9Kqynjp+o7jaHBaWMjRSOSqC +xj0ThnxghxsXnUZEP+Wc/e38JQ15Ob8LRe5AnigP65ArvC3Fk9p19uV9JkX+gBtr +LSzQqSD0As+4SQc8iE7zoAH8Oz2t1WXGOJnhw/D3DZBoEis4TBXctc9FMYYWtCSH +2wfqUMTVUru6vOG9dv1w/7lQr4+YIHJGGai9p+XEqLGmkH/JkHwQ11XJZcBjKQtQ +1jTfryzMtCGtdRBvF9Au2JCWz3zrVdh4Pz6pj8NTQ87izf97u3RvmnnZIT9RSPWF +LVfkFSLNExguMqreWi8NzI79rfP08bMAhIcEFQIDAQABAoIBAEABJAdm9Bg2/Y0p +3ZOdWjIHhqZ/8XJNhpAGjqXsRi75QovGjHLYXzIybQX4wSnNjV/x9ET9j34dj1iK +jwnL4TAXuiOOnNCZufN8NoeLojSRi7BNUkrmDyuEyKqj18sntZyVVTmfbWOKQTV+ +3ER9TNM2u/iZG5nc2gMmrACSvAopktYvBYRPHAmCR8ZLkE6ZjPeOKcJbr6F5D9Ie +X5+QKsoG0by7Gu8eHkOh0WtufeOM38OfSStDpPW5eu5JOFrtgEQPolaMi0Mw2h4D +aaNLUQbmk1I9JMS17yVJFnTrrKTSeHFiC16rwKWJvG0S4DUfsyMW9u0oog4sYrAT +5j0+oAECgYEA8ZD2QRbuZD52F6SPB6tV0LokUijKLLzUW1+d8R54sW7ATurCSspU +J9CRSmyi9cfFn9ReP776dRBgtfYmf6IV5Ju6fH86+B2+WI/2YLqbMkpcAILnRF80 +IlfJK4mDy0ljxvubUNaEUcZj/bpINx5PwddU7Xl6POWuYxXzBn4TZNUCgYEAzmmH +t+Qv9l+LxxaNDIdZYtWhkYUWGzupiLKWHjrnprqmFLODDCbfA95Y1jxK3tBxZ3Yp +TRD9XJ/0+PrLK992OUDvlPxX4XSMZPMtsqLSAwlnAvUzzZQimhyhC8rx2eogbDSx +Wf0pKqMRVJdk6zFEMKNx+Lcgvpv5rCWoIJ7twkECgYAJdFf/AivAZqVulXU3oqAQ +NEjZolkPWTCihuKCnmOw5hnGvO9vx+11RXd6Rzg1kGUOtVwe+JWK4WI3nPOyySA8 +O1AYMU6YiWl6w9+rt4H9fOWO65Crn2JF+dOYzaAH485w6kYQ6uRw4ufk9VaAOcJ7 +XrcnODrtiTvDCwfg+CxAJQKBgFHlCNXrESR9ECYzSk8YPFy8SdhEp1qytzbnNCxW +TqaWE2LPPkVJ/t24ECMf1MzGgtf7x7Mt9HgVdsp6JrYHeQ6KNwQzgmKPLUy4Nv9T +HmPaDSbdRmpgRcJDbZoSMRa2j5qe5WbAzN5/yFZ5oq6140ow7v0xGyrFE7A7WJNo +uwiBAoGAMyptWRsi0vFMXy/0b26whVj2LGFLRDS03bT3XhyQmCnp+f+03GHbuggM +/IAf3RMj8oub1RrPoFqzd78n0CnZLwk+A6w8EMkKDFCY1sLESy7dVtPdS91deIxk +H07fMxqzLYQO6XplcmqsD06PuTLmWYDjA9e8hSSMQo1n+rSw4ko= +-----END RSA PRIVATE KEY----- diff --git a/workers/create_ssl_cert_worker.rb b/workers/create_ssl_cert_worker.rb new file mode 100644 index 00000000..f0a7f8ae --- /dev/null +++ b/workers/create_ssl_cert_worker.rb @@ -0,0 +1,17 @@ +class CreateSSLCertWorker + include Sidekiq::Worker + sidekiq_options queue: :create_ssl_auth_worker, retry: 100, backtrace: true + + sidekiq_retry_in do |count| + 180 + end + + def perform(site_id, challenge) + site = Site[site_id] + + challenge = $letsencrypt.challenge_from_hash JSON.parse(challenge) + if challenge.verify_status == 'valid' + site.obtain_ssl_certificate + end + end +end diff --git a/workers/request_ssl_auth_worker.rb b/workers/request_ssl_auth_worker.rb new file mode 100644 index 00000000..86ba90dc --- /dev/null +++ b/workers/request_ssl_auth_worker.rb @@ -0,0 +1,15 @@ +class RequestSSLAuthWorker + include Sidekiq::Worker + sidekiq_options queue: :request_ssl_auth_worker, retry: 100, backtrace: true + + sidekiq_retry_in do |count| + 180 + end + + def perform(site_id) + site = Site[site_id] + challenge = site.request_ssl_authorization + + CreateSSLCertWorker.perform_in 5.seconds, site_id, challenge.to_h.to_json + end +end