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.
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