grass 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.sass-cache/e3d4c2039fc7a8446e752aad5ac08f85d7457f92/(__TEMPLATE__)c +0 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +10 -0
  9. data/bin/grass +89 -0
  10. data/config/grass.rb +27 -0
  11. data/db/migrate/1_create_grass_sources.rb +24 -0
  12. data/grass.gemspec +43 -0
  13. data/lib/grass.rb +68 -0
  14. data/lib/grass/cache.rb +52 -0
  15. data/lib/grass/core_ext/kernel.rb +8 -0
  16. data/lib/grass/endpoints/api.rb +70 -0
  17. data/lib/grass/endpoints/front.rb +122 -0
  18. data/lib/grass/file_sync.rb +70 -0
  19. data/lib/grass/goliath/rack/auth_barrier.rb +109 -0
  20. data/lib/grass/goliath/rack/cache.rb +37 -0
  21. data/lib/grass/goliath/rack/cors.rb +45 -0
  22. data/lib/grass/goliath/rack/secure_headers.rb +20 -0
  23. data/lib/grass/goliath/rack/validator.rb +52 -0
  24. data/lib/grass/helpers/i18n_helper.rb +91 -0
  25. data/lib/grass/helpers/render_helper.rb +35 -0
  26. data/lib/grass/key.rb +137 -0
  27. data/lib/grass/render.rb +27 -0
  28. data/lib/grass/render/layout.rb +11 -0
  29. data/lib/grass/render/page.rb +31 -0
  30. data/lib/grass/render/renderer.rb +35 -0
  31. data/lib/grass/render/script.rb +27 -0
  32. data/lib/grass/render/stylesheet.rb +13 -0
  33. data/lib/grass/render/text.rb +11 -0
  34. data/lib/grass/render/view.rb +34 -0
  35. data/lib/grass/render/yui_renderer.rb +27 -0
  36. data/lib/grass/source.rb +107 -0
  37. data/lib/grass/tasks/db.rake +67 -0
  38. data/lib/grass/version.rb +3 -0
  39. data/lib/templates/app/Gemfile +9 -0
  40. data/lib/templates/app/Procfile +3 -0
  41. data/lib/templates/app/Rakefile +7 -0
  42. data/lib/templates/app/app/assets/scripts/application.en.js.coffee +1 -0
  43. data/lib/templates/app/app/assets/stylesheets/application.en.css.scss +4 -0
  44. data/lib/templates/app/app/content/pages/about.en.html.erb +3 -0
  45. data/lib/templates/app/app/content/pages/index.en.md.erb +5 -0
  46. data/lib/templates/app/app/views/layouts/application.en.html.erb +14 -0
  47. data/lib/templates/app/app/views/pages/show.en.html.erb +4 -0
  48. data/lib/templates/app/app/views/shared.en.html.erb +9 -0
  49. data/lib/templates/app/config/cache.yml +20 -0
  50. data/lib/templates/app/config/database.yml +35 -0
  51. data/lib/templates/app/config/grass.rb +27 -0
  52. data/lib/templates/app/db/migrate/1_create_grass_sources.rb +24 -0
  53. data/lib/templates/app/haproxy.cfg +43 -0
  54. data/lib/templates/app/public/favicon.ico +0 -0
  55. data/lib/templates/app/public/robots.txt +2 -0
  56. data/lib/templates/app/server.rb +7 -0
  57. data/test/dummy/app/content/texts/testapi.en.txt +1 -0
  58. data/test/dummy/config/cache.yml +23 -0
  59. data/test/dummy/config/database.yml +35 -0
  60. data/test/dummy/config/dummy.rb +1 -0
  61. data/test/dummy/config/haproxy.cfg +37 -0
  62. data/test/dummy/public/favicon.ico +0 -0
  63. data/test/dummy/public/robots.txt +2 -0
  64. data/test/minitest_helper.rb +38 -0
  65. data/test/support/grass.rb +21 -0
  66. data/test/support/test.jpg +0 -0
  67. data/test/test_api.rb +74 -0
  68. data/test/test_front.rb +52 -0
  69. data/test/test_key.rb +118 -0
  70. data/test/test_source.rb +51 -0
  71. data/test/test_source_file.rb +47 -0
  72. data/test/test_source_render.rb +54 -0
  73. data/vendor/yuicompressor-2.4.8.jar +0 -0
  74. 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