plezi 0.8.4 → 0.8.5

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.
data/lib/plezi/oauth.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # requires the open-uri library.
2
- require 'open-uri'
2
+ # require 'open-uri'
3
+ require 'net/http'
3
4
  # require the OAuth2 controller
4
- require 'plezi/oauth/auth_controller.rb'
5
+ require 'plezi/oauth/auth_controller.rb'
@@ -1,61 +1,80 @@
1
- require 'open-uri'
1
+
2
2
 
3
3
 
4
4
 
5
5
  module Plezi
6
6
 
7
7
  #########################################
8
- # This is a social Authentication Controller
8
+ # This is a social Authentication Controller for OAuth2 services.
9
+ # This controller allows you to easily deploy Facebook Login, Google Login and any other OAuth2 complient login service.
10
+ #
11
+ # To include this controller in your application, you need to require it using:
12
+ #
13
+ # require 'plezi/oauth'
9
14
  #
10
- # This controller currently supports:
15
+ # It is possible to manualy register any OAuth 2.0 authentication service using the `register_service` method:
16
+ #
17
+ # register_service(:foo,
18
+ # app_id: 'registered app id / client id',
19
+ # app_secret: 'registered app secret / client secret',
20
+ # auth_url: "https://foo.bar.com/o/oauth2/auth",
21
+ # token_url: "https://foo.bar.com/oauth2/v3/token",
22
+ # profile_url: "https://foo.bar/oauth2/v1/userinfo",
23
+ # scope: "profile email")
24
+ #
25
+ # The `.register_service` method will be automatically called for the following login services:
11
26
  #
12
27
  # - Facebook authentication using the Graph API, v. 2.3 - [see Facebook documentation](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.3).
13
28
  # - Google authentication using the OAuth 2.0 API - [see Google documentation](https://developers.google.com/identity/protocols/OAuth2WebServer).
14
29
  #
15
- # It's also possible to manualy register any OAuth 2.0 authentication service using the `register_service` method.
16
- #
17
- # To use this Controller in your application, make sure the following variables are set:
30
+ # To enjoy autorgistration for Facebook or Google, make sure the following environment variables are set:
18
31
  # ENV['FB_APP_ID'] = {facebook_app_id}
19
32
  # ENV['FB_APP_SECRET'] = {facebook_app_secret}
20
33
  # ENV['GOOGLE_APP_ID'] = {google_app_id}
21
34
  # ENV['GOOGLE_APP_SECRET'] = {google_app_secret}
22
35
  #
23
- # Add the following route to routes.rb file (as always, route placement order effects behavior):
36
+ # To add the OAuth routes to the routes, use the following short-cut method (add it to the `routes.rb`) file:
24
37
  #
25
38
  # create_auth_shared_route do |service, remote_user_id, email, full_remote_response|
26
39
  # # perform any specific app authentication logic such as saving the info.
27
40
  # # return the current user or false if the callback is called with an authentication failure.
28
41
  # end
29
42
  #
30
- # The `create_auth_shared_route` method is a shortcut for calling the `#shared_route` method with the relevant arguments and setting the OAuth2Ctrl callback.
43
+ # \* Notice that, as always, route placement order effects behavior, so that routes are checked according to order of creation.
44
+ #
45
+ # The `create_auth_shared_route` method is a shortcut taht calls the `#shared_route` method with the relevant arguments and sets the OAuth2Ctrl callback.
31
46
  #
32
47
  # Use the following links for social authentication:
33
48
  #
34
- # - Facebook: "/auth/facebook?redirect_after=/foo/bar"
35
- # - Google: "/auth/google?redirect_after=/foo/bar"
36
- # - foo_service: "/auth/foo_service?redirect_after=/foo/bar"
49
+ # - Facebook: "/auth/facebook"
50
+ # - Google: "/auth/google"
51
+ # - foo_service: "/auth/foo_service"
37
52
  #
