-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
153 lines (130 loc) · 3.57 KB
/
client.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
package api
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/http/cookiejar"
"time"
"github.com/fabiankachlock/tapo-api/pkg/klap"
)
// ApiClient is the main struct to interact with the raw Tapo API.
type ApiClient struct {
Ip net.IP
Email string
Password string
HandshakeTS time.Time
url string
client *http.Client
cipher *klap.KLAPCipher
cookieJar *cookiejar.Jar
}
// NewClient creates a new ApiClient.
func NewClient(ip, email, password string) (*ApiClient, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
client := &ApiClient{
Ip: net.ParseIP(ip),
Email: email,
Password: password,
url: fmt.Sprintf("http://%s/app", ip),
cookieJar: jar,
client: &http.Client{
Jar: jar,
},
}
return client, nil
}
// Login logs in to the Tapo API.
func (d *ApiClient) Login() error {
hashedUsername := sha1.Sum([]byte(d.Email))
hashedPassword := sha1.Sum([]byte(d.Password))
authHash := sha256.Sum256(append(hashedUsername[:], hashedPassword[:]...))
localSeed := make([]byte, 16)
rand.Read(localSeed)
remoteSeed, err := d.handshake1(d.url, localSeed, authHash[:])
if err != nil {
return err
}
err = d.handshake2(d.url, localSeed, remoteSeed, authHash[:])
if err != nil {
return err
}
d.cipher = klap.NewCipher(localSeed, remoteSeed, authHash[:])
return nil
}
// RefreshSession refreshes the authentication session of the client.
func (d *ApiClient) RefreshSession() error {
// clear cookies
jar, err := cookiejar.New(nil)
if err != nil {
return err
}
d.client = &http.Client{
Jar: jar,
}
return d.Login()
}
// Request sends a request to the Tapo API.
func (d *ApiClient) Request(method string, params interface{}) ([]byte, error) {
request := map[string]interface{}{
"method": method,
"params": params,
"requestTimeMilis": time.Now().UnixMilli(),
"terminalUUID": "00-00-00-00-00-00",
}
requestData, err := json.Marshal(request)
if err != nil {
return []byte{}, err
}
payload, seq, err := d.cipher.Encrypt(requestData)
if err != nil {
return []byte{}, err
}
resp, err := d.client.Post(fmt.Sprintf("%s/request?seq=%d", d.url, seq), "application/x-www-form-urlencoded", bytes.NewReader(payload))
if err != nil {
return []byte{}, err
}
buf := make([]byte, resp.ContentLength)
resp.Body.Read(buf)
decrypted, err := d.cipher.Decrypt(seq, buf)
if err != nil {
return []byte{}, err
}
return decrypted, nil
}
func (d *ApiClient) handshake1(url string, localSeed []byte, authHash []byte) ([]byte, error) {
resp, err := d.client.Post(fmt.Sprintf("%s/handshake1", url), "application/x-www-form-urlencoded", bytes.NewReader(localSeed))
if err != nil {
return []byte{}, err
}
buf := make([]byte, resp.ContentLength)
resp.Body.Read(buf)
remoteSeed := buf[0:16]
serverHash := buf[16:]
localHash := sha256.Sum256(append(append(localSeed, remoteSeed...), authHash...))
if string(localHash[:]) != string(serverHash) {
return []byte{}, errors.New("hashes dont match")
}
return remoteSeed, nil
}
func (d *ApiClient) handshake2(url string, localSeed, remoteSeed, authHash []byte) error {
payload := sha256.Sum256(append(append(remoteSeed, localSeed...), authHash...))
resp, err := d.client.Post(fmt.Sprintf("%s/handshake2", url), "application/x-www-form-urlencoded", bytes.NewReader(payload[:]))
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Println(resp.Status)
return errors.New("handshake 2 failed")
}
return nil
}