An experimental Telegram Server implementation in Python
An experimental Telegram server written from scratch in Python. Development chat: linked group to @ChameleonGram.
sendCode()
. (note to self: inspect the MTProto workers in chrome://inspect/#workers
)msg_id
/seq_no
according
to the
Telegram specificationtools/websocket_proxy.js
pts
, qts
, etc.authorize()
method.server.py
:
old = False
tests/
directory with patched assertions from client libraries.assert
statements
are disabled with python -O
, leading to missing important checks.g_a
/g_b
.piltover/__main__.py
, and use a database for auth
keys/messages/users/updates (probably with SQLAlchemy and alambic due to
reliable database migrations).This project is currently not meant to be used to host custom Telegram instances, as most security measures are currently barely in place. For now, it can be used by MTProto clients developers to understand why their code fails, whereas Telegram just closes the connection with a -404 error code.
That being said, it is planned in future to make it usable for most basic Telegram featues, including but not limited to, sending and receiving text and media messages, media, search.
This can be really useful for bots developers that would like to have a testing sandbox that doesn't ratelimit their bots.
The server is meant to be used as a library, providing 100% control of every answer
authorize()
An example quick-start (incomplete) code would look like this:
import asyncio
from piltover.server import Server, Client, Request
from piltover.utils import gen_keys
async def main():
pilt = Server(server_keys=gen_keys())
# Running on localhost
# Port: 4430
@pilt.on_message("ping")
async def pong(client: Client, request: Request):
print("Received ping:", request.obj)
return {
"_": "pong",
"msg_id": request.msg_id,
"ping_id": request.obj.ping_id,
}
await pilt.serve()
asyncio.run(main())
$ poetry install --no-root
$ poetry run python -m piltover
# Server running on 127.0.0.1:4430...
Of course, this minimal setup is far from complete, and will only work for auth key generation and pings.
$ git clone https://github.com/DavideGalilei/piltover
$ cd piltover
Follow instructions at: https://python-poetry.org/docs/#installation
$ poetry install --no-root
$ poetry run python tools/gen_tl.py update
$ poetry run python -m piltover
Now wait until it loads correctly and fire a Ctrl-C to stop the process.
You should see a line looking like this at the beginning
2023-11-05 19:52:31.171 | INFO | __main__:main:49 - Pubkey fingerprint: -6bff292cf4837025 (9400d6d30b7c8fdb)
Get the fingerprint hex string and save it for later (some clients need it).
In this case, the unsigned fingerprint is 9400d6d30b7c8fdb
, but only for this
example. Do not reuse this key fingerprint, as it will be different in your
setup.
At this point, two files should have been generated in your directory. Namely,
data/secrets/privkey.asc
and data/secrets/pubkey.asc
. Keep in mind that some
clients might need the PKCS1 public key in the normal ascii format.
Some others like pyrogram, do not have a RSA key parser and hardcode the number/exponent. To extract it, you can use this command:
$ grep -v -- - data/secrets/pubkey.asc | tr -d \\n | base64 -d | openssl asn1parse -inform DER -i
An example output would look like this:
0:d=0 hl=4 l= 266 cons: SEQUENCE
4:d=1 hl=4 l= 257 prim: INTEGER :C3AE9457FDB44F47B91B9389401933F2D0B27357FE116ED7640798784829FDBC66295169D1D323AB664FD6920EFBAAC8725DA7EACAA491D1F1EEC8259CA68E4CFE86FC6823C903A323DE46C0E64B8DD5C93A188711C1BF78FCBE0C99904227A66C9135241DD8B92A0AD88AB3A6734BC13B57FA38614BB2AA79F3EF0920D577928F7E689B7B5B0A1A8A48DA9D7E4C28F2A8F1AAEDA22AC4DA05324C1CB67538ADFE1AC3201B34A85189B0765E6C79FF443433837B540D6295BF9EE95B8CDA709868C450BE9730C9FCC7442011129AFB45187C2A1913A4974709E9666865C4F06067E981BF57950A0395B45C3A7322FD36F77D803FF97897BC00D5687A3CB575D1
265:d=1 hl=2 l= 3 prim: INTEGER :010001
Note the exponent (010001
) and the prime number: (C3AE94...B575D1
). Save
those values for later.
git clone --depth=1 https://github.com/pyrogram/pyrogram
PublicKey(int(
prime 16), int(
exponent , 16))
"127.0.0.1"
(localhost)DataCenter.__new__
method below, replace every return with
return (ip,
4430 )
, instead of ports 80/443python3 -m pip install -e .
test.py
worksgit clone --depth=1 https://github.com/LonamiWebs/telethon
DEFAULT_DC_ID = 2
DEFAULT_IPV4_IP = '127.0.0.1'
DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a'
DEFAULT_PORT = 4430
data/secrets/pubkey.asc
file, and
add it there with add_key("""
key here """, old=False)
python3 -m pip install -e .
telethon_test.py
works127.0.0.1
(localhost), and every port
with 4430
data/secrets/pubkey.asc
file on your piltover folder. Important: check
the newlines thoroughly and make sure they are there, or it won't work.tdesk
$ rm -rf tdata/ DebugLogs/ log.txt && c && ./Telegram
127.0.0.1
(localhost) only in the case you're running the app with an emulator on the
same machine the server is running. Otherwise, change it with e.g.
192.168.1.35
(the LAN ip address of your machine).4430
data/secrets/pubkey.asc
file on your piltover folder. Important: check
the newlines thoroughly and make sure they are there, or it won't work. This
took me way too much debugging time to realize that the missing newlines was
the cause of the app crashes.$ git clone https://github.com/morethanwords/tweb
$ cd tweb
$ npm i -g pnpm
$ pnpm install
const chosenServer = `wss://...`
to:const chosenServer = `ws://127.0.0.1:3000/proxy`;
127.0.0.1
(localhost) and 3000
(websocket proxy port)
https://github.com/morethanwords/tweb/blob/f2827d9c19616a560346bd1662665ca30dc54668/src/lib/mtproto/dcConfigurator.ts#L58-L70
modulus
to the lowercase string of prime
obtained previously$ poetry run python tools/websocket_proxy.py
npm start
https://0.0.0.0:8080/
)0xef
: Abridged0xeeeeeeee
: Intermediate0xdddddddd
: Padded Intermediate[length: 4 bytes][0x00000000]
: TCP Full, distinguishable by the empty
seq_no
(0x00000000
)[presumably random bytes]
: Usually and
Obfuscated
transportTCP Full
and Obfuscated
transports, a buffered
reader is needed, to allow for peeking the stream without consuming it.tools/gen_tl.py
) utility uses jinja2
to generate the
api_tl.py
/ mtproto_tl.py
files from the official TDesktop repo. (#TODO
retrieve as much old schema layers for multi-layer support)authorize()
method of the
piltover's Server
class.pq
decomposition, a proof of work to
avoid clients' DoS to the serverRSA_PAD
to encrypt the inner data payloadSHA1(auth_key)
)invokeWithLayer(initConnection(getConfig(...)))
One day, my Telegram account stopped working properly due to an internal server
error originating from a supposedly corrupted message I forwarded. Every time
the client tried to fetch new messages from private chats, it would face a
[500 STORE_INVALID_OBJECT_TYPE]
error. Hopefully, the bug was fixed in ~1/2
days after being reported, but the fact that it happened at all motivated me
enough to try building my own server. In several days, I managed to make it
kinda work :)
List of other server implementations I found:
Various applications similar to Telegram (probably using a custom MTProto backend):