txgh-server 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/lib/txgh-server/application.rb +141 -0
  3. data/lib/txgh-server/download_handler.rb +85 -0
  4. data/lib/txgh-server/github_request_auth.rb +28 -0
  5. data/lib/txgh-server/response.rb +15 -0
  6. data/lib/txgh-server/response_helpers.rb +26 -0
  7. data/lib/txgh-server/stream_response.rb +37 -0
  8. data/lib/txgh-server/tgz_stream_response.rb +39 -0
  9. data/lib/txgh-server/transifex_request_auth.rb +53 -0
  10. data/lib/txgh-server/triggers/handler.rb +50 -0
  11. data/lib/txgh-server/triggers/pull_handler.rb +18 -0
  12. data/lib/txgh-server/triggers/push_handler.rb +18 -0
  13. data/lib/txgh-server/triggers.rb +7 -0
  14. data/lib/txgh-server/version.rb +3 -0
  15. data/lib/txgh-server/webhooks/github/delete_handler.rb +37 -0
  16. data/lib/txgh-server/webhooks/github/handler.rb +20 -0
  17. data/lib/txgh-server/webhooks/github/ping_handler.rb +18 -0
  18. data/lib/txgh-server/webhooks/github/push_handler.rb +124 -0
  19. data/lib/txgh-server/webhooks/github/request_handler.rb +113 -0
  20. data/lib/txgh-server/webhooks/github.rb +11 -0
  21. data/lib/txgh-server/webhooks/transifex/hook_handler.rb +94 -0
  22. data/lib/txgh-server/webhooks/transifex/request_handler.rb +78 -0
  23. data/lib/txgh-server/webhooks/transifex.rb +8 -0
  24. data/lib/txgh-server/webhooks.rb +6 -0
  25. data/lib/txgh-server/zip_stream_response.rb +19 -0
  26. data/lib/txgh-server.rb +23 -0
  27. data/spec/application_spec.rb +347 -0
  28. data/spec/download_handler_spec.rb +91 -0
  29. data/spec/github_request_auth_spec.rb +39 -0
  30. data/spec/helpers/github_payload_builder.rb +141 -0
  31. data/spec/helpers/integration_setup.rb +47 -0
  32. data/spec/integration/cassettes/github_l10n_hook_endpoint.yml +536 -0
  33. data/spec/integration/cassettes/pull.yml +47 -0
  34. data/spec/integration/cassettes/push.yml +544 -0
  35. data/spec/integration/cassettes/transifex_hook_endpoint.yml +221 -0
  36. data/spec/integration/config/tx.config +10 -0
  37. data/spec/integration/hooks_spec.rb +159 -0
  38. data/spec/integration/payloads/github_postbody.json +161 -0
  39. data/spec/integration/payloads/github_postbody_l10n.json +136 -0
  40. data/spec/integration/payloads/github_postbody_release.json +136 -0
  41. data/spec/integration/triggers_spec.rb +45 -0
  42. data/spec/spec_helper.rb +26 -0
  43. data/spec/tgz_stream_response_spec.rb +59 -0
  44. data/spec/transifex_request_auth_spec.rb +39 -0
  45. data/spec/webhooks/github/delete_handler_spec.rb +38 -0
  46. data/spec/webhooks/github/ping_handler_spec.rb +16 -0
  47. data/spec/webhooks/github/push_handler_spec.rb +106 -0
  48. data/spec/webhooks/transifex/hook_handler_spec.rb +136 -0
  49. data/spec/zip_stream_response_spec.rb +58 -0
  50. data/txgh-server.gemspec +24 -0
  51. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2b139797c18ad7ef9fafb4b133c87955f4a43d51
