Support for TLS MTProxy

This commit is contained in:
Wizou 2022-01-11 04:14:23 +01:00
parent 16258cb5ba
commit d6d656c8fe
6 changed files with 237 additions and 43 deletions

View file

@ -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.
<a name="join-channel"></a>
### 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);
```
<a name="msg-by-name"></a>
### 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);
```
<a name="join-channel"></a>
### 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);
```
<a name="add-members"></a>
### Add/Invite/Remove someone in a chat
```csharp

View file

@ -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<long> _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<int> 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<long> 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
/// <returns>MIME type of the document/thumbnail</returns>
public async Task<string> 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<Update>(),
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");

View file

@ -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<int> 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;

View file

@ -358,8 +358,10 @@ namespace TL
partial class Contacts_ResolvedPeer
{
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved.UserOrChat.ToInputPeer();
/// <returns>A <see cref="TL.User"/>, or <see langword="null"/> if the username was for a channel</returns>
public User User => peer is PeerUser pu ? users[pu.user_id] : null;
public ChatBase Chat => peer is PeerChat or PeerChannel ? chats[peer.ID] : null;
/// <returns>A <see cref="Channel"/> or <see cref="ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
public ChatBase Chat => peer is PeerChannel or PeerChat ? chats[peer.ID] : null;
}
partial class Updates_ChannelDifferenceBase

View file

@ -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<uint, Type> Table = new()
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly static Dictionary<uint, Type> Table = new()
{
[0xF35C6D01] = typeof(RpcResult),
[0x5BB8E511] = typeof(_Message),

161
src/TlsStream.cs Normal file
View file

@ -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<int> 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<TlsStream> 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;
}
}
}