| 1 | package bridge |
| 2 | |
| 3 | import ( |
| 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 | |
| 19 | type 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 | |
| 28 | type 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 | |
| 44 | type Config struct { |
| 45 | *Bridge |
| 46 | |
| 47 | Remote chan config.Message |
| 48 | } |
| 49 | |
| 50 | // Factory is the factory function to create a bridge |
| 51 | type Factory func(*Config) Bridger |
| 52 | |
| 53 | // New is a basic constructor. More important fields are populated |
| 54 | // in gateway/gateway.go (AddBridge method). |
| 55 | func 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 | |
| 74 | func (b *Bridge) JoinChannels() error { |
| 75 | return b.joinChannels(b.Channels, b.Joined) |
| 76 | } |
| 77 | |
| 78 | // SetChannelMembers sets the newMembers to the bridge ChannelMembers |
| 79 | func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) { |
| 80 | b.Lock() |
| 81 | b.ChannelMembers = newMembers |
| 82 | b.Unlock() |
| 83 | } |
| 84 | |
| 85 | func (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 | |
| 100 | func (b *Bridge) GetConfigKey(key string) string { |
| 101 | return b.Account + "." + key |
| 102 | } |
| 103 | |
| 104 | func (b *Bridge) IsKeySet(key string) bool { |
| 105 | return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key) |
| 106 | } |
| 107 | |
| 108 | func (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 | |
| 116 | func (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 | |
| 124 | func (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 | |
| 132 | func (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 | |
| 140 | func (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? |
| 158 | func (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 | |
| 180 | var errHttpGetNotOk = errors.New("HTTP server responded non-OK code") |
| 181 | |
| 182 | func 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). |
| 188 | func (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. |
| 233 | func (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 | |
| 268 | func (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 | |
| 272 | func (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 | |
| 276 | func (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 | |
| 280 | func (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. |
| 288 | func (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. |
| 296 | func (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 | |
| 314 | type errFileTooLarge struct { |
| 315 | FileName string |
| 316 | Size int |
| 317 | MaxSize int |
| 318 | } |
| 319 | |
| 320 | func (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 | |
| 324 | type errFileBlacklisted struct { |
| 325 | FileName string |
| 326 | } |
| 327 | |
| 328 | func (e *errFileBlacklisted) Error() string { |
| 329 | return fmt.Sprintf("File %#v matches the backlist, not downloading it", e.FileName) |
| 330 | } |
| 331 | |
| 332 | func (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 | |