4
+ data.tar.gz: c9adb77e0a0717ddd19c4dbad1c4d310a3d3638e
5
+ SHA512:
6
+ metadata.gz: 0b66010e996e5b83b8b70fde64e11922df901979f0c796dbf953bc08b064e61cb54b25756f3491ab238dbaf7bda2476e6bf981109ab12d7babfa8397d7e889eb
7
+ data.tar.gz: c24933279950cbb05c47e20256895629cb92be9b7802f954b917d2f8e68f71bdf05c54e70880b93a0050c4b7fc81f709e93f92ad4a86a720f966303888bac432
@@ -0,0 +1,141 @@
1
+ require 'sinatra'
2
+ require 'sinatra/json'
3
+ require 'sinatra/reloader'
4
+ require 'sinatra/streaming'
5
+
6
+ module TxghServer
7
+ module RespondWith
8
+ def respond_with(resp)
9
+ env['txgh.response'] = resp
10
+
11
+ if resp.streaming?
12
+ response.headers.merge!(resp.headers)
13
+
14
+ stream do |out|
15
+ begin
16
+ resp.write_to(out)
17
+ rescue => e
18
+ Txgh.events.publish_error(e)
19
+ raise e
20
+ end
21
+ end
22
+ else
23
+ status resp.status
24
+ json resp.body
25
+ end
26
+ end
27
+ end
28
+
29
+ class Application < Sinatra::Base
30
+ include TxghServer
31
+
32
+ helpers Sinatra::Streaming
33
+ helpers RespondWith
34
+
35
+ configure do
36
+ set :logging, nil
37
+ logger = Txgh::TxLogger.logger
38
+ set :logger, logger
39
+ end
40
+
41
+ def initialize(app = nil)
42
+ super(app)
43
+ end
44
+
45
+ get '/health_check' do
46
+ respond_with(
47
+ Response.new(200, {})
48
+ )
49
+ end
50
+
51
+ get '/config' do
52
+ config = Txgh::Config::KeyManager.config_from_project(params[:project_slug])
53
+ branch = Txgh::Utils.absolute_branch(params[:branch])
54
+
55
+ begin
56
+ tx_config = Txgh::Config::TxManager.tx_config(
57
+ config.transifex_project, config.github_repo, branch
58
+ )
59
+
60
+ data = tx_config.to_h
61
+ data.merge!(branch_slug: Txgh::Utils.slugify(branch)) if branch
62
+
63
+ status 200
64
+ json data: data
65
+ rescue Txgh::ConfigNotFoundError => e
66
+ status 404
67
+ json [{ error: e.message }]
68
+ rescue => e
69
+ status 500
70
+ json [{ error: e.message }]
71
+ end
72
+ end
73
+
74
+ get '/download.:format' do
75
+ respond_with(
76
+ DownloadHandler.handle_request(request, settings.logger)
77
+ )
78
+ end
79
+ end
80
+
81
+ # Hooks are protected endpoints used for data integration between Github and
82
+ # Transifex. They live under the /hooks namespace (see config.ru)
83
+ class WebhookEndpoints < Sinatra::Base
84
+ include TxghServer::Webhooks
85
+ helpers RespondWith
86
+
87
+ configure do
88
+ set :logging, nil
89
+ logger = Txgh::TxLogger.logger
90
+ set :logger, logger
91
+ end
92
+
93
+ configure :development do
94
+ register Sinatra::Reloader
95
+ end
96
+
97
+ def initialize(app = nil)
98
+ super(app)
99
+ end
100
+
101
+ post '/transifex' do
102
+ respond_with(
103
+ Transifex::RequestHandler.handle_request(request, settings.logger)
104
+ )
105
+ end
106
+
107
+ post '/github' do
108
+ respond_with(
109
+ Github::RequestHandler.handle_request(request, settings.logger)
110
+ )
111
+ end
112
+ end
113
+
114
+ class TriggerEndpoints < Sinatra::Base
115
+ include TxghServer::Triggers
116
+
117
+ helpers RespondWith
118
+
119
+ configure do
120
+ set :logging, nil
121
+ logger = Txgh::TxLogger.logger
122
+ set :logger, logger
123
+ end
124
+
125
+ configure :development do
126
+ register Sinatra::Reloader
127
+ end
128
+
129
+ patch '/push' do
130
+ respond_with(
131
+ PushHandler.handle_request(request, settings.logger)
132
+ )
133
+ end
134
+
135
+ patch '/pull' do
136
+ respond_with(
137
+ PullHandler.handle_request(request, settings.logger)
138
+ )
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,85 @@
1
+ module TxghServer
2
+ class DownloadHandler
3
+ DEFAULT_FORMAT = '.zip'
4
+
5
+ # includes response helpers in both the class and the singleton class
6
+ include ResponseHelpers
7
+
8
+ class << self
9
+ def handle_request(request, logger = nil)
10
+ handle_safely do
11
+ config = config_from(request)
12
+ project, repo = [config.transifex_project, config.github_repo]
13
+ params = params_from(request)
14
+ handler = new(project, repo, params, logger)
15
+ handler.execute
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def config_from(request)
22
+ Txgh::Config::KeyManager.config_from_project(
23
+ request.params.fetch('project_slug')
24
+ )
25
+ end
26
+
27
+ def params_from(request)
28
+ request.params.merge(
29
+ 'format' => format_from(request)
30
+ )
31
+ end
32
+
33
+ def format_from(request)
34
+ # sinatra is dumb and doesn't include any of the URL captures in the
35
+ # request params or env hash
36
+ File.extname(request.env['REQUEST_PATH'])
37
+ end
38
+
39
+ def handle_safely
40
+ yield
41
+ rescue => e
42
+ respond_with_error(500, "Internal server error: #{e.message}", e)
43
+ end
44
+ end
45
+
46
+ attr_reader :project, :repo, :params, :logger
47
+
48
+ def initialize(project, repo, params, logger)
49
+ @project = project
50
+ @repo = repo
51
+ @params = params
52
+ @logger = logger
53
+ end
54
+
55
+ def execute
56
+ downloader = Txgh::ResourceDownloader.new(
57
+ project, repo, params['branch'], languages: project.languages
58
+ )
59
+
60
+ response_class.new(attachment, downloader.each)
61
+ end
62
+
63
+ private
64
+
65
+ def attachment
66
+ project.name
67
+ end
68
+
69
+ def format
70
+ params.fetch('format', DEFAULT_FORMAT)
71
+ end
72
+
73
+ def response_class
74
+ case format
75
+ when '.zip'
76
+ ZipStreamResponse
77
+ when '.tgz'
78
+ TgzStreamResponse
79
+ else
80
+ raise TxghInternalError,
81
+ "'#{format}' is not a valid download format"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ require 'openssl'
2
+
3
+ module TxghServer
4
+ class GithubRequestAuth
5
+ HMAC_DIGEST = OpenSSL::Digest.new('sha1')
6
+ RACK_HEADER = 'HTTP_X_HUB_SIGNATURE'
7
+ GITHUB_HEADER = 'X-Hub-Signature'
8
+
9
+ class << self
10
+ def authentic_request?(request, secret)
11
+ request.body.rewind
12
+ expected_signature = header_value(request.body.read, secret)
13
+ actual_signature = request.env[RACK_HEADER]
14
+ actual_signature == expected_signature
15
+ end
16
+
17
+ def header_value(content, secret)
18
+ "sha1=#{digest(content, secret)}"
19
+ end
20
+
21
+ private
22
+
23
+ def digest(content, secret)
24
+ OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, content)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module TxghServer
2
+ class Response
3
+ attr_reader :status, :body, :error
4
+
5
+ def initialize(status, body, error = nil)
6
+ @status = status
7
+ @body = body
8
+ @error = error
9
+ end
10
+
11
+ def streaming?
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ module TxghServer
2
+ module ResponseHelpers
3
+ private
4
+
5
+ def respond_with(status, body, e = nil)
6
+ TxghServer::Response.new(status, body, e)
7
+ end
8
+
9
+ def respond_with_error(status, message, e = nil)
10
+ respond_with(status, error(message), e)
11
+ end
12
+
13
+ def error(message)
14
+ [{ error: message }]
15
+ end
16
+
17
+ def data(body)
18
+ { data: body }
19
+ end
20
+
21
+ # includes these methods in the singleton class as well
22
+ def self.included(base)
23
+ base.extend(self)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require 'mime/types'
2
+
3
+ module TxghServer
4
+ class StreamResponse
5
+ attr_reader :attachment, :enum
6
+
7
+ def initialize(attachment, enum)
8
+ @attachment = attachment
9
+ @enum = enum
10
+ end
11
+
12
+ def write_to(stream)
13
+ raise NotImplementedError,
14
+ "please implement #{__method__} in derived classes"
15
+ end
16
+
17
+ def file_extension
18
+ raise NotImplementedError,
19
+ "please implement #{__method__} in derived classes"
20
+ end
21
+
22
+ def headers
23
+ @headers ||= {
24
+ 'Content-Disposition' => "attachment; filename=\"#{attachment}#{file_extension}\"",
25
+ 'Content-Type' => MIME::Types.type_for(file_extension).first.content_type
26
+ }
27
+ end
28
+
29
+ def streaming?
30
+ true
31
+ end
32
+
33
+ def error
34
+ nil
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ require 'rubygems/package'
2
+ require 'stringio'
3
+ require 'zlib'
4
+
5
+ module TxghServer
6
+ class TgzStreamResponse < StreamResponse
7
+ PERMISSIONS = 0644
8
+
9
+ def write_to(stream)
10
+ Zlib::GzipWriter.wrap(stream) do |gz|
11
+ pipe = StringIO.new('', 'wb')
12
+ tar = Gem::Package::TarWriter.new(pipe)
13
+
14
+ enum.each do |file_name, contents|
15
+ tar.add_file(file_name, PERMISSIONS) do |f|
16
+ f.write(contents)
17
+ end
18
+
19
+ flush(tar, pipe, gz)
20
+ stream.flush
21
+ end
22
+
23
+ flush(tar, pipe, gz)
24
+ end
25
+ end
26
+
27
+ def file_extension
28
+ '.tgz'
29
+ end
30
+
31
+ private
32
+
33
+ def flush(tar, pipe, gz)
34
+ tar.flush
35
+ gz.write(pipe.string)
36
+ pipe.reopen('')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ require 'openssl'
2
+
3
+ module TxghServer
4
+ class TransifexRequestAuth
5
+ HMAC_DIGEST = OpenSSL::Digest.new('sha1')
6
+ RACK_HEADER = 'HTTP_X_TX_SIGNATURE'
7
+ TRANSIFEX_HEADER = 'X-TX-Signature'
8
+
9
+ class << self
10
+ def authentic_request?(request, secret)
11
+ request.body.rewind
12
+ expected_signature = header_value(request.body.read, secret)
13
+ actual_signature = request.env[RACK_HEADER]
14
+ actual_signature == expected_signature
15
+ end
16
+
17
+ def header_value(content, secret)
18
+ digest(transform(content), secret)
19
+ end
20
+
21
+ private
22
+
23
+ # In order to generate a correct HMAC hash, the request body must be
24
+ # parsed and made to look like a python map. If you're thinking that's
25
+ # weird, you're correct, but it's apparently expected behavior.
26
+ def transform(content)
27
+ params = URI.decode_www_form(content)
28
+
29
+ params = params.map do |key, val|
30
+ key = "'#{key}'"
31
+ val = interpret_val(val)
32
+ "#{key}: #{val}"
33
+ end
34
+
35
+ "{#{params.join(', ')}}"
36
+ end
37
+
38
+ def interpret_val(val)
39
+ if val =~ /\A[\d]+\z/
40
+ val
41
+ else
42
+ "u'#{val}'"
43
+ end
44
+ end
45
+
46
+ def digest(content, secret)
47
+ Base64.encode64(
48
+ OpenSSL::HMAC.digest(HMAC_DIGEST, secret, content)
49
+ ).strip
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ module TxghServer
2
+ module Triggers
3
+ class Handler
4
+
5
+ # includes response helpers in both the class and the singleton class
6
+ include ResponseHelpers
7
+
8
+ class << self
9
+ def handle_request(request, logger)
10
+ handle_safely do
11
+ config = Txgh::Config::KeyManager.config_from_project(
12
+ request.params.fetch('project_slug')
13
+ )
14
+
15
+ handler_for(config, request, logger).execute
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def handle_safely
22
+ yield
23
+ rescue => e
24
+ respond_with_error(500, "Internal server error: #{e.message}", e)
25
+ end
26
+
27
+ def handler_for(config, request, logger)
28
+ new(
29
+ project: config.transifex_project,
30
+ repo: config.github_repo,
31
+ branch: request.params.fetch('branch'),
32
+ resource_slug: request.params.fetch('resource_slug'),
33
+ logger: logger
34
+ )
35
+ end
36
+ end
37
+
38
+ attr_reader :project, :repo, :branch, :resource_slug, :logger
39
+
40
+ def initialize(options = {})
41
+ @project = options[:project]
42
+ @repo = options[:repo]
43
+ @branch = Txgh::Utils.absolute_branch(options[:branch])
44
+ @resource_slug = options[:resource_slug]
45
+ @logger = options[:logger]
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module TxghServer
2
+ module Triggers
3
+ class PullHandler < Handler
4
+
5
+ def execute
6
+ puller.pull_slug(resource_slug)
7
+ respond_with(200, true)
8
+ end
9
+
10
+ private
11
+
12
+ def puller
13
+ @puller ||= Txgh::Puller.new(project, repo, branch)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module TxghServer
2
+ module Triggers
3
+ class PushHandler < Handler
4
+
5
+ def execute
6
+ pusher.push_slug(resource_slug)
7
+ respond_with(200, true)
8
+ end
9
+
10
+ private
11
+
12
+ def pusher
13
+ @pusher ||= Txgh::Pusher.new(project, repo, branch)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module TxghServer
2
+ module Triggers
3
+ autoload :Handler, 'txgh-server/triggers/handler'
4
+ autoload :PullHandler, 'txgh-server/triggers/pull_handler'
5
+ autoload :PushHandler, 'txgh-server/triggers/push_handler'
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module TxghServer
2
+ VERSION = '1.0.0.beta1'
3
+ end
@@ -0,0 +1,37 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Github
4
+ class DeleteHandler < Handler
5
+
6
+ include Txgh::CategorySupport
7
+
8
+ def execute
9
+ perform_delete if should_handle_request?
10
+ respond_with(200, true)
11
+ end
12
+
13
+ private
14
+
15
+ def perform_delete
16
+ deleter.delete_resources
17
+ end
18
+
19
+ def deleter
20
+ @deleter ||= Txgh::ResourceDeleter.new(project, repo, branch)
21
+ end
22
+
23
+ def should_handle_request?
24
+ # ref_type can be either 'branch' or 'tag' - we only care about branches
25
+ payload['ref_type'] == 'branch' &&
26
+ repo.should_process_ref?(branch) &&
27
+ project.auto_delete_resources?
28
+ end
29
+
30
+ def branch
31
+ Txgh::Utils.absolute_branch(payload['ref'].sub(/^refs\//, ''))
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ require 'logger'
2
+
3
+ module TxghServer
4
+ module Webhooks
5
+ module Github
6
+ class Handler
7
+ include ResponseHelpers
8
+
9
+ attr_reader :project, :repo, :payload, :logger
10
+
11
+ def initialize(options = {})
12
+ @project = options.fetch(:project)
13
+ @repo = options.fetch(:repo)
14
+ @payload = options.fetch(:payload)
15
+ @logger = options.fetch(:logger) { Logger.new(STDOUT) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Github
4
+ # Handles github's ping event, which is a test event fired whenever a new
5
+ # webhook is set up.
6
+ class PingHandler
7
+ include ResponseHelpers
8
+
9
+ def initialize(options = {})
10
+ end
11
+
12
+ def execute
13
+ respond_with(200, {})
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end