From d6d656c8fe094fd6a78cda93eb1446f2c33148f3 Mon Sep 17 00:00:00 2001
From: Wizou <11647984+wiz0u@users.noreply.github.com>
Date: Tue, 11 Jan 2022 04:14:23 +0100
Subject: [PATCH] Support for TLS MTProxy
---
EXAMPLES.md | 20 +++---
src/Client.cs | 72 ++++++++++++---------
src/Helpers.cs | 17 ++++-
src/TL.Helpers.cs | 4 +-
src/TL.Table.cs | 6 +-
src/TlsStream.cs | 161 ++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 237 insertions(+), 43 deletions(-)
create mode 100644 src/TlsStream.cs
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 65aa6d3..98800e5 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -14,16 +14,6 @@ and add at least these variables with adequate value: **api_id, api_hash, phone_
Remember that these are just simple example codes that you should adjust to your needs.
In real production code, you might want to properly test the success of each operation or handle exceptions.
-
-### Join a channel/group by @channelname
-```csharp
-using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
-await client.LoginUserIfNeeded();
-var resolved = await client.Contacts_ResolveUsername("channelname"); // without the @
-if (resolved.Chat is Channel channel)
- await client.Channels_JoinChannel(channel);
-```
-
### Send a message to someone by @username
```csharp
@@ -218,6 +208,16 @@ var channel = (Channel)chats.chats[1234567890]; // the channel we want
var participants = await client.Channels_GetAllParticipants(channel);
```
+
+### Join a channel/group by @channelname
+```csharp
+using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
+await client.LoginUserIfNeeded();
+var resolved = await client.Contacts_ResolveUsername("channelname"); // without the @
+if (resolved.Chat is Channel channel)
+ await client.Channels_JoinChannel(channel);
+```
+
### Add/Invite/Remove someone in a chat
```csharp
diff --git a/src/Client.cs b/src/Client.cs
index 939eddc..76bea08 100644
--- a/src/Client.cs
+++ b/src/Client.cs
@@ -54,7 +54,7 @@ namespace WTelegram
private readonly Session _session;
private Session.DCSession _dcSession;
private TcpClient _tcpClient;
- private NetworkStream _networkStream;
+ private Stream _networkStream;
private IObject _lastSentMsg;
private long _lastRecvMsgId;
private readonly List _msgsToAck = new();
@@ -223,6 +223,7 @@ namespace WTelegram
private async Task DoConnectAsync()
{
+ _cts = new();
IPEndPoint endpoint = null;
byte[] preamble, secret = null;
int dcId = _dcSession?.DcID ?? 0;
@@ -235,9 +236,10 @@ namespace WTelegram
var server = parms["server"];
int port = int.Parse(parms["port"]);
var str = parms["secret"]; // can be hex or base64
- secret = str.All("0123456789ABCDEFabcdef".Contains) ? Convert.FromHexString(str) :
+ var secretBytes = secret = str.All("0123456789ABCDEFabcdef".Contains) ? Convert.FromHexString(str) :
System.Convert.FromBase64String(str.Replace('_', '/').Replace('-', '+') + new string('=', (2147483644 - str.Length) % 4));
- if ((secret.Length == 17 && secret[0] == 0xDD) || (secret.Length >= 21 && secret[0] == 0xEE))
+ var tlsMode = secret.Length >= 21 && secret[0] == 0xEE;
+ if (tlsMode || (secret.Length == 17 && secret[0] == 0xDD))
{
_paddedMode = true;
secret = secret[1..17];
@@ -245,6 +247,9 @@ namespace WTelegram
else if (secret.Length != 16) throw new ArgumentException("Invalid/unsupported secret", nameof(secret));
Helpers.Log(2, $"Connecting to DC {dcId} via MTProxy {server}:{port}...");
_tcpClient = await TcpHandler(server, port);
+ _networkStream = _tcpClient.GetStream();
+ if (tlsMode)
+ _networkStream = await TlsStream.HandshakeAsync(_networkStream, secret, secretBytes[17..], _cts.Token);
#else
throw new Exception("Library was not compiled with OBFUSCATION symbol");
#endif
@@ -305,8 +310,8 @@ namespace WTelegram
throw;
}
_tcpClient = tcpClient;
+ _networkStream = _tcpClient.GetStream();
}
- _networkStream = _tcpClient.GetStream();
byte protocolId = (byte)(_paddedMode ? 0xDD : 0xEE);
#if OBFUSCATION
@@ -314,9 +319,8 @@ namespace WTelegram
#else
preamble = new byte[] { protocolId, protocolId, protocolId, protocolId };
#endif
- await _networkStream.WriteAsync(preamble, 0, preamble.Length);
+ await _networkStream.WriteAsync(preamble, 0, preamble.Length, _cts.Token);
- _cts = new();
_saltChangeCounter = 0;
_reactorTask = Reactor(_networkStream, _cts);
_sendSemaphore.Release();
@@ -445,7 +449,7 @@ namespace WTelegram
}
}
- private async Task Reactor(NetworkStream stream, CancellationTokenSource cts)
+ private async Task Reactor(Stream stream, CancellationTokenSource cts)
{
const int MinBufferSize = 1024;
var data = new byte[MinBufferSize];
@@ -454,17 +458,19 @@ namespace WTelegram
IObject obj = null;
try
{
- if (await FullReadAsync(stream, data, 4, cts.Token) != 4)
+ if (await stream.FullReadAsync(data, 4, cts.Token) != 4)
throw new ApplicationException(ConnectionShutDown);
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, 4);
#endif
int payloadLen = BinaryPrimitives.ReadInt32LittleEndian(data);
- if (payloadLen > data.Length)
+ if (payloadLen <= 0)
+ throw new ApplicationException("Could not read frame data : Invalid payload length");
+ else if (payloadLen > data.Length)
data = new byte[payloadLen];
else if (Math.Max(payloadLen, MinBufferSize) < data.Length / 4)
data = new byte[Math.Max(payloadLen, MinBufferSize)];
- if (await FullReadAsync(stream, data, payloadLen, cts.Token) != payloadLen)
+ if (await stream.FullReadAsync(data, payloadLen, cts.Token) != payloadLen)
throw new ApplicationException("Could not read frame data : Connection shut down");
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, payloadLen);
@@ -629,17 +635,6 @@ namespace WTelegram
};
}
- private static async Task FullReadAsync(Stream stream, byte[] buffer, int length, CancellationToken ct = default)
- {
- for (int offset = 0; offset < length;)
- {
- var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
- if (read == 0) return offset;
- offset += read;
- }
- return length;
- }
-
private async Task SendAsync(IObject msg, bool isContent)
{
if (_dcSession.AuthKeyID != 0 && isContent && CheckMsgsToAck() is MsgsAck msgsAck)
@@ -743,7 +738,7 @@ namespace WTelegram
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
- if ((msg.seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msg.msg_id);
+ if ((msg.seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
var pos = reader.BaseStream.Position;
try
{
@@ -801,11 +796,30 @@ namespace WTelegram
}
else
{
- result = reader.ReadTLObject();
- if (MsgIdToStamp(msgId) >= _session.SessionStart)
- Helpers.Log(4, $" → {result?.GetType().Name,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}");
+ string typeName;
+ var ctorNb = reader.ReadUInt32();
+ if (ctorNb == Layer.VectorCtor)
+ {
+ reader.BaseStream.Position -= 4;
+ var array = reader.ReadTLVector(typeof(IObject[]));
+ if (array.Length > 0)
+ {
+ for (type = array.GetValue(0).GetType(); type.BaseType != typeof(object);) type = type.BaseType;
+ typeName = type.Name + "[]";
+ }
+ else
+ typeName = "object[]";
+ result = array;
+ }
else
- Helpers.Log(1, $" → {result?.GetType().Name,-37} for past msgId #{(short)msgId.GetHashCode():X4}");
+ {
+ result = reader.ReadTLObject(ctorNb);
+ typeName = result?.GetType().Name;
+ }
+ if (MsgIdToStamp(msgId) >= _session.SessionStart)
+ Helpers.Log(4, $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}");
+ else
+ Helpers.Log(1, $" → {typeName,-37} for past msgId #{(short)msgId.GetHashCode():X4}");
}
return new RpcResult { req_msg_id = msgId, result = result };
}
@@ -1219,7 +1233,7 @@ namespace WTelegram
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
{
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
- read = await FullReadAsync(stream, bytes, bytes.Length);
+ read = await stream.FullReadAsync(bytes, bytes.Length, default);
await _parallelTransfers.WaitAsync();
bytesLeft -= read;
var task = SavePart(file_part, bytes);
@@ -1369,7 +1383,7 @@ namespace WTelegram
/// MIME type of the document/thumbnail
public async Task DownloadFileAsync(Document document, Stream outputStream, PhotoSizeBase thumbSize = null, ProgressCallback progress = null)
{
- if (thumbSize is PhotoStrippedSize psp)
+ if (thumbSize is PhotoStrippedSize psp)
return InflateStrippedThumb(outputStream, psp.bytes) ? "image/jpeg" : null;
var fileLocation = document.ToFileLocation(thumbSize);
var fileType = await DownloadFileAsync(fileLocation, outputStream, document.dc_id, thumbSize?.FileSize ?? document.size, progress);
@@ -1617,7 +1631,7 @@ namespace WTelegram
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty(),
chats = (await this.Messages_GetChats(new[] { chat.chat_id })).chats };
case InputPeerChannel channel:
- return await this.Channels_EditAdmin(channel, user,
+ return await this.Channels_EditAdmin(channel, user,
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
default:
throw new ArgumentException("This method works on Chat & Channel only");
diff --git a/src/Helpers.cs b/src/Helpers.cs
index 2fbfa03..06d00c2 100644
--- a/src/Helpers.cs
+++ b/src/Helpers.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Numerics;
using System.Text.Json;
-using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
namespace WTelegram
{
@@ -40,6 +42,19 @@ namespace WTelegram
#endif
}
+ public static async Task FullReadAsync(this Stream stream, byte[] buffer, int length, CancellationToken ct)
+ {
+ for (int offset = 0; offset < length;)
+ {
+#pragma warning disable CA1835
+ var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
+#pragma warning restore CA1835
+ if (read == 0) return offset;
+ offset += read;
+ }
+ return length;
+ }
+
internal static byte[] ToBigEndian(ulong value) // variable-size buffer
{
int i = 1;
diff --git a/src/TL.Helpers.cs b/src/TL.Helpers.cs
index 8b6efa9..10ac32a 100644
--- a/src/TL.Helpers.cs
+++ b/src/TL.Helpers.cs
@@ -358,8 +358,10 @@ namespace TL
partial class Contacts_ResolvedPeer
{
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved.UserOrChat.ToInputPeer();
+ /// A , or if the username was for a channel
public User User => peer is PeerUser pu ? users[pu.user_id] : null;
- public ChatBase Chat => peer is PeerChat or PeerChannel ? chats[peer.ID] : null;
+ /// A or , or if the username was for a user
+ public ChatBase Chat => peer is PeerChannel or PeerChat ? chats[peer.ID] : null;
}
partial class Updates_ChannelDifferenceBase
diff --git a/src/TL.Table.cs b/src/TL.Table.cs
index b0d249d..6518461 100644
--- a/src/TL.Table.cs
+++ b/src/TL.Table.cs
@@ -1,6 +1,7 @@
-// This file is generated automatically using the Generator class
+// This file is generated automatically
using System;
using System.Collections.Generic;
+using System.ComponentModel;
namespace TL
{
@@ -14,7 +15,8 @@ namespace TL
internal const uint MsgContainerCtor = 0x73F1F8DC;
internal const uint BadMsgCtor = 0xA7EFF811;
- internal readonly static Dictionary Table = new()
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public readonly static Dictionary Table = new()
{
[0xF35C6D01] = typeof(RpcResult),
[0x5BB8E511] = typeof(_Message),
diff --git a/src/TlsStream.cs b/src/TlsStream.cs
new file mode 100644
index 0000000..3d3f706
--- /dev/null
+++ b/src/TlsStream.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+
+// necessary for .NET Standard 2.0 compilation:
+#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+
+namespace WTelegram
+{
+ class TlsStream : Stream
+ {
+ public TlsStream(Stream innerStream) => _innerStream = innerStream;
+ private readonly Stream _innerStream;
+ private int _tlsFrameleft;
+ private readonly byte[] _tlsSendHeader = new byte[] { 0x17, 0x03, 0x03, 0, 0 };
+ private readonly byte[] _tlsReadHeader = new byte[5];
+ static readonly byte[] TlsServerHelloPart3 = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03 };
+ static readonly byte[] TlsClientPrefix = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01 };
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length => throw new NotSupportedException();
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+ public override void Flush() => _innerStream.Flush();
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct)
+ {
+ if (_tlsFrameleft == 0)
+ {
+ if (await _innerStream.FullReadAsync(_tlsReadHeader, 5, ct) != 5)
+ return 0;
+ if (_tlsReadHeader[0] != 0x17 || _tlsReadHeader[1] != 0x03 || _tlsReadHeader[2] != 0x03)
+ throw new ApplicationException("Could not read frame data : Invalid TLS header");
+ _tlsFrameleft = (_tlsReadHeader[3] << 8) + _tlsReadHeader[4];
+ }
+ var read = await _innerStream.ReadAsync(buffer, offset, Math.Min(count, _tlsFrameleft), ct);
+ _tlsFrameleft -= read;
+ return read;
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int start, int count, CancellationToken ct)
+ {
+ for (int offset = 0; offset < count;)
+ {
+ int len = Math.Min(count - offset, 2878);
+ _tlsSendHeader[3] = (byte)(len >> 8);
+ _tlsSendHeader[4] = (byte)len;
+ await _innerStream.WriteAsync(_tlsSendHeader, 0, _tlsSendHeader.Length, ct);
+ await _innerStream.WriteAsync(buffer, start + offset, len, ct);
+ offset += len;
+ }
+ }
+
+ public static async Task HandshakeAsync(Stream stream, byte[] key, byte[] domain, CancellationToken ct)
+ {
+ var clientHello = TlsClientHello(key, domain);
+ await stream.WriteAsync(clientHello, 0, clientHello.Length, ct);
+
+ var part1 = new byte[5];
+ if (await stream.FullReadAsync(part1, 5, ct) == 5)
+ if (part1[0] == 0x16 && part1[1] == 0x03 && part1[2] == 0x03)
+ {
+ var part2size = BinaryPrimitives.ReadUInt16BigEndian(part1.AsSpan(3));
+ var part23 = new byte[part2size + TlsServerHelloPart3.Length + 2];
+ if (await stream.FullReadAsync(part23, part23.Length, ct) == part23.Length)
+ if (TlsServerHelloPart3.SequenceEqual(part23.Skip(part2size).Take(TlsServerHelloPart3.Length)))
+ {
+ var part4size = BinaryPrimitives.ReadUInt16BigEndian(part23.AsSpan(part23.Length - 2));
+ var part4 = new byte[part4size];
+ if (await stream.FullReadAsync(part4, part4size, ct) == part4size)
+ {
+ var serverDigest = part23[6..38];
+ Array.Clear(part23, 6, 32); // clear server digest from received parts
+ var hmc = new HMACSHA256(key); // hash the client digest + all received parts
+ hmc.TransformBlock(clientHello, 11, 32, null, 0);
+ hmc.TransformBlock(part1, 0, part1.Length, null, 0);
+ hmc.TransformBlock(part23, 0, part23.Length, null, 0);
+ hmc.TransformFinalBlock(part4, 0, part4.Length);
+ if (serverDigest.SequenceEqual(hmc.Hash))
+ {
+ Helpers.Log(2, "TLS Handshake succeeded");
+ await stream.WriteAsync(TlsClientPrefix, 0, TlsClientPrefix.Length, ct);
+ return new TlsStream(stream);
+ }
+ }
+ }
+ }
+ throw new ApplicationException("TLS Handshake failed");
+ }
+
+ static readonly byte[] TlsClientHello1 = new byte[] {
+ 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 };
+ static readonly byte[] TlsClientHello2 = new byte[] {
+ 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
+ 0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93 };
+ static readonly byte[] TlsClientHello3 = new byte[] {
+ 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
+ 0, 0, // grease 4
+ 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00,
+ 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31,
+ 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00,
+ 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06,
+ 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29,
+ 0, 0, // grease 4
+ 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20 };
+ static readonly byte[] TlsClientHello4 = new byte[] {
+ 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a,
+ 0, 0, // grease 6
+ 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
+ 0, 0, // grease 3
+ 0x00, 0x01, 0x00, 0x00, 0x15 };
+
+ static byte[] TlsClientHello(byte[] key, byte[] domain)
+ {
+ int dlen = domain.Length;
+ var greases = new byte[7];
+ Encryption.RNG.GetBytes(greases);
+ for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A);
+ if (greases[3] == greases[2]) greases[3] = (byte)(0x10 ^ greases[3]);
+
+ var buffer = new byte[517];
+ TlsClientHello1.CopyTo(buffer, 0);
+ TlsClientHello2.CopyTo(buffer, 80);
+ buffer[43] = buffer[77] = 0x20;
+ Encryption.RNG.GetBytes(buffer, 44, 32);
+ buffer[78] = buffer[79] = greases[0];
+ buffer[114] = buffer[115] = greases[2];
+ buffer[121] = (byte)(dlen + 5);
+ buffer[123] = (byte)(dlen + 3);
+ buffer[126] = (byte)dlen;
+ domain.CopyTo(buffer, 127);
+ TlsClientHello3.CopyTo(buffer, 127 + dlen);
+ buffer[142 + dlen] = buffer[143 + dlen] = greases[4];
+ buffer[219 + dlen] = buffer[220 + dlen] = greases[4];
+ Encryption.RNG.GetBytes(buffer, 228 + dlen, 32); // public key
+ TlsClientHello4.CopyTo(buffer, 260 + dlen);
+ buffer[271 + dlen] = buffer[272 + dlen] = greases[6];
+ buffer[288 + dlen] = buffer[289 + dlen] = greases[3];
+ buffer[296 + dlen] = (byte)(220 - dlen);
+
+ // patch-in digest with timestamp
+ using var hmac = new HMACSHA256(key);
+ var digest = hmac.ComputeHash(buffer);
+ var stamp = BinaryPrimitives.ReadInt32LittleEndian(digest.AsSpan(28));
+ stamp ^= (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ BinaryPrimitives.WriteInt32LittleEndian(digest.AsSpan(28), stamp);
+ digest.CopyTo(buffer, 11);
+
+ return buffer;
+ }
+ }
+}