├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── encryptor ├── encrypt.py └── requirements.txt └── proxy ├── Dockerfile ├── cmd └── proxy │ └── main.go ├── config.yaml ├── config.yaml.example ├── go.mod ├── go.sum └── pkg ├── config └── config.go ├── crypto └── aes.go ├── database └── db.go ├── entities └── telegram.go └── http ├── telegram.go └── url.go /.env.example: -------------------------------------------------------------------------------- 1 | PROXY_AES_KEY=IWizpqppShhiZY8yHfvKc7O2qTpm5ovf 2 | PROXY_AES_IV=bYizp3omhKciZ1xz 3 | REDIS_PASSWORD=secure-_-password 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 4 | 5 | ### Go ### 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | ### Go Patch ### 29 | /vendor/ 30 | /Godeps/ 31 | 32 | # End of https://www.toptal.com/developers/gitignore/api/go 33 | 34 | 35 | # Created by https://www.toptal.com/developers/gitignore/api/python 36 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 37 | 38 | ### Python ### 39 | # Byte-compiled / optimized / DLL files 40 | __pycache__/ 41 | *.py[cod] 42 | *$py.class 43 | 44 | # C extensions 45 | *.so 46 | 47 | # Distribution / packaging 48 | .Python 49 | build/ 50 | develop-eggs/ 51 | dist/ 52 | downloads/ 53 | eggs/ 54 | .eggs/ 55 | lib/ 56 | lib64/ 57 | parts/ 58 | sdist/ 59 | var/ 60 | wheels/ 61 | share/python-wheels/ 62 | *.egg-info/ 63 | .installed.cfg 64 | *.egg 65 | MANIFEST 66 | 67 | # PyInstaller 68 | # Usually these files are written by a python script from a template 69 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .nox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | *.py,cover 88 | .hypothesis/ 89 | .pytest_cache/ 90 | cover/ 91 | 92 | # Translations 93 | *.mo 94 | *.pot 95 | 96 | # Django stuff: 97 | *.log 98 | local_settings.py 99 | db.sqlite3 100 | db.sqlite3-journal 101 | 102 | # Flask stuff: 103 | instance/ 104 | .webassets-cache 105 | 106 | # Scrapy stuff: 107 | .scrapy 108 | 109 | # Sphinx documentation 110 | docs/_build/ 111 | 112 | # PyBuilder 113 | .pybuilder/ 114 | target/ 115 | 116 | # Jupyter Notebook 117 | .ipynb_checkpoints 118 | 119 | # IPython 120 | profile_default/ 121 | ipython_config.py 122 | 123 | # pyenv 124 | # For a library or package, you might want to ignore these files since the code is 125 | # intended to run in multiple environments; otherwise, check them in: 126 | # .python-version 127 | 128 | # pipenv 129 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 130 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 131 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 132 | # install all needed dependencies. 133 | #Pipfile.lock 134 | 135 | # poetry 136 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 137 | # This is especially recommended for binary packages to ensure reproducibility, and is more 138 | # commonly ignored for libraries. 139 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 140 | #poetry.lock 141 | 142 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 143 | __pypackages__/ 144 | 145 | # Celery stuff 146 | celerybeat-schedule 147 | celerybeat.pid 148 | 149 | # SageMath parsed files 150 | *.sage.py 151 | 152 | # Environments 153 | .env 154 | .venv 155 | env/ 156 | venv/ 157 | ENV/ 158 | env.bak/ 159 | venv.bak/ 160 | 161 | # Spyder project settings 162 | .spyderproject 163 | .spyproject 164 | 165 | # Rope project settings 166 | .ropeproject 167 | 168 | # mkdocs documentation 169 | /site 170 | 171 | # mypy 172 | .mypy_cache/ 173 | .dmypy.json 174 | dmypy.json 175 | 176 | # Pyre type checker 177 | .pyre/ 178 | 179 | # pytype static type analyzer 180 | .pytype/ 181 | 182 | # Cython debug symbols 183 | cython_debug/ 184 | 185 | # PyCharm 186 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 187 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 188 | # and can be added to the global gitignore or merged into this file. For a more nuclear 189 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 190 | #.idea/ 191 | 192 | # End of https://www.toptal.com/developers/gitignore/api/python 193 | 194 | 195 | **/venv 196 | .vscode 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eyüp Can ELMA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram\* File Backend 2 | 3 | **Please use it at your own risk.** 4 | 5 | This proxy backend helps you serve a file sent to your Telegram bot on your website by using Telegram servers directly. Instead of your bot token in plain text, it uses an AES encrypted version of the token. So your bot token doesn't get exposed to the outside world while you'll still be able to serve a file using Telegram's servers as a file backend. 6 | 7 | As an example of encrypting a bot token in programming languages other than Go, I included a Python script to encrypt the given bot token. Since you will need an encrypted token to request files from the proxy server, you can use this to generate encrypted bot tokens. 8 | 9 | ## Set up 10 | 11 | 1. Encrypt your bot token 12 | 13 | ```bash 14 | python encryptor/encrypt.py 'YOUR_BOT_TOKEN' 15 | # 9444e7c7861ef9dcdec4174225f2f77e 16 | ``` 17 | 18 | 2. Run a redis instance 19 | 3. Run the proxy 20 | 4. Visit the proxy server. E.g `http://localhost:4627//` 21 | 22 | ## \* Disclosure and warning 23 | 24 | - **Please use it at your own risk.** 25 | - We are not affiliated with, funded, or in any way associated with Telegram Messenger™. 26 | -------------------------------------------------------------------------------- /encryptor/encrypt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import binascii 5 | from Crypto.Cipher import AES 6 | 7 | 8 | MODE = AES.MODE_CFB 9 | BLOCK_SIZE = 16 10 | SEGMENT_SIZE = 128 11 | 12 | logger = logging.getLogger(__name__) 13 | logging.basicConfig(level=logging.DEBUG, format='%(message)s') 14 | 15 | 16 | class Encryptor: 17 | @classmethod 18 | def _pad_string(cls, value): 19 | length = len(value) 20 | pad_size = BLOCK_SIZE - (length % BLOCK_SIZE) 21 | return (value.ljust(length + pad_size, '\x00')).encode('utf-8') 22 | 23 | @classmethod 24 | def encrypt(cls, key, iv, plaintext): 25 | key_bytes = bytes(key.encode('utf-8')) 26 | iv_bytes = bytes(iv.encode('utf-8')) 27 | aes = AES.new(key_bytes, MODE, iv_bytes, segment_size=SEGMENT_SIZE) 28 | plaintext = cls._pad_string(plaintext) 29 | encrypted_text = aes.encrypt(plaintext) 30 | return binascii.hexlify(encrypted_text) 31 | 32 | 33 | def main(): 34 | if len(sys.argv) != 2: 35 | raise ValueError("No plain text provided") 36 | 37 | plain_text = sys.argv[1] 38 | token = Encryptor.encrypt( 39 | os.getenv('PROXY_AES_KEY'), 40 | os.getenv('PROXY_AES_IV'), 41 | plain_text) 42 | encrypted = token.decode('utf-8') 43 | return encrypted 44 | 45 | 46 | if __name__ == '__main__': 47 | logger.info(main()) 48 | -------------------------------------------------------------------------------- /encryptor/requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.12.0 2 | -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod ./ 6 | COPY go.sum ./ 7 | RUN go mod download 8 | 9 | COPY ./ ./ 10 | 11 | RUN go build -o /proxy cmd/proxy/main.go 12 | 13 | EXPOSE 4627 14 | 15 | CMD [ "/proxy" ] 16 | -------------------------------------------------------------------------------- /proxy/cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/elmsec/telegram-file-backend/pkg/config" 6 | pkg_http "github.com/elmsec/telegram-file-backend/pkg/http" 7 | "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | ) 12 | 13 | func main() { 14 | appConfig := config.InitConfig() 15 | 16 | proxy := httputil.NewSingleHostReverseProxy(&url.URL{ 17 | Scheme: "https", 18 | Host: "api.telegram.org", 19 | }) 20 | 21 | director := proxy.Director 22 | proxy.Director = func(req *http.Request) { 23 | director(req) 24 | req.Host = req.URL.Host 25 | 26 | // parse url and decrypt the bot token and the file id 27 | botToken, fileId, err := pkg_http.ParseUrl(req.URL.RequestURI()[1:]) 28 | if err != nil { 29 | log.Println("Cannot parse URL: ", err) 30 | return 31 | } 32 | // get file 33 | telegramResponse, err := pkg_http.GetFile(botToken, fileId) 34 | if err != nil { 35 | log.Println("Cannot get file: ", err) 36 | return 37 | } 38 | 39 | // proxy to the telegram servers 40 | newPath := fmt.Sprintf("/file/bot%s/%s", botToken, telegramResponse.Result.FilePath) 41 | req.URL.Path = newPath 42 | } 43 | proxy.ModifyResponse = func(resp *http.Response) error { 44 | resp.Header.Del("Content-Disposition") 45 | return nil 46 | } 47 | 48 | url := fmt.Sprintf("%s:%d", appConfig.Host, appConfig.Port) 49 | log.Fatal(http.ListenAndServe(url, proxy)) 50 | } 51 | -------------------------------------------------------------------------------- /proxy/config.yaml: -------------------------------------------------------------------------------- 1 | host: localhost 2 | port: 4627 3 | 4 | redis: 5 | host: localhost 6 | port: 6379 7 | db: 1 8 | -------------------------------------------------------------------------------- /proxy/config.yaml.example: -------------------------------------------------------------------------------- 1 | host: localhost 2 | port: 4627 3 | 4 | redis: 5 | host: localhost 6 | port: 6379 7 | db: 1 8 | -------------------------------------------------------------------------------- /proxy/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elmsec/telegram-file-backend 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.4 7 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 8 | ) 9 | 10 | require ( 11 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 13 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 14 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /proxy/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 10 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 11 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 12 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 15 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 16 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 17 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 18 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 19 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 20 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 21 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 22 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 24 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 28 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 29 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 30 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 31 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 32 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 33 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 34 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 35 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 36 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 37 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 38 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 42 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 46 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 47 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 51 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 52 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 53 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 54 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 55 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 70 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 74 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 75 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 84 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 85 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 86 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 87 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 88 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 89 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 90 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 94 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 95 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 100 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 101 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 102 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /proxy/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type Config struct { 11 | Host string `yaml:"host"` 12 | Port int `yaml:"port"` 13 | Secret string `yaml:"secret"` 14 | IV string `yaml:"iv"` 15 | Redis struct { 16 | Host string `yaml:"host"` 17 | Port int `yaml:"port"` 18 | DB int `yaml:"db"` 19 | } `yaml:"redis"` 20 | } 21 | 22 | func InitConfig() *Config { 23 | var configs Config 24 | filename, _ := filepath.Abs("config.yaml") 25 | yamlFile, _ := os.ReadFile(filename) 26 | yaml.Unmarshal(yamlFile, &configs) 27 | return &configs 28 | } 29 | -------------------------------------------------------------------------------- /proxy/pkg/crypto/aes.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/hex" 7 | "strings" 8 | ) 9 | 10 | func DecryptPayload(key []byte, iv []byte, encryptedString string) (string, error) { 11 | cipherText, err := hex.DecodeString(encryptedString) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | // prepare decryption based on key and iv 17 | block, err := aes.NewCipher(key) 18 | if err != nil { 19 | return "", err 20 | } 21 | stream := cipher.NewCFBDecrypter(block, iv) 22 | 23 | // decrypt 24 | stream.XORKeyStream(cipherText, cipherText) 25 | 26 | // fix 27 | result := strings.ReplaceAll(string(cipherText), "\x00", "") 28 | return result, nil 29 | } 30 | -------------------------------------------------------------------------------- /proxy/pkg/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/elmsec/telegram-file-backend/pkg/config" 8 | 9 | "github.com/go-redis/redis/v8" 10 | ) 11 | 12 | var appConfig = config.InitConfig() 13 | 14 | func NewRedisClient() *redis.Client { 15 | redisConf := appConfig.Redis 16 | redisDB := redis.NewClient(&redis.Options{ 17 | Addr: fmt.Sprintf("%s:%d", redisConf.Host, redisConf.Port), 18 | DB: redisConf.DB, 19 | Password: os.Getenv("REDIS_PASSWORD"), 20 | }) 21 | 22 | return redisDB 23 | } 24 | -------------------------------------------------------------------------------- /proxy/pkg/entities/telegram.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type TelegramResponseResult struct { 4 | FileId string `json:"file_id"` 5 | FilePath string `json:"file_path"` 6 | } 7 | type TelegramResponse struct { 8 | Ok bool `json:"ok"` 9 | Result TelegramResponseResult `json:"result"` 10 | } 11 | -------------------------------------------------------------------------------- /proxy/pkg/http/telegram.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/elmsec/telegram-file-backend/pkg/database" 13 | "github.com/elmsec/telegram-file-backend/pkg/entities" 14 | ) 15 | 16 | var redisClient = database.NewRedisClient() 17 | var ctx = context.Background() 18 | var BASE_URL = "https://api.telegram.org/bot" 19 | 20 | func GetFile(botToken, fileId string) (*entities.TelegramResponse, error) { 21 | cachedFile, _ := GetCachedFile(fileId) 22 | if cachedFile != nil { 23 | return cachedFile, nil 24 | } 25 | 26 | return GetRemoteFile(botToken, fileId) 27 | } 28 | 29 | func GetCachedFile(fileId string) (*entities.TelegramResponse, error) { 30 | var fileObj entities.TelegramResponse 31 | fileStr, err := redisClient.Get(ctx, fileId).Result() 32 | 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | // parse cached file 38 | err = json.Unmarshal([]byte(fileStr), &fileObj) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &fileObj, nil 43 | } 44 | 45 | func GetRemoteFile(botToken, fileId string) (*entities.TelegramResponse, error) { 46 | var fileObj entities.TelegramResponse 47 | 48 | // get remote data 49 | url := fmt.Sprintf("%s%s/getFile?file_id=%s", BASE_URL, botToken, fileId) 50 | resp, err := http.Get(url) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // check status code 57 | if resp.StatusCode != http.StatusOK { 58 | return nil, errors.New("Cannot fetch data: " + resp.Status) 59 | } 60 | 61 | defer resp.Body.Close() 62 | err = json.NewDecoder(resp.Body).Decode(&fileObj) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | err = AddFileToCache(fileId, fileObj) 68 | if err != nil { 69 | // just log it 70 | log.Println(err) 71 | } 72 | 73 | return &fileObj, nil 74 | } 75 | 76 | func AddFileToCache(fileId string, fileObj entities.TelegramResponse) error { 77 | fileObjJson, err := json.Marshal(fileObj) 78 | if err != nil { 79 | return err 80 | } 81 | // Telegram guarantees at least one hour of validity 82 | cachePeriod := time.Hour 83 | err = redisClient.Set(ctx, fileId, fileObjJson, time.Duration(cachePeriod)).Err() 84 | return err 85 | } 86 | -------------------------------------------------------------------------------- /proxy/pkg/http/url.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/elmsec/telegram-file-backend/pkg/crypto" 8 | ) 9 | 10 | var key = []byte(os.Getenv("PROXY_AES_KEY")) 11 | var iv = []byte(os.Getenv("PROXY_AES_IV")) 12 | 13 | func ParseUrl(path string) (botToken string, fileId string, err error) { 14 | information := strings.SplitN(path, "/", 2) 15 | fileId = information[1] 16 | 17 | encryptedString := information[0] 18 | botToken, err = crypto.DecryptPayload(key, iv, encryptedString) 19 | if err != nil { 20 | return "", "", err 21 | } 22 | 23 | return botToken, fileId, nil 24 | } 25 | --------------------------------------------------------------------------------