txgh-server 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f70d20e0073d7671f6010e1dfd962c7e4758482d
4
- data.tar.gz: 9d1167e2d2b25fdbde79051bc93c32535d3fec04
3
+ metadata.gz: 759f65691df917f6705c476c987285b4941b9343
4
+ data.tar.gz: ba32bd6e35a211fa01757d30609949fb48006f4f
5
5
  SHA512:
6
- metadata.gz: b3a6abf97a38ed2e92d887ff03e38b5c4327f89816d3c8e87a99ec8f0300326262568f9c5755d08f4451b54fbafe652110dd7685b4c36c4464ffd8c8aa658920
7
- data.tar.gz: 25147bc9bd2612a674869236fd13eb78b74aacb58edaf6e5bae058cfa6a97d789d314f21bb62c86ca5fb0840a3ec401153596e4e812058c76918b1cec9d567d5
6
+ metadata.gz: e5683453efde01a21ed1b3f9410a8ae11509764aeeee0323f320d0c4cb86ffa20526b6cb45b3bd04a9ecbce344fbee910cc4bb69a678bd5606381891e8362ec9
7
+ data.tar.gz: 07b1d539ddb62965a186b4987e7b4d1e315018e42a4dcd7e0af149877cc7d60df7ae18e7105d4e68629b844038c02b9acaa3178fd5ac451daca1e913cbfd98af
@@ -9,12 +9,12 @@ module TxghServer
9
9
  class << self
10
10
  def authentic_request?(request, secret)
11
11
  request.body.rewind
12
- expected_signature = header_value(request.body.read, secret)
12
+ expected_signature = compute_signature(request.body.read, secret)
13
13
  actual_signature = request.env[RACK_HEADER]
14
14
  actual_signature == expected_signature
15
15
  end
16
16
 
17
- def header_value(content, secret)
17
+ def compute_signature(content, secret)
18
18
  "sha1=#{digest(content, secret)}"
19
19
  end
20
20
 
@@ -1,22 +1,32 @@
1
+ require 'json'
1
2
  require 'openssl'
2
3
  require 'base64'
3
4
 
4
5
  module TxghServer
5
6
  class TransifexRequestAuth
6
- HMAC_DIGEST = OpenSSL::Digest.new('sha1')
7
- RACK_HEADER = 'HTTP_X_TX_SIGNATURE'
8
- TRANSIFEX_HEADER = 'X-TX-Signature'
7
+ HMAC_DIGEST = OpenSSL::Digest.new('sha256')
8
+ RACK_HEADER = 'HTTP_X_TX_SIGNATURE_V2'
9
+ TRANSIFEX_HEADER = 'X-TX-Signature-V2'
9
10
 
10
11
  class << self
11
12
  def authentic_request?(request, secret)
12
13
  request.body.rewind
13
- expected_signature = header_value(request.body.read, secret)
14
+
15
+ expected_signature = compute_signature(
16
+ http_verb: request.request_method,
17
+ date_str: request.env['HTTP_DATE'],
18
+ url: request.env['HTTP_X_TX_URL'],
19
+ content: request.body.read,
20
+ secret: secret
21
+ )
22
+
14
23
  actual_signature = signature_from(request)
15
24
  actual_signature == expected_signature
16
25
  end
17
26
 
18
- def header_value(content, secret)
19
- digest(transform(content), secret)
27
+ def compute_signature(http_verb: 'POST', url:, date_str:, content:, secret:)
28
+ data = [http_verb, url, date_str, Digest::MD5.hexdigest(content)]
29
+ digest(data.join("\n"), secret)
20
30
  end
21
31
 
22
32
  def signature_from(request)
@@ -25,29 +35,6 @@ module TxghServer
25
35
 
26
36
  private
27
37
 
