package matterclient import ( "crypto/md5" //nolint:gosec "crypto/tls" "errors" "fmt" "net/http" "net/http/cookiejar" "net/url" "strings" "time" "github.com/gorilla/websocket" "github.com/jpillora/backoff" "github.com/mattermost/mattermost-server/v5/model" ) func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { var resp *model.Response var appErr *model.AppError var logmsg = "trying login" var err error for { m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server) if m.Credentials.Token != "" { resp, err = m.doLoginToken() if err != nil { return err } } else { m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass) } appErr = resp.Error if appErr != nil { d := b.Duration() m.logger.Debug(appErr.DetailedError) if firstConnection { if appErr.Message == "" { return errors.New(appErr.DetailedError) } return errors.New(appErr.Message) } m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d) time.Sleep(d) logmsg = "retrying login" continue } break } // reset timer b.Reset() return nil } func (m *MMClient) doLoginToken() (*model.Response, error) { var resp *model.Response var logmsg = "trying login" m.Client.AuthType = model.HEADER_BEARER m.Client.AuthToken = m.Credentials.Token if m.Credentials.CookieToken { m.logger.Debugf(logmsg + " with cookie (MMAUTH) token") m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token) } else { m.logger.Debugf(logmsg + " with personal token") } m.User, resp = m.Client.GetMe("") if resp.Error != nil { return resp, resp.Error } if m.User == nil { m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass) return resp, errors.New("invalid token") } return resp, nil } func (m *MMClient) handleLoginToken() error { switch { case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN): token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=") if len(token) != 2 { return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken") } m.Credentials.Token = token[1] m.Credentials.CookieToken = true case strings.Contains(m.Credentials.Pass, "token="): token := strings.Split(m.Credentials.Pass, "token=") if len(token) != 2 { return errors.New("incorrect personal token. valid input is token=yourtoken") } m.Credentials.Token = token[1] } return nil } func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error { uriScheme := "https://" if m.NoTLS { uriScheme = "http://" } // login to mattermost m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server) m.Client.HttpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec Proxy: http.ProxyFromEnvironment, } m.Client.HttpClient.Timeout = time.Second * 10 // handle MMAUTHTOKEN and personal token if err := m.handleLoginToken(); err != nil { return err } // check if server alive, retry until if err := m.serverAlive(firstConnection, b); err != nil { return err } return nil } // initialize user and teams func (m *MMClient) initUser() error { m.Lock() defer m.Unlock() // we only load all team data on initial login. // all other updates are for channels from our (primary) team only. //m.logger.Debug("initUser(): loading all team data") teams, resp := m.Client.GetTeamsForUser(m.User.Id, "") if resp.Error != nil { return resp.Error } for _, team := range teams { idx := 0 max := 200 usermap := make(map[string]*model.User) mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "") if resp.Error != nil { return errors.New(resp.Error.DetailedError) } for len(mmusers) > 0 { for _, user := range mmusers { usermap[user.Id] = user } mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "") if resp.Error != nil { return errors.New(resp.Error.DetailedError) } idx++ time.Sleep(time.Millisecond * 200) } m.logger.Infof("found %d users in team %s", len(usermap), team.Name) t := &Team{Team: team, Users: usermap, Id: team.Id} mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "") if resp.Error != nil { return resp.Error } t.Channels = mmchannels mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "") if resp.Error != nil { return resp.Error } t.MoreChannels = mmchannels m.OtherTeams = append(m.OtherTeams, t) if team.Name == m.Credentials.Team { m.Team = t m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id) } // add all users for k, v := range t.Users { m.Users[k] = v } } return nil } func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error { defer b.Reset() for { d := b.Duration() // bogus call to get the serverversion _, resp := m.Client.Logout() if resp.Error != nil { return fmt.Errorf("%#v", resp.Error.Error()) } if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) { return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) } if !m.SkipVersionCheck { m.ServerVersion = resp.ServerVersion if m.ServerVersion == "" { m.logger.Debugf("Server not up yet, reconnecting in %s", d) time.Sleep(d) } else { m.logger.Infof("Found version %s", m.ServerVersion) return nil } } else { return nil } } } func (m *MMClient) wsConnect() { b := &backoff.Backoff{ Min: time.Second, Max: 5 * time.Minute, Jitter: true, } m.WsConnected = false wsScheme := "wss://" if m.NoTLS { wsScheme = "ws://" } // setup websocket connection wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket" header := http.Header{} header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) m.logger.Debugf("WsClient: making connection: %s", wsurl) for { wsDialer := &websocket.Dialer{ TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec Proxy: http.ProxyFromEnvironment, } var err error m.WsClient, _, err = wsDialer.Dial(wsurl, header) if err != nil { d := b.Duration() m.logger.Debugf("WSS: %s, reconnecting in %s", err, d) time.Sleep(d) continue } break } m.logger.Debug("WsClient: connected") m.WsSequence = 1 m.WsPingChan = make(chan *model.WebSocketResponse) // only start to parse WS messages when login is completely done m.WsConnected = true } func (m *MMClient) createCookieJar(token string) *cookiejar.Jar { var cookies []*http.Cookie jar, _ := cookiejar.New(nil) firstCookie := &http.Cookie{ Name: "MMAUTHTOKEN", Value: token, Path: "/", Domain: m.Credentials.Server, } cookies = append(cookies, firstCookie) cookieURL, _ := url.Parse("https://" + m.Credentials.Server) jar.SetCookies(cookieURL, cookies) return jar } func (m *MMClient) checkAlive() error { // check if session still is valid _, resp := m.Client.GetMe("") if resp.Error != nil { return resp.Error } m.logger.Debug("WS PING") return m.sendWSRequest("ping", nil) } func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error { req := &model.WebSocketRequest{} req.Seq = m.WsSequence req.Action = action req.Data = data m.WsSequence++ m.logger.Debugf("sendWsRequest %#v", req) return m.WsClient.WriteJSON(req) } func supportedVersion(version string) bool { if strings.HasPrefix(version, "3.8.0") || strings.HasPrefix(version, "3.9.0") || strings.HasPrefix(version, "3.10.0") || strings.HasPrefix(version, "4.") || strings.HasPrefix(version, "5.") { return true } return false } func digestString(s string) string { return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec }