grass 0.0.1
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/.gitignore +17 -0
- data/.sass-cache/e3d4c2039fc7a8446e752aad5ac08f85d7457f92/(__TEMPLATE__)c +0 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +10 -0
- data/bin/grass +89 -0
- data/config/grass.rb +27 -0
- data/db/migrate/1_create_grass_sources.rb +24 -0
- data/grass.gemspec +43 -0
- data/lib/grass.rb +68 -0
- data/lib/grass/cache.rb +52 -0
- data/lib/grass/core_ext/kernel.rb +8 -0
- data/lib/grass/endpoints/api.rb +70 -0
- data/lib/grass/endpoints/front.rb +122 -0
- data/lib/grass/file_sync.rb +70 -0
- data/lib/grass/goliath/rack/auth_barrier.rb +109 -0
- data/lib/grass/goliath/rack/cache.rb +37 -0
- data/lib/grass/goliath/rack/cors.rb +45 -0
- data/lib/grass/goliath/rack/secure_headers.rb +20 -0
- data/lib/grass/goliath/rack/validator.rb +52 -0
- data/lib/grass/helpers/i18n_helper.rb +91 -0
- data/lib/grass/helpers/render_helper.rb +35 -0
- data/lib/grass/key.rb +137 -0
- data/lib/grass/render.rb +27 -0
- data/lib/grass/render/layout.rb +11 -0
- data/lib/grass/render/page.rb +31 -0
- data/lib/grass/render/renderer.rb +35 -0
- data/lib/grass/render/script.rb +27 -0
- data/lib/grass/render/stylesheet.rb +13 -0
- data/lib/grass/render/text.rb +11 -0
- data/lib/grass/render/view.rb +34 -0
- data/lib/grass/render/yui_renderer.rb +27 -0
- data/lib/grass/source.rb +107 -0
- data/lib/grass/tasks/db.rake +67 -0
- data/lib/grass/version.rb +3 -0
- data/lib/templates/app/Gemfile +9 -0
- data/lib/templates/app/Procfile +3 -0
- data/lib/templates/app/Rakefile +7 -0
- data/lib/templates/app/app/assets/scripts/application.en.js.coffee +1 -0
- data/lib/templates/app/app/assets/stylesheets/application.en.css.scss +4 -0
- data/lib/templates/app/app/content/pages/about.en.html.erb +3 -0
- data/lib/templates/app/app/content/pages/index.en.md.erb +5 -0
- data/lib/templates/app/app/views/layouts/application.en.html.erb +14 -0
- data/lib/templates/app/app/views/pages/show.en.html.erb +4 -0
- data/lib/templates/app/app/views/shared.en.html.erb +9 -0
- data/lib/templates/app/config/cache.yml +20 -0
- data/lib/templates/app/config/database.yml +35 -0
- data/lib/templates/app/config/grass.rb +27 -0
- data/lib/templates/app/db/migrate/1_create_grass_sources.rb +24 -0
- data/lib/templates/app/haproxy.cfg +43 -0
- data/lib/templates/app/public/favicon.ico +0 -0
- data/lib/templates/app/public/robots.txt +2 -0
- data/lib/templates/app/server.rb +7 -0
- data/test/dummy/app/content/texts/testapi.en.txt +1 -0
- data/test/dummy/config/cache.yml +23 -0
- data/test/dummy/config/database.yml +35 -0
- data/test/dummy/config/dummy.rb +1 -0
- data/test/dummy/config/haproxy.cfg +37 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/robots.txt +2 -0
- data/test/minitest_helper.rb +38 -0
- data/test/support/grass.rb +21 -0
- data/test/support/test.jpg +0 -0
- data/test/test_api.rb +74 -0
- data/test/test_front.rb +52 -0
- data/test/test_key.rb +118 -0
- data/test/test_source.rb +51 -0
- data/test/test_source_file.rb +47 -0
- data/test/test_source_render.rb +54 -0
- data/vendor/yuicompressor-2.4.8.jar +0 -0
- metadata +399 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'goliath/api'
|
2
|
+
require 'json'
|
3
|
+
require 'grass'
|
4
|
+
require 'grass/key'
|
5
|
+
require 'grass/source'
|
6
|
+
require 'grass/helpers/i18n_helper'
|
7
|
+
require 'grass/goliath/rack/secure_headers'
|
8
|
+
require 'grass/goliath/rack/cache'
|
9
|
+
require 'active_support/inflector'
|
10
|
+
|
11
|
+
module Grass
|
12
|
+
|
13
|
+
class Front < Goliath::API
|
14
|
+
|
15
|
+
module Helper
|
16
|
+
def request_data
|
17
|
+
{
|
18
|
+
language_info: language_info(),
|
19
|
+
country_info: country_info(),
|
20
|
+
params: params,
|
21
|
+
http_host: env['HTTP_HOST'],
|
22
|
+
request_path: env["REQUEST_PATH"]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
DEFAULT_PAGE = ENV['DEFAULT_PAGE'] || "index"
|
28
|
+
|
29
|
+
include Helpers::I18nHelper
|
30
|
+
include Helper
|
31
|
+
|
32
|
+
Dir.glob("#{Grass.app_root}/helpers/*.rb").each do |file|
|
33
|
+
require file
|
34
|
+
include File.basename(file,".rb").classify.constantize
|
35
|
+
end
|
36
|
+
|
37
|
+
use Goliath::Rack::SecureHeaders
|
38
|
+
use Goliath::Rack::Params
|
39
|
+
use Goliath::Rack::Render
|
40
|
+
use Goliath::Rack::Validation::RequestMethod, %w(GET)
|
41
|
+
use(Rack::Static,
|
42
|
+
:root => "#{Grass.root}/public",
|
43
|
+
:urls => Dir.glob("#{Grass.root}/public/*").map{|file| "/#{::File.basename(file)}" },
|
44
|
+
:cache_control => ENV['CACHE_CONTROL'] || "no-cache")
|
45
|
+
use Goliath::Rack::Cache
|
46
|
+
|
47
|
+
def response(env)
|
48
|
+
self.public_send env['REQUEST_METHOD'].downcase, env
|
49
|
+
end
|
50
|
+
|
51
|
+
def get(env)
|
52
|
+
|
53
|
+
set_locale
|
54
|
+
|
55
|
+
id = get_id
|
56
|
+
|
57
|
+
data = id =~ /scripts|styles/ ? {} : request_data
|
58
|
+
|
59
|
+
headers = {}
|
60
|
+
|
61
|
+
return fresh(id,data) if Grass.env == "development" && !config['enable_cache_for_development']
|
62
|
+
|
63
|
+
# try memcache or render freshly
|
64
|
+
if cached_response = Source.read_cache(Source.generate_cachekey(id,data))
|
65
|
+
# puts "----> CACHED!!!"
|
66
|
+
|
67
|
+
mime_type, body = cached_response
|
68
|
+
headers = {"Content-Type" => mime_type}
|
69
|
+
status = 200
|
70
|
+
|
71
|
+
else
|
72
|
+
status, headers, body = fresh(id,data)
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
[status,headers,body]
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def get_id
|
83
|
+
# set default home
|
84
|
+
id = env["REQUEST_PATH"] == "/" ? "/pages/#{DEFAULT_PAGE}" : env["REQUEST_PATH"]
|
85
|
+
|
86
|
+
# remove trailing slash
|
87
|
+
id = id[0..-2] if id.end_with?("/")
|
88
|
+
|
89
|
+
# ensure locale
|
90
|
+
unless id =~ /#{Key::KEY_REGEX[:locale]}/
|
91
|
+
id = "/#{I18n.locale}/#{id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# add pages as default collection
|
95
|
+
unless id =~ /#{Key::KEY_REGEX[:dir]}/
|
96
|
+
id = id.split("/").insert(2,"pages").join("/")
|
97
|
+
end
|
98
|
+
|
99
|
+
id.gsub("//","/")
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_file key
|
103
|
+
raise Goliath::Validation::NotFoundError unless source = Source[key].first
|
104
|
+
# if Grass.env == "development"
|
105
|
+
# source.file.read
|
106
|
+
# source.commit!
|
107
|
+
# end
|
108
|
+
source
|
109
|
+
end
|
110
|
+
|
111
|
+
def fresh id, data = {}
|
112
|
+
file = get_file(id)
|
113
|
+
if file.type == "page"
|
114
|
+
file.render(data)
|
115
|
+
file.cache!
|
116
|
+
end
|
117
|
+
[200, {"Content-Type" => file.mime_type} ,file.read]
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Grass
|
4
|
+
|
5
|
+
module FileSync
|
6
|
+
|
7
|
+
class FileProxy
|
8
|
+
|
9
|
+
def initialize source
|
10
|
+
@source = source
|
11
|
+
FileUtils.mkdir_p File.dirname(@source.filepath)
|
12
|
+
exists? ? read : write(@source.raw)
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?
|
16
|
+
File.exists? @source.filepath
|
17
|
+
end
|
18
|
+
|
19
|
+
def read
|
20
|
+
begin
|
21
|
+
value = File.read(@source.filepath)
|
22
|
+
@source.update(raw: value) if dirty?
|
23
|
+
value
|
24
|
+
end rescue nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def write value
|
28
|
+
begin
|
29
|
+
File.open(@source.filepath,File::RDWR|File::CREAT){ |f|
|
30
|
+
f.truncate(0); f.rewind; f.write(value)
|
31
|
+
}
|
32
|
+
value
|
33
|
+
end rescue nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete
|
37
|
+
File.delete(@source.filepath) rescue nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def dirty?
|
41
|
+
@source.raw.nil? || File.mtime(@source.filepath).to_i > @source.updated_at.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.included(base)
|
47
|
+
base.send :after_initialize, :init_file, if: 'binary.nil?'
|
48
|
+
base.send :before_save, :write_file, if: 'binary.nil?'
|
49
|
+
base.send :after_destroy, :delete_file, if: 'binary.nil?'
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :file
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def init_file
|
57
|
+
@file = FileProxy.new(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete_file
|
61
|
+
@file.delete
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_file
|
65
|
+
@file.write(self.raw)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
class AuthBarrier
|
4
|
+
|
5
|
+
include Goliath::Rack::BarrierAroundware
|
6
|
+
include Goliath::Validation
|
7
|
+
|
8
|
+
attr_reader :db # Memcache Client
|
9
|
+
attr_accessor :access_token
|
10
|
+
|
11
|
+
class MissingApikeyError < BadRequestError ; end
|
12
|
+
class InvalidApikeyError < UnauthorizedError ; end
|
13
|
+
|
14
|
+
def initialize(env, db_name)
|
15
|
+
@db = env.config[db_name]
|
16
|
+
super(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
def pre_process
|
20
|
+
env.trace('pre_process_beg')
|
21
|
+
validate_apikey!
|
22
|
+
|
23
|
+
# the results of the afirst deferrable will be set right into access_token (and the request into successes)
|
24
|
+
get_access_token
|
25
|
+
|
26
|
+
# On non-GET non-HEAD requests, we have to check auth now.
|
27
|
+
unless lazy_authorization?
|
28
|
+
perform # yield execution until user_info has arrived
|
29
|
+
check_authorization!
|
30
|
+
end
|
31
|
+
|
32
|
+
env.trace('pre_process_end')
|
33
|
+
return Goliath::Connection::AsyncResponse
|
34
|
+
end
|
35
|
+
|
36
|
+
def post_process
|
37
|
+
env.trace('post_process_beg')
|
38
|
+
# [:access_token, :status, :headers, :body].each{|attr| env.logger.info(("%23s\t%s" % [attr, self.send(attr).inspect[0..200]])) }
|
39
|
+
|
40
|
+
# inject_headers
|
41
|
+
|
42
|
+
# We have to check auth now, we skipped it before
|
43
|
+
if lazy_authorization?
|
44
|
+
check_authorization!
|
45
|
+
end
|
46
|
+
|
47
|
+
env.trace('post_process_end')
|
48
|
+
[status, headers, body]
|
49
|
+
end
|
50
|
+
|
51
|
+
def lazy_authorization?
|
52
|
+
(env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_access_token
|
56
|
+
@access_token = db.get(apikey_path) rescue nil
|
57
|
+
# puts "GET KEY #{apikey_path.inspect} -> #{@access_token.inspect}"
|
58
|
+
@access_token
|
59
|
+
end
|
60
|
+
|
61
|
+
def accept_response(handle, *args)
|
62
|
+
env.trace("received_#{handle}")
|
63
|
+
super(handle, *args)
|
64
|
+
end
|
65
|
+
|
66
|
+
# =======================================================================
|
67
|
+
|
68
|
+
def validate_apikey!
|
69
|
+
raise MissingApikeyError.new("Missing Api Key") if apikey.to_s.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_authorization!
|
73
|
+
unless access_token && account_valid?
|
74
|
+
raise InvalidApikeyError.new("Invalid Api Key")
|
75
|
+
else
|
76
|
+
renew_token
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def apikey
|
81
|
+
env.params['apikey']
|
82
|
+
end
|
83
|
+
|
84
|
+
def apikey_path
|
85
|
+
Arms::Auth.keypath(apikey)
|
86
|
+
end
|
87
|
+
|
88
|
+
def account_valid?
|
89
|
+
# puts "VALID? #{Digest::MD5.hexdigest(apikey) == access_token[:token]},#{account_belongs_to_host?},#{Arms::Auth.can?(access_token[:mode],env['REQUEST_METHOD'])}"
|
90
|
+
# is token or key altered?
|
91
|
+
Digest::MD5.hexdigest(apikey) == access_token[:token] &&
|
92
|
+
# is on right host?
|
93
|
+
account_belongs_to_host? &&
|
94
|
+
# mode is able to do HTTP VERB?
|
95
|
+
Arms::Auth.can?(access_token[:mode],env['REQUEST_METHOD'])
|
96
|
+
end
|
97
|
+
|
98
|
+
def renew_token
|
99
|
+
db.touch apikey_path, Arms::Auth::TTLS[access_token[:mode]] unless access_token[:ttl].nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def account_belongs_to_host?
|
103
|
+
return true if access_token[:mode] == Arms::Auth::ADMIN
|
104
|
+
[access_token[:hosts]].flatten.join(",") =~ /#{env['HTTP_ORIGIN'] || env['SERVER_NAME']}/
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Very Simple Middleware to deal with ETag & If-None_Match Headers
|
2
|
+
|
3
|
+
require "goliath/rack/async_middleware"
|
4
|
+
|
5
|
+
module Goliath
|
6
|
+
module Rack
|
7
|
+
class Cache
|
8
|
+
|
9
|
+
include Goliath::Rack::AsyncMiddleware
|
10
|
+
|
11
|
+
def post_process(env, status, headers, body)
|
12
|
+
if body.is_a?(String) && (Grass.env == "production" || env.config['enable_cache_for_development'])
|
13
|
+
# Generate ETag for body
|
14
|
+
etag = etag_for(body)
|
15
|
+
|
16
|
+
# Add ETag header
|
17
|
+
headers['ETag'] = etag
|
18
|
+
|
19
|
+
# Response with status 304 without body
|
20
|
+
if env['HTTP_IF_NONE_MATCH'] == etag
|
21
|
+
status = 304
|
22
|
+
body = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
[status,headers,body]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Generates ETag for given string
|
31
|
+
def etag_for content
|
32
|
+
Digest::MD5.hexdigest(content)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'goliath/rack'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
class Cors
|
6
|
+
include Goliath::Rack::AsyncMiddleware
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
:origin => '*',
|
10
|
+
:methods => 'GET',
|
11
|
+
:headers => 'Accept, Authorization, Content-Type, Origin',
|
12
|
+
:expose_headers => 'Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Location, Pragma'
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(app, options = {})
|
16
|
+
super(app)
|
17
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
22
|
+
[200, cors_headers, []]
|
23
|
+
else
|
24
|
+
super(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def post_process(env, status, headers, body)
|
29
|
+
[status, cors_headers.merge(headers), body]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def cors_headers
|
35
|
+
headers = {}
|
36
|
+
headers['Access-Control-Allow-Origin'] = @options[:origin]
|
37
|
+
headers['Access-Control-Allow-Methods'] = @options[:methods]
|
38
|
+
headers['Access-Control-Allow-Headers'] = @options[:headers]
|
39
|
+
headers['Access-Control-Expose-Headers'] = @options[:expose_headers]
|
40
|
+
headers
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
class SecureHeaders
|
4
|
+
|
5
|
+
include Goliath::Rack::AsyncMiddleware
|
6
|
+
|
7
|
+
HEADERS = {
|
8
|
+
'X-Frame-Options' => 'SAMEORIGIN',
|
9
|
+
'X-XSS-Protection' => '1; mode=block',
|
10
|
+
'X-Content-Type-Options' => 'nosniff'
|
11
|
+
}
|
12
|
+
|
13
|
+
def post_process(env, status, headers, body)
|
14
|
+
headers.update HEADERS
|
15
|
+
[status, headers, body]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
module Validator
|
4
|
+
module_function
|
5
|
+
ERROR = 'error'
|
6
|
+
|
7
|
+
# @param status_code [Integer] HTTP status code for this error.
|
8
|
+
# @param msg [String] message to inject into the response body.
|
9
|
+
# @param headers [Hash] Response headers to preserve in an error response;
|
10
|
+
# (the Content-Length header, if any, is removed)
|
11
|
+
def validation_error(status_code, msg, headers={})
|
12
|
+
headers.delete('Content-Length')
|
13
|
+
[status_code, headers, {ERROR => msg}]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Execute a block of code safely.
|
17
|
+
#
|
18
|
+
# If the block raises any exception that derives from
|
19
|
+
# Goliath::Validation::Error (see specifically those in
|
20
|
+
# goliath/validation/standard_http_errors.rb), it will be turned into the
|
21
|
+
# corresponding 4xx response with a corresponding message.
|
22
|
+
#
|
23
|
+
# If the block raises any other kind of error, we log it and return a
|
24
|
+
# less-communicative 500 response.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# # will convert the ForbiddenError exception into a 403 response
|
28
|
+
# # and an uncaught error in do_something_risky! into a 500 response
|
29
|
+
# safely(env, headers) do
|
30
|
+
# raise ForbiddenError unless account_info['valid'] == true
|
31
|
+
# do_something_risky!
|
32
|
+
# [status, headers, body]
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
#
|
36
|
+
# @param env [Goliath::Env] The current request env
|
37
|
+
# @param headers [Hash] Response headers to preserve in an error response
|
38
|
+
#
|
39
|
+
def safely(env, headers={})
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
rescue Goliath::Validation::Error => e
|
43
|
+
validation_error(e.status_code, e.message, headers)
|
44
|
+
rescue Exception => e
|
45
|
+
env.logger.error(e.message)
|
46
|
+
env.logger.error(e.backtrace.join("\n"))
|
47
|
+
validation_error(500, e.message, headers)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|