53
+ # You can control the page to which the user will return once authentication is complete
54
+ # (even when authentication fails) by setting the "redirect_after" parameter into the GET request in the url. for example:
55
+ #
56
+ # - Google: "/auth/google?redirect_after=/foo/bar"
38
57
  class OAuth2Ctrl
39
58
 
40
- # Sets (or gets) the callback to be called after authentication is attempeted.
59
+ # Sets (or gets) the global callback to be called after authentication is attempeted.
41
60
  #
42
61
  # Accepts a block that will be called with the following parameters:
43
62
  # service_name:: the name of the service. i.e. :facebook, :google, etc'.
44
63
  # service_token:: the authentication token returned by the service. This token should be stored for future access
45
64
  # remote_user_id:: service's user id.
46
- # remote_user_email:: users primamry email, if registed with the service.
65
+ # remote_user_email:: user's primamry email, as (and if) registed with the service.
47
66
  # remote_response:: a Hash with the complete user data response (including email and id).
48
67
  #
49
- # If the authentication fails for Facebook, the block will be called with the following values `auth_callback.call(nil, ni, {server: :responce, might_be: :empty})`
68
+ # If the authentication fails for the service, the block will be called with the following values `auth_callback.call(nil, ni, {server: :responce, might_be: :empty})`
50
69
  #
51
70
  # The block will be run in the context of the controller and all the controller's methods will be available to it.
52
71
  #
53
72
  # i.e.:
54
- # OAuth2Ctrl.auth_callback |service, service_token, id, email, res| cookies["#{service}_pl_auth_token".to_sym], cookies["#{service}_user_id".to_sym], cookies["#{service}_user_email".to_sym] = service_token, id, email }
73
+ # OAuth2Ctrl.auth_callback |service, service_token, id, email, full_res| PL.info "OAuth got: #{full_res.to_s}"; cookies["#{service}_pl_auth_token".to_sym], cookies["#{service}_user_id".to_sym], cookies["#{service}_user_email".to_sym] = service_token, id, email }
55
74
  #
56
75
  # defaults to the example above, which isn't a very sercure behavior, but allows for easy testing.
57
76
  def self.auth_callback &block
58
- block_given? ? (@auth_callback = block) : ( @auth_callback ||= (Proc.new {|service, service_token, id, email, res| Plezi.info "deafult callback called for #{service}, with response: #{res.to_s}"; cookies["#{service}_pl_auth_token".to_sym], cookies["#{service}_user_id".to_sym], cookies["#{service}_user_email".to_sym] = service_token, id, email}) )
77
+ block_given? ? (@@auth_callback = block) : ( @@auth_callback ||= (Proc.new {|service, service_token, id, email, res| Plezi.info "deafult callback called for #{service}, with response: #{res.to_s}"; cookies["#{service}_pl_auth_token".to_sym], cookies["#{service}_user_id".to_sym], cookies["#{service}_user_email".to_sym] = service_token, id, email}) )
59
78
  end
60
79
 
61
80
 
@@ -69,20 +88,20 @@ module Plezi
69
88
  # options:: a Hash of options, some of which are required.
70
89
  #
71
90
  # The options are:
72
- # app_id:: the aplication's unique ID registered with the service. i.e. ENV[FB_APP_ID] (storing these in environment variables is safer then hardcoding them)
73
- # app_secret:: the aplication's unique secret registered with the service.
74
- # auth_url:: the authentication URL. This is the url to which the user is redirected. i.e.: "https://www.facebook.com/dialog/oauth"
75
- # token_url:: the token request URL. This is the url used to switch the single-use code into a persistant authentication token. i.e.: "https://www.googleapis.com/oauth2/v3/token"
76
- # profile_url:: the URL used to ask the service for the user's profile (the service's API url). i.e.: "https://graph.facebook.com/v2.3/me"
91
+ # app_id:: Required. The aplication's unique ID registered with the service. i.e. ENV [FB_APP_ID] (storing these in environment variables is safer then hardcoding them)
92
+ # app_secret:: Required. The aplication's unique secret registered with the service.
93
+ # auth_url:: Required. The authentication URL. This is the url to which the user is redirected. i.e.: "https://www.facebook.com/dialog/oauth"
94
+ # token_url:: Required. The token request URL. This is the url used to switch the single-use code into a persistant authentication token. i.e.: "https://www.googleapis.com/oauth2/v3/token"
95
+ # profile_url:: Required. The URL used to ask the service for the user's profile (the service's API url). i.e.: "https://graph.facebook.com/v2.3/me"
77
96
  # scope:: a String representing the scope requested. i.e. 'email profile'.
