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