-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathcontroller.rb
More file actions
233 lines (192 loc) · 7.9 KB
/
controller.rb
File metadata and controls
233 lines (192 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
require "chatops"
module Chatops
module Controller
class ConfigurationError < StandardError ; end
extend ActiveSupport::Concern
included do
before_action :ensure_valid_chatops_url, if: :should_authenticate_chatops?
before_action :ensure_valid_chatops_timestamp, if: :should_authenticate_chatops?
before_action :ensure_valid_chatops_signature, if: :should_authenticate_chatops?
before_action :ensure_valid_chatops_nonce, if: :should_authenticate_chatops?
before_action :ensure_chatops_authenticated, if: :should_authenticate_chatops?
before_action :ensure_user_given
before_action :ensure_method_exists
end
def list
chatops = self.class.chatops
chatops.each { |name, hash| hash[:path] = name }
render :json => {
namespace: self.class.chatops_namespace,
help: self.class.chatops_help,
error_response: self.class.chatops_error_response,
methods: chatops,
version: "3" }
end
def process(*args)
setup_params!
if params[:chatop].present?
params[:action] = params[:chatop]
args[0] = params[:action]
unless self.respond_to?(params[:chatop].to_sym)
raise AbstractController::ActionNotFound
end
end
super(*args)
rescue AbstractController::ActionNotFound
return jsonrpc_method_not_found
end
def execute_chatop
# This needs to exist for route declarations, but we'll be overriding
# things in #process to make a method the action.
end
protected
def setup_params!
json_body.each do |key, value|
next if params.has_key? key
params[key] = value
end
@jsonrpc_params = params.delete(:params) if params.has_key? :params
self.params = params.permit(:action, :chatop, :controller, :id, :mention_slug, :message_id, :method, :room_id, :user, :raw_command, :thread_id)
end
def jsonrpc_params
@jsonrpc_params ||= ActionController::Parameters.new
end
def json_body
hash = {}
if request.content_mime_type == Mime[:json]
hash = ActiveSupport::JSON.decode(request.raw_post) || {}
end
hash.with_indifferent_access
end
# `options` supports any of the optional fields documented
# in the [protocol](../../docs/protocol-description.md).
def jsonrpc_success(message, options: {})
response = { :result => message.to_s }
# do not allow options to override message
options.delete(:result)
jsonrpc_response response.merge(options)
end
alias_method :chatop_send, :jsonrpc_success
def jsonrpc_parse_error
jsonrpc_error(-32700, 500, "Parse error")
end
def jsonrpc_invalid_request
jsonrpc_error(-32600, 400, "Invalid request")
end
def jsonrpc_method_not_found
jsonrpc_error(-32601, 404, "Method not found")
end
def jsonrpc_invalid_params(message)
message ||= "Invalid parameters"
jsonrpc_error(-32602, 400, message.to_s)
end
alias_method :jsonrpc_failure, :jsonrpc_invalid_params
def jsonrpc_error(number, http_status, message)
jsonrpc_response({ :error => { :code => number, :message => message.to_s } }, http_status)
end
def jsonrpc_response(hash, http_status = nil)
http_status ||= 200
render :status => http_status,
:json => { :jsonrpc => "2.0",
:id => params[:id] }.merge(hash)
end
def ensure_user_given
return true unless chatop_names.include?(params[:action].to_sym)
return true if params[:user].present?
jsonrpc_invalid_params("A username must be supplied as 'user'")
end
def ensure_chatops_authenticated
raise ConfigurationError.new("You need to add a client's public key in .pem format via #{Chatops.public_key_env_var_name}") unless Chatops.public_key.present?
body = request.raw_post || ""
@chatops_urls.each do |url|
signature_string = [url, @chatops_nonce, @chatops_timestamp, body].join("\n")
# We return this just to aid client debugging.
response.headers["Chatops-Signature-String"] = Base64.strict_encode64(signature_string)
if signature_valid?(Chatops.public_key, @chatops_signature, signature_string) ||
signature_valid?(Chatops.alt_public_key, @chatops_signature, signature_string)
return true
end
end
return jsonrpc_error(-32800, 403, "Not authorized")
end
def ensure_valid_chatops_url
unless Chatops.auth_base_urls.present?
raise ConfigurationError.new("You need to set the server's base URL to authenticate chatops RPC via #{Chatops.auth_base_url_env_var_name}")
end
@chatops_urls = Chatops.auth_base_urls.map { |url| url.chomp("/") + request.path }
end
def ensure_valid_chatops_nonce
@chatops_nonce = request.headers["Chatops-Nonce"]
return jsonrpc_error(-32801, 403, "A Chatops-Nonce header is required") unless @chatops_nonce.present?
end
def ensure_valid_chatops_signature
signature_header = request.headers["Chatops-Signature"]
begin
# "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" }
signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h
@chatops_signature = signature_items["signature"]
rescue NoMethodError
# The signature header munging, if something's amiss, can produce a `nil` that raises a
# no method error. We'll just carry on; the nil signature will raise below
end
unless @chatops_signature.present?
return jsonrpc_error(-32802, 403, "Failed to parse signature header")
end
end
def ensure_valid_chatops_timestamp
@chatops_timestamp = request.headers["Chatops-Timestamp"]
time = Time.iso8601(@chatops_timestamp)
if !(time > Chatops::ALLOWED_TIME_SKEW_MINS.minute.ago && time < Chatops::ALLOWED_TIME_SKEW_MINS.minute.from_now)
return jsonrpc_error(-32803, 403, "Chatops timestamp not within #{Chatops::ALLOWED_TIME_SKEW_MINS} minutes of server time: #{@chatops_timestamp} vs #{Time.now.utc.iso8601}")
end
rescue ArgumentError, TypeError
# time parsing or missing can raise these
return jsonrpc_error(-32804, 403, "Invalid Chatops-Timestamp: #{@chatops_timestamp}")
end
def request_is_chatop?
(chatop_names + [:list]).include?(params[:action].to_sym)
end
def chatops_test_auth?
Rails.env.test? && request.env["CHATOPS_TESTING_AUTH"]
end
def should_authenticate_chatops?
request_is_chatop? && !chatops_test_auth?
end
def signature_valid?(key_string, signature, signature_string)
return false unless key_string.present?
digest = OpenSSL::Digest::SHA256.new
decoded_signature = Base64.decode64(signature)
public_key = OpenSSL::PKey::RSA.new(key_string)
public_key.verify(digest, decoded_signature, signature_string)
end
def ensure_method_exists
return jsonrpc_method_not_found unless (chatop_names + [:list]).include?(params[:action].to_sym)
end
def chatop_names
self.class.chatops.keys
end
module ClassMethods
def chatop(method_name, regex, help, &block)
chatops[method_name] = { help: help,
regex: regex.source,
params: regex.names }
define_method method_name, &block
end
%w{namespace help error_response}.each do |setting|
method_name = "chatops_#{setting}".to_sym
variable_name = "@#{method_name}".to_sym
define_method method_name do |*args|
assignment = args.first
if assignment.present?
instance_variable_set variable_name, assignment
end
instance_variable_get variable_name.to_sym
end
end
def chatops
@chatops ||= {}
@chatops
end
end
end
end