78
97
  #
79
- # There will be an attempt to aitomatically register Facebook and Google login services under these conditions:
98
+ # There will be an attempt to automatically register Facebook and Google login services under these conditions:
80
99
  #
81
- # * For Facebook: Both ENV['FB_APP_ID'] && ENV['FB_APP_SECRET'] have been defined.
82
- # * For Google: Both ENV['GOOGLE_APP_ID'] && ENV['GOOGLE_APP_SECRET'] have been defined.
100
+ # * For Facebook: Both ENV ['FB_APP_ID'] && ENV ['FB_APP_SECRET'] have been defined.
101
+ # * For Google: Both ENV ['GOOGLE_APP_ID'] && ENV ['GOOGLE_APP_SECRET'] have been defined.
83
102
  #
84
103
  #
85
- # Just for reference, at the time of this writing:
104
+ # The auto registration uses the following urls (updated to June 5, 2015):
86
105
  #
87
106
  # * facebook auth_url: "https://www.facebook.com/dialog/oauth"
88
107
  # * facebook token_url: "https://graph.facebook.com/v2.3/oauth/access_token"
@@ -91,6 +110,8 @@ module Plezi
91
110
  # * google token_url: "https://www.googleapis.com/oauth2/v3/token"
92
111
  # * google profile_url: "https://www.googleapis.com/plus/v1/people/me"
93
112
  #
113
+ # to change the default url's for Facebook or Google, simpley re-register the service using this method.
114
+ #
94
115
  def self.register_service service_name, options
95
116
  raise "Cannot register service, missing required information." unless service_name && options[:auth_url] && options[:token_url] && options[:profile_url] && options[:app_id] && options[:app_secret]
96
117
  # for google, scope is space delimited. for facebook it's comma delimited
@@ -201,8 +222,8 @@ end
201
222
  #
202
223
  # The method can be called only once and will self-destruct.
203
224
  def create_auth_shared_route options = {}, &block
204
- shared_route "auth/(:id)/(:code)" , Plezi::OAuth2Ctrl
225
+ shared_route "auth/(:id)" , Plezi::OAuth2Ctrl
205
226
  undef create_auth_shared_route
206
- Plezi::OAuth2Ctrl.auth_callback = block if block
227
+ Plezi::OAuth2Ctrl.auth_callback &block if block
207
228
  Plezi::OAuth2Ctrl
208
229
  end
@@ -296,6 +296,17 @@ module Plezi
296
296
  end
297
297
  end
298
298
 
299
+
300
+ # # HTTPProtocol - to do list
301
+ #
302
+ # XML::
303
+ # support for XML HTTP body types?
304
+ #
305
+ # Charset::
306
+ # parse chareset for incoming content-type in the multipart request body? (or leave if binary?)
307
+
308
+
309
+
299
310
  ## Heroku/extra headers info
300
311
 
301
312
  # All headers are considered to be case-insensitive, as per HTTP Specification.
