]> git.sesse.net Git - casparcg/blob - protocol/amcp/AMCPProtocolStrategy.cpp
6009b3e930c73199656270718f288ac327ed0ee2
[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                                                            command_name;
99                 AMCPCommand::ptr_type                                           command;
100                 error_state                                                                     error                   = error_state::no_error;
101                 std::shared_ptr<AMCPCommandQueue>                       queue;
102         };
103
104         //The paser method expects message to be complete messages with the delimiter stripped away.
105         //Thesefore the AMCPProtocolStrategy should be decorated with a delimiter_based_chunking_strategy
106         void Parse(const std::wstring& message, ClientInfoPtr client)
107         {
108                 CASPAR_LOG_COMMUNICATION(info) << L"Received message from " << client->address() << ": " << message << L"\\r\\n";
109
110                 command_interpreter_result result;
111                 if(interpret_command_string(message, result, client))
112                 {
113                         if(result.lock && !result.lock->check_access(client))
114                                 result.error = error_state::access_error;
115                         else
116                                 result.queue->AddCommand(result.command);
117                 }
118
119                 if (result.error != error_state::no_error)
120                 {
121                         std::wstringstream answer;
122
123                         switch(result.error)
124                         {
125                         case error_state::command_error:
126                                 answer << L"400 ERROR\r\n" << message << "\r\n";
127                                 break;
128                         case error_state::channel_error:
129                                 answer << L"401 " << result.command_name << " ERROR\r\n";
130                                 break;
131                         case error_state::parameters_error:
132                                 answer << L"402 " << result.command_name << " ERROR\r\n";
133                                 break;
134                         case error_state::access_error:
135                                 answer << L"503 " << result.command_name << " FAILED\r\n";
136                                 break;
137                         case error_state::unknown_error:
138                                 answer << L"500 FAILED\r\n";
139                                 break;
140                         default:
141                                 CASPAR_THROW_EXCEPTION(programming_error()
142                                                 << msg_info(L"Unhandled error_state enum constant " + boost::lexical_cast<std::wstring>(static_cast<int>(result.error))));
143                         }
144                         client->send(answer.str());
145                 }
146         }
147
148 private:
149         bool interpret_command_string(const std::wstring& message, command_interpreter_result& result, ClientInfoPtr client)
150         {
151                 try
152                 {
153                         std::list<std::wstring> tokens;
154                         tokenize(message, tokens);
155
156                         // Discard GetSwitch
157                         if (!tokens.empty() && tokens.front().at(0) == L'/')
158                                 tokens.pop_front();
159
160                         std::wstring request_id;
161
162                         if (!tokens.empty() && boost::iequals(tokens.front(), L"REQ"))
163                         {
164                                 tokens.pop_front();
165
166                                 if (tokens.empty())
167                                 {
168                                         result.error = error_state::parameters_error;
169                                         return false;
170                                 }
171
172                                 request_id = tokens.front();
173                                 tokens.pop_front();
174                         }
175
176                         // Fail if no more tokens.
177                         if (tokens.empty())
178                         {
179                                 result.error = error_state::command_error;
180                                 return false;
181                         }
182
183                         // Consume command name
184                         result.command_name = boost::to_upper_copy(tokens.front());
185                         tokens.pop_front();
186
187                         // Determine whether the next parameter is a channel spec or not
188                         int channel_index = -1;
189                         int layer_index = -1;
190                         std::wstring channel_spec;
191
192                         if (!tokens.empty())
193                         {
194                                 channel_spec = tokens.front();
195                                 std::wstring channelid_str = boost::trim_copy(channel_spec);
196                                 std::vector<std::wstring> split;
197                                 boost::split(split, channelid_str, boost::is_any_of("-"));
198
199                                 // Use non_throwing lexical cast to not hit exception break point all the time.
200                                 if (try_lexical_cast(split[0], channel_index))
201                                 {
202                                         --channel_index;
203
204                                         if (split.size() > 1)
205                                                 try_lexical_cast(split[1], layer_index);
206
207                                         // Consume channel-spec
208                                         tokens.pop_front();
209                                 }
210                         }
211
212                         bool is_channel_command = channel_index != -1;
213
214                         // Create command instance
215                         if (is_channel_command)
216                         {
217                                 result.command = repo_->create_channel_command(result.command_name, client, channel_index, layer_index, tokens);
218
219                                 if (result.command)
220                                 {
221                                         result.lock = repo_->channels().at(channel_index).lock;
222                                         result.queue = commandQueues_.at(channel_index + 1);
223                                 }
224                                 else // Might be a non channel command, although the first argument is numeric
225                                 {
226                                         // Restore backed up channel spec string.
227                                         tokens.push_front(channel_spec);
228                                         result.command = repo_->create_command(result.command_name, client, tokens);
229
230                                         if (result.command)
231                                                 result.queue = commandQueues_.at(0);
232                                 }
233                         }
234                         else
235                         {
236                                 result.command = repo_->create_command(result.command_name, client, tokens);
237
238                                 if (result.command)
239                                         result.queue = commandQueues_.at(0);
240                         }
241
242                         if (!result.command)
243                                 result.error = error_state::command_error;
244                         else
245                         {
246                                 std::vector<std::wstring> parameters(tokens.begin(), tokens.end());
247
248                                 result.command->parameters() = std::move(parameters);
249
250                                 if (result.command->parameters().size() < result.command->minimum_parameters())
251                                         result.error = error_state::parameters_error;
252                         }
253
254                         if (result.command)
255                                 result.command->set_request_id(std::move(request_id));
256                 }
257                 catch (std::out_of_range&)
258                 {
259                         CASPAR_LOG(error) << "Invalid channel specified.";
260                         result.error = error_state::channel_error;
261                 }
262                 catch (...)
263                 {
264                         CASPAR_LOG_CURRENT_EXCEPTION();
265                         result.error = error_state::unknown_error;
266                 }
267
268                 return result.error == error_state::no_error;
269         }
270
271         template<typename C>
272         std::size_t tokenize(const std::wstring& message, C& pTokenVector)
273         {
274                 //split on whitespace but keep strings within quotationmarks
275                 //treat \ as the start of an escape-sequence: the following char will indicate what to actually put in the string
276
277                 std::wstring currentToken;
278
279                 bool inQuote = false;
280                 bool getSpecialCode = false;
281
282                 for(unsigned int charIndex=0; charIndex<message.size(); ++charIndex)
283                 {
284                         if(getSpecialCode)
285                         {
286                                 //insert code-handling here
287                                 switch(message[charIndex])
288                                 {
289                                 case L'\\':
290                                         currentToken += L"\\";
291                                         break;
292                                 case L'\"':
293                                         currentToken += L"\"";
294                                         break;
295                                 case L'n':
296                                         currentToken += L"\n";
297                                         break;
298                                 default:
299                                         break;
300                                 };
301                                 getSpecialCode = false;
302                                 continue;
303                         }
304
305                         if(message[charIndex]==L'\\')
306                         {
307                                 getSpecialCode = true;
308                                 continue;
309                         }
310
311                         if(message[charIndex]==L' ' && inQuote==false)
312                         {
313                                 if(!currentToken.empty())
314                                 {
315                                         pTokenVector.push_back(currentToken);
316                                         currentToken.clear();
317                                 }
318                                 continue;
319                         }
320
321                         if(message[charIndex]==L'\"')
322                         {
323                                 inQuote = !inQuote;
324
325                                 if(!currentToken.empty() || !inQuote)
326                                 {
327                                         pTokenVector.push_back(currentToken);
328                                         currentToken.clear();
329                                 }
330                                 continue;
331                         }
332
333                         currentToken += message[charIndex];
334                 }
335
336                 if(!currentToken.empty())
337                 {
338                         pTokenVector.push_back(currentToken);
339                         currentToken.clear();
340                 }
341
342                 return pTokenVector.size();
343         }
344 };
345
346 AMCPProtocolStrategy::AMCPProtocolStrategy(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
347         : impl_(spl::make_unique<impl>(name, repo))
348 {
349 }
350 AMCPProtocolStrategy::~AMCPProtocolStrategy() {}
351 void AMCPProtocolStrategy::Parse(const std::wstring& msg, IO::ClientInfoPtr pClientInfo) { impl_->Parse(msg, pClientInfo); }
352
353
354 }       //namespace amcp
355 }}      //namespace caspar