// Copyright 2004-2017 The Poderosa Project. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //#define TRACE_XMODEM using System; using System.Diagnostics; using System.IO; using System.Threading; using Poderosa.Protocols; using System.Threading.Tasks; using System.Windows.Forms; namespace Poderosa.XZModem { /// /// XMODEM protocol base class /// internal abstract class XModem : ModemBase { protected const byte SOH = 0x01; protected const byte STX = 0x02; protected const byte EOT = 0x04; protected const byte ACK = 0x06; protected const byte NAK = 0x15; protected const byte CAN = 0x18; protected const byte CPMEOF = 0x1a; protected const byte LETTER_C = 0x43; private readonly byte[] _singleByteBuff = new byte[1]; private readonly XZModemDialog _parent; protected XModem(XZModemDialog dialog) : base(dialog) { _parent = dialog; } #region IModalTerminalTask public override string Caption { get { return "XMODEM"; } } #endregion protected void Send(byte[] data, int len) { _connection.Socket.Transmit(data, 0, len); } protected void Send(byte ch) { lock (_singleByteBuff) { _singleByteBuff[0] = ch; Send(_singleByteBuff, 1); } } protected void SetProgressValue(long pos) { _parent.SetProgressValue(pos); } protected void Trace(string message) { #if TRACE_XMODEM Debug.WriteLine(message); #endif } protected void Trace(string format, params object[] args) { #if TRACE_XMODEM Debug.WriteLine(format, args); #endif } } /// /// XMODEM receiver /// internal class XModemReceiver : XModem { private const int WAIT_CRCBLOCK_TIMEOUT = 3000; private const int WAIT_BLOCK_TIMEOUT = 10000; private const int MAX_ERROR = 10; private const int MODE_CHECKSUM = 0; private const int MODE_CRC = 1; private struct BlockTypeInfo { public readonly bool HasCRC; public readonly int BlockSize; public readonly int DataOffset; public readonly int DataLength; public BlockTypeInfo(bool hasCRC, int blockSize, int dataOffset, int dataLength) { HasCRC = hasCRC; BlockSize = blockSize; DataOffset = dataOffset; DataLength = dataLength; } } private readonly string _filePath; private FileStream _output; private Task _monitorTask; private bool _teminateMonitorTask; private bool _fileClosed; private long _fileSize = 0; private readonly byte[] _pendingBuff = new byte[1024]; private int _pendingLen = 0; private readonly byte[] _recvBuff = new byte[1029]; private int _recvLen = 0; private byte _nextSequenceNumber; private int _mode; // MODE_CHECKSUM or MODE_CRC private long _lastReceptionTimeUtcTicks; private long _lastBlockTimeUtcTicks; // count of the consecutive errors private int _errorCount; // true when the file transfer is aborting private bool _aborting; public XModemReceiver(XZModemDialog parent, string filePath) : base(parent) { _filePath = filePath; _lastReceptionTimeUtcTicks = _lastBlockTimeUtcTicks = DateTime.UtcNow.Ticks; } public override bool IsReceivingTask { get { return true; } } private void Monitor() { int crcModeRetries = 0; while (!Volatile.Read(ref _teminateMonitorTask)) { long last = Interlocked.Read(ref _lastBlockTimeUtcTicks); int elapsedMsec = (int)((DateTime.UtcNow.Ticks - last) / TimeSpan.TicksPerMillisecond); int mode = Volatile.Read(ref _mode); int sn = Volatile.Read(ref _nextSequenceNumber); if (mode == MODE_CRC && sn == 1 && elapsedMsec > WAIT_CRCBLOCK_TIMEOUT) { if (crcModeRetries < 3) { crcModeRetries++; Interlocked.Exchange(ref _lastBlockTimeUtcTicks, DateTime.UtcNow.Ticks); Trace("<-- Retry: C"); Send(LETTER_C); } else { Interlocked.Exchange(ref _lastBlockTimeUtcTicks, DateTime.UtcNow.Ticks); Volatile.Write(ref _mode, MODE_CHECKSUM); Trace("<-- Retry: NAK"); Send(NAK); // fallback into checksum mode } goto Continue; } if (elapsedMsec > WAIT_BLOCK_TIMEOUT) { Abort(XZModemPlugin.Instance.Strings.GetString("Message.XModem.ReceivingTimedOut"), false); break; } Continue: Thread.Sleep(200); } Trace("exit monitor thread"); } protected override void OnStart() { _output = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); _teminateMonitorTask = false; _mode = MODE_CRC; _nextSequenceNumber = 1; _errorCount = 0; Thread.MemoryBarrier(); _monitorTask = Task.Run(() => Monitor()); Trace("<-- C"); Send(LETTER_C); // request CRC mode } private void StopMonitor() { if (_monitorTask != null) { Volatile.Write(ref _teminateMonitorTask, true); // don't wait completion of monitor task here // because cancellation may be invoked from monitor task. _monitorTask = null; } } protected override void OnAbort(string message, bool closeDialog) { // stop monitor thread StopMonitor(); // run aborting sequence Task.Run(() => { _aborting = true; Thread.MemoryBarrier(); Thread.Sleep(200); Trace("<-- CAN"); Send(CAN); Send(CAN); DiscardAllIncomingData(); Completed(true, closeDialog, message); }); } protected override void OnStopped() { StopMonitor(); _fileClosed = true; if (_output != null) { _output.Close(); Trace("file closed"); } } public override void Dispose() { StopMonitor(); if (_output != null) { _output.Dispose(); } } public override void OnReception(ByteDataFragment fragment) { Interlocked.Exchange(ref _lastReceptionTimeUtcTicks, DateTime.UtcNow.Ticks); if (_aborting) { return; } byte[] data = fragment.Buffer; int offset = fragment.Offset; int length = fragment.Length; BlockTypeInfo blockInfo; if (_recvLen > 0) { blockInfo = GetBlockTypeInfo(_recvBuff[0], Volatile.Read(ref _mode)); } else { blockInfo = new BlockTypeInfo(); // update later } for (int i = 0; i < length; i++) { byte c = data[offset + i]; if (_recvLen == 0) { if (c == EOT) { Trace("--> EOT"); FlushPendingBuffer(true); Trace("<-- ACK"); Send(ACK); Completed(false, true, XZModemPlugin.Instance.Strings.GetString("Message.XModem.ReceiveComplete")); return; } if (c != SOH && c != STX) { continue; // skip } // determine expected block type blockInfo = GetBlockTypeInfo(c, Volatile.Read(ref _mode)); } _recvBuff[_recvLen++] = c; if (_recvLen >= blockInfo.BlockSize) { goto BlockReceived; } } return; BlockReceived: // a block has been received Interlocked.Exchange(ref _lastBlockTimeUtcTicks, DateTime.UtcNow.Ticks); Trace("--> {0:X2} {1:X2} ...({2})", _recvBuff[0], _recvBuff[1], _recvLen); // check sequence number if (_recvBuff[1] != _nextSequenceNumber || _recvBuff[2] != (255 - _nextSequenceNumber)) { Trace("<-- NAK (bad seq)"); goto Error; } // check CRC or checksum if (blockInfo.HasCRC) { ushort crc = Crc16.Update(Crc16.InitialValue, _recvBuff, blockInfo.DataOffset, blockInfo.DataLength); int crcIndex = blockInfo.DataOffset + blockInfo.DataLength; if (_recvBuff[crcIndex] != (byte)(crc >> 8) || _recvBuff[crcIndex + 1] != (byte)crc) { // CRC error Trace("<-- NAK (CRC error)"); goto Error; } } else { byte checksum = 0; int index = blockInfo.DataOffset; for (int n = 0; n < blockInfo.DataLength; ++n) { checksum += _recvBuff[index++]; } if (_recvBuff[index] != checksum) { // checksum error Trace("<-- NAK (checksum error)"); goto Error; } } // ok _nextSequenceNumber++; FlushPendingBuffer(false); SaveToPendingBuffer(_recvBuff, blockInfo.DataOffset, blockInfo.DataLength); _errorCount = 0; _recvLen = 0; Send(ACK); return; Error: _recvLen = 0; _errorCount++; if (_errorCount > MAX_ERROR) { Abort(XZModemPlugin.Instance.Strings.GetString("Message.XModem.CouldNotReceiveCorrectData"), false); } else { Send(NAK); } } private void FlushPendingBuffer(bool isLastBlock) { if (isLastBlock) { while (_pendingLen > 0 && _pendingBuff[_pendingLen - 1] == CPMEOF) { _pendingLen--; } } if (_pendingLen > 0) { if (!Volatile.Read(ref _fileClosed)) { _output.Write(_pendingBuff, 0, _pendingLen); } _fileSize += _pendingLen; _pendingLen = 0; } SetProgressValue(_fileSize); } private void SaveToPendingBuffer(byte[] buff, int offset, int length) { Buffer.BlockCopy(buff, offset, _pendingBuff, 0, length); _pendingLen = length; } private BlockTypeInfo GetBlockTypeInfo(byte firstByte, int mode) { if (firstByte == STX) { // XMODEM/1k return new BlockTypeInfo(true, 1029, 3, 1024); } if (mode == MODE_CRC) { // XMODEM/CRC return new BlockTypeInfo(true, 133, 3, 128); } // XMODEM return new BlockTypeInfo(false, 132, 3, 128); } private void DiscardAllIncomingData() { while (true) { long last = Interlocked.Read(ref _lastReceptionTimeUtcTicks); if ((DateTime.UtcNow.Ticks - last) > 500 * TimeSpan.TicksPerMillisecond) { return; } Thread.Sleep(100); } } } /// /// XMODEM sender /// internal class XModemSender : XModem { private const int RESPONSE_TIMEOUT = 15000; private readonly string _filePath; private long _fileSize; private FileStream _input; private Task _monitorTask; private bool _teminateMonitorTask; private bool _fileClosed; private byte _sequenceNumber; private bool _crcMode; private long _prevPos; private long _nextPos; private readonly byte[] _sendBuff = new byte[1029]; private long _lastResponseTimeUtcTicks; private enum State { None, AfterEOT, Aborting, Stopped, } private volatile State _state = State.None; public XModemSender(XZModemDialog parent, string filePath) : base(parent) { _filePath = filePath; } public override bool IsReceivingTask { get { return false; } } protected override void OnStart() { _fileSize = new FileInfo(_filePath).Length; _input = new FileStream(_filePath, FileMode.Open, FileAccess.Read); _sequenceNumber = 1; _teminateMonitorTask = false; _prevPos = _nextPos = 0; _crcMode = false; _state = State.None; _lastResponseTimeUtcTicks = DateTime.UtcNow.Ticks; Thread.MemoryBarrier(); _monitorTask = Task.Run(() => Monitor()); } private void Monitor() { while (!Volatile.Read(ref _teminateMonitorTask)) { long last = Interlocked.Read(ref _lastResponseTimeUtcTicks); if (DateTime.UtcNow.Ticks - last > RESPONSE_TIMEOUT * TimeSpan.TicksPerMillisecond) { Abort(XZModemPlugin.Instance.Strings.GetString("Message.XModem.NoResponse"), false); break; } Thread.Sleep(200); } Trace("exit monitor thread"); } private void StopMonitor() { if (_monitorTask != null) { Volatile.Write(ref _teminateMonitorTask, true); // don't wait completion of sending task here // because cancellation may be invoked from sending task. _monitorTask = null; } } protected override void OnAbort(string message, bool closeDialog) { // stop monitor thread StopMonitor(); // run aborting sequence Task.Run(() => { _state = State.Aborting; Thread.MemoryBarrier(); // CAN mast be sent after the ACK or NAK from peer DateTime limit = DateTime.UtcNow.AddMilliseconds(3000); SpinWait.SpinUntil(() => { if (DateTime.UtcNow > limit) { // timeout return true; } if (_state == State.Stopped) { // CAN has been sent return true; } return false; }); Thread.MemoryBarrier(); if (_state != State.Stopped) { // no response ? Send(CAN); Send(CAN); Thread.Sleep(500); } Completed(true, closeDialog, message); }); } protected override void OnStopped() { StopMonitor(); _fileClosed = true; Thread.MemoryBarrier(); if (_input != null) { _input.Close(); Trace("file closed"); } } public override void Dispose() { StopMonitor(); if (_input != null) { _input.Dispose(); } } public override void OnReception(ByteDataFragment fragment) { if (_state == State.Stopped) { return; } if (_state == State.Aborting) { Send(CAN); Send(CAN); _state = State.Stopped; return; } byte[] data = fragment.Buffer; int offset = fragment.Offset; int length = fragment.Length; byte response; for (int i = 0; i < length; ++i) { byte c = data[offset + i]; if (c == LETTER_C || c == ACK || c == NAK || c == CAN) { response = c; goto GotResponse; } } return; GotResponse: Interlocked.Exchange(ref _lastResponseTimeUtcTicks, DateTime.UtcNow.Ticks); switch (response) { case NAK: Trace("--> NAK"); Resend: if (_state == State.AfterEOT) { Trace("<-- EOT(resend)"); Send(EOT); } else { SendBlock(_crcMode, true); } break; case LETTER_C: Trace("--> C"); _crcMode = true; goto Resend; case ACK: Trace("--> ACK"); if (_state == State.AfterEOT) { _state = State.Stopped; Completed(false, true, XZModemPlugin.Instance.Strings.GetString("Message.XModem.SendComplete")); } else { SendBlock(_crcMode, false); } break; case CAN: Trace("--> CAN"); _state = State.Stopped; Abort(XZModemPlugin.Instance.Strings.GetString("Message.ZModem.Aborted"), false); break; } } private void SendBlock(bool useCrc, bool resend) { if (_input == null || _fileClosed) { return; } if (resend) { Trace("Seek to {0}", _prevPos); _input.Seek(_prevPos, SeekOrigin.Begin); } else { _prevPos = _nextPos; _sequenceNumber++; } int dataLength; if (useCrc) { dataLength = (_fileSize - _prevPos < 1024L) ? 128 : 1024; } else { dataLength = 128; } int readLen = _input.Read(_sendBuff, 3, dataLength); _nextPos = _input.Position; if (readLen == 0) { _state = State.AfterEOT; Trace("<-- EOT"); Send(EOT); return; } _sendBuff[0] = (dataLength == 1024) ? STX : SOH; _sendBuff[1] = _sequenceNumber; _sendBuff[2] = (byte)(255 - _sequenceNumber); for (int i = 3 + readLen; i < 3 + dataLength; ++i) { _sendBuff[i] = CPMEOF; } int blockLen = 3 + dataLength; if (useCrc) { ushort crc = Crc16.Update(Crc16.InitialValue, _sendBuff, 3, dataLength); _sendBuff[blockLen++] = (byte)(crc >> 8); _sendBuff[blockLen++] = (byte)crc; } else { byte checksum = 0; for (int i = 3; i < 3 + dataLength; ++i) { checksum += _sendBuff[i]; } _sendBuff[blockLen++] = checksum; } Trace("<-- {0:X2} {1:X2} ...({2})", _sendBuff[0], _sendBuff[1], blockLen); Send(_sendBuff, blockLen); SetProgressValue((int)_nextPos); } } }