]> git.sesse.net Git - casparcg/blob - protocol/amcp/AMCPProtocolStrategy.cpp
#568 Fixed bug where not all error responses was preprended with RES [requestId]
[casparcg] / protocol / amcp / AMCPProtocolStrategy.cpp
1 /*
2 * Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>
3 *
4 * This file is part of CasparCG (www.casparcg.com).
5 *
6 * CasparCG is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * CasparCG is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Author: Nicklas P Andersson
20 */
21
22
23 #include "../StdAfx.h"
24
25 #include "AMCPProtocolStrategy.h"
26 #include "amcp_shared.h"
27 #include "AMCPCommand.h"
28 #include "AMCPCommandQueue.h"
29 #include "amcp_command_repository.h"
30
31 #include <stdio.h>
32 #include <string.h>
33 #include <algorithm>
34 #include <cctype>
35 #include <future>
36
37 #include <core/help/help_repository.h>
38 #include <core/help/help_sink.h>
39
40 #include <boost/algorithm/string/trim.hpp>
41 #include <boost/algorithm/string/split.hpp>
42 #include <boost/algorithm/string/replace.hpp>
43 #include <boost/lexical_cast.hpp>
44
45 #if defined(_MSC_VER)
46 #pragma warning (push, 1) // TODO: Legacy code, just disable warnings
47 #endif
48
49 namespace caspar { namespace protocol { namespace amcp {
50
51 using IO::ClientInfoPtr;
52
53 template <typename Out, typename In>
54 bool try_lexical_cast(const In& input, Out& result)
55 {
56         Out saved = result;
57         bool success = boost::conversion::detail::try_lexical_convert(input, result);
58
59         if (!success)
60                 result = saved; // Needed because of how try_lexical_convert is implemented.
61
62         return success;
63 }
64
65 struct AMCPProtocolStrategy::impl
66 {
67 private:
68         std::vector<AMCPCommandQueue::ptr_type>         commandQueues_;
69         spl::shared_ptr<amcp_command_repository>        repo_;
70
71 public:
72         impl(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
73                 : repo_(repo)
74         {
75                 commandQueues_.push_back(spl::make_shared<AMCPCommandQueue>(L"General Queue for " + name));
76
77                 for (int i = 0; i < repo_->channels().size(); ++i)
78                 {
79                         commandQueues_.push_back(spl::make_shared<AMCPCommandQueue>(
80                                         L"Channel " + boost::lexical_cast<std::wstring>(i + 1) + L" for " + name));
81                 }
82         }
83
84         ~impl() {}
85
86         enum class error_state {
87                 no_error = 0,
88                 command_error,
89                 channel_error,
90                 parameters_error,
91                 unknown_error,
92                 access_error
93         };
94
95         struct command_interpreter_result
96         {
97                 std::shared_ptr<caspar::IO::lock_container>     lock;
98                 std::wstring                                                            request_id;
99                 std::wstring                                                            command_name;
100                 AMCPCommand::ptr_type                                           command;
101                 error_state                                                                     error                   = error_state::no_error;
102                 std::shared_ptr<AMCPCommandQueue>                       queue;
103         };
104
105         //The paser method expects message to be complete messages with the delimiter stripped away.
106         //Thesefore the AMCPProtocolStrategy should be decorated with a delimiter_based_chunking_strategy
107         void Parse(const std::wstring& message, ClientInfoPtr client)
108         {
109                 CASPAR_LOG_COMMUNICATION(info) << L"Received message from " << client->address() << ": " << message << L"\\r\\n";
110
111                 command_interpreter_result result;
112                 if(interpret_command_string(message, result, client))
113                 {
114                         if(result.lock && !result.lock->check_access(client))
115                                 result.error = error_state::access_error;
116                         else
117                                 result.queue->AddCommand(result.command);
118                 }
119
120                 if (result.error != error_state::no_error)
121                 {
122                         std::wstringstream answer;
123
124                         if (!result.request_id.empty())
125                                 answer << L"RES " << result.request_id << L" ";
126
127                         switch(result.error)
128                         {
129                         case error_state::command_error:
130                                 answer << L"400 ERROR\r\n" << message << "\r\n";
131                                 break;
132                         case error_state::channel_error:
133                                 answer << L"401 " << result.command_name << " ERROR\r\n";
134                                 break;
135                         case error_state::parameters_error:
136                                 answer << L"402 " << result.command_name << " ERROR\r\n";
137                                 break;
138                         case error_state::access_error:
139                                 answer << L"503 " << result.command_name << " FAILED\r\n";
140                                 break;
141                         case error_state::unknown_error:
142                                 answer << L"500 FAILED\r\n";
143                                 break;
144                         default:
145                                 CASPAR_THROW_EXCEPTION(programming_error()
146                                                 << msg_info(L"Unhandled error_state enum constant " + boost::lexical_cast<std::wstring>(static_cast<int>(result.error))));
147                         }
148                         client->send(answer.str());
149                 }
150         }
151
152 private:
153         bool interpret_command_string(const std::wstring& message, command_interpreter_result& result, ClientInfoPtr client)
154         {
155                 try
156                 {
157                         std::list<std::wstring> tokens;
158                         tokenize(message, tokens);
159
160                         // Discard GetSwitch
161                         if (!tokens.empty() && tokens.front().at(0) == L'/')
162                                 tokens.pop_front();
163
164                         if (!tokens.empty() && boost::iequals(tokens.front(), L"REQ"))
165                         {
166                                 tokens.pop_front();
167
168                                 if (tokens.empty())
169                                 {
170                                         result.error = error_state::parameters_error;
171                                         return false;
172                                 }
173
174                                 result.request_id = tokens.front();
175                                 tokens.pop_front();
176                         }
177
178                         // Fail if no more tokens.
179                         if (tokens.empty())
180                         {
181                                 result.error = error_state::command_error;
182                                 return false;
183                         }
184
185                         // Consume command name
186                         result.command_name = boost::to_upper_copy(tokens.front());
187                         tokens.pop_front();
188
189                         // Determine whether the next parameter is a channel spec or not
190                         int channel_index = -1;
191                         int layer_index = -1;
192                         std::wstring channel_spec;
193
194                         if (!tokens.empty())
195                         {
196                                 channel_spec = tokens.front();
197                                 std::wstring channelid_str = boost::trim_copy(channel_spec);
198                                 std::vector<std::wstring> split;
199                                 boost::split(split, channelid_str, boost::is_any_of("-"));
200
201                                 // Use non_throwing lexical cast to not hit exception break point all the time.
202                                 if (try_lexical_cast(split[0], channel_index))
203                                 {
204                                         --channel_index;
205
206                                         if (split.size() > 1)
207                                                 try_lexical_cast(split[1], layer_index);
208
209                                         // Consume channel-spec
210                                         tokens.pop_front();
211                                 }
212                         }
213
214                         bool is_channel_command = channel_index != -1;
215
216                         // Create command instance
217                         if (is_channel_command)
218                         {
219                                 result.command = repo_->create_channel_command(result.command_name, client, channel_index, layer_index, tokens);
220
221                                 if (result.command)
222                                 {
223                                         result.lock = repo_->channels().at(channel_index).lock;
224                                         result.queue = commandQueues_.at(channel_index + 1);
225                                 }
226                                 else // Might be a non channel command, although the first argument is numeric
227                                 {
228                                         // Restore backed up channel spec string.
229                                         tokens.push_front(channel_spec);
230                                         result.command = repo_->create_command(result.command_name, client, tokens);
231
232                                         if (result.command)
233                                                 result.queue = commandQueues_.at(0);
234                                 }
235                         }
236                         else
237                         {
238                                 result.command = repo_->create_command(result.command_name, client, tokens);
239
240                                 if (result.command)
241                                         result.queue = commandQueues_.at(0);
242                         }
243
244                         if (!result.command)
245                                 result.error = error_state::command_error;
246                         else
247                         {
248                                 std::vector<std::wstring> parameters(tokens.begin(), tokens.end());
249
250                                 result.command->parameters() = std::move(parameters);
251
252                                 if (result.command->parameters().size() < result.command->minimum_parameters())
253                                         result.error = error_state::parameters_error;
254                         }
255
256                         if (result.command)
257                                 result.command->set_request_id(result.request_id);
258                 }
259                 catch (std::out_of_range&)
260                 {
261                         CASPAR_LOG(error) << "Invalid channel specified.";
262                         result.error = error_state::channel_error;
263                 }
264                 catch (...)
265                 {
266                         CASPAR_LOG_CURRENT_EXCEPTION();
267                         result.error = error_state::unknown_error;
268                 }
269
270                 return result.error == error_state::no_error;
271         }
272
273         template<typename C>
274         std::size_t tokenize(const std::wstring& message, C& pTokenVector)
275         {
276                 //split on whitespace but keep strings within quotationmarks
277                 //treat \ as the start of an escape-sequence: the following char will indicate what to actually put in the string
278
279                 std::wstring currentToken;
280
281                 bool inQuote = false;
282                 bool getSpecialCode = false;
283
284                 for(unsigned int charIndex=0; charIndex<message.size(); ++charIndex)
285                 {
286                         if(getSpecialCode)
287                         {
288                                 //insert code-handling here
289                                 switch(message[charIndex])
290                                 {
291                                 case L'\\':
292                                         currentToken += L"\\";
293                                         break;
294                                 case L'\"':
295                                         currentToken += L"\"";
296                                         break;
297                                 case L'n':
298                                         currentToken += L"\n";
299                                         break;
300                                 default:
301                                         break;
302                                 };
303                                 getSpecialCode = false;
304                                 continue;
305                         }
306
307                         if(message[charIndex]==L'\\')
308                         {
309                                 getSpecialCode = true;
310                                 continue;
311                         }
312
313                         if(message[charIndex]==L' ' && inQuote==false)
314                         {
315                                 if(!currentToken.empty())
316                                 {
317                                         pTokenVector.push_back(currentToken);
318                                         currentToken.clear();
319                                 }
320                                 continue;
321                         }
322
323                         if(message[charIndex]==L'\"')
324                         {
325                                 inQuote = !inQuote;
326
327                                 if(!currentToken.empty() || !inQuote)
328                                 {
329                                         pTokenVector.push_back(currentToken);
330                                         currentToken.clear();
331                                 }
332                                 continue;
333                         }
334
335                         currentToken += message[charIndex];
336                 }
337
338                 if(!currentToken.empty())
339                 {
340                         pTokenVector.push_back(currentToken);
341                         currentToken.clear();
342                 }
343
344                 return pTokenVector.size();
345         }
346 };
347
348 AMCPProtocolStrategy::AMCPProtocolStrategy(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
349         : impl_(spl::make_unique<impl>(name, repo))
350 {
351 }
352 AMCPProtocolStrategy::~AMCPProtocolStrategy() {}
353 void AMCPProtocolStrategy::Parse(const std::wstring& msg, IO::ClientInfoPtr pClientInfo) { impl_->Parse(msg, pClientInfo); }
354
355
356 }       //namespace amcp
357 }}      //namespace caspar