From 376b0bed3412997dc7140d6e40c314189d35d0b2 Mon Sep 17 00:00:00 2001 From: Pierre Jambet Date: Wed, 25 Jan 2017 12:55:54 -0800 Subject: [PATCH 1/2] JobsController: Accept base64 encoded payloads Pings can now be base64 encoded. Simply base 64 encode the public id and pass the `?use_base64=true` query string parameter. This is useful when using RSA encryption and sending a publicly encrypted payload. Dealing with base64 encoded payload simplifies integration with other platforms (such as the JVM) since we're not relying on some internal ruby byte representation like the default implementation does. --- README.md | 31 +++++++++ app/controllers/jobs_controller.rb | 8 +++ config/routes.rb | 3 +- spec/controllers/jobs_controller_spec.rb | 10 ++- spec/requests/jobs_spec.rb | 86 +++++++++++++++--------- 5 files changed, 103 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 4f7513d..63d6664 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,37 @@ You may optionally set a private RSA key to encrypt the `public_id` to uniquely CRONUT_PRIVATE_KEY: -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- +Sending an encrypted payload from ruby is fairly straightforward, you can simply +encrypt the payload with : +`OpenSSL::PKey::RSA.new(public_key_string_coming_from_the_env_a_file_or_anywhere_else).public_encrypt(str)` +and attach this string to a POST request. + +Here is an example with faraday: + +```ruby +host = "https://your.cronut.host.com" +public_key = ENV["CRONUT_PUBLIC_KEY"] # Or from a file, the string should look like "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----" +conn = Faraday.new(host) do |c| + c.request :url_encoded + c.use Faraday::Adapter::NetHttp + c.headers = { + "X-CRONUT-API-TOKEN" => "" + } +end + +conn.post "/ping/", {:public_id => OpenSSL::PKey::RSA.new(public_key).public_encrypt(str)} +``` + +Another (simpler) approach is to use the [ping-me-maybe gem](https://github.com/harrystech/ping-me-maybe) + +#### Base64 option + +Ruby uses a non standard string representation of the underlying encrypted bytes +and this can be quite tricky to reproduce when trying to ping cronut from +another environment, such as the JVM. +In this can you can base 64 encode the encrypted bytes and use the `/v2/ping` +endpoint instead. It expects the payload to be base 64 encoded by default + Usage ----- On Cronut dashboard, you can schedule two types of jobs: interval jobs and cron jobs. diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb index b645c75..e4fa828 100644 --- a/app/controllers/jobs_controller.rb +++ b/app/controllers/jobs_controller.rb @@ -105,6 +105,9 @@ def ping begin str = params[:public_id] if Encryptor.enabled? + if use_base64? + str = Base64.decode64(str) + end str = Encryptor.decrypt(str) end array = str.split("-") @@ -114,6 +117,7 @@ def ping end @job = Job.find_by_public_id!(array[1]) rescue Exception => e + puts e.message raise ActiveRecord::RecordNotFound.new('Not Found') end @@ -132,6 +136,10 @@ def verify_api_token private + def use_base64? + params[:use_base64].to_s == "true" + end + def job_params params.require(:job).permit(:name, :notifications, {notification_ids: []}, :frequency, :cron_expression, :buffer_time) end diff --git a/config/routes.rb b/config/routes.rb index 8a7496d..f1bb73c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,8 @@ resources :notifications resources :jobs - post 'ping/' => "jobs#ping" + post 'ping' => "jobs#ping" + post 'v2/ping' => "jobs#ping", defaults: {use_base64: true} root to: 'jobs#index' # The priority is based upon order of creation: diff --git a/spec/controllers/jobs_controller_spec.rb b/spec/controllers/jobs_controller_spec.rb index aee032a..e5fd132 100644 --- a/spec/controllers/jobs_controller_spec.rb +++ b/spec/controllers/jobs_controller_spec.rb @@ -235,7 +235,7 @@ end end - describe "GET ping" do + describe "POST ping" do before(:each) do invalid_basic_auth_login @job = IntervalJob.create!({:name => "Test IntervalJob", :frequency => 600}) @@ -299,6 +299,14 @@ response.status.should eq 200 @job.last_successful_time.should_not be_nil end + + it "pings with valid token and a base64 encoded payload" do + request.headers[JobsController::API_TOKEN_HEADER] = @token.token + post :ping, {:public_id => Base64.strict_encode64(Encryptor.encrypt(@str)), use_base64: "true"}, valid_session + @job.reload + response.status.should eq 200 + @job.last_successful_time.should_not be_nil + end end end diff --git a/spec/requests/jobs_spec.rb b/spec/requests/jobs_spec.rb index 4d81701..dc7e41c 100644 --- a/spec/requests/jobs_spec.rb +++ b/spec/requests/jobs_spec.rb @@ -40,7 +40,8 @@ ActiveRecord::Base.connection.reset_pk_sequence!('notifications') end - describe "POST ping" do + context "ping" do + before(:each) do #invalid_basic_auth_login @job = IntervalJob.create!({:name => "Test IntervalJob", :frequency => 600}) @@ -57,42 +58,61 @@ @token.destroy end - it "ignores pings with unencrypted public id" do - headers[JobsController::API_TOKEN_HEADER] = @token.token - expect { - post "/ping", {:public_id => @str}, headers - }.to raise_error(ActiveRecord::RecordNotFound) - @job.reload - @job.last_successful_time.should be_nil - end + describe "POST ping" do + it "ignores pings with unencrypted public id" do + headers[JobsController::API_TOKEN_HEADER] = @token.token + expect { + post "/ping", {:public_id => @str}, headers + }.to raise_error(ActiveRecord::RecordNotFound) + @job.reload + @job.last_successful_time.should be_nil + end - it "ignores pings with encrypted wrong id" do - wrong_str = "#{Time.now.to_i.to_s}-abc" - headers[JobsController::API_TOKEN_HEADER] = @token.token - expect { - post "/ping", {:public_id => Encryptor.encrypt(wrong_str)}, headers - }.to raise_error(ActiveRecord::RecordNotFound) - @job.reload - @job.last_successful_time.should be_nil - end + it "ignores pings with encrypted wrong id" do + wrong_str = "#{Time.now.to_i.to_s}-abc" + headers[JobsController::API_TOKEN_HEADER] = @token.token + expect { + post "/ping", {:public_id => Encryptor.encrypt(wrong_str)}, headers + }.to raise_error(ActiveRecord::RecordNotFound) + @job.reload + @job.last_successful_time.should be_nil + end - it "ignores pings with encrypted wrong date" do - wrong_str = "#{(Time.now - 31.seconds).to_i.to_s}-#{@job.public_id}" - headers[JobsController::API_TOKEN_HEADER] = @token.token - expect { - post "/ping", {:public_id => Encryptor.encrypt(wrong_str)}, headers - }.to raise_error(ActiveRecord::RecordNotFound) - @job.reload - @job.last_successful_time.should be_nil + it "ignores pings with encrypted wrong date" do + wrong_str = "#{(Time.now - 31.seconds).to_i.to_s}-#{@job.public_id}" + headers[JobsController::API_TOKEN_HEADER] = @token.token + expect { + post "/ping", {:public_id => Encryptor.encrypt(wrong_str)}, headers + }.to raise_error(ActiveRecord::RecordNotFound) + @job.reload + @job.last_successful_time.should be_nil + end + + it "pings with valid token" do + headers[JobsController::API_TOKEN_HEADER] = @token.token + post "/ping", {:public_id => Encryptor.encrypt(@str)}, headers + @job.reload + response.status.should eq 200 + @job.last_successful_time.should_not be_nil + end + + it "pings with valid token and a base 64 payload" do + headers[JobsController::API_TOKEN_HEADER] = @token.token + post "/ping", {:public_id => Base64.strict_encode64(Encryptor.encrypt(@str)), use_base64: true}, headers + @job.reload + response.status.should eq 200 + @job.last_successful_time.should_not be_nil + end end - it "pings with valid token" do - headers[JobsController::API_TOKEN_HEADER] = @token.token - post "/ping", {:public_id => Encryptor.encrypt(@str)}, headers - @job.reload - response.status.should eq 200 - @job.last_successful_time.should_not be_nil + describe "POST v2/ping" do + it "pings with valid token and a base64 encoded payload" do + headers[JobsController::API_TOKEN_HEADER] = @token.token + post '/v2/ping', {:public_id => Base64.strict_encode64(Encryptor.encrypt(@str))}, headers + @job.reload + response.status.should eq 200 + @job.last_successful_time.should_not be_nil + end end end - end From e9eb268b0b7075d1dae6a3a76d0d51fcd524e0cf Mon Sep 17 00:00:00 2001 From: Pierre Jambet Date: Thu, 26 Jan 2017 09:18:24 -0800 Subject: [PATCH 2/2] JobsController: rescue StandardError rescuing StandardError is preferred over Exception since there are some Exception we might not want to rescue from. --- app/controllers/jobs_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb index e4fa828..7424ee4 100644 --- a/app/controllers/jobs_controller.rb +++ b/app/controllers/jobs_controller.rb @@ -116,7 +116,7 @@ def ping raise "Timestamp does not match" end @job = Job.find_by_public_id!(array[1]) - rescue Exception => e + rescue StandardError => e puts e.message raise ActiveRecord::RecordNotFound.new('Not Found') end