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