Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

Viewing file on branch master

1package bridge
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "net/url"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/matterbridge-org/matterbridge/bridge/config"
16 "github.com/sirupsen/logrus"
17)
18
19type Bridger interface {
20 Send(msg config.Message) (string, error)
21 Connect() error
22 JoinChannel(channel config.ChannelInfo) error
23 Disconnect() error
24 NewHttpRequest(method, uri string, body io.Reader) (*http.Request, error)
25 NewHttpClient(proxy string) (*http.Client, error)
26}
27
28type Bridge struct {
29 Bridger
30 *sync.RWMutex
31
32 Name string
33 Account string
34 Protocol string
35 Channels map[string]config.ChannelInfo
36 Joined map[string]bool
37 ChannelMembers *config.ChannelMembers
38 Log *logrus.Entry
39 Config config.Config
40 General *config.Protocol
41 HttpClient *http.Client // Unique HTTP settings per bridge
42}
43
44type Config struct {
45 *Bridge
46
47 Remote chan config.Message
48}
49
50// Factory is the factory function to create a bridge
51type Factory func(*Config) Bridger
52
53// New is a basic constructor. More important fields are populated
54// in gateway/gateway.go (AddBridge method).
55func New(bridge *config.Bridge) *Bridge {
56 accInfo := strings.Split(bridge.Account, ".")
57 if len(accInfo) != 2 {
58 log.Fatalf("config failure, account incorrect: %s", bridge.Account)
59 }
60
61 protocol := accInfo[0]
62 name := accInfo[1]
63
64 return &Bridge{
65 RWMutex: new(sync.RWMutex),
66 Channels: make(map[string]config.ChannelInfo),
67 Name: name,
68 Protocol: protocol,
69 Account: bridge.Account,
70 Joined: make(map[string]bool),
71 }
72}
73
74func (b *Bridge) JoinChannels() error {
75 return b.joinChannels(b.Channels, b.Joined)
76}
77
78// SetChannelMembers sets the newMembers to the bridge ChannelMembers
79func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) {
80 b.Lock()
81 b.ChannelMembers = newMembers
82 b.Unlock()
83}
84
85func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
86 for ID, channel := range channels {
87 if !exists[ID] {
88 b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
89 time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond)
90 err := b.JoinChannel(channel)
91 if err != nil {
92 return err
93 }
94 exists[ID] = true
95 }
96 }
97 return nil
98}
99
100func (b *Bridge) GetConfigKey(key string) string {
101 return b.Account + "." + key
102}
103
104func (b *Bridge) IsKeySet(key string) bool {
105 return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key)
106}
107
108func (b *Bridge) GetBool(key string) bool {
109 val, ok := b.Config.GetBool(b.GetConfigKey(key))
110 if !ok {
111 val, _ = b.Config.GetBool("general." + key)
112 }
113 return val
114}
115
116func (b *Bridge) GetInt(key string) int {
117 val, ok := b.Config.GetInt(b.GetConfigKey(key))
118 if !ok {
119 val, _ = b.Config.GetInt("general." + key)
120 }
121 return val
122}
123
124func (b *Bridge) GetString(key string) string {
125 val, ok := b.Config.GetString(b.GetConfigKey(key))
126 if !ok {
127 val, _ = b.Config.GetString("general." + key)
128 }
129 return val
130}
131
132func (b *Bridge) GetStringSlice(key string) []string {
133 val, ok := b.Config.GetStringSlice(b.GetConfigKey(key))
134 if !ok {
135 val, _ = b.Config.GetStringSlice("general." + key)
136 }
137 return val
138}
139
140func (b *Bridge) GetStringSlice2D(key string) [][]string {
141 val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key))
142 if !ok {
143 val, _ = b.Config.GetStringSlice2D("general." + key)
144 }
145 return val
146}
147
148// NewHttpClient produces a single unified http.Client per bridge.
149//
150// This allows to have project-wide defaults (timeout) as well as
151// bridge-configurable values (`http_proxy`).
152//
153// This method is left public so that if that's needed, a bridge can
154// override this constructor.
155//
156// TODO: maybe protocols without HTTP downloads at all could override
157// this method and return nil? Or the other way around?
158func (b *Bridge) NewHttpClient(http_proxy string) (*http.Client, error) {
159 if http_proxy != "" {
160 proxyUrl, err := url.Parse(b.GetString("http_proxy"))
161 if err != nil {
162 return nil, err
163 }
164
165 b.Log.Debugf("%s using HTTP proxy %s", b.Protocol, proxyUrl)
166
167 return &http.Client{
168 Timeout: time.Second * 15,
169 Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)},
170 }, nil
171 }
172
173 b.Log.Debugf("%s not using HTTP proxy", b.Protocol)
174
175 return &http.Client{
176 Timeout: time.Second * 5,
177 }, nil
178}
179
180var errHttpGetNotOk = errors.New("HTTP server responded non-OK code")
181
182func HttpGetNotOkError(uri string, code int) error {
183 return fmt.Errorf("%w: %s returned code %d", errHttpGetNotOk, uri, code)
184}
185
186// HttpGetBytes returns bytes from a given URI, if the request
187// succeeds and HTTP response status is 200 (OK).
188func (b *Bridge) HttpGetBytes(uri string) (*[]byte, error) {
189 req, err := b.Bridger.NewHttpRequest("GET", uri, nil)
190 if err != nil {
191 return nil, err
192 }
193
194 b.Log.Debugf("Getting HTTP bytes with request: %#v", req)
195
196 resp, err := b.HttpClient.Do(req)
197 if err != nil {
198 return nil, err
199 }
200
201 if resp.StatusCode != http.StatusOK {
202 return nil, HttpGetNotOkError(uri, resp.StatusCode)
203 }
204
205 var buf bytes.Buffer
206
207 _, err = io.Copy(&buf, resp.Body)
208 if err != nil {
209 return nil, err
210 }
211
212 err = resp.Body.Close()
213 if err != nil {
214 return nil, err
215 }
216
217 data := buf.Bytes()
218
219 return &data, nil
220}
221
222// HttpUpload uploads data to a URI, and validates the response status code.
223//
224// Params:
225//
226// - method: specific HTTP verb
227// - uri: remote URL
228// - headers: map of headers to insert in the request
229// - data: raw bytes to upload in the body
230// - ok_status: list of HTTP status codes considered successful (default: 200, 201)
231//
232// The response body is always discarded.
233func (b *Bridge) HttpUpload(method string, uri string, headers map[string]string, data *[]byte, ok_status []int) error {
234 req, err := b.Bridger.NewHttpRequest(method, uri, bytes.NewReader(*data))
235 if err != nil {
236 return err
237 }
238
239 for header_name, header_value := range headers {
240 req.Header.Set(header_name, header_value)
241 }
242
243 b.Log.Debugf("HTTP upload request: %v", req)
244
245 resp, err := b.HttpClient.Do(req)
246 if err != nil {
247 return err
248 }
249
250 err = resp.Body.Close()
251 if err != nil {
252 return err
253 }
254
255 // By default, allow codes 200 (OK) and 201 (CREATED)
256 if len(ok_status) == 0 {
257 ok_status = []int{200, 201}
258 }
259
260 for _, expected_code := range ok_status {
261 b.Log.Debugf("Successful file upload with code %d", expected_code)
262 return nil
263 }
264
265 return HttpGetNotOkError(uri, resp.StatusCode)
266}
267
268func (b *Bridge) AddAttachmentFromURL(msg *config.Message, filename string, id string, comment string, uri string) error {
269 return b.addAttachment(msg, filename, id, comment, uri, nil, false)
270}
271
272func (b *Bridge) AddAttachmentFromBytes(msg *config.Message, filename string, id string, comment string, data *[]byte) error {
273 return b.addAttachment(msg, filename, id, comment, "", data, false)
274}
275
276func (b *Bridge) AddAvatarFromURL(msg *config.Message, filename string, id string, comment string, uri string) error {
277 return b.addAttachment(msg, filename, id, comment, uri, nil, true)
278}
279
280func (b *Bridge) AddAvatarFromBytes(msg *config.Message, filename string, id string, comment string, data *[]byte) error {
281 return b.addAttachment(msg, filename, id, comment, "", data, true)
282}
283
284// NewHttpRequest produces a new http.Request instance with bridge-specific settings.
285//
286// This is used by bridges where HTTP downloads require a cookie/token, by overriding
287// this method in the bridge struct.
288func (b *Bridge) NewHttpRequest(method, uri string, body io.Reader) (*http.Request, error) {
289 return http.NewRequest(method, uri, body)
290}
291
292// Internal method including common parts to attachment/avatar handling methods.
293//
294// This method will process received bytes. If bytes are not set, they will be downloaded from the given URL.
295// If neither data bytes nor uri is provided, this will be a hard error because there's a logic error somewhere.
296func (b *Bridge) addAttachment(msg *config.Message, filename string, id string, comment string, uri string, data *[]byte, avatar bool) error {
297 if data != nil {
298 return b.addAttachmentProcess(msg, filename, id, comment, uri, data, avatar)
299 }
300
301 if uri == "" {
302 // This should never happen
303 b.Log.Fatalf("Logic error in bridge %s: attachment should have either URL or data set, neither was provided", b.Protocol)
304 }
305
306 data, err := b.HttpGetBytes(uri)
307 if err != nil {
308 return err
309 }
310
311 return b.addAttachmentProcess(msg, filename, id, comment, uri, data, avatar)
312}
313
314type errFileTooLarge struct {
315 FileName string
316 Size int
317 MaxSize int
318}
319
320func (e *errFileTooLarge) Error() string {
321 return fmt.Sprintf("File %#v to large to download (%#v). MediaDownloadSize is %#v", e.FileName, e.Size, e.MaxSize)
322}
323
324type errFileBlacklisted struct {
325 FileName string
326}
327
328func (e *errFileBlacklisted) Error() string {
329 return fmt.Sprintf("File %#v matches the backlist, not downloading it", e.FileName)
330}
331
332func (b *Bridge) addAttachmentProcess(msg *config.Message, filename string, id string, comment string, uri string, data *[]byte, avatar bool) error {
333 size := len(*data)
334 if size > b.General.MediaDownloadSize {
335 return &errFileTooLarge{
336 FileName: filename,
337 Size: size,
338 MaxSize: b.General.MediaDownloadSize,
339 }
340 }
341
342 // Apply `MediaDownloadBlackList` regexes
343 if b.Config.IsFilenameBlacklisted(filename) {
344 return &errFileBlacklisted{
345 FileName: filename,
346 }
347 }
348
349 b.Log.Debugf("Download OK %#v %#v", filename, size)
350 msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
351 Name: filename,
352 Data: data,
353 URL: uri,
354 Comment: comment,
355 Avatar: avatar,
356 // TODO: if id is not set, maybe use hash of bytes?
357 NativeID: id,
358 })
359
360 return nil
361}
362