28
- # In order to generate a correct HMAC hash, the request body must be
29
- # parsed and made to look like a python map. If you're thinking that's
30
- # weird, you're correct, but it's apparently expected behavior.
31
- def transform(content)
32
- params = URI.decode_www_form(content)
33
-
34
- params = params.map do |key, val|
35
- key = "'#{key}'"
36
- val = interpret_val(val)
37
- "#{key}: #{val}"
38
- end
39
-
40
- "{#{params.join(', ')}}"
41
- end
42
-
43
- def interpret_val(val)
44
- if val =~ /\A[\d]+\z/
45
- val
46
- else
47
- "u'#{val}'"
48
- end
49
- end
50
-
51
38
  def digest(content, secret)
52
39
  Base64.encode64(
53
40
  OpenSSL::HMAC.digest(HMAC_DIGEST, secret, content)
@@ -1,3 +1,3 @@
1
1
  module TxghServer
2
- VERSION = '2.4.0'
2
+ VERSION = '3.0.0'
3
3
  end
@@ -1,4 +1,4 @@
1
- require 'uri'
1
+ require 'json'
2
2
 
3
3
  module TxghServer
4
4
  module Webhooks
@@ -20,7 +20,10 @@ module TxghServer
20
20
  end
21
21
 
22
22
  def handle_request
23
- publish_event
23
+ if publish_event
24
+ # the event handled the request
25
+ return respond_with(204, '') # no content
26
+ end
24
27
 
25
28
  handle_safely do
26
29
  handler = TxghServer::Webhooks::Transifex::HookHandler.new(
@@ -38,13 +41,18 @@ module TxghServer
38
41
  private
39
42
 
40
43
  def publish_event
41
- Txgh.events.publish(
42
- 'transifex.webhook_received', {
43
- payload: payload,
44
- raw_payload: raw_payload,
45
- signature: TransifexRequestAuth.signature_from(request)
46
- }
47
- )
44
+ if Txgh.events.channel_hash.include?('transifex.webhook_received')
45
+ Txgh.events.publish(
46
+ 'transifex.webhook_received', {
47
+ payload: payload,
48
+ raw_payload: raw_payload,
49
+ signature: TransifexRequestAuth.signature_from(request),
50
+ url: request.env['HTTP_X_TX_URL'],
51
+ date_str: request.env['HTTP_DATE'],
52
+ http_verb: request.request_method
53
+ }
54
+ )
55
+ end
48
56
  end
49
57
 
50
58
  def handle_safely
@@ -83,11 +91,9 @@ module TxghServer
83
91
  end
84
92
 
85
93
  def payload
86
- @payload ||= begin
87
- Txgh::Utils.deep_symbolize_keys(
88
- Hash[URI.decode_www_form(raw_payload)]
89
- )
90
- end
94
+ @payload ||= Txgh::Utils.deep_symbolize_keys(
95
+ JSON.parse(raw_payload)
96
+ )
91
97
  end
92
98
 
93
99
  end
@@ -109,10 +109,19 @@ describe TxghServer::WebhookEndpoints do
109
109
 
110
110
  describe '/transifex' do
111
111
  def sign_with(body)
112
+ date_str = Time.now.strftime('%a, %d %b %Y %H:%M:%S GMT')
113
+
114
+ header('Date', date_str)
115
+ header('X-Tx-Url', 'http://example.org/transifex')
116
+
112
117
  header(
113
118
  TxghServer::TransifexRequestAuth::TRANSIFEX_HEADER,
114
- TxghServer::TransifexRequestAuth.header_value(
115
- body, config.transifex_project.webhook_secret
119
+ TxghServer::TransifexRequestAuth.compute_signature(
120
+ http_verb: 'POST',
121
+ url: 'http://example.org/transifex',
122
+ date_str: date_str,
123
+ content: body,
124
+ secret: config.transifex_project.webhook_secret
116
125
  )
117
126
  )
118
127
  end
@@ -128,7 +137,7 @@ describe TxghServer::WebhookEndpoints do
128
137
  }
129
138
  end
130
139
 
131
- let(:payload) { URI.encode_www_form(params.to_a) }
140
+ let(:payload) { params.to_json }
132
141
 
133
142
  before(:each) do
134
143
  allow(TxghServer::Webhooks::Transifex::HookHandler).to(
@@ -145,7 +154,7 @@ describe TxghServer::WebhookEndpoints do
145
154
  receive(:execute).and_return(respond_with(200, true))
146
155
  )
147
156
 
148
- payload = URI.encode_www_form(params.to_a)
157
+ payload = params.to_json
149
158
  sign_with payload
150
159
  post '/transifex', payload
151
160
  expect(last_response).to be_ok
@@ -174,7 +183,7 @@ describe TxghServer::WebhookEndpoints do
174
183
  def sign_with(body)
175
184
  header(
176
185
  TxghServer::GithubRequestAuth::GITHUB_HEADER,
177
- TxghServer::GithubRequestAuth.header_value(
186
+ TxghServer::GithubRequestAuth.compute_signature(
178
187
  body, config.github_repo.webhook_secret
179
188
  )
180
189
  )
@@ -30,9 +30,9 @@ describe GithubRequestAuth do
30
30
  end
31
31
  end
32
32
 
33
- describe '.header' do
33
+ describe '.compute_signature' do
34
34
  it 'calculates the signature and formats it as an http header' do
35
- value = GithubRequestAuth.header_value(params, secret)
35
+ value = GithubRequestAuth.compute_signature(params, secret)
36
36
  expect(value).to eq("sha1=#{valid_signature}")
37
37
  end
38
38
  end
@@ -1,10 +1,11 @@
1
1
  class TestRequest
2
- attr_reader :body, :params, :headers
2
+ attr_reader :body, :params, :headers, :request_method
3
3
  alias_method :env, :headers
4
4
 
5
- def initialize(body: , params: {}, headers: {})
5
+ def initialize(body: , params: {}, headers: {}, request_method: 'POST')
6
6
  @body = StringIO.new(body)
7
7
  @params = params
8
8
  @headers = headers
9
+ @request_method = request_method
9
10
  end
10
11
  end
@@ -93,14 +93,27 @@ describe 'hook integration tests', integration: true do
93
93
  def sign_github_request(body)
94
94
  header(
95
95
  GithubRequestAuth::GITHUB_HEADER,
96
- GithubRequestAuth.header_value(body, config.github_repo.webhook_secret)
96
+ GithubRequestAuth.compute_signature(
97
+ body, config.github_repo.webhook_secret
98
+ )
97
99
  )
98
100
  end
99
101
 
100
102
  def sign_transifex_request(body)
103
+ date_str = Time.now.strftime('%a, %d %b %Y %H:%M:%S GMT')
104
+
105
+ header('Date', date_str)
106
+ header('X-Tx-Url', 'http://example.org/transifex')
107
+
101
108
  header(
102
- TransifexRequestAuth::TRANSIFEX_HEADER,
103
- TransifexRequestAuth.header_value(body, config.transifex_project.webhook_secret)
109
+ TxghServer::TransifexRequestAuth::TRANSIFEX_HEADER,
110
+ TxghServer::TransifexRequestAuth.compute_signature(
111
+ http_verb: 'POST',
112
+ url: 'http://example.org/transifex',
113
+ date_str: date_str,
114
+ content: body,
115
+ secret: config.transifex_project.webhook_secret
116
+ )
104
117
  )
105
118
  end
106
119
 
@@ -115,7 +128,7 @@ describe 'hook integration tests', integration: true do
115
128
  'language' => 'el_GR', 'translated' => 100
116
129
  }
117
130
 
118
- payload = URI.encode_www_form(params.to_a)
131
+ payload = params.to_json
119
132
 
120
133
  sign_transifex_request(payload)
121
134
  post '/transifex', payload
@@ -5,13 +5,24 @@ include TxghServer
5
5
 
6
6
  describe TransifexRequestAuth do
7
7
  let(:secret) { 'abc123' }
8
- let(:params) { 'param1=value1&param2=value2&param3=123' }
9
- let(:valid_signature) { 'pXucIcivBezpfNgCGTHKYeDve84=' }
8
+ let(:params) { { param1: 'value1', param2: 'value2', param3: 123 }.to_json }
9
+ let(:date_str) { Time.now.strftime('%a, %d %b %Y %H:%M:%S GMT') }
10
+ let(:http_verb) { 'POST' }
11
+ let(:url) { 'http://example.com/transifex' }
12
+ let(:valid_signature) do
13
+ data = [http_verb, url, date_str, Digest::MD5.hexdigest(params)]
14
+ Base64.encode64(
15
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, data.join("\n"))
16
+ ).strip
17
+ end
10
18
 
11
19
  describe '.authentic_request?' do
12
20
  it 'returns true if the request is signed correctly' do
13
21
  request = Rack::Request.new(
14
22
  TransifexRequestAuth::RACK_HEADER => valid_signature,
23
+ 'HTTP_DATE' => date_str,
24
+ 'REQUEST_METHOD' => http_verb,
25
+ 'HTTP_X_TX_URL' => url,
15
26
  'rack.input' => StringIO.new(params)
16
27
  )
17
28
 
@@ -30,9 +41,16 @@ describe TransifexRequestAuth do
30
41
  end
31
42
  end
32
43
 
33
- describe '.header' do
44
+ describe '.compute_signature' do
34
45
  it 'calculates the signature and formats it as an http header' do
35
- value = TransifexRequestAuth.header_value(params, secret)
46
+ value = TransifexRequestAuth.compute_signature(
47
+ http_verb: http_verb,
48
+ date_str: date_str,
49
+ url: url,
50
+ content: params,
51
+ secret: secret
52
+ )
53
+
36
54
  expect(value).to eq(valid_signature)
37
55
  end
38
56
  end
@@ -9,15 +9,17 @@ describe Transifex::RequestHandler do
9
9
  include StandardTxghSetup
10
10
 
11
11
  let(:logger) { NilLogger.new }
12
- let(:body) { URI.encode_www_form(payload.to_a) }
12
+ let(:body) { payload.to_json }
13
13
  let(:signature) { 'abc123' }
14
14
  let(:headers) { { TxghServer::TransifexRequestAuth::RACK_HEADER => signature } }
15
- let(:request) { TestRequest.new(body: body, headers: headers) }
15
+ let(:request) { TestRequest.new(body: body, headers: headers, request_method: 'POST') }
16
16
  let(:handler) { described_class.new(request, logger) }
17
17
  let(:payload) { { 'project' => project_name, 'resource' => resource_slug, 'language' => 'pt' } }
18
18
 
19
19
  describe '#handle_request' do
20
20
  it 'publishes an event' do
21
+ Txgh.events.subscribe('transifex.webhook_received') { false }
22
+
21
23
  expect { handler.handle_request }.to(
22
24
  change { Txgh.events.published_in('transifex.webhook_received').size }.by(1)
23
25
  )
@@ -26,6 +28,13 @@ describe Transifex::RequestHandler do
26
28
  expect(event[:options][:payload]).to eq(Txgh::Utils.deep_symbolize_keys(payload))
27
29
  expect(event[:options][:raw_payload]).to eq(body)
28
30
  expect(event[:options][:signature]).to eq(signature)
31
+ expect(event[:options][:http_verb]).to eq('POST')
32
+ end
33
+
34
+ it 'returns a 204 if the event handles the request' do
35
+ Txgh.events.subscribe('transifex.webhook_received') { true }
36
+ response = handler.handle_request
37
+ expect(response.status).to eq(204)
29
38
  end
30
39
 
31
40
  it 'does not execute if unauthorized' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: txgh-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Jackowski
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-11-03 00:00:00.000000000 Z
12
+ date: 2018-02-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mime-types