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