pushcart 0.0.1.proto1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +7 -0
  4. data/Rakefile +40 -0
  5. data/app/assets/javascripts/pushcart/application.js +13 -0
  6. data/app/assets/stylesheets/pushcart/application.css +15 -0
  7. data/app/controllers/concerns/pushcart/getable.rb +127 -0
  8. data/app/controllers/concerns/pushcart/postable.rb +113 -0
  9. data/app/controllers/pushcart/application_controller.rb +4 -0
  10. data/app/controllers/pushcart/pub_sub/base_controller.rb +4 -0
  11. data/app/controllers/pushcart/pub_sub/subscriptions_controller.rb +8 -0
  12. data/app/helpers/pushcart/application_helper.rb +4 -0
  13. data/app/jobs/pushcart/subscription_renewal_job.rb +9 -0
  14. data/app/views/layouts/pushcart/application.html.erb +14 -0
  15. data/config/routes.rb +6 -0
  16. data/lib/pushcart.rb +16 -0
  17. data/lib/pushcart/composable.rb +10 -0
  18. data/lib/pushcart/connectable.rb +34 -0
  19. data/lib/pushcart/deliverable.rb +5 -0
  20. data/lib/pushcart/engine.rb +5 -0
  21. data/lib/pushcart/errors.rb +4 -0
  22. data/lib/pushcart/requestable.rb +68 -0
  23. data/lib/pushcart/stateable.rb +166 -0
  24. data/lib/pushcart/subscribable.rb +25 -0
  25. data/lib/pushcart/version.rb +3 -0
  26. data/lib/tasks/pushcart_tasks.rake +4 -0
  27. data/test/connectable_test.rb +24 -0
  28. data/test/dummy/README.rdoc +28 -0
  29. data/test/dummy/Rakefile +6 -0
  30. data/test/dummy/app/assets/javascripts/application.js +13 -0
  31. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  32. data/test/dummy/app/controllers/application_controller.rb +5 -0
  33. data/test/dummy/app/helpers/application_helper.rb +2 -0
  34. data/test/dummy/app/models/subscription.rb +9 -0
  35. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  36. data/test/dummy/bin/bundle +3 -0
  37. data/test/dummy/bin/rails +4 -0
  38. data/test/dummy/bin/rake +4 -0
  39. data/test/dummy/bin/setup +29 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +26 -0
  42. data/test/dummy/config/boot.rb +5 -0
  43. data/test/dummy/config/database.yml +25 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +41 -0
  46. data/test/dummy/config/environments/production.rb +79 -0
  47. data/test/dummy/config/environments/test.rb +42 -0
  48. data/test/dummy/config/initializers/assets.rb +11 -0
  49. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  51. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/test/dummy/config/initializers/inflections.rb +16 -0
  53. data/test/dummy/config/initializers/mime_types.rb +4 -0
  54. data/test/dummy/config/initializers/session_store.rb +3 -0
  55. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  56. data/test/dummy/config/locales/en.yml +23 -0
  57. data/test/dummy/config/routes.rb +4 -0
  58. data/test/dummy/config/secrets.yml +22 -0
  59. data/test/dummy/db/migrate/20151016222203_create_subscriptions.rb +59 -0
  60. data/test/dummy/db/schema.rb +51 -0
  61. data/test/dummy/db/test.sqlite3 +0 -0
  62. data/test/dummy/log/development.log +0 -0
  63. data/test/dummy/log/test.log +5270 -0
  64. data/test/dummy/public/404.html +67 -0
  65. data/test/dummy/public/422.html +67 -0
  66. data/test/dummy/public/500.html +66 -0
  67. data/test/dummy/public/favicon.ico +0 -0
  68. data/test/fixtures/subscriptions.yml +6 -0
  69. data/test/integration/content_delivery_test.rb +58 -0
  70. data/test/integration/hub_request_test.rb +127 -0
  71. data/test/pushcart_test.rb +7 -0
  72. data/test/requestable_test.rb +7 -0
  73. data/test/routes_test.rb +9 -0
  74. data/test/state_test.rb +13 -0
  75. data/test/test_helper.rb +27 -0
  76. metadata +349 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2621b1fb0b34606a1385ef7d7e4e3f6d4d78033e
