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