MockLinkFTP.cc 17.6 KB
Newer Older
1 2
/****************************************************************************
 *
Gus Grubba's avatar
Gus Grubba committed
3
 * (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 5 6 7 8 9
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/

10

11
#include "MockLinkFTP.h"
12 13
#include "MockLink.h"

14 15 16 17 18 19
const MockLinkFTP::ErrorMode_t MockLinkFTP::rgFailureModes[] = {
    MockLinkFTP::errModeNoResponse,
    MockLinkFTP::errModeNakResponse,
    MockLinkFTP::errModeNoSecondResponse,
    MockLinkFTP::errModeNakSecondResponse,
    MockLinkFTP::errModeBadSequence,
20
};
21
const size_t MockLinkFTP::cFailureModes = sizeof(MockLinkFTP::rgFailureModes) / sizeof(MockLinkFTP::rgFailureModes[0]);
22

23
const MockLinkFTP::FileTestCase MockLinkFTP::rgFileTestCases[MockLinkFTP::cFileTestCases] = {
24
    // File fits one Read Ack packet, partially filling data
25
    { "partial.qgc",    sizeof(((MavlinkFTP::Request*)0)->data) - 1,     1,    false},
26
    // File fits one Read Ack packet, exactly filling all data
27
    { "exact.qgc",      sizeof(((MavlinkFTP::Request*)0)->data),         1,    true },
28
    // File is larger than a single Read Ack packets, requires multiple Reads
29
    { "multi.qgc",      sizeof(((MavlinkFTP::Request*)0)->data) + 1,     2,    false },
30
};
31

32 33 34 35
MockLinkFTP::MockLinkFTP(uint8_t systemIdServer, uint8_t componentIdServer, MockLink* mockLink)
    : _systemIdServer   (systemIdServer)
    , _componentIdServer(componentIdServer)
    , _mockLink         (mockLink)
36
{
37 38 39
    srand(0); // make sure unit tests are deterministic
}

40
void MockLinkFTP::ensureNullTemination(MavlinkFTP::Request* request)
41 42 43
{
    if (request->hdr.size < sizeof(request->data)) {
        request->data[request->hdr.size] = '\0';
44

45 46 47
    } else {
        request->data[sizeof(request->data)-1] = '\0';
    }
48 49
}

50 51
/// @brief Handles List command requests. Only supports root folder paths.
///         File list returned is set using the setFileList method.
52
void MockLinkFTP::_listCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
53
{
54 55
    // FIXME: Does not support directories that span multiple packets
    
56
    MavlinkFTP::Request  ackResponse;
Don Gagne's avatar
Don Gagne committed
57
    QString                     path;
58
    uint16_t                    outgoingSeqNumber = _nextSeqNumber(seqNumber);
Don Gagne's avatar
Don Gagne committed
59

60 61
    ensureNullTemination(request);

62 63 64
    // We only support root path
    path = (char *)&request->data[0];
    if (!path.isEmpty() && path != "/") {
65
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdListDirectory);
66 67 68 69 70
        return;
    }
    
    // Offset requested is past the end of the list
    if (request->hdr.offset > (uint32_t)_fileList.size()) {
71
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdListDirectory);
72 73 74
        return;
    }
    
75 76
    ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
    ackResponse.hdr.req_opcode = MavlinkFTP::kCmdListDirectory;
77
    ackResponse.hdr.session = 0;
78
    ackResponse.hdr.offset = request->hdr.offset;
79
    ackResponse.hdr.size = 0;
80

81 82 83 84 85
    if (request->hdr.offset == 0) {
        // Requesting first batch of file names
        Q_ASSERT(_fileList.size());
        char *bufPtr = (char *)&ackResponse.data[0];
        for (int i=0; i<_fileList.size(); i++) {
86
            strcpy(bufPtr, _fileList[i].toStdString().c_str());
Don Gagne's avatar
Don Gagne committed
87 88
            uint8_t cchFilename = static_cast<uint8_t>(strlen(bufPtr));
            Q_ASSERT(cchFilename);
89 90 91
            ackResponse.hdr.size += cchFilename + 1;
            bufPtr += cchFilename + 1;
        }
92

93
        _sendResponse(senderSystemId, senderComponentId, &ackResponse, outgoingSeqNumber);
94 95
    } else if (_errMode == errModeNakSecondResponse) {
        // Nak error all subsequent requests
96
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdListDirectory);
97 98 99 100
        return;
    } else if (_errMode == errModeNoSecondResponse) {
        // No response for all subsequent requests
        return;
101
    } else {
102
        // FIXME: Does not support directories that span multiple packets
103
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdListDirectory);
104 105 106
    }
}

107
void MockLinkFTP::_openCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
108
{
109 110 111 112
    MavlinkFTP::Request response;
    QString             path;
    uint16_t            outgoingSeqNumber = _nextSeqNumber(seqNumber);
    QString             tmpFilename;
113
    
114 115
    ensureNullTemination(request);

116 117
    size_t cchPath = strnlen((char *)request->data, sizeof(request->data));
    Q_ASSERT(cchPath != sizeof(request->data));
118
    Q_UNUSED(cchPath); // Fix initialized-but-not-referenced warning on release builds
119
    path = (char *)request->data;
120 121

    _currentFile.close();
122 123
    
    // Check path against one of our known test cases
124 125 126
    for (const FileTestCase& testCase: rgFileTestCases) {
        if (path == testCase.filename) {
            tmpFilename = _createTestCaseTempFile(testCase);
127 128 129
            break;
        }
    }
130

131
    if (tmpFilename.isEmpty()) {
132
        if (path == "/version.json") {
133
            tmpFilename = ":MockLink/Version.MetaData.json";
134 135 136
        }
    }

137 138 139 140 141 142 143 144
    if (!tmpFilename.isEmpty()) {
        _currentFile.setFileName(tmpFilename);
        if (!_currentFile.open(QIODevice::ReadOnly)) {
            _sendNakErrno(senderSystemId, senderComponentId, _currentFile.error(), outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
            return;
        }
    } else {
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFailFileNotFound, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
145 146 147
        return;
    }
    
148 149 150
    response.hdr.opcode     = MavlinkFTP::kRspAck;
    response.hdr.req_opcode = MavlinkFTP::kCmdOpenFileRO;
    response.hdr.session    = _sessionId;
151
    
152 153
    // Data contains file length
    response.hdr.size = sizeof(uint32_t);
154
    response.openFileLength = _currentFile.size();
155
    
156
    _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
157 158
}

159
void MockLinkFTP::_readCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
160
{
161 162
    MavlinkFTP::Request	response;
    uint16_t			outgoingSeqNumber = _nextSeqNumber(seqNumber);
163 164

    if (request->hdr.session != _sessionId) {
165
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
166 167 168
        return;
    }
    
169
    uint32_t readOffset = request->hdr.offset;  // offset into file for reading
170

171 172 173 174
    if (readOffset != 0) {
        // If we get here it means the client is requesting additional data past the first request
        if (_errMode == errModeNakSecondResponse) {
            // Nak error all subsequent requests
175
            _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
176 177 178 179 180 181 182
            return;
        } else if (_errMode == errModeNoSecondResponse) {
            // No rsponse for all subsequent requests
            return;
        }
    }
    
183 184
    if (readOffset >= _currentFile.size()) {
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
185 186 187
        return;
    }
    
188 189 190 191
    uint8_t cBytesToRead = (uint8_t)qMin((qint64)sizeof(response.data), _currentFile.size() - readOffset);
    _currentFile.seek(readOffset);
    QByteArray bytes = _currentFile.read(cBytesToRead);
    memcpy(response.data, bytes.constData(), cBytesToRead);
192
    
193
    // We should always have written something, otherwise there is something wrong with the code above
194
    Q_ASSERT(cBytesToRead);
195
    
196 197 198 199 200
    response.hdr.session    = _sessionId;
    response.hdr.size       = cBytesToRead;
    response.hdr.offset     = request->hdr.offset;
    response.hdr.opcode     = MavlinkFTP::kRspAck;
    response.hdr.req_opcode = MavlinkFTP::kCmdReadFile;
201

202
    _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
203 204
}

205
void MockLinkFTP::_burstReadCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
206 207
{
    uint16_t                outgoingSeqNumber = _nextSeqNumber(seqNumber);
208
    MavlinkFTP::Request    response;
209 210

    if (request->hdr.session != _sessionId) {
211
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
212 213 214 215 216 217 218
        return;
    }
    
    uint32_t readOffset = 0;	// offset into file for reading
    uint32_t ackOffset = 0;     // offset for ack
    uint8_t cDataAck;           // number of bytes in ack
    
219
    while (readOffset < _currentFile.size()) {
220 221 222 223 224 225
        cDataAck = 0;
        
        if (readOffset != 0) {
            // If we get here it means the client is requesting additional data past the first request
            if (_errMode == errModeNakSecondResponse) {
                // Nak error all subsequent requests
226
                _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
227 228 229 230 231 232 233
                return;
            } else if (_errMode == errModeNoSecondResponse) {
                // No response for all subsequent requests
                return;
            }
        }
        
234 235 236 237 238
        uint8_t cBytesToRead = (uint8_t)qMin((qint64)sizeof(response.data), _currentFile.size());
        _currentFile.seek(readOffset);
        QByteArray bytes = _currentFile.read(cBytesToRead);
        memcpy(response.data, bytes.constData(), cBytesToRead);

239
        // We should always have written something, otherwise there is something wrong with the code above
240
        Q_ASSERT(cBytesToRead);
241 242 243
        
        response.hdr.session = _sessionId;
        response.hdr.size = cDataAck;
244 245 246
        response.hdr.offset = cBytesToRead;
        response.hdr.opcode = MavlinkFTP::kRspAck;
        response.hdr.req_opcode = MavlinkFTP::kCmdBurstReadFile;
247
        
248
        _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
249 250 251 252 253
        
        outgoingSeqNumber = _nextSeqNumber(outgoingSeqNumber);
        ackOffset += cDataAck;
    }
	
254
    _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
255 256
}

257
void MockLinkFTP::_terminateCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
258
{
259 260
    uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);

261
    if (request->hdr.session != _sessionId) {
262
        _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
263 264 265
        return;
    }
    
266
    _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
267
	
268 269 270
    emit terminateCommandReceived();
}

271
void MockLinkFTP::_resetCommand(uint8_t senderSystemId, uint8_t senderComponentId, uint16_t seqNumber)
272
{
273 274
    uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
    
275
    _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdResetSessions);
276 277 278
    
    emit resetCommandReceived();
}
279

280
void MockLinkFTP::handleFTPMessage(const mavlink_message_t& message)
281 282 283 284
{
    if (message.msgid != MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
        return;
    }
285
    
286
    MavlinkFTP::Request  ackResponse;
287

288 289 290 291 292 293
    mavlink_file_transfer_protocol_t requestFTP;
    mavlink_msg_file_transfer_protocol_decode(&message, &requestFTP);
    
    if (requestFTP.target_system != _systemIdServer) {
        return;
    }
294

295
    MavlinkFTP::Request* request = (MavlinkFTP::Request*)&requestFTP.payload[0];
296

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
	if (_randomDropsEnabled) {
	    if (rand() % 3 == 0) {
	        qDebug() << "FileServer: Random drop of incoming packet";
	        return;
	    }
	}

	if (_lastReplyValid && request->hdr.seqNumber + 1 == _lastReplySequence) {
	    // this is the same request as the one we replied to last. It means the (n)ack got lost, and the GCS
	    // resent the request
	    qDebug() << "FileServer: resending response";
	    _mockLink->respondWithMavlinkMessage(_lastReply);
	    return;
	}

312
    uint16_t incomingSeqNumber = request->hdr.seqNumber;
313 314
    uint16_t outgoingSeqNumber = _nextSeqNumber(incomingSeqNumber);
    
315
    if (request->hdr.opcode != MavlinkFTP::kCmdResetSessions && request->hdr.opcode != MavlinkFTP::kCmdTerminateSession) {
316 317 318 319 320
        if (_errMode == errModeNoResponse) {
            // Don't respond to any requests, this shold cause the client to eventually timeout waiting for the ack
            return;
        } else if (_errMode == errModeNakResponse) {
            // Nak all requests, the actual error send back doesn't really matter as long as it's an error
321
            _sendNak(message.sysid, message.compid, MavlinkFTP::kErrFail, outgoingSeqNumber, (MavlinkFTP::OpCode_t)request->hdr.opcode);
322 323
            return;
        }
324
    }
325

Don Gagne's avatar
Don Gagne committed
326
    switch (request->hdr.opcode) {
327
        case MavlinkFTP::kCmdNone:
328
            // ignored, always acked
329
            ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
Don Gagne's avatar
Don Gagne committed
330 331
            ackResponse.hdr.session = 0;
            ackResponse.hdr.size = 0;
332
            _sendResponse(message.sysid, message.compid, &ackResponse, outgoingSeqNumber);
Don Gagne's avatar
Don Gagne committed
333 334
            break;

335
        case MavlinkFTP::kCmdListDirectory:
336
            _listCommand(message.sysid, message.compid, request, incomingSeqNumber);
337
            break;
338
            
339
        case MavlinkFTP::kCmdOpenFileRO:
340
            _openCommand(message.sysid, message.compid, request, incomingSeqNumber);
341
            break;
Don Gagne's avatar
Don Gagne committed
342

343
        case MavlinkFTP::kCmdReadFile:
344
            _readCommand(message.sysid, message.compid, request, incomingSeqNumber);
345
            break;
346

347 348
        case MavlinkFTP::kCmdBurstReadFile:
            _burstReadCommand(message.sysid, message.compid, request, incomingSeqNumber);
349 350
            break;

351
        case MavlinkFTP::kCmdTerminateSession:
352
            _terminateCommand(message.sysid, message.compid, request, incomingSeqNumber);
353 354
            break;

355
        case MavlinkFTP::kCmdResetSessions:
356 357 358
            _resetCommand(message.sysid, message.compid, incomingSeqNumber);
            break;
            
359 360
        default:
            // nack for all NYI opcodes
361
            _sendNak(message.sysid, message.compid, MavlinkFTP::kErrUnknownCommand, outgoingSeqNumber, (MavlinkFTP::OpCode_t)request->hdr.opcode);
362 363 364
            break;
    }
}
Don Gagne's avatar
Don Gagne committed
365

366
/// @brief Sends an Ack
367
void MockLinkFTP::_sendAck(uint8_t targetSystemId, uint8_t targetComponentId, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
368
{
369
    MavlinkFTP::Request ackResponse;
370
    
371
    ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
372
	ackResponse.hdr.req_opcode = reqOpcode;
373 374 375
    ackResponse.hdr.session = 0;
    ackResponse.hdr.size = 0;
    
376
    _sendResponse(targetSystemId, targetComponentId, &ackResponse, seqNumber);
377 378
}

379
void MockLinkFTP::_sendNak(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::ErrorCode_t error, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
Don Gagne's avatar
Don Gagne committed
380
{
381
    MavlinkFTP::Request nakResponse;
Don Gagne's avatar
Don Gagne committed
382

383 384 385 386 387
    nakResponse.hdr.opcode      = MavlinkFTP::kRspNak;
    nakResponse.hdr.req_opcode  = reqOpcode;
    nakResponse.hdr.session     = 0;
    nakResponse.hdr.size        = 1;
    nakResponse.data[0]         = error;
Don Gagne's avatar
Don Gagne committed
388
    
389
    _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
Don Gagne's avatar
Don Gagne committed
390 391
}

392 393 394 395 396 397 398 399 400 401 402 403 404 405
void MockLinkFTP::_sendNakErrno(uint8_t targetSystemId, uint8_t targetComponentId, uint8_t nakErrno, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
{
    MavlinkFTP::Request nakResponse;

    nakResponse.hdr.opcode      = MavlinkFTP::kRspNak;
    nakResponse.hdr.req_opcode  = reqOpcode;
    nakResponse.hdr.session     = 0;
    nakResponse.hdr.size        = 2;
    nakResponse.data[0]         = MavlinkFTP::kErrFailErrno;
    nakResponse.data[1]         = nakErrno;

    _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
}

406
/// @brief Emits a Request through the messageReceived signal.
407
void MockLinkFTP::_sendResponse(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::Request* request, uint16_t seqNumber)
Don Gagne's avatar
Don Gagne committed
408
{
409
    request->hdr.seqNumber = seqNumber;
410 411
    _lastReplySequence = seqNumber;
    _lastReplyValid = true;
412
    
413 414
    mavlink_msg_file_transfer_protocol_pack_chan(_systemIdServer,               // System ID
                                                 _componentIdServer,            // Component ID
415
                                                 _mockLink->mavlinkChannel(),
416 417
                                                 &_lastReply,                   // Mavlink Message to pack into
                                                 0,                             // Target network
418 419
                                                 targetSystemId,
                                                 targetComponentId,
420
                                                 (uint8_t*)request);            // Payload
421 422 423 424 425 426 427

	if (_randomDropsEnabled) {
	    if (rand() % 3 == 0) {
	        qDebug() << "FileServer: Random drop of outgoing packet";
	        return;
	    }
	}
Don Gagne's avatar
Don Gagne committed
428
    
429
    _mockLink->respondWithMavlinkMessage(_lastReply);
Lorenz Meier's avatar
Lorenz Meier committed
430
}
431 432 433

/// @brief Generates the next sequence number given an incoming sequence number. Handles generating
/// bad sequence numbers when errModeBadSequence is set.
434
uint16_t MockLinkFTP::_nextSeqNumber(uint16_t seqNumber)
435 436 437 438 439 440 441 442
{
    uint16_t outgoingSeqNumber = seqNumber + 1;
    
    if (_errMode == errModeBadSequence) {
        outgoingSeqNumber++;
    }
    return outgoingSeqNumber;
}
443 444 445 446 447 448 449 450 451 452 453

QString MockLinkFTP::_createTestCaseTempFile(const FileTestCase& testCase)
{
    QGCTemporaryFile tmpFile("MockLinkFTPTestCase");
    tmpFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
    for (int i=0; i<testCase.length; i++) {
        tmpFile.write(QByteArray(1, i % 255));
    }
    tmpFile.close();
    return tmpFile.fileName();
}