1 /* Ricochet - https://ricochet.im/
2  * Copyright (C) 2014, John Brooks <john.brooks@dereferenced.net>
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  *      notice, this list of conditions and the following disclaimer.
10  *
11  *    * Redistributions in binary form must reproduce the above
12  *      copyright notice, this list of conditions and the following disclaimer
13  *      in the documentation and/or other materials provided with the
14  *      distribution.
15  *
16  *    * Neither the names of the copyright owners nor the names of its
17  *      contributors may be used to endorse or promote products derived from
18  *      this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #include "OutboundConnector.h"
34 #include "utils/Useful.h"
35 #include "tor/TorSocket.h"
36 #include "ControlChannel.h"
37 #include "AuthHiddenServiceChannel.h"
38 #include <QSharedPointer>
39 
40 using namespace Protocol;
41 
42 namespace Protocol
43 {
44 
45 class OutboundConnectorPrivate : public QObject
46 {
47     Q_OBJECT
48 
49 public:
50     OutboundConnector *q;
51     Tor::TorSocket *socket;
52     QSharedPointer<Connection> connection;
53     QString hostname;
54     quint16 port;
55     OutboundConnector::Status status;
56     CryptoKey authPrivateKey;
57     QString errorMessage;
58     QTimer errorRetryTimer;
59     int errorRetryCount;
60 
OutboundConnectorPrivate(OutboundConnector * q)61     OutboundConnectorPrivate(OutboundConnector *q)
62         : QObject(q)
63         , q(q)
64         , socket(0)
65         , port(0)
66         , status(OutboundConnector::Inactive)
67         , errorRetryCount(0)
68     {
69         connect(&errorRetryTimer, &QTimer::timeout, this, &OutboundConnectorPrivate::retryAfterError);
70     }
71 
72     void setStatus(OutboundConnector::Status status);
73     void setError(const QString &errorMessage);
74 
75 public slots:
76     void onConnected();
77     void startAuthentication();
78     void abort();
79     void retryAfterError();
80 };
81 
82 }
83 
OutboundConnector(QObject * parent)84 OutboundConnector::OutboundConnector(QObject *parent)
85     : QObject(parent), d(new OutboundConnectorPrivate(this))
86 {
87 }
88 
~OutboundConnector()89 OutboundConnector::~OutboundConnector()
90 {
91 }
92 
setAuthPrivateKey(const CryptoKey & key)93 void OutboundConnector::setAuthPrivateKey(const CryptoKey &key)
94 {
95     if (!key.isLoaded() || !key.isPrivate()) {
96         BUG() << "Cannot make outbound connection without a valid private key";
97         return;
98     }
99 
100     d->authPrivateKey = key;
101 }
102 
connectToHost(const QString & hostname,quint16 port)103 bool OutboundConnector::connectToHost(const QString &hostname, quint16 port)
104 {
105     if (port <= 0 || hostname.isEmpty()) {
106         d->errorMessage = QStringLiteral("Invalid hostname or port");
107         d->setStatus(Error);
108         return false;
109     }
110 
111     if (d->status == Ready) {
112         BUG() << "Reusing an OutboundConnector object";
113         d->errorMessage = QStringLiteral("Outbound connection handler was already used");
114         d->setStatus(Error);
115         return false;
116     }
117 
118     if (isActive() && hostname == d->hostname && port == d->port)
119         return true;
120 
121     // There is no reason to be connecting to anything but onions for now, so add a safety net here
122     if (!hostname.endsWith(QLatin1String(".onion"))) {
123         d->errorMessage = QStringLiteral("Invalid (non-onion) hostname");
124         d->setStatus(Error);
125         return false;
126     }
127 
128     abort();
129 
130     d->hostname = hostname;
131     d->port = port;
132 
133     d->socket = new Tor::TorSocket(this);
134     connect(d->socket, &Tor::TorSocket::connected, d, &OutboundConnectorPrivate::onConnected);
135     d->setStatus(Connecting);
136     d->socket->connectToHost(d->hostname, d->port);
137     return true;
138 }
139 
abort()140 void OutboundConnector::abort()
141 {
142     d->abort();
143     d->hostname.clear();
144     d->port = 0;
145     d->errorRetryCount = 0;
146     d->errorRetryTimer.stop();
147     d->errorMessage.clear();
148     d->setStatus(Inactive);
149 }
150 
abort()151 void OutboundConnectorPrivate::abort()
152 {
153     if (connection) {
154         connection->close();
155         connection.clear();
156     }
157 
158     if (socket) {
159         socket->disconnect(this);
160         delete socket;
161         socket = 0;
162     }
163 }
164 
status() const165 OutboundConnector::Status OutboundConnector::status() const
166 {
167     return d->status;
168 }
169 
isActive() const170 bool OutboundConnector::isActive() const
171 {
172     return d->status > Inactive && d->status < Ready;
173 }
174 
errorMessage() const175 QString OutboundConnector::errorMessage() const
176 {
177     return d->errorMessage;
178 }
179 
takeConnection()180 QSharedPointer<Connection> OutboundConnector::takeConnection()
181 {
182     QSharedPointer<Connection> c(d->connection);
183     if (status() != Ready || !c) {
184         BUG() << "Cannot take connection when not in the Ready state";
185         return c;
186     }
187 
188     Q_ASSERT(!d->socket);
189     d->connection.clear();
190     d->setStatus(Inactive);
191 
192     return c;
193 }
194 
setStatus(OutboundConnector::Status value)195 void OutboundConnectorPrivate::setStatus(OutboundConnector::Status value)
196 {
197     if (status == value)
198         return;
199 
200     bool wasActive = q->isActive();
201     status = value;
202     emit q->statusChanged();
203     if (wasActive != q->isActive())
204         emit q->isActiveChanged();
205 }
206 
setError(const QString & message)207 void OutboundConnectorPrivate::setError(const QString &message)
208 {
209     abort();
210     errorMessage = message;
211     setStatus(OutboundConnector::Error);
212 
213     // XXX This is a bad solution, but it will hold until we can revisit the
214     // reconnecting and connection error behavior as a whole.
215     if (++errorRetryCount > 5) {
216         qDebug() << "Outbound connection attempt has had five errors in a row, stopping attempts";
217         return;
218     }
219 
220     errorRetryTimer.setSingleShot(true);
221     errorRetryTimer.start(60 * 1000);
222     qDebug() << "Retrying outbound connection attempt in 60 seconds after an error";
223 }
224 
retryAfterError()225 void OutboundConnectorPrivate::retryAfterError()
226 {
227     if (status != OutboundConnector::Error) {
228         qDebug() << "Error retry timer triggered, but not in an error state anymore. Ignoring.";
229         return;
230     }
231 
232     if (hostname.isEmpty() || port <= 0) {
233         qDebug() << "Connection info cleared during error retry period, stopping OutboundConnector";
234         q->abort();
235         return;
236     }
237 
238     q->connectToHost(hostname, port);
239 }
240 
onConnected()241 void OutboundConnectorPrivate::onConnected()
242 {
243     if (!socket || status != OutboundConnector::Connecting) {
244         BUG() << "OutboundConnector connected in an unexpected state";
245         setError(QStringLiteral("Connected in an unexpected state"));
246         return;
247     }
248 
249     connection = QSharedPointer<Connection>(new Connection(socket, Connection::ClientSide), &QObject::deleteLater);
250 
251     // Socket is now owned by connection
252     Q_ASSERT(socket->parent() == connection);
253     socket->setReconnectEnabled(false);
254     socket = 0;
255 
256     connect(connection.data(), &Connection::ready, this, &OutboundConnectorPrivate::startAuthentication);
257     // XXX Needs special treatment in UI (along with some other error types here)
258     connect(connection.data(), &Connection::versionNegotiationFailed, this,
259         [this]() {
260             setError(QStringLiteral("Protocol version negotiation failed with peer"));
261         }
262     );
263     connect(connection.data(), &Connection::oldVersionNegotiated, q, &OutboundConnector::oldVersionNegotiated);
264     setStatus(OutboundConnector::Initializing);
265 }
266 
startAuthentication()267 void OutboundConnectorPrivate::startAuthentication()
268 {
269     if (!connection || status != OutboundConnector::Initializing) {
270         BUG() << "OutboundConnector startAuthentication in an unexpected state";
271         setError(QStringLiteral("Connected in an unexpected state"));
272         return;
273     }
274 
275     if (!authPrivateKey.isLoaded() || !authPrivateKey.isPrivate()) {
276         qDebug() << "Skipping authentication for OutboundConnector without a private key";
277         setStatus(OutboundConnector::Ready);
278         emit q->ready();
279         return;
280     }
281 
282     // XXX Timeouts and errors and all of that
283     AuthHiddenServiceChannel *authChannel = new AuthHiddenServiceChannel(Channel::Outbound, connection.data());
284     connect(authChannel, &AuthHiddenServiceChannel::authSuccessful, this,
285         [this]() {
286             setStatus(OutboundConnector::Ready);
287             emit q->ready();
288         }
289     );
290     connect(authChannel, &AuthHiddenServiceChannel::authFailed, this,
291         [this]() {
292             qDebug() << "Authentication failed for outbound connection to" << hostname;
293             setError(QStringLiteral("Authentication failed"));
294         }
295     );
296 
297     // Set the Authenticating state when we send the actual authentication message
298     connect(authChannel, &Channel::channelOpened, this,
299         [this]() {
300             setStatus(OutboundConnector::Authenticating);
301         }
302     );
303 
304     authChannel->setPrivateKey(authPrivateKey);
305     if (!authChannel->openChannel()) {
306         setError(QStringLiteral("Unable to open authentication channel"));
307     }
308 }
309 
310 #include "OutboundConnector.moc"
311