-
Notifications
You must be signed in to change notification settings - Fork 17
/
login.go
270 lines (250 loc) · 8.21 KB
/
login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package cbyge
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"time"
"github.com/pkg/errors"
)
// DefaultCorpID is the corporation ID used by the C by GE app.
const DefaultCorpID = "1007d2ad150c4000"
const (
authURL = "https://api.gelighting.com/v2/user_auth"
verifyCodeURL = "https://api.gelighting.com/v2/two_factor/email/verifycode"
twoFactorURL = "https://api.gelighting.com/v2/user_auth/two_factor"
userInfoURL = "https://api.gelighting.com/v2/user/%d"
devicesURL = "https://api.gelighting.com/v2/user/%d/subscribe/devices"
devicePropertyURL = "https://api.gelighting.com/v2/product/%s/device/%d/property"
)
type OptionalDate struct {
Date *time.Time
}
func (o *OptionalDate) UnmarshalJSON(d []byte) error {
if bytes.Equal(d, []byte("null")) {
o.Date = nil
return nil
}
var dateStr string
if err := json.Unmarshal(d, &dateStr); err != nil {
return err
}
if dateStr == "" {
o.Date = nil
return nil
}
return json.Unmarshal(d, &o.Date)
}
type SessionInfo struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
UserID uint32 `json:"user_id"`
ExpireIn int `json:"expire_in"`
Authorize string `json:"authorize"`
}
type UserInfo struct {
Gender int `json:"gender"`
ActiveDate OptionalDate `json:"active_date"`
Source int `json:"source"`
PasswordInited bool `json:"passwd_inited"`
IsValid bool `json:"is_valid"`
Nickname string `json:"nickname"`
ID uint32 `json:"id"`
CreateDate OptionalDate `json:"create_date"`
Email string `json:"email"`
RegionID int `json:"region_id"`
AuthorizeCode string `json:"authorize_code"`
CertificateNo string `json:"certificate_no"`
CertificateType int `json:"certificate_type"`
CorpID string `json:"corp_id"`
PrivacyCode string `json:"privacy_code"`
Account string `json:"account"`
Age int `json:"age"`
Status int `json:"status"`
}
type DeviceInfo struct {
AccessKey int64 `json:"access_key"`
ActiveCode string `json:"active_code"`
ActiveDate OptionalDate `json:"active_date"`
AuthorizeCode string `json:"authorize_code"`
FirmwareVersion int `json:"firmware_version"`
Groups string `json:"groups"`
ID uint32 `json:"id"`
IsActive bool `json:"is_active"`
IsOnline bool `json:"is_online"`
LastLogin OptionalDate `json:"last_login"`
MAC string `json:"mac"`
MCUVersion int `json:"mcu_version"`
Name string `json:"name"`
ProductID string `json:"product_id"`
Role int `json:"role"`
Source int `json:"source"`
SubscribeDate string `json:"subscribe_date"`
}
type DeviceProperties struct {
Bulbs []struct {
DeviceID int64 `json:"deviceID"`
DisplayName string `json:"displayName"`
SwitchID uint64 `json:"switchID"`
} `json:"bulbsArray"`
}
// Login authenticates with the server to create a new session.
// This only works for older accounts, not newer "Cync" accounts.
//
// The resulting error can be checked with IsCredentialsError() to see if it
// resulted from a bad login.
//
// If corpID is "", then DefaultCorpID is used.
func Login(email, password, corpID string) (*SessionInfo, error) {
if corpID == "" {
corpID = DefaultCorpID
}
jsonObj := map[string]string{"email": email, "password": password, "corp_id": corpID}
return doLoginRequest(authURL, jsonObj)
}
// Login2FA authenticates using two-factor authentication, which is required
// for newer "Cync" accounts.
//
// This method returns a callback which should be called with the emailed
// verification code.
func Login2FA(email, password, corpID string) (func(code string) (*SessionInfo, error), error) {
if err := Login2FAStage1(email, corpID); err != nil {
return nil, err
}
return func(code string) (*SessionInfo, error) {
return Login2FAStage2(email, password, corpID, code)
}, nil
}
// Login2FAStage1 sends a two-factor authentication email
// to the user. Complete the login using Login2FAStage2.
func Login2FAStage1(email, corpID string) error {
if corpID == "" {
corpID = DefaultCorpID
}
jsonObj := map[string]string{
"email": email,
"local_lang": "en-us",
"corp_id": corpID,
}
data, _ := json.Marshal(jsonObj)
resp, err := http.Post(verifyCodeURL, "application/json", bytes.NewReader(data))
if err != nil {
return errors.Wrap(err, "login")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("login: got return code %d", resp.StatusCode)
}
return nil
}
// Login2FAStage2 completes the two-factor authentication
// process, creating a session if the code and password is
// correct.
func Login2FAStage2(email, password, corpID, code string) (*SessionInfo, error) {
if corpID == "" {
corpID = DefaultCorpID
}
jsonObj := map[string]string{
"email": email,
"password": password,
"two_factor": code,
"corp_id": corpID,
"resource": randomLoginResource(),
}
return doLoginRequest(twoFactorURL, jsonObj)
}
func doLoginRequest(url string, obj interface{}) (*SessionInfo, error) {
data, _ := json.Marshal(obj)
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "login")
}
defer resp.Body.Close()
data, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "login")
}
if err := decodeRemoteError(data, "login"); err != nil {
return nil, err
}
var response SessionInfo
if err := json.Unmarshal(data, &response); err != nil {
return nil, errors.Wrap(err, "login")
}
return &response, nil
}
func randomLoginResource() string {
res := ""
for i := 0; i < 16; i++ {
res += string('a' + rune(rand.Intn(26)))
}
return res
}
// GetUserInfo gets UserInfo using information from Login.
func GetUserInfo(userID uint32, accessToken string) (*UserInfo, error) {
urlStr := fmt.Sprintf(userInfoURL, userID)
var response UserInfo
if err := makeAPICall(urlStr, accessToken, &response, "get user info"); err != nil {
return nil, err
}
return &response, nil
}
// GetDevices gets the devices using information from Login.
func GetDevices(userID uint32, accessToken string) ([]*DeviceInfo, error) {
urlStr := fmt.Sprintf(devicesURL, userID)
var response []*DeviceInfo
if err := makeAPICall(urlStr, accessToken, &response, "get devices"); err != nil {
return nil, err
}
return response, nil
}
// GetDeviceProperties gets extended device information.
//
// The resulting error can be checked with IsPropertyNotExistsError(), to
// check if the device has no properties.
func GetDeviceProperties(accessToken, productID string, deviceID uint32) (*DeviceProperties, error) {
urlStr := fmt.Sprintf(devicePropertyURL, productID, deviceID)
var response DeviceProperties
if err := makeAPICall(urlStr, accessToken, &response, "get device properties"); err != nil {
// Ignore JSON errors, since JSON parsing fails for some
// devices: https://github.com/unixpickle/cbyge/issues/4.
var err1 *json.SyntaxError
var err2 *json.UnmarshalTypeError
if errors.As(err, &err1) || errors.As(err, &err2) {
return nil, &RemoteError{
Code: RemoteErrorCodePropertyNotExists,
Msg: "failed to parse JSON from response: " + err.Error(),
Context: "get device properties",
}
}
return nil, err
}
return &response, nil
}
func makeAPICall(urlStr, accessToken string, response interface{}, ctx string) error {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return errors.Wrap(err, ctx)
}
req.Header.Add("Access-Token", accessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, ctx)
}
if err := decodeRemoteError(data, ctx); err != nil {
// Context is baked into this error, and we don't want to
// wrap it so the error type is always *RemoteError.
return err
}
if err := json.Unmarshal(data, response); err != nil {
return errors.Wrap(err, ctx)
}
return nil
}