1 ///
2 /// Pianod specializations of Football connections & services.
3 /// @file connection.cpp - pianod
4 /// @author Perette Barella
5 /// @date 2014-11-28
6 /// @copyright Copyright 2014-2020 Devious Fish. All rights reserved.
7 ///
8
9 #include <config.h>
10
11 #include <cassert>
12
13 #include "football.h"
14 #include "footparser.h"
15 #include "fundamentals.h"
16 #include "response.h"
17 #include "connection.h"
18 #include "enum.h"
19 #include "musictypes.h"
20 #include "mediamanager.h"
21 #include "servicemanager.h"
22 #include "engine.h"
23 #include "filter.h"
24 #include "querylist.h"
25
26
27 bool PianodConnection::broadcast_user_actions = true;
28 time_t WaitEvent::nextTimeout = FAR_FUTURE;
29
~PianodConnection()30 PianodConnection::~PianodConnection () {
31 }
32
33
34 /// Parser for options for events/WAIT commands.
35 namespace WaitOptions {
36
37 /** Event/Wait for options */
38 enum class Option {
39 Timeout = 1
40 };
41
42 /** WaitOptionsParser is the parser that fills in the WaitEvent structure. */
43 class Parser : public Football::OptionParser<WaitEvent, Option> {
44 virtual int handleOption (Option option, WaitEvent &dest) override;
45 public:
Parser()46 Parser () {
47 static const Parser::ParseDefinition waitOptionStatements [] = {
48 { Option::Timeout, "timeout {#duration:0-999999999} ..." }, // how long to wait before failing
49 { (Option) CMD_INVALID, NULL }
50 };
51 addStatements (waitOptionStatements);
52 };
53 };
54
55
56 /** AudioOptions is the parser that fills in the AudioSettings structure. */
handleOption(Option option,WaitEvent & dest)57 int Parser::handleOption (Option option, WaitEvent &dest) {
58 switch (option) {
59 case Option::Timeout:
60 dest.timeout = time (nullptr) + atoi (argv ("duration"));
61 return FB_PARSE_SUCCESS;
62 }
63 assert (0);
64 return FB_PARSE_FAILURE;
65 }
66
67 /// An instance of the wait options parser.
68 Parser parser;
69 }
70
71
72
73
74 /*
75 * Event handlers
76 */
77
78 /** Handle football-layer errors that happen on pianod connections.
79 This overrides the built-in method, formatting errors in accordance
80 with pianod protocol. */
commandError(ssize_t reason,const char * where)81 void PianodConnection::commandError (ssize_t reason, const char *where) {
82 switch (reason) {
83 // Parser Errors
84 case FB_PARSE_FAILURE:
85 this << E_BUG;
86 return;
87 case FB_PARSE_INCOMPLETE:
88 printf ("%03d Command incomplete after %s\n", E_BAD_COMMAND, where);
89 return;
90 case FB_PARSE_INVALID_KEYWORD:
91 printf ("%03d Bad command %s\n", E_BAD_COMMAND, where);
92 return;
93 case FB_PARSE_NUMERIC:
94 printf ("%03d Numeric value expected: %s\n", E_BAD_COMMAND, where);
95 return;
96 case FB_PARSE_RANGE:
97 printf ("%03d Numeric value out of range: %s\n", E_BAD_COMMAND, where);
98 return;
99 case FB_PARSE_EXTRA_TERMS:
100 printf ("%03d Run-on command at %s\n", E_BAD_COMMAND, where);
101 return;
102 case FB_ERROR_BADALLOC:
103 if (where)
104 this << Response (E_RESOURCE, where);
105 else
106 this << E_RESOURCE;
107 return;
108 case FB_ERROR_EXCEPTION:
109 if (where)
110 this << Response (E_NAK, where);
111 else
112 this << E_NAK;
113 return;
114 default:
115 assert (reason > 0);
116 this << (RESPONSE_CODE) (reason < 0 ? E_NAK : reason);
117 }
118 }
119
120 /** Handle custom exceptions that may occur. Pass other exceptions to Football.
121 @param except An exception that happened while dispatching a command. */
commandException(std::exception_ptr except)122 void PianodConnection::commandException (std::exception_ptr except) {
123 try {
124 std::rethrow_exception (except);
125 } catch (const Query::impossible &e) {
126 this << Response (E_MEDIA_ACTION,
127 "Query complexity exceeds source ability");
128 } catch (const CommandError &error) {
129 this << error;
130 } catch (...) {
131 Football::Connection::commandException(except);
132 }
133 }
134
permissionDenied()135 void PianodConnection::permissionDenied() {
136 this << E_UNAUTHORIZED;
137 }
138
139
newConnection()140 void PianodConnection::newConnection () {
141 // Greet the connection, allocate any resources
142 this << S_OK << I_WELCOME;
143 sendEffectivePrivileges();
144 source (media_manager);
145 service().audioEngine()->sendStatus (*this);
146 };
147
updateConnection()148 void PianodConnection::updateConnection () {
149 service().audioEngine()->updateStatus (*this);
150 }
151
connectionClose()152 void PianodConnection::connectionClose () {
153 // Free user context resources
154 if (user) {
155 announce (A_SIGNED_OUT);
156 user = nullptr;
157 service().usersChangedNotification();
158 }
159 }
160
161 /*
162 * Getters
163 */
164
165
166 /** Get connected user's rank.
167 @return User's rank, or visitor rank if not authenticated. */
effectiveRank(void)168 Rank PianodConnection::effectiveRank (void) {
169 return authenticated() ? user->getRank () : User::getVisitorRank ();
170 }
171
172 // Determine if a user or visitor has a rank or better.
haveRank(Rank minimum)173 bool PianodConnection::haveRank (Rank minimum) {
174 return (effectiveRank () >= minimum);
175 };
176
177 /** Determine if the user has a privilege.
178 Visitors cannot be assigned privileges but they may be implied by rank.
179 @param priv The privilege to check.
180 @return True if the user either has the privilege or it is implied by rank. */
havePrivilege(Privilege priv)181 bool PianodConnection::havePrivilege (Privilege priv) {
182 assert (Enum<Privilege>().isValid (priv));
183 // Administrators inherently get certain privileges
184 if (haveRank (Rank::Administrator) &&
185 (priv == Privilege::Service || priv == Privilege::Tuner)) {
186 return true;
187 }
188 if (haveRank (Rank::Standard) && priv == Privilege::Queue)
189 return true;
190 return authenticated() ? user->havePrivilege (priv) : false;
191 };
192
193 /*
194 * Events
195 */
196
197 /** Begin waiting for an event on a connection.
198 @param type The type of event to wait for.
199 @param detail A pointer representing a specific event instance to wait for. */
waitForEvent(WaitEvent::Type type,const void * detail)200 void PianodConnection::waitForEvent (WaitEvent::Type type, const void *detail) {
201 assert (pending.event == WaitEvent::Type::None);
202 assert (type != WaitEvent::Type::None);
203 pending.event = type;
204 pending.parameter = detail;
205 acceptInput (false);
206 }
207
208 /** Interpret options and begin waiting for an event on a connection.
209 @param type The type of event to wait for.
210 @param detail A pointer representing a specific event instance to wait for.
211 @return True on success, false if event options are invalid. */
waitForEventWithOptions(WaitEvent::Type type,const void * detail)212 bool PianodConnection::waitForEventWithOptions (WaitEvent::Type type, const void *detail) {
213 assert (pending.event == WaitEvent::Type::None);
214 assert (type != WaitEvent::Type::None);
215 if (WaitOptions::parser.interpret (argvFrom("options"), pending, this) != FB_PARSE_SUCCESS)
216 return false;
217 pending.event = type;
218 pending.parameter = detail;
219 acceptInput (false);
220 if (pending.timeout < WaitEvent::nextTimeout)
221 WaitEvent::nextTimeout = pending.timeout;
222 return true;
223 }
224
225 /** Process an event for a connection.
226 If waiting for the event, the status is announced and input is resumed.
227 If not waiting, or waiting for a different event, nothing happens.
228 @param type The type of event occurring.
229 @param detail A pointer representing a specific event instance.
230 @param reply The status to report if the event applies to the connection. */
event(WaitEvent::Type type,const void * detail,RESPONSE_CODE reply)231 void PianodConnection::event (WaitEvent::Type type, const void *detail, RESPONSE_CODE reply) {
232 assert (type != WaitEvent::Type::None);
233 if (pending.event == type && pending.parameter == detail) {
234 acceptInput (true);
235 this << reply;
236 if (pending.close_after_event) {
237 Football::Connection::close();
238 }
239 pending = WaitEvent {};
240 }
241 }
242
243 /** Check if a pending event has timed out. If so, fire it with failure.
244 Otherwise, check then pending event for a closer next timeout time. */
checkTimeouts()245 void PianodConnection::checkTimeouts () {
246 if (pending.timeout) {
247 time_t now = time (nullptr);
248 if (pending.timeout <= now) {
249 event (pending.event, pending.parameter, E_TIMEOUT);
250 } else {
251 if (pending.timeout < WaitEvent::nextTimeout) {
252 WaitEvent::nextTimeout = pending.timeout;
253 }
254 }
255 }
256 }
257
258
259 /** Close after events are handled, or now if not waiting on one. */
close_after_events()260 void PianodConnection::close_after_events () {
261 if (pending.event == WaitEvent::Type::None) {
262 close();
263 } else {
264 pending.close_after_event = true;
265 }
266 }
267
268 /*
269 * Transmitters
270 */
271
272
273 /** Announce to all connections that a user performed some action.
274 If the broadcast feature is disabled, logs user action instead.
275 @param code Enumeration identifying user action taken.
276 @param parameter Details of action, usually target object's name. */
announce(RESPONSE_CODE code,const char * parameter)277 void PianodConnection::announce (RESPONSE_CODE code, const char *parameter) {
278 assert (this);
279 assert (code == V_YELL || code >= 1000);
280
281 if (broadcast_user_actions || code == V_YELL) {
282 sendcflog (LOG_USERACTION, &service(), "%03d %s %s%s%s\n",
283 code == V_YELL ? V_YELL : V_USERACTION, username().c_str(),
284 ResponseText (code), parameter ? ": " : "",
285 parameter ? parameter : "");
286 } else {
287 // Log it in case action logging is turned on
288 flog (LOG_WHERE (LOG_USERACTION), username(), " ",
289 ResponseText (code), parameter ? ": " : "", parameter ? parameter : "");
290 // If status indicates something happened, send the generic version of the message.
291 }
292 }
293
294 /** Report the selected source to the connection. */
sendSelectedSource()295 void PianodConnection::sendSelectedSource() {
296 assert (_source);
297 printf("%03d %s: %d %s %s\n",
298 V_SELECTEDSOURCE, ResponseText (V_SELECTEDSOURCE),
299 (int) _source->serialNumber(),
300 _source->kind(), _source->name().c_str());
301 }
302
303
304 /** Transmit the user's effective privileges. */
sendEffectivePrivileges()305 void PianodConnection::sendEffectivePrivileges () {
306 std::string privs = User::getRankName (effectiveRank());
307 privs.reserve (int (Privilege::Count) * 20); // Preallocate space for efficiency.
308 for (Privilege p : Enum <Privilege>()) {
309 if (havePrivilege (p)) {
310 privs += " ";
311 privs += User::getPrivilegeName (p);
312 }
313 }
314 this << Response (I_USER_PRIVILEGES, privs);
315 }
316
317
318 /*
319 * PianodService
320 */
~PianodService()321 PianodService::~PianodService() {
322 if (engine) delete engine;
323 }
324
325 /** When a service has been completely shut down, remove it from the service manager. */
serviceShutdown(void)326 void PianodService::serviceShutdown (void) {
327 service_manager->removeRoom (this);
328 }
329
usersChangedNotification(void)330 void PianodService::usersChangedNotification (void) {
331 engine->usersChangedNotification();
332 }
333