| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | #include "MessageClient.hpp"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include <stdexcept>
 | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  | #include <vector>
 | 
					
						
							|  |  |  | #include <algorithm>
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  | #include <QApplication>
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | #include <QUdpSocket>
 | 
					
						
							|  |  |  | #include <QHostInfo>
 | 
					
						
							|  |  |  | #include <QTimer>
 | 
					
						
							|  |  |  | #include <QQueue>
 | 
					
						
							|  |  |  | #include <QByteArray>
 | 
					
						
							|  |  |  | #include <QHostAddress>
 | 
					
						
							| 
									
										
										
										
											2018-08-05 11:33:30 -04:00
										 |  |  | #include <QColor>
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-18 17:24:07 -04:00
										 |  |  | #include "DriftingDateTime.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | #include "pimpl_impl.hpp"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include "moc_MessageClient.cpp"
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | class MessageClient::impl | 
					
						
							|  |  |  |   : public QUdpSocket | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |   Q_OBJECT | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | public: | 
					
						
							|  |  |  |   impl (QString const& id, QString const& version, QString const& revision, | 
					
						
							|  |  |  |         port_type server_port, MessageClient * self) | 
					
						
							|  |  |  |     : self_ {self} | 
					
						
							|  |  |  |     , id_ {id} | 
					
						
							|  |  |  |     , version_ {version} | 
					
						
							|  |  |  |     , revision_ {revision} | 
					
						
							|  |  |  |     , server_port_ {server_port} | 
					
						
							|  |  |  |     , schema_ {2}  // use 2 prior to negotiation not 1 which is broken
 | 
					
						
							|  |  |  |     , heartbeat_timer_ {new QTimer {this}} | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     connect (heartbeat_timer_, &QTimer::timeout, this, &impl::heartbeat); | 
					
						
							|  |  |  |     connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-07 22:20:28 -04:00
										 |  |  |     heartbeat_timer_->start (15 * 1000); | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // bind to an ephemeral port
 | 
					
						
							|  |  |  |     bind (); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ~impl () | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     closedown (); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   enum StreamStatus {Fail, Short, OK}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   void parse_message (QByteArray const& msg); | 
					
						
							|  |  |  |   void pending_datagrams (); | 
					
						
							|  |  |  |   void heartbeat (); | 
					
						
							|  |  |  |   void closedown (); | 
					
						
							|  |  |  |   StreamStatus check_status (QDataStream const&) const; | 
					
						
							|  |  |  |   void send_message (QByteArray const&); | 
					
						
							|  |  |  |   void send_message (QDataStream const& out, QByteArray const& message) | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |       if (OK == check_status (out)) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           send_message (message); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           Q_EMIT self_->error ("Error creating UDP message"); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   Q_SLOT void host_info_results (QHostInfo); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   MessageClient * self_; | 
					
						
							|  |  |  |   QString id_; | 
					
						
							|  |  |  |   QString version_; | 
					
						
							|  |  |  |   QString revision_; | 
					
						
							|  |  |  |   QString server_string_; | 
					
						
							|  |  |  |   port_type server_port_; | 
					
						
							|  |  |  |   QHostAddress server_; | 
					
						
							|  |  |  |   quint32 schema_; | 
					
						
							|  |  |  |   QTimer * heartbeat_timer_; | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  |   std::vector<QHostAddress> blocked_addresses_; | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // hold messages sent before host lookup completes asynchronously
 | 
					
						
							|  |  |  |   QQueue<QByteArray> pending_messages_; | 
					
						
							|  |  |  |   QByteArray last_message_; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | #include "MessageClient.moc"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::host_info_results (QHostInfo host_info) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   if (QHostInfo::NoError != host_info.error ()) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ()); | 
					
						
							|  |  |  |       pending_messages_.clear (); // discard
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   else if (host_info.addresses ().size ()) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  |       auto server = host_info.addresses ()[0]; | 
					
						
							|  |  |  |       if (blocked_addresses_.end () == std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server)) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           server_ = server; | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  |           // send initial heartbeat which allows schema negotiation
 | 
					
						
							|  |  |  |           heartbeat (); | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  |           // clear any backlog
 | 
					
						
							|  |  |  |           while (pending_messages_.size ()) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               send_message (pending_messages_.dequeue ()); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       else | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  |           Q_EMIT self_->error ("UDP server blocked, please try another"); | 
					
						
							|  |  |  |           pending_messages_.clear (); // discard
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::pending_datagrams () | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   while (hasPendingDatagrams ()) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       QByteArray datagram; | 
					
						
							|  |  |  |       datagram.resize (pendingDatagramSize ()); | 
					
						
							|  |  |  |       QHostAddress sender_address; | 
					
						
							|  |  |  |       port_type sender_port; | 
					
						
							|  |  |  |       if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port)) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           parse_message (datagram); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::parse_message (QByteArray const& msg) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   try | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |         if(msg.isEmpty()){ | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         QJsonParseError e; | 
					
						
							|  |  |  |         QJsonDocument d = QJsonDocument::fromJson(msg, &e); | 
					
						
							|  |  |  |         if(e.error != QJsonParseError::NoError){ | 
					
						
							|  |  |  |             Q_EMIT self_->error(QString {"MessageClient json parse error:  %1"}.arg(e.errorString())); | 
					
						
							| 
									
										
										
										
											2018-08-07 22:41:01 -04:00
										 |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |         if(!d.isObject()){ | 
					
						
							|  |  |  |             Q_EMIT self_->error(QString {"MessageClient json parse error: json is not an object"}); | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Message m; | 
					
						
							|  |  |  |         m.read(d.object()); | 
					
						
							|  |  |  |         Q_EMIT self_->message(m); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |     } | 
					
						
							|  |  |  |   catch (std::exception const& e) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       Q_EMIT self_->error (QString {"MessageClient exception: %1"}.arg (e.what ())); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   catch (...) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       Q_EMIT self_->error ("Unexpected exception in MessageClient"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::heartbeat () | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |    if (server_port_ && !server_.isNull ()) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |       Message m("PING", "", QMap<QString, QVariant>{ | 
					
						
							|  |  |  |         {"NAME", QVariant(QApplication::applicationName())}, | 
					
						
							|  |  |  |         {"VERSION", QVariant(QApplication::applicationVersion())}, | 
					
						
							| 
									
										
										
										
											2018-09-18 17:24:07 -04:00
										 |  |  |         {"UTC", QVariant(DriftingDateTime::currentDateTimeUtc().toMSecsSinceEpoch())} | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |       }); | 
					
						
							|  |  |  |       writeDatagram (m.toJson(), server_, server_port_); | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::closedown () | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |    if (server_port_ && !server_.isNull ()) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  |        Message m("CLOSE"); | 
					
						
							|  |  |  |        writeDatagram (m.toJson(), server_, server_port_); | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::impl::send_message (QByteArray const& message) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   if (server_port_) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       if (!server_.isNull ()) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           if (message != last_message_) // avoid duplicates
 | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               writeDatagram (message, server_, server_port_); | 
					
						
							|  |  |  |               last_message_ = message; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           pending_messages_.enqueue (message); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | auto MessageClient::impl::check_status (QDataStream const& stream) const -> StreamStatus | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   auto stat = stream.status (); | 
					
						
							|  |  |  |   StreamStatus result {Fail}; | 
					
						
							|  |  |  |   switch (stat) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |     case QDataStream::ReadPastEnd: | 
					
						
							|  |  |  |       result = Short; | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     case QDataStream::ReadCorruptData: | 
					
						
							|  |  |  |       Q_EMIT self_->error ("Message serialization error: read corrupt data"); | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     case QDataStream::WriteFailed: | 
					
						
							|  |  |  |       Q_EMIT self_->error ("Message serialization error: write error"); | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     default: | 
					
						
							|  |  |  |       result = OK; | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   return result; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision, | 
					
						
							|  |  |  |                               QString const& server, port_type server_port, QObject * self) | 
					
						
							|  |  |  |   : QObject {self} | 
					
						
							|  |  |  |   , m_ {id, version, revision, server_port, this} | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   connect (&*m_, static_cast<void (impl::*) (impl::SocketError)> (&impl::error) | 
					
						
							|  |  |  |            , [this] (impl::SocketError e) | 
					
						
							|  |  |  |            { | 
					
						
							|  |  |  | #if defined (Q_OS_WIN) && QT_VERSION >= 0x050500
 | 
					
						
							|  |  |  |              if (e != impl::NetworkError // take this out when Qt 5.5
 | 
					
						
							|  |  |  |                                          // stops doing this
 | 
					
						
							|  |  |  |                                          // spuriously
 | 
					
						
							|  |  |  |                  && e != impl::ConnectionRefusedError) // not
 | 
					
						
							|  |  |  |                                                        // interested
 | 
					
						
							|  |  |  |                                                        // in this with
 | 
					
						
							|  |  |  |                                                        // UDP socket
 | 
					
						
							|  |  |  | #else
 | 
					
						
							|  |  |  |              Q_UNUSED (e); | 
					
						
							|  |  |  | #endif
 | 
					
						
							|  |  |  |                { | 
					
						
							|  |  |  |                  Q_EMIT error (m_->errorString ()); | 
					
						
							|  |  |  |                } | 
					
						
							|  |  |  |            }); | 
					
						
							|  |  |  |   set_server (server); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | QHostAddress MessageClient::server_address () const | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   return m_->server_; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | auto MessageClient::server_port () const -> port_type | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   return m_->server_port_; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::set_server (QString const& server) | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2018-08-17 10:07:17 -04:00
										 |  |  |     qDebug() << "server changed to" << server; | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  |   m_->server_.clear (); | 
					
						
							|  |  |  |   m_->server_string_ = server; | 
					
						
							|  |  |  |   if (!server.isEmpty ()) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       // queue a host address lookup
 | 
					
						
							|  |  |  |       QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo))); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void MessageClient::set_server_port (port_type server_port) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   m_->server_port_ = server_port; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-08 17:15:49 -04:00
										 |  |  | void MessageClient::send(Message const &message){ | 
					
						
							|  |  |  |   m_->send_message(message.toJson()); | 
					
						
							| 
									
										
										
										
											2018-08-07 22:41:01 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-08 21:28:33 -05:00
										 |  |  | void MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress const& dest_address | 
					
						
							|  |  |  |                                        , port_type dest_port) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   if (dest_port && !dest_address.isNull ()) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       m_->writeDatagram (message, dest_address, dest_port); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-05 14:49:51 -05:00
										 |  |  | void MessageClient::add_blocked_destination (QHostAddress const& a) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   m_->blocked_addresses_.push_back (a); | 
					
						
							|  |  |  |   if (a == m_->server_) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       m_->server_.clear (); | 
					
						
							|  |  |  |       Q_EMIT error ("UDP server blocked, please try another"); | 
					
						
							|  |  |  |       m_->pending_messages_.clear (); // discard
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |