txgh-server 1.0.0.beta1

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