pushcart 0.0.1.proto1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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