plezi 0.8.4 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
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