@@ -0,0 +1,172 @@
1
+ module Plezi
2
+
3
+ # Websocket client objects are members of this class.
4
+ #
5
+ # This is a VERY simple Websocket client. It doesn't support cookies, HTTP authentication or... well... anything, really.
6
+ # It's just a simple client used for the Plezi framework's testing. It's usful for simple WebSocket connections, but no more.
7
+ class WebsocketClient
8
+ attr_accessor :response, :request
9
+
10
+ class RequestEmulator < Hash
11
+ def service
12
+ self[:connection]
13
+ end
14
+ end
15
+
16
+ def initialize request
17
+ @response = WSResponse.new request
18
+ @options = request[:options]
19
+ @on_message = @options[:on_message]
20
+ raise "Websocket client must have an #on_message Proc." unless @on_message && @on_message.is_a?(Proc)
21
+ @on_connect = @options[:on_connect]
22
+ @on_disconnect = @options[:on_disconnect]
23
+ end
24
+
25
+ def on_message(data = false, &block)
26
+ unless data
27
+ @on_message = block if block
28
+ return @on_message
29
+ end
30
+ instance_exec( data, &@on_message)
31
+ end
32
+
33
+ def on_connect(&block)
34
+ if block
35
+ @on_connect = block
36
+ return @on_connect
37
+ end
38
+ instance_exec(&@on_connect) if @on_connect
39
+ end
40
+
41
+ def on_disconnect(&block)
42
+ if block
43
+ @on_disconnect = block
44
+ return @on_disconnect
45
+ end
46
+ instance_exec(&@on_disconnect) if @on_disconnect
47
+ end
48
+
49
+ #disconnects the Websocket.
50
+ def disconnect
51
+ @response.close if @response
52
+ end
53
+
54
+ # sends data through the socket. a shortcut for ws_client.response <<
55
+ def << data
56
+ @response << data
57
+ end
58
+
59
+ # Create a simple Websocket Client(!)
60
+ #
61
+ # This method accepts two parameters:
62
+ # url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
63
+ # options:: a Hash with options to be used. The options will be used to define
64
+ # &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
65
+ #
66
+ # The method will either return a WebsocketClient instance object or it will raise an exception.
67
+ #
68
+ # An on_message Proc must be defined, or the method will fail.
69
+ #
70
+ # The on_message Proc can be defined using the optional block:
71
+ #
72
+ # WebsocketClient.connect_to("ws://localhost:3000/") {|data| response << data} #echo example
73
+ #
74
+ # OR, the on_message Proc can be defined using the options Hash:
75
+ #
76
+ # WebsocketClient.connect_to("ws://localhost:3000/", on_connect: -> {}, on_message: -> {|data| response << data})
77
+ #
78
+ # The #on_message(data), #on_connect and #on_disconnect methods will be executed within the context of the WebsocketClient
79
+ # object, and will have natice acess to the Websocket response object.
80
+ #
81
+ # After the WebsocketClient had been created, it's possible to update the #on_message and #on_disconnect methods:
82
+ #
83
+ # # updates #on_message
84
+ # wsclient.on_message do |data|
85
+ # response << "I'll disconnect on the next message!"
86
+ # # updates #on_message again.
87
+ # on_message {|data| disconnect }
88
+ # end
89
+ #
90
+ #
91
+ # !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
92
+ # so that even SSL connections are subject to a possible man in the middle attack.
93
+ def self.connect_to url, options={}, &block
94
+ options[:on_message] ||= block
95
+ options[:handler] = WebsocketClient
96
+ options[:protocol] = EventMachine::Protocol
97
+ url = URI.parse(url) unless url.is_a?(URI)
98
+ connection_type = EventMachine::Connection
99
+
100
+ socket = false #implement the connection, ssl vs. no ssl
101
+ if url.scheme == "https" || url.scheme == "wss"
102
+ connection_type = EventMachine::SSLConnection
103
+ options[:ssl_client] = true
104
+ url.port ||= 443
105
+ end
106
+ url.port ||= 80
107
+ socket = TCPSocket.new(url.host, url.port)
108
+ connection = connection_type.new socket, options
109
+ psedo_request = RequestEmulator.new
110
+ psedo_request[:connection] = connection
111
+ psedo_request[:client_ip] = 'WS Client'
112
+ psedo_request[:url] = url
113
+ psedo_request[:options] = options
114
+ WSProtocol.client_handshake psedo_request
115
+ connection.handler
116
+ rescue => e
117
+ socket.close if socket
118
+ raise e
119
+ end
120
+
121
+ end
122
+
123
+ class WSProtocol < EventMachine::Protocol
124
+ def self.client_handshake psedo_request, timeout = 5
125
+ connection = psedo_request[:connection]
126
+ url = psedo_request[:url]
127
+ # send protocol upgrade request
128
+ websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*')
129
+ connection.send "GET #{url.path}#{url.query.to_s.empty? ? '' : ('?' + url.query)} HTTP/1.1\r\nHost: #{url.host}#{url.port ? (':'+url.port.to_s) : ''}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
130
+ # wait for answer - make sure we don't over-read
131
+ # (a websocket message might be sent immidiately after connection is established)
132
+ reply = ''
133
+ reply.force_encoding('binary')
134
+ start_time = Time.now
135
+ stop_reply = "\r\n\r\n"
136
+ until reply[-4..-1] == stop_reply
137
+ (reply << connection.read(1)) rescue (sleep 0.1)
138
+ raise Timeout::Error, "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply.dump}" if Time.now >= (start_time + 5)
139
+ end
140
+ # review reply
141
+ raise 'Connection Refused.' unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i)
142
+ raise 'Websocket Key Authentication failed.' unless reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i) && reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i)[1] == Digest::SHA1.base64digest(websocket_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
143
+ # set-up handler response object.
144
+ connection.handler = WebsocketClient.new psedo_request
145
+
146
+ # raise "not yet implemented"
147
+
148
+ # set the connetion's protocol to a new WSProtocol instance
149
+ connection.protocol = self.new psedo_request[:connection], psedo_request[:options]
150
+ # add the socket to the EventMachine IO reactor
151
+ EventMachine.add_io connection.socket, connection
152
+ true
153
+ end
154
+ end
155
+ end
156
+
157
+
158
+ ######
159
+ ## example requests
160
+
161
+ # GET /nickname HTTP/1.1
162
+ # Upgrade: websocket
163
+ # Connection: Upgrade
164
+ # Host: localhost:3000
165
+ # Origin: https://www.websocket.org
166
+ # Cookie: test=my%20cookies; user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
167
+ # Pragma: no-cache
168
+ # Cache-Control: no-cache
169
+ # Sec-WebSocket-Key: 1W9B64oYSpyRL/yuc4k+Ww==
170
+ # Sec-WebSocket-Version: 13
171
+ # Sec-WebSocket-Extensions: x-webkit-deflate-frame
172
+ # User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
data/lib/plezi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plezi
2
- VERSION = "0.8.4"
2
+ VERSION = "0.8.5"
3
3
  end
data/resources/Gemfile CHANGED
@@ -86,3 +86,4 @@ gem 'plezi'
86
86
  ## but first, please remember that AcriveRecord needs extra attention when multi-threading
87
87
  # gem 'activerecord', :require => 'active_record'
88
88
  # gem 'bcrypt', '~> 3.1.7'
89
+ # gem 'standalone_migrations' # will provide better rake tasks support for ActiveRecord
@@ -0,0 +1,33 @@
1
+ # The following are common database setting as recomended by Heroku:
2
+ default: &default
3
+ adapter: postgresql
4
+ host: localhost
5
+ username: user
6
+
7
+ development:
8
+ <<: *default
9
+ database: app-dev
10
+
11
+ production:
12
+ <<: *default
13
+ database: app-dev
14
+
15
+ # # Here is an Sqlite3 example DB.
16
+ # # You can use it to update the default if you want to use Sqlite3
17
+ # sqlite3_db:
18
+ # adapter: sqlite3
19
+ # pool: 5
20
+ # timeout: 5000
21
+ # database: db/db.sqlite3
22
+
23
+
24
+ # # Here is a MySQL example DB.
25
+ # # You can use it to update the default if you want to use MySQL
26
+ # mysql_db:
27
+ # adapter: mysql2
28
+ # encoding: utf8
29
+ # username: root
30
+ # password: password
31
+ # host: localhost
32
+ # port: 3306
33
+ # database: database_name
@@ -7,19 +7,13 @@
7
7
  #
8
8
  ############
9
9
  ## ActiveRecord without Rails
10
- # more info @:
10
+ # more info at:
11
11
  # https://www.youtube.com/watch?v=o0SzhgYntK8
12
12
  # demo code here:
13
13
  # https://github.com/stungeye/ActiveRecord-without-Rails
14
14
  if defined? ActiveRecord
15
-
16
- if defined? SQLite3
17
- ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => Root.join('db','db.sqlite3').to_s, encoding: 'unicode'
18
- elsif defined? PG
19
- ActiveRecord::Base.establish_connection :adapter => 'postgresql', encoding: 'unicode'
20
- else
21
- Plezi.logger.warning "ActiveRecord database adapter not auto-set. Update the db_ac_config.rb file"
22
- end
15
+ puts "Loading ActiveRecord database setting: #{ENV['ENV']}"
16
+ ActiveRecord::Base.establish_connection( YAML::load( File.open( Root.join('db', 'config.yml').to_s ) )[ ENV["ENV"].to_s ] )
23
17
 
24
18
  # if debugging purposes, uncomment this line to see the ActiveRecord's generated SQL:
25
19
  # ActiveRecord::Base.logger = Plezi.logger
@@ -27,32 +21,39 @@ if defined? ActiveRecord
27
21
  # Uncomment this line to make the logger output look nicer in Windows.
28
22
  # ActiveSupport::LogSubscriber.colorize_logging = false
29
23
 
24
+ # Load ActiveRecord Tasks, if implemented
30
25
  if defined? Rake
31
- ##########
32
- # start rake segment
33
-
34
- namespace :db do
35
-
36
- desc "Migrate the database so that it is fully updated, using db/migrate."
37
- task :migrate do
38
- ActiveRecord::Base.logger = Plezi.logger
39
- ActiveRecord::Migrator.migrate(Root.join('db', 'migrate').to_s, ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
40
- end
41
-
42
- desc "Seed the database using the db/seeds.rb file"
43
- task :seed do
44
- if ::File.exists? Root.join('db','seeds.rb').to_s
45
- load Root.join('db','seeds.rb').to_s
46
- else
47
- puts "the seeds file doesn't exists. please create a seeds.db file and place it in the db folder for the app."
48
- end
49
- end
50
-
51
- end
52
-
53
- # end rake segment
54
- ##########
26
+
27
+ begin
28
+ require 'standalone_migrations'
29
+ StandaloneMigrations::Tasks.load_tasks
30
+ rescue Exception => e
31
+ ActiveRecord::Tasks::DatabaseTasks.env = ENV['ENV'] || 'development'
32
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = YAML.load(File.read(Root.join('db', 'config.yml').to_s))
33
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = Root.join('db').to_s
34
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = Root.join( 'db', 'fixtures').to_s
35
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [Root.join('db', 'migrate').to_s]
36
+ ActiveRecord::Tasks::DatabaseTasks.seed_loader = Class.new do
37
+ def self.load_seed
38
+ filename = Root.join('db', 'seeds.rb').to_s
39
+ unless File.file?(filename)
40
+ IO.write filename, ''
41
+ end
42
+ load filename
43
+ end
44
+ end
45
+ ActiveRecord::Tasks::DatabaseTasks.root = Root.to_s
46
+
47
+ task :environment do
48
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration
49
+ ActiveRecord::Base.establish_connection ActiveRecord::Tasks::DatabaseTasks.env.to_sym
50
+ end
51
+
52
+ load 'active_record/railties/databases.rake'
53
+
54
+ end
55
55
  end
56
56
 
57
+
57
58
  end
58
59