4
+ data.tar.gz: 2ee9ce5b166dd3a51443798d01dfa263ba7b655c
5
+ SHA512:
6
+ metadata.gz: fc21913f640a1ee3ec05252a765774398b5f7a6ad07bf1cb2fab09beeb4d6264396cf6a864d82f835f972965396b197fb8b5ff22dfdb17a3b9bb68833c583876
7
+ data.tar.gz: 260ee10070e9d7da4fd1391ae4bb349ec96b9d33baa2782d596175b51934e8be27575045f29ab915ce21cc3bec17e1aabfab718ed50fa99e3b1d48de3e80c03b
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Chris Kalafarski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # Pushcart
2
+
3
+ [![Gem Version](http://img.shields.io/gem/v/pushcart.svg)](https://rubygems.org/gems/pushcart)
4
+ [![Dependency Status](https://gemnasium.com/scour/pushcart.svg)](https://gemnasium.com/scour/pushcart)
5
+ [![Build Status](https://travis-ci.org/scour/pushcart.svg)](https://travis-ci.org/scour/pushcart)
6
+ [![Code Climate](https://codeclimate.com/github/scour/pushcart/badges/gpa.svg)](https://codeclimate.com/github/scour/pushcart)
7
+ [![Coverage Status](https://coveralls.io/repos/scour/pushcart/badge.svg?branch=master&service=github)](https://coveralls.io/github/scour/pushcart?branch=master)
@@ -0,0 +1,40 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "Pushcart"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.rdoc")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load "rails/tasks/engine.rake"
19
+
20
+ load "rails/tasks/statistics.rake"
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require "rake/testtask"
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << "lib"
28
+ t.libs << "test"
29
+ t.pattern = "test/**/*_test.rb"
30
+ t.verbose = false
31
+ end
32
+
33
+ task :console do
34
+ require "pry"
35
+ require "pushcart"
36
+ ARGV.clear
37
+ Pry.start
38
+ end
39
+
40
+ task default: :test
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,127 @@
1
+ module Pushcart
2
+ # Hubs send GET requests to a subscription's callback URL in several
3
+ # cases:
4
+
5
+ # * To indicate that a publisher determined the a subscription
6
+ # request should not accepted and the subscription is being denied
7
+ # * To notify the subscriber that a previously accepted subscription is now
8
+ # being denied
9
+ # * To verify that the subscriber sent a previously received request to
10
+ # subscribe or unsubscribe to a given topic
11
+ #
12
+ # Hubs do not expect a response in the case of denials. Intent verification
13
+ # responses MUST be 2xx with a body equal to the `hub.challenge` parameter
14
+ # if the subscriber is verifying the intent, otherwise a 404 response MUST
15
+ # be sent.
16
+ module Getable
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ before_action :expecting_callback, only: :get
21
+ before_action :validate_callback_params, only: :get
22
+ end
23
+
24
+ def get
25
+ subscription_denied? ? subscription_denied_get : intent_verification_get
26
+ end
27
+
28
+ private
29
+
30
+ def subscription_denied_get
31
+ # TODO add support for Location Header?
32
+
33
+ # @subscription.deny!(reason: params['hub.reason'])
34
+ render status: 200, nothing: true
35
+ end
36
+
37
+ def intent_verification_get
38
+ if subscribe_intent_verification?
39
+ subscribe_intent_verification_get
40
+ elsif unsubscribe_intent_verification?
41
+ unsubscribe_intent_verification_get
42
+ end
43
+ end
44
+
45
+ def subscribe_intent_verification_get
46
+ # TODO
47
+ end
48
+
49
+ def unsubscribe_intent_verification_get
50
+ # TODO
51
+ end
52
+
53
+ def expecting_callback
54
+ # TODO
55
+ # unless expecting_any_callback
56
+ # render status: 404, nothing: true
57
+ # end
58
+ end
59
+
60
+ def expecting_any_callback
61
+ expecting_subscribe_callback ||
62
+ expecting_unsubscribe_callback ||
63
+ expecting_denied_callback
64
+ end
65
+
66
+ def expecting_subscribe_callback
67
+ # TODO
68
+ end
69
+
70
+ def expecting_unsubscribe_callback
71
+ # TODO
72
+ end
73
+
74
+ def expecting_denied_callback
75
+ # TODO
76
+ end
77
+
78
+ # `The hub.mode` and `hub.topic` parameters are required for all GET
79
+ # requests made to a subscription's callback URL. The `hub.topic` value
80
+ # must match the topic of the subscription that originally registered the
81
+ # subscription, and the only valid `hub.mode` values are "subscribe",
82
+ # "unsubscribe", and "denied".
83
+ def validate_callback_params
84
+ unless validate_callback_params_topic && validate_callback_params_mode
85
+ render status: 404, nothing: true
86
+ end
87
+ end
88
+
89
+ def validate_callback_params_topic
90
+ params["hob.topic"] && params["hob.topic"] == @subscription.topic
91
+ end
92
+
93
+ def validate_callback_params_mode
94
+ validate_denied_callback_params_mode ||
95
+ validate_subscribe_callback_params_mode ||
96
+ validate_unsubscribe_callback_params_mode
97
+ end
98
+
99
+ def validate_denied_callback_params_mode
100
+ params["hub.mode"] == "denied"
101
+ end
102
+ alias_method :subscription_denied?, :validate_denied_callback_params_mode
103
+
104
+ def validate_subscribe_callback_params_mode
105
+ params["hub.mode"] == "subscribe" &&
106
+ !params["hub.challenge"].blank? &&
107
+ !params["hub.lease_seconds"].blank?
108
+ end
109
+
110
+ def validate_unsubscribe_callback_params_mode
111
+ params["hub.mode"] == "unsubscribe" &&
112
+ !params["hub.challenge"].blank?
113
+ end
114
+
115
+ def intent_verification?
116
+ subscribe_intent_verification? || unsubscribe_intent_verification?
117
+ end
118
+
119
+ def subscribe_intent_verification?
120
+ params["hub.mode"] == "subscribe"
121
+ end
122
+
123
+ def unsubscribe_intent_verification?
124
+ params["hub.mode"] == "unsubscribe"
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,113 @@
1
+ require "link_header"
2
+
3
+ module Pushcart
4
+ # The only case where a POST request is sent to a subscription's callback
5
+ # URL is when the hub is attempting to deliver new content. The hub MAY
6
+ # distribute content as a diff between two consecutive versions of the
7
+ # content being delivered. The request MUST have a `Content-Type`
8
+ # corresponding to the type of the topic, and MUST include a Link Header
9
+ # with `rel=hub` pointing to the hub and `rel=self` set to the topic that's
10
+ # being updated.
11
+ #
12
+ # Any response other than 2xx, including redirects, could be considered by
13
+ # the hub to be a failure, and deliveries could be retried. The body of
14
+ # this response will be ignored by the hub. A 2xx response should be sent as
15
+ # soon as the payload arrives, even if it has not been processed yet, or
16
+ # processing fails.
17
+ module Postable
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ before_action :expecting_content_distribution, only: :post
22
+ before_action :verify_content_type, only: :post
23
+ before_action :verify_link_headers, only: :post
24
+ before_action :authenticate_content_distribution, only: :post
25
+ end
26
+
27
+ def post
28
+ # TODO
29
+ # @subscription.process_content_distribution_payload()
30
+ render status: 202, nothing: true
31
+ end
32
+
33
+ private
34
+
35
+ # Ensure that the given subscription is in a state where it is expecting to
36
+ # receive content from a hub
37
+ def expecting_content_distribution
38
+ # unless request.content_type == @subscription.active?
39
+ # render status: 400, nothing: true
40
+ # end
41
+ end
42
+
43
+ # https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#contentdistribution
44
+ # This request MUST have a Content-Type corresponding to the type of the
45
+ # topic.
46
+ def verify_content_type
47
+ # unless request.content_type == @subscription.topic_content_type
48
+ # render status: 400, nothing: true
49
+ # end
50
+ end
51
+
52
+ # https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#contentdistribution
53
+ # The request MUST include a Link Header [RFC5988] with rel=hub pointing to
54
+ # the Hub as well as a Link Header [RFC5988] with rel=self set to the topic
55
+ # that's being updated. The Hub SHOULD combine both headers into a single
56
+ # Link Header [RFC5988].
57
+ def verify_link_headers
58
+ # unless verify_link_header(:hub, @subscription.hub) &&
59
+ # verify_link_header(:self, @subscription.topic)
60
+ # render status: 400, nothing: true
61
+ # end
62
+ end
63
+
64
+ def verify_link_header(rel, value)
65
+ links = LinkHeader.parse(request.headers["Link"]).to_a
66
+ !links.detect { |l| l[1][0][1] == rel.to_s && l[0] == value }.empty?
67
+ end
68
+
69
+ # https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#authednotify
70
+ # If the subscriber supplied a value for hub.secret in their subscription
71
+ # request, the hub MUST generate an HMAC signature of the payload and
72
+ # include that signature in the request headers of the content distribution
73
+ # request. The X-Hub-Signature header's value MUST be in the form
74
+ # sha1=signature where signature is a 40-byte, hexadecimal representation of
75
+ # a SHA1 signature [RFC3174]. The signature MUST be computed using the HMAC
76
+ # algorithm [RFC2104] with the request body as the data and the hub.secret
77
+ # as the key.
78
+ #
79
+ # When subscribers receive a content distribution request with the
80
+ # X-Hub-Signature header specified, they SHOULD recompute the SHA1 signature
81
+ # with the shared secret using the same method as the hub. If the signature
82
+ # does not match, subscribers MUST still return a 2xx success response to
83
+ # acknowledge receipt, but locally ignore the message as invalid. Using this
84
+ # technique along with HTTPS [RFC2818] for subscription requests enables
85
+ # simple subscribers to receive authenticated notifications from hubs
86
+ # without the need for subscribers to run an HTTPS [RFC2818] server.
87
+ def authenticate_content_distribution
88
+ # The PuSH 0.4 spec does not explicitly say what to do when a hub is
89
+ # required to send a signature and it does not.
90
+ unless request.headers["X-Hub-Signature"] =~ /sha1=./
91
+ render status: 403, nothing: true
92
+ return
93
+ end
94
+
95
+ hub_signature = request.headers["X-Hub-Signature"].split("=")[1]
96
+ body = request.body.read
97
+
98
+ # TODO Do this check on the model
99
+ digest = OpenSSL::Digest::SHA1.new
100
+ key = @subscription.hmac_secret_key # TODO
101
+ data = body
102
+ hmac = OpenSSL::HMAC.hexdigest(digest, key, data)
103
+
104
+ unless hmac == hub_signature
105
+ # If the signature does not match, subscribers MUST still return a 2xx
106
+ # success response to acknowledge receipt...
107
+ # (but exit, because we don't want to process the data)
108
+ # render status: 202, nothing: true
109
+ return
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,4 @@
1
+ module Pushcart
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pushcart
2
+ class PubSub::BaseController < ApplicationController
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ module Pushcart
2
+ class PubSub::SubscriptionsController < PubSub::BaseController
3
+ include Postable
4
+ include Getable
5
+
6
+ skip_before_action :verify_authenticity_token
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module Pushcart
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module Pushcart
2
+ class SubscriptionRenewalJob < ActiveJob::Base
3
+ queue_as "pushcart_pubsub"
4
+
5
+ def perform(subscription)
6
+ # TODO
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Pushcart</title>
5
+ <%= stylesheet_link_tag "pushcart/application", media: "all" %>
6
+ <%= javascript_include_tag "pushcart/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,6 @@
1
+ Pushcart::Engine.routes.draw do
2
+ namespace :pub_sub, path: "" do
3
+ get "subscriptions/:id/callback" => "subscriptions#get"
4
+ post "subscriptions/:id/callback" => "subscriptions#post"
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ require "aasm"
2
+ require "excon"
3
+ require "faraday"
4
+ require "link_header"
5
+ require "validate_url"
6
+ require "pushcart/engine"
7
+ require "pushcart/errors"
8
+ require "pushcart/connectable"
9
+ require "pushcart/deliverable"
10
+ require "pushcart/requestable"
11
+ require "pushcart/stateable"
12
+ require "pushcart/composable"
13
+ require "pushcart/subscribable"
14
+
15
+ module Pushcart
16
+ end
@@ -0,0 +1,10 @@
1
+ module Pushcart
2
+ module Composable
3
+ extend ActiveSupport::Concern
4
+
5
+ include Connectable
6
+ include Requestable
7
+ include Deliverable
8
+ include Stateable
9
+ end
10
+ end
@@ -0,0 +1,34 @@
1
+ module Pushcart
2
+ module Connectable
3
+ extend ActiveSupport::Concern
4
+
5
+ def callback_uri
6
+ raise ConfigurationError, "Incomplete callback URI configuration" if !ENV["PUSH_CALLBACK_SCHEME"] || !ENV["PUSH_CALLBACK_HOST"] || !ENV["PUSH_CALLBACK_PORT"]
7
+ raise ArgumentError, "ID required for callback URI" if !id
8
+
9
+ scheme = ENV["PUSH_CALLBACK_SCHEME"]
10
+ host = ENV["PUSH_CALLBACK_HOST"]
11
+ port = ENV["PUSH_CALLBACK_PORT"].to_i
12
+ path = "/subscriptions/#{id}/callback"
13
+
14
+ components = { host: host, port: port, path: path }
15
+ (scheme == "https" ? URI::HTTPS : URI::HTTP).build(components)
16
+ end
17
+
18
+ def connection(mode:)
19
+ # TODO Better error
20
+ raise "Missing" if !callback_uri || !mode || !topic || !hmac_secret_key
21
+
22
+ Faraday.new do |conn|
23
+ conn.adapter Faraday.default_adapter
24
+ conn.request :url_encoded
25
+ conn.params = {
26
+ "hub.callback": callback_uri.to_s,
27
+ "hub.mode": mode,
28
+ "hub.topic": topic,
29
+ "hub.secret": hmac_secret_key
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end