firebase-ruby 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/bin/fbrb +51 -0
- data/firebase-ruby.gemspec +18 -0
- data/lib/firebase-ruby.rb +4 -0
- data/lib/firebase-ruby/auth.rb +85 -0
- data/lib/firebase-ruby/database.rb +86 -0
- data/lib/firebase-ruby/http.rb +172 -0
- data/lib/firebase-ruby/logger.rb +24 -0
- data/lib/firebase-ruby/trollop.rb +911 -0
- data/lib/firebase-ruby/version.rb +5 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 693b989e038daf52ac6f06a2a978614a3d0fee2b
|
4
|
+
data.tar.gz: 4c88d12f7167b180dc8cc5b7707a700e1e803f63
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a8319b29c0b88f9640d86604fae62bec3a323a243f40caf4a0c32b56db2f5d6914945ed0d3ed80367a5a751cf98565367679cbe55118ffe3680f54c63687b4f
|
7
|
+
data.tar.gz: 75677e5b82c80ebf6302abf25a995ba7126c3af2f467db9a59f6605b288bb1434fd9e325b1cd68e61ce66a64acda1fed70e879586a2c2f95f46d86a0615ee93d
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Ken J.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
data/bin/fbrb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'firebase-ruby/trollop'
|
3
|
+
require 'firebase-ruby'
|
4
|
+
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
banner "fbrb [options] <URL>"
|
8
|
+
opt :data, 'HTTP POST data', type: :string
|
9
|
+
opt :id, 'Project ID', type: :string
|
10
|
+
opt :key, 'JSON file with private key', type: :string
|
11
|
+
opt :log, 'Log file', type: :string
|
12
|
+
opt :path, 'Path', type: :string
|
13
|
+
opt :request, 'Specify request command to use', type: :string, short: 'X', default: 'get'
|
14
|
+
opt :verbose, 'Verbose mode'
|
15
|
+
end
|
16
|
+
|
17
|
+
if opts[:log_given]
|
18
|
+
Firebase.logger = Logger.new(opts[:log])
|
19
|
+
Firebase.logger.level = Logger::WARN
|
20
|
+
end
|
21
|
+
|
22
|
+
if opts[:verbose]
|
23
|
+
Firebase.logger = Logger.new(STDOUT) unless opts[:log_given]
|
24
|
+
Firebase.logger.level = Logger::DEBUG
|
25
|
+
end
|
26
|
+
log = Firebase.logger
|
27
|
+
|
28
|
+
log.debug("Command line arguments: #{opts}")
|
29
|
+
|
30
|
+
path = opts[:path]
|
31
|
+
path ||= ARGV.shift
|
32
|
+
|
33
|
+
Trollop::die :path, "is missing" if path.nil?
|
34
|
+
|
35
|
+
db = Firebase::Database.new()
|
36
|
+
db.set_auth_with_keyfile(opts[:key])
|
37
|
+
|
38
|
+
method = opts[:request].downcase.to_sym
|
39
|
+
|
40
|
+
case method
|
41
|
+
when :get, :delete
|
42
|
+
data = db.public_send(method, path)
|
43
|
+
when :put, :patch, :post
|
44
|
+
if opts[:data_given]
|
45
|
+
data = db.public_send(method, path, opts[:data])
|
46
|
+
else
|
47
|
+
Trollop::die :data, "is missing"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts data if data
|
@@ -0,0 +1,18 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
|
2
|
+
require 'firebase-ruby/version'
|
3
|
+
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'firebase-ruby'
|
7
|
+
s.version = Firebase::Version
|
8
|
+
s.authors = ['Ken J.']
|
9
|
+
s.email = ['kenjij@gmail.com']
|
10
|
+
s.summary = %q{Pure simple Ruby based Firebase REST library}
|
11
|
+
s.description = %q{Firebase REST library written in pure Ruby without external dependancy.}
|
12
|
+
s.homepage = 'https://github.com/kenjij/firebase-ruby'
|
13
|
+
s.license = 'MIT'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split($/)
|
16
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module Firebase
|
4
|
+
|
5
|
+
class Auth
|
6
|
+
|
7
|
+
GOOGLE_JWT_SCOPE = 'https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/userinfo.email'
|
8
|
+
GOOGLE_JWT_AUD = 'https://www.googleapis.com/oauth2/v4/token'
|
9
|
+
GOOGLE_ALGORITHM = 'RS256'
|
10
|
+
GOOGLE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
11
|
+
GOOGLE_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
12
|
+
|
13
|
+
attr_reader :project_id
|
14
|
+
attr_reader :client_email
|
15
|
+
attr_reader :access_token
|
16
|
+
attr_reader :expires
|
17
|
+
|
18
|
+
def initialize(path)
|
19
|
+
load_privatekeyfile(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return a valid access token; it will retrieve a new token if necessary
|
23
|
+
def valid_token
|
24
|
+
return access_token if access_token && !expiring?
|
25
|
+
return access_token if request_access_token
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# If token is expiring within a minute
|
30
|
+
def expiring?
|
31
|
+
return true if expires - Time.now < 60
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
# If token has already expired
|
36
|
+
def expired?
|
37
|
+
return true if expires - Time.now <= 0
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @param path [String] JSON file with private key
|
44
|
+
def load_privatekeyfile(path)
|
45
|
+
raise ArgumentError, 'private key file path missing' unless path
|
46
|
+
Firebase.logger.debug("Loading private key file: #{path}")
|
47
|
+
cred = JSON.parse(IO.read(path), {symbolize_names: true})
|
48
|
+
@private_key = cred[:private_key]
|
49
|
+
@project_id = cred[:project_id]
|
50
|
+
@client_email = cred[:client_email]
|
51
|
+
Firebase.logger.info('Auth.load_privatekeyfile done.')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Request new token from Google
|
55
|
+
def request_access_token
|
56
|
+
Firebase.logger.info('Requesting access token to Google')
|
57
|
+
res = HTTP.post_form(GOOGLE_TOKEN_URL, jwt)
|
58
|
+
Firebase.logger.debug("HTTP response code: #{res[:code]}")
|
59
|
+
if res.class == Hash && res[:code] == 200
|
60
|
+
data = JSON.parse(res[:body], {symbolize_names: true})
|
61
|
+
@access_token = data[:access_token]
|
62
|
+
@expires = Time.now + data[:expires_in]
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate JWT claim
|
69
|
+
def jwt
|
70
|
+
pkey = OpenSSL::PKey::RSA.new(@private_key)
|
71
|
+
now_ts = Time.now.to_i
|
72
|
+
payload = {
|
73
|
+
iss: client_email,
|
74
|
+
scope: GOOGLE_JWT_SCOPE,
|
75
|
+
aud: GOOGLE_JWT_AUD,
|
76
|
+
iat: now_ts,
|
77
|
+
exp: now_ts + 60
|
78
|
+
}
|
79
|
+
jwt = JWT.encode payload, pkey, GOOGLE_ALGORITHM
|
80
|
+
return {grant_type: GOOGLE_GRANT_TYPE, assertion: jwt}
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Firebase
|
2
|
+
|
3
|
+
class Database
|
4
|
+
|
5
|
+
FIREBASE_URL_TEMPLATE = 'https://%s.firebaseio.com/'
|
6
|
+
|
7
|
+
attr_accessor :auth, :print, :shallow
|
8
|
+
|
9
|
+
def initialize()
|
10
|
+
end
|
11
|
+
|
12
|
+
def set_auth_with_keyfile(filepath)
|
13
|
+
@auth = Auth.new(filepath)
|
14
|
+
end
|
15
|
+
|
16
|
+
def project_id=(id)
|
17
|
+
@project_id = id
|
18
|
+
end
|
19
|
+
|
20
|
+
def project_id
|
21
|
+
return @project_id if @project_id
|
22
|
+
return auth.project_id if auth
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(path)
|
27
|
+
return operate(__method__, path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(path, data)
|
31
|
+
return operate(__method__, path, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def patch(path, data)
|
35
|
+
return operate(__method__, path, data)
|
36
|
+
end
|
37
|
+
|
38
|
+
def post(path, data)
|
39
|
+
return operate(__method__, path, data)
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(path)
|
43
|
+
return operate(__method__, path)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def operate(method, path, data = nil)
|
49
|
+
case method
|
50
|
+
when :get, :delete
|
51
|
+
res_data = http.public_send(method, path: format_path(path))
|
52
|
+
when :put, :patch, :post
|
53
|
+
data = JSON.fast_generate(data) if data.class == Hash
|
54
|
+
res_data = http.public_send(method, path: format_path(path), body: data)
|
55
|
+
end
|
56
|
+
return handle_response_data(res_data)
|
57
|
+
end
|
58
|
+
|
59
|
+
def http
|
60
|
+
unless @http
|
61
|
+
url = FIREBASE_URL_TEMPLATE % project_id
|
62
|
+
headers = {
|
63
|
+
'Authorization' => "Bearer #{auth.valid_token}",
|
64
|
+
'Content-Type' => 'application/json'
|
65
|
+
}
|
66
|
+
@http = HTTP.new(url, headers)
|
67
|
+
end
|
68
|
+
return @http
|
69
|
+
end
|
70
|
+
|
71
|
+
def format_path(path)
|
72
|
+
path = '/' + path unless path.start_with?('/')
|
73
|
+
return path + '.json'
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_response_data(data)
|
77
|
+
if data[:code] != 200
|
78
|
+
Firebase.logger.error("HTTP response error: #{data[:code]}\n#{data[:message]}")
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
return JSON.parse(data[:body], {symbolize_names: true})
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
|
5
|
+
module Firebase
|
6
|
+
|
7
|
+
class HTTP
|
8
|
+
|
9
|
+
METHOD_HTTP_CLASS = {
|
10
|
+
get: Net::HTTP::Get,
|
11
|
+
put: Net::HTTP::Put,
|
12
|
+
patch: Net::HTTP::Patch,
|
13
|
+
post: Net::HTTP::Post,
|
14
|
+
delete: Net::HTTP::Delete
|
15
|
+
}
|
16
|
+
|
17
|
+
def self.get(url, params)
|
18
|
+
h = HTTP.new(url)
|
19
|
+
data = h.get(params: params)
|
20
|
+
h.close
|
21
|
+
return data
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.post_form(url, params)
|
25
|
+
h = HTTP.new(url)
|
26
|
+
data = h.post(params: params)
|
27
|
+
h.close
|
28
|
+
return data
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :init_uri, :http
|
32
|
+
attr_accessor :headers
|
33
|
+
|
34
|
+
def initialize(url, hdrs = nil)
|
35
|
+
@init_uri = URI(url)
|
36
|
+
raise ArgumentError, 'Invalid URL' unless @init_uri.class <= URI::HTTP
|
37
|
+
@http = Net::HTTP.new(init_uri.host, init_uri.port)
|
38
|
+
http.use_ssl = init_uri.scheme == 'https'
|
39
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
40
|
+
self.headers = hdrs
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(path: nil, params: nil, query: nil)
|
44
|
+
return operate(__method__, path: path, params: params, query: query)
|
45
|
+
end
|
46
|
+
|
47
|
+
def post(path: nil, params: nil, body: nil, query: nil)
|
48
|
+
return operate(__method__, path: path, params: params, body: body, query: query)
|
49
|
+
end
|
50
|
+
|
51
|
+
def put(path: nil, params: nil, body: nil, query: nil)
|
52
|
+
return operate(__method__, path: path, params: params, body: body, query: query)
|
53
|
+
end
|
54
|
+
|
55
|
+
def patch(path: nil, params: nil, body: nil, query: nil)
|
56
|
+
return operate(__method__, path: path, params: params, body: body, query: query)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete(path: nil, params: nil, query: nil)
|
60
|
+
return operate(__method__, path: path, params: params, query: query)
|
61
|
+
end
|
62
|
+
|
63
|
+
def close
|
64
|
+
http.finish if http.started?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def operate(method, path: nil, params: nil, body: nil, query: nil)
|
70
|
+
uri = uri_with_path(path)
|
71
|
+
case method
|
72
|
+
when :get, :delete
|
73
|
+
if params
|
74
|
+
query = URI.encode_www_form(params)
|
75
|
+
Firebase.logger.info('Created urlencoded query from params')
|
76
|
+
else
|
77
|
+
uri.query = query
|
78
|
+
end
|
79
|
+
req = METHOD_HTTP_CLASS[method].new(uri)
|
80
|
+
when :put, :patch, :post
|
81
|
+
uri.query = query if query
|
82
|
+
req = METHOD_HTTP_CLASS[method].new(uri)
|
83
|
+
if params
|
84
|
+
req.form_data = params
|
85
|
+
Firebase.logger.info('Created form data from params')
|
86
|
+
elsif body
|
87
|
+
req.body = body
|
88
|
+
end
|
89
|
+
else
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
data = send(req)
|
93
|
+
data = redirect(method, uri, params: params, body: body, query: query) if data.class <= URI::HTTP
|
94
|
+
return data
|
95
|
+
end
|
96
|
+
|
97
|
+
def uri_with_path(path)
|
98
|
+
uri = init_uri.clone
|
99
|
+
uri.path = path unless path.nil?
|
100
|
+
return uri
|
101
|
+
end
|
102
|
+
|
103
|
+
def send(req)
|
104
|
+
inject_headers_to(req)
|
105
|
+
unless http.started?
|
106
|
+
Firebase.logger.info('HTTP session not started; starting now')
|
107
|
+
http.start
|
108
|
+
Firebase.logger.debug("Opened connection to #{http.address}:#{http.port}")
|
109
|
+
end
|
110
|
+
Firebase.logger.debug("Sending HTTP #{req.method} request to #{req.path}")
|
111
|
+
Firebase.logger.debug("Body size: #{req.body.length}") if req.request_body_permitted?
|
112
|
+
res = http.request(req)
|
113
|
+
return handle_response(res)
|
114
|
+
end
|
115
|
+
|
116
|
+
def inject_headers_to(req)
|
117
|
+
return if headers.nil?
|
118
|
+
headers.each do |k, v|
|
119
|
+
req[k] = v
|
120
|
+
end
|
121
|
+
Firebase.logger.info('Header injected into HTTP request header')
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_response(res)
|
125
|
+
if res.connection_close?
|
126
|
+
Firebase.logger.info('HTTP response header says connection close; closing session now')
|
127
|
+
close
|
128
|
+
end
|
129
|
+
case res
|
130
|
+
when Net::HTTPRedirection
|
131
|
+
Firebase.logger.info('HTTP response was a redirect')
|
132
|
+
data = URI(res['Location'])
|
133
|
+
if data.class == URI::Generic
|
134
|
+
data = uri_with_path(res['Location'])
|
135
|
+
Firebase.logger.debug("Full URI object built for local redirect with path: #{data.path}")
|
136
|
+
end
|
137
|
+
# when Net::HTTPSuccess
|
138
|
+
# when Net::HTTPClientError
|
139
|
+
# when Net::HTTPServerError
|
140
|
+
else
|
141
|
+
data = {
|
142
|
+
code: res.code.to_i,
|
143
|
+
headers: res.to_hash,
|
144
|
+
body: res.body,
|
145
|
+
message: res.msg
|
146
|
+
}
|
147
|
+
end
|
148
|
+
return data
|
149
|
+
end
|
150
|
+
|
151
|
+
def redirect(method, uri, params: nil, body: nil, query: nil)
|
152
|
+
if uri.host == init_uri.host && uri.port == init_uri.port
|
153
|
+
Firebase.logger.info("Local #{method.upcase} redirect, reusing HTTP session")
|
154
|
+
new_http = http
|
155
|
+
else
|
156
|
+
Firebase.logger.info("External #{method.upcase} redirect, spawning new HTTP object")
|
157
|
+
new_http = HTTP.new("#{uri.scheme}://#{uri.host}#{uri.path}", headers)
|
158
|
+
end
|
159
|
+
case method
|
160
|
+
when :get, :delete
|
161
|
+
data = operate(method, uri, params: params, query: query)
|
162
|
+
when :put, :patch, :post
|
163
|
+
data = new_http.public_send(method, uri, params: params, body: body, query: query)
|
164
|
+
else
|
165
|
+
data = nil
|
166
|
+
end
|
167
|
+
return data
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|