etna 0.1.11
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.
- checksums.yaml +7 -0
- data/lib/etna/application.rb +90 -0
- data/lib/etna/auth.rb +128 -0
- data/lib/etna/client.rb +144 -0
- data/lib/etna/command.rb +26 -0
- data/lib/etna/controller.rb +111 -0
- data/lib/etna/cross_origin.rb +71 -0
- data/lib/etna/describe_routes.rb +31 -0
- data/lib/etna/errors.rb +37 -0
- data/lib/etna/ext.rb +13 -0
- data/lib/etna/hmac.rb +86 -0
- data/lib/etna/logger.rb +29 -0
- data/lib/etna/parse_body.rb +41 -0
- data/lib/etna/route.rb +165 -0
- data/lib/etna/server.rb +84 -0
- data/lib/etna/sign_service.rb +67 -0
- data/lib/etna/spec/auth.rb +28 -0
- data/lib/etna/spec.rb +1 -0
- data/lib/etna/symbolize_params.rb +30 -0
- data/lib/etna/test_auth.rb +80 -0
- data/lib/etna/user.rb +79 -0
- data/lib/etna.rb +17 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6b95bd70a5a778deaaeb05408e95dd043dfd41914993986d770cf8c29f6ba54b
|
4
|
+
data.tar.gz: 9e2d68c4fcb362108ce3ebf203cf7e88fba5045e6b87164c13f7feb4a9811e17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dfae596fc44742b0fd087cabd93ca8a2132f8501662cab18a2c46e86d33db9a84852ff061299bf5e694f70da1a62ed1b0057e6523799cec19b031fec3cd171ae
|
7
|
+
data.tar.gz: 2146b65d50ea6c9b610a762c3043748113a79c3f603fe0366f3519c81314f36983d0ea59d542e4a512ea01f8e0862a3e1f403884b755d077e75de53c5dae72a9
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# a module to use for base application classes
|
2
|
+
# it is an included module rather than a class to spare
|
3
|
+
# us the burden of writing class Blah < Etna::Application
|
4
|
+
# whenever we want to use it as a container
|
5
|
+
|
6
|
+
require_relative './sign_service'
|
7
|
+
require 'singleton'
|
8
|
+
|
9
|
+
module Etna::Application
|
10
|
+
def self.included(other)
|
11
|
+
other.include Singleton
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.find(klass)
|
15
|
+
Kernel.const_get(
|
16
|
+
klass.name.split('::').first
|
17
|
+
).instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.register(app)
|
21
|
+
@instance = app
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.instance
|
25
|
+
@instance
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
Etna::Application.register(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure(opts)
|
33
|
+
@config = opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_logger
|
37
|
+
@logger = Etna::Logger.new(
|
38
|
+
# The name of the log_file, required.
|
39
|
+
config(:log_file),
|
40
|
+
# Number of old copies of the log to keep.
|
41
|
+
config(:log_copies) || 5,
|
42
|
+
# How large the log can get before overturning.
|
43
|
+
config(:log_size) || 1048576
|
44
|
+
)
|
45
|
+
log_level = (config(:log_level) || 'warn').upcase.to_sym
|
46
|
+
|
47
|
+
@logger.level = Logger.const_defined?(log_level) ? Logger.const_get(log_level) : Logger::WARN
|
48
|
+
end
|
49
|
+
|
50
|
+
# the application logger is available globally
|
51
|
+
attr_reader :logger
|
52
|
+
|
53
|
+
def config(type)
|
54
|
+
@config[environment][type]
|
55
|
+
end
|
56
|
+
|
57
|
+
def sign
|
58
|
+
@sign ||= Etna::SignService.new(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def environment
|
62
|
+
(ENV["#{self.class.name.upcase}_ENV"] || :development).to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_descendents(klass)
|
66
|
+
ObjectSpace.each_object(Class).select do |k|
|
67
|
+
k < klass
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_command(config, cmd = :help, *args)
|
72
|
+
cmd = cmd.to_sym
|
73
|
+
if commands.key?(cmd)
|
74
|
+
commands[cmd].setup(config)
|
75
|
+
commands[cmd].execute(*args)
|
76
|
+
else
|
77
|
+
commands[:help].execute
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def commands
|
82
|
+
@commands ||= Hash[
|
83
|
+
find_descendents(Etna::Command).map do |c|
|
84
|
+
cmd = c.new
|
85
|
+
[ cmd.name, cmd ]
|
86
|
+
end
|
87
|
+
]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
data/lib/etna/auth.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require_relative 'user'
|
2
|
+
require_relative 'hmac'
|
3
|
+
|
4
|
+
module Etna
|
5
|
+
class Auth
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
|
13
|
+
# There are three ways to authenticate.
|
14
|
+
# Either the route does not require auth,
|
15
|
+
# you have an hmac or you have a valid token.
|
16
|
+
# Both of these will not validate individual
|
17
|
+
# permissions; this is up to the controller
|
18
|
+
if [ approve_noauth(request), approve_hmac(request), approve_user(request) ].all?{|approved| !approved}
|
19
|
+
return fail_or_redirect(request)
|
20
|
+
end
|
21
|
+
|
22
|
+
@app.call(request.env)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def application
|
28
|
+
@application ||= Etna::Application.instance
|
29
|
+
end
|
30
|
+
|
31
|
+
def server
|
32
|
+
@server ||= application.class.const_get(:Server)
|
33
|
+
end
|
34
|
+
|
35
|
+
def params(request)
|
36
|
+
request.env['rack.request.params']
|
37
|
+
end
|
38
|
+
|
39
|
+
def auth(request, type)
|
40
|
+
(etna_param(request, :authorization, nil) || '')[/\A#{type.capitalize} (.*)\z/,1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.etna_url_param(item)
|
44
|
+
:"X-Etna-#{item.to_s.split(/_/).map(&:capitalize).join('-')}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def etna_param(request, item, fill='X_ETNA_')
|
48
|
+
# This comes either from header variable
|
49
|
+
# HTTP_X_SOME_NAME or parameter X-Etna-Some-Name
|
50
|
+
#
|
51
|
+
# We prefer the param so we can use the header elsewhere
|
52
|
+
|
53
|
+
params(request)[Etna::Auth.etna_url_param(item)] || request.env["HTTP_#{fill}#{item.upcase}"]
|
54
|
+
end
|
55
|
+
|
56
|
+
# If the application asks for a redirect for unauthorized users
|
57
|
+
def fail_or_redirect(request, msg = 'You are unauthorized')
|
58
|
+
return [ 401, { 'Content-Type' => 'text/html' }, [msg] ] unless application.config(:auth_redirect)
|
59
|
+
|
60
|
+
uri = URI(
|
61
|
+
application.config(:auth_redirect).chomp('/') + '/login'
|
62
|
+
)
|
63
|
+
uri.query = URI.encode_www_form(refer: request.url)
|
64
|
+
return [ 302, { 'Location' => uri.to_s }, [] ]
|
65
|
+
end
|
66
|
+
|
67
|
+
def approve_noauth(request)
|
68
|
+
route = server.find_route(request)
|
69
|
+
|
70
|
+
return route && route.noauth?
|
71
|
+
end
|
72
|
+
|
73
|
+
def approve_user(request)
|
74
|
+
token = request.cookies[application.config(:token_name)] || auth(request, :etna)
|
75
|
+
|
76
|
+
return false unless token
|
77
|
+
|
78
|
+
begin
|
79
|
+
payload, header = application.sign.jwt_decode(token)
|
80
|
+
return request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
|
81
|
+
rescue
|
82
|
+
# bail out if anything goes wrong
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Names and order of the fields to be signed.
|
88
|
+
def approve_hmac(request)
|
89
|
+
hmac_signature = etna_param(request, :signature)
|
90
|
+
|
91
|
+
return false unless hmac_signature
|
92
|
+
|
93
|
+
return false unless headers = etna_param(request, :headers)
|
94
|
+
|
95
|
+
headers = headers.split(/,/).map do |header|
|
96
|
+
[ header.to_sym, etna_param(request, header) ]
|
97
|
+
end.to_h
|
98
|
+
|
99
|
+
# Now expect the standard headers
|
100
|
+
hmac_params = {
|
101
|
+
method: request.request_method,
|
102
|
+
host: request.host,
|
103
|
+
path: request.path,
|
104
|
+
|
105
|
+
expiration: etna_param(request, :expiration),
|
106
|
+
id: etna_param(request, :id),
|
107
|
+
nonce: etna_param(request, :nonce),
|
108
|
+
headers: headers,
|
109
|
+
test_signature: hmac_signature
|
110
|
+
}
|
111
|
+
|
112
|
+
begin
|
113
|
+
hmac = Etna::Hmac.new(application, hmac_params)
|
114
|
+
rescue Exception => e
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
118
|
+
request.env['etna.hmac'] = hmac
|
119
|
+
|
120
|
+
return nil unless hmac.valid?
|
121
|
+
|
122
|
+
# success! set the hmac header params as regular params
|
123
|
+
params(request).update(headers)
|
124
|
+
|
125
|
+
return true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/etna/client.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'net/http/persistent'
|
2
|
+
require 'net/http/post/multipart'
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Etna
|
6
|
+
class Client
|
7
|
+
def initialize(host, token)
|
8
|
+
@host = host.sub(%r!/$!,'')
|
9
|
+
@token = token
|
10
|
+
|
11
|
+
set_routes
|
12
|
+
|
13
|
+
define_route_helpers
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :routes
|
17
|
+
|
18
|
+
def route_path(route, params)
|
19
|
+
Etna::Route.path(route[:route], params)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def set_routes
|
25
|
+
response = options('/')
|
26
|
+
status_check!(response)
|
27
|
+
@routes = JSON.parse(response.body, symbolize_names: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def define_route_helpers
|
31
|
+
@routes.each do |route|
|
32
|
+
next unless route[:name]
|
33
|
+
self.define_singleton_method(route[:name]) do |params={}|
|
34
|
+
|
35
|
+
missing_params = (route[:params] - params.keys.map(&:to_s))
|
36
|
+
unless missing_params.empty?
|
37
|
+
raise ArgumentError, "Missing required #{missing_params.size > 1 ?
|
38
|
+
'params' : 'param'} #{missing_params.join(', ')}"
|
39
|
+
end
|
40
|
+
|
41
|
+
response = send(route[:method].downcase, route_path(route, params), params)
|
42
|
+
if block_given?
|
43
|
+
yield response
|
44
|
+
else
|
45
|
+
if response.content_type == 'application/json'
|
46
|
+
return JSON.parse(response.body, symbolize_names: true)
|
47
|
+
else
|
48
|
+
return response.body
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def persistent_connection
|
56
|
+
@http ||= begin
|
57
|
+
http = Net::HTTP::Persistent.new
|
58
|
+
http.read_timeout = 3600
|
59
|
+
http
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def multipart_post(endpoint, content, &block)
|
64
|
+
uri = request_uri(endpoint)
|
65
|
+
multipart = Net::HTTP::Post::Multipart.new uri.path, content
|
66
|
+
multipart.add_field('Authorization', "Etna #{@token}")
|
67
|
+
request(uri, multipart, &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def post(endpoint, params={}, &block)
|
71
|
+
body_request(Net::HTTP::Post, endpoint, params, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get(endpoint, params={}, &block)
|
75
|
+
query_request(Net::HTTP::Get, endpoint, params, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def options(endpoint, params={}, &block)
|
79
|
+
query_request(Net::HTTP::Options, endpoint, params, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete(endpoint, params={}, &block)
|
83
|
+
body_request(Net::HTTP::Delete, endpoint, params, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def body_request(type, endpoint, params={}, &block)
|
87
|
+
uri = request_uri(endpoint)
|
88
|
+
req = type.new(uri.path,request_params)
|
89
|
+
req.body = params.to_json
|
90
|
+
request(uri, req, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def query_request(type, endpoint, params={}, &block)
|
94
|
+
uri = request_uri(endpoint)
|
95
|
+
uri.query = URI.encode_www_form(params)
|
96
|
+
req = type.new(uri.request_uri, request_params)
|
97
|
+
request(uri, req, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def request_uri(endpoint)
|
101
|
+
URI("#{@host}#{endpoint}")
|
102
|
+
end
|
103
|
+
|
104
|
+
def request_params
|
105
|
+
{
|
106
|
+
'Content-Type' => 'application/json',
|
107
|
+
'Accept'=> 'application/json, text/*',
|
108
|
+
'Authorization'=>"Etna #{@token}"
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def status_check!(response)
|
113
|
+
status = response.code.to_i
|
114
|
+
if status >= 400
|
115
|
+
msg = response.content_type == 'application/json' ?
|
116
|
+
json_error(response.body) :
|
117
|
+
response.body
|
118
|
+
raise Etna::Error.new(msg, status)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def json_error(body)
|
123
|
+
msg = JSON.parse(body, symbolize_names: true)
|
124
|
+
if (msg.has_key?(:errors) && msg[:errors].is_a?(Array))
|
125
|
+
return msg[:errors].join(', ')
|
126
|
+
elsif msg.has_key?(:error)
|
127
|
+
return msg[:error]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def request(uri, data)
|
132
|
+
if block_given?
|
133
|
+
persistent_connection.request(uri, data) do |response|
|
134
|
+
status_check!(response)
|
135
|
+
yield response
|
136
|
+
end
|
137
|
+
else
|
138
|
+
response = persistent_connection.request(uri, data)
|
139
|
+
status_check!(response)
|
140
|
+
return response
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/etna/command.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Etna
|
2
|
+
class Command
|
3
|
+
class << self
|
4
|
+
def usage(desc)
|
5
|
+
define_method :usage do
|
6
|
+
" #{"%-30s" % name}#{desc}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
self.class.name.snake_case.split(/::/).last.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
# To be overridden during inheritance.
|
16
|
+
def execute
|
17
|
+
raise 'Command is not implemented'
|
18
|
+
end
|
19
|
+
|
20
|
+
# To be overridden during inheritance, to e.g. connect to a database.
|
21
|
+
# Should be called with super by inheriting method.
|
22
|
+
def setup(config)
|
23
|
+
Etna::Application.find(self.class).configure(config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Etna
|
4
|
+
class Controller
|
5
|
+
def initialize(request, action = nil)
|
6
|
+
@request = request
|
7
|
+
@action = action
|
8
|
+
@response = Rack::Response.new
|
9
|
+
@params = @request.env['rack.request.params']
|
10
|
+
@errors = []
|
11
|
+
@server = @request.env['etna.server']
|
12
|
+
@logger = @request.env['etna.logger']
|
13
|
+
@user = @request.env['etna.user']
|
14
|
+
@request_id = @request.env['etna.request_id']
|
15
|
+
end
|
16
|
+
|
17
|
+
def log(line)
|
18
|
+
@logger.warn(request_msg(line))
|
19
|
+
end
|
20
|
+
|
21
|
+
def response(&block)
|
22
|
+
return instance_eval(&block) if block_given?
|
23
|
+
|
24
|
+
return send(@action) if @action
|
25
|
+
|
26
|
+
[501, {}, ['This controller is not implemented.']]
|
27
|
+
rescue Etna::Error => e
|
28
|
+
@logger.error(request_msg("Exiting with #{e.status}, #{e.message}"))
|
29
|
+
return failure(e.status, error: e.message)
|
30
|
+
rescue Exception => e
|
31
|
+
@logger.error(request_msg('Caught unspecified error'))
|
32
|
+
@logger.error(request_msg(e.message))
|
33
|
+
e.backtrace.each do |trace|
|
34
|
+
@logger.error(request_msg(trace))
|
35
|
+
end
|
36
|
+
return failure(500, error: 'Server error.')
|
37
|
+
end
|
38
|
+
|
39
|
+
def require_params(*params)
|
40
|
+
missing_params = params.reject{|p| @params.key?(p) }
|
41
|
+
raise Etna::BadRequest, "Missing param #{missing_params.join(', ')}" unless missing_params.empty?
|
42
|
+
end
|
43
|
+
alias_method :require_param, :require_params
|
44
|
+
|
45
|
+
def route_path(name, params={})
|
46
|
+
@server.class.route_path(@request, name, params)
|
47
|
+
end
|
48
|
+
|
49
|
+
def route_url(name, params={})
|
50
|
+
path = route_path(name,params)
|
51
|
+
return nil unless path
|
52
|
+
@request.scheme + '://' + @request.host + path
|
53
|
+
end
|
54
|
+
|
55
|
+
# methods for returning a view
|
56
|
+
VIEW_PATH = :VIEW_PATH
|
57
|
+
|
58
|
+
def view(name)
|
59
|
+
txt = File.read("#{self.class::VIEW_PATH}/#{name}.html")
|
60
|
+
@response['Content-Type'] = 'text/html'
|
61
|
+
@response.write(txt)
|
62
|
+
@response.finish
|
63
|
+
end
|
64
|
+
|
65
|
+
def erb_partial(name)
|
66
|
+
txt = File.read("#{self.class::VIEW_PATH}/#{name}.html.erb")
|
67
|
+
ERB.new(txt).result(binding)
|
68
|
+
end
|
69
|
+
|
70
|
+
def erb_view(name)
|
71
|
+
@response['Content-Type'] = 'text/html'
|
72
|
+
@response.write(erb_partial(name))
|
73
|
+
@response.finish
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def success(msg, content_type='text/plain')
|
79
|
+
@response['Content-Type'] = content_type
|
80
|
+
@response.write(msg)
|
81
|
+
@response.finish
|
82
|
+
end
|
83
|
+
|
84
|
+
def success_json(params)
|
85
|
+
success(params.to_json, 'application/json')
|
86
|
+
end
|
87
|
+
|
88
|
+
def failure(status, msg)
|
89
|
+
@response['Content-Type'] = 'application/json'
|
90
|
+
@response.status = status
|
91
|
+
@response.write(msg.to_json)
|
92
|
+
@response.finish
|
93
|
+
end
|
94
|
+
|
95
|
+
def success?
|
96
|
+
@errors.empty?
|
97
|
+
end
|
98
|
+
|
99
|
+
def error(msg)
|
100
|
+
if msg.is_a?(Array)
|
101
|
+
@errors.concat(msg)
|
102
|
+
else
|
103
|
+
@errors.push(msg)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def request_msg(msg)
|
108
|
+
"#{@request_id} #{msg}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Etna
|
2
|
+
class CrossOrigin
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
request = Rack::Request.new(env)
|
9
|
+
|
10
|
+
# Don't filter unless this is a CORS request
|
11
|
+
return actual_response(request) unless has_header?(request, :origin)
|
12
|
+
|
13
|
+
# OPTIONS requests are a 'preflight' request
|
14
|
+
if request.request_method == 'OPTIONS'
|
15
|
+
return preflight_response(request)
|
16
|
+
end
|
17
|
+
|
18
|
+
# The actual request following a preflight should
|
19
|
+
# set some extra headers
|
20
|
+
return postflight_response(request)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def subdomain(host)
|
26
|
+
host.split(/\./)[-2..-1]
|
27
|
+
end
|
28
|
+
|
29
|
+
def origin_allowed?(request)
|
30
|
+
subdomain(URI.parse(header(request, :origin)).host) == subdomain(request.host)
|
31
|
+
end
|
32
|
+
|
33
|
+
def actual_response(request)
|
34
|
+
@app.call(request.env)
|
35
|
+
end
|
36
|
+
|
37
|
+
def postflight_response(request)
|
38
|
+
status, headers, body = actual_response(request)
|
39
|
+
|
40
|
+
if origin_allowed?(request)
|
41
|
+
headers.update(
|
42
|
+
'Access-Control-Allow-Origin' => header(request, :origin),
|
43
|
+
'Access-Control-Allow-Credentials' => 'true'
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
return [ status, headers, body ]
|
48
|
+
end
|
49
|
+
|
50
|
+
def preflight_response(request)
|
51
|
+
[
|
52
|
+
200,
|
53
|
+
origin_allowed?(request) ? {
|
54
|
+
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
55
|
+
'Access-Control-Allow-Headers' => header(request, :access_control_request_headers),
|
56
|
+
'Access-Control-Allow-Origin' => header(request, :origin),
|
57
|
+
'Access-Control-Allow-Credentials' => 'true'
|
58
|
+
} : {},
|
59
|
+
['']
|
60
|
+
]
|
61
|
+
end
|
62
|
+
|
63
|
+
def header(request, name)
|
64
|
+
request.get_header("HTTP_#{name.to_s.upcase}")
|
65
|
+
end
|
66
|
+
|
67
|
+
def has_header?(request, name)
|
68
|
+
request.has_header?("HTTP_#{name.to_s.upcase}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Etna
|
2
|
+
class DescribeRoutes
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
request = Rack::Request.new(env)
|
9
|
+
|
10
|
+
return @app.call(env) unless request.request_method == 'OPTIONS' && request.path == '/'
|
11
|
+
|
12
|
+
return [ 200, { 'Content-Type' => 'application/json' }, [ route_json ] ]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def route_json
|
18
|
+
server.routes.map do |route|
|
19
|
+
route.to_hash
|
20
|
+
end.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
def application
|
24
|
+
@application ||= Etna::Application.instance
|
25
|
+
end
|
26
|
+
|
27
|
+
def server
|
28
|
+
@server ||= application.class.const_get(:Server)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/etna/errors.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Etna
|
4
|
+
class Error < StandardError
|
5
|
+
attr_reader :level, :status
|
6
|
+
def initialize(msg = 'The application had an error', status=500)
|
7
|
+
super(msg)
|
8
|
+
@level = Logger::WARN
|
9
|
+
@status = status
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Forbidden < Etna::Error
|
14
|
+
def initialize(msg = 'Action not permitted', status = 403)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Unauthorized < Etna::Error
|
20
|
+
def initialize(msg = 'Unauthorized request', status = 401)
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class BadRequest < Etna::Error
|
26
|
+
def initialize(msg = 'Client error', status = 422)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ServerError < Etna::Error
|
32
|
+
def initialize(msg = 'Server error', status = 500)
|
33
|
+
super
|
34
|
+
@level = Logger::ERROR
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/etna/ext.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class String
|
2
|
+
def snake_case
|
3
|
+
return downcase if match(/\A[A-Z]+\z/)
|
4
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
|
5
|
+
gsub(/([a-z])([A-Z])/, '\1_\2').
|
6
|
+
downcase
|
7
|
+
end
|
8
|
+
|
9
|
+
def camel_case
|
10
|
+
return self if self !~ /_/ && self =~ /[A-Z]+.*/
|
11
|
+
split('_').map{|e| e.capitalize}.join
|
12
|
+
end
|
13
|
+
end
|