forked from DeanThompson/zhihu-go
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsession.go
293 lines (248 loc) · 7.62 KB
/
session.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package zhihu
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/juju/persistent-cookiejar"
)
// Auth 是用于登录的信息,保存了用户名和密码
type Auth struct {
Account string `json:"account"`
Password string `json:"password"`
loginType string // phone_num 或 email
loginURL string // 通过 Account 判断
}
// isEmail 判断是否通过邮箱登录
func (auth *Auth) isEmail() bool {
return isEmail(auth.Account)
}
// isPhone 判断是否通过手机号登录
func (auth *Auth) isPhone() bool {
return regexp.MustCompile(`^1[0-9]{10}$`).MatchString(auth.Account)
}
func (auth *Auth) toForm() url.Values {
if auth.isEmail() {
auth.loginType = "email"
auth.loginURL = makeZhihuLink("/login/email")
} else if auth.isPhone() {
auth.loginType = "phone_num"
auth.loginURL = makeZhihuLink("/login/phone_num")
} else {
panic("无法判断登录类型: " + auth.Account)
}
values := url.Values{}
logger.Info("登录类型:%s, 登录地址:%s", auth.loginType, auth.loginURL)
values.Set(auth.loginType, auth.Account)
values.Set("password", auth.Password)
values.Set("remember_me", "true") // import!
return values
}
// Session 保持和知乎服务器的会话,用于向服务器发起请求获取 HTML 或 JSON 数据
type Session struct {
auth *Auth
client *http.Client
}
type loginResult struct {
R int `json:"r"`
Msg string `json:"msg"`
ErrorCode int `json:"errcode"`
Data interface{} `json:"data"`
}
// NewSession 创建并返回一个 *Session 对象,
// 这里没有初始化登录账号信息,账号信息用 `LoadConfig` 通过配置文件进行设置
func NewSession() *Session {
s := new(Session)
cookieJar, _ := cookiejar.New(nil)
s.client = &http.Client{
Jar: cookieJar,
}
return s
}
// LoadConfig 从配置文件中读取账号信息
// 配置文件 是 JSON 格式:
// {
// "account": "[email protected]",
// "password": "p@ssw0rd"
// }
func (s *Session) LoadConfig(cfg string) {
fd, err := os.Open(cfg)
if err != nil {
panic("无法打开配置文件 config.json: " + err.Error())
}
defer fd.Close()
auth := new(Auth)
err = json.NewDecoder(fd).Decode(&auth)
if err != nil {
panic("解析配置文件出错: " + err.Error())
}
s.auth = auth
// TODO 如果设置了与上一次不一样的账号,最好把 cookies 重置
}
// Login 登录并保存 cookies
func (s *Session) Login() error {
if s.authenticated() {
logger.Success("已经是登录状态,不需要重复登录")
return nil
}
form := s.buildLoginForm().Encode()
body := strings.NewReader(form)
req, err := http.NewRequest("POST", s.auth.loginURL, body)
if err != nil {
logger.Error("构造登录请求失败:%s", err.Error())
return err
}
headers := newHTTPHeaders(true)
headers.Set("Content-Length", strconv.Itoa(len(form)))
headers.Set("Content-Type", "application/x-www-form-urlencoded")
headers.Set("Referer", baseZhihuURL)
req.Header = headers
logger.Info("登录中,用户名:%s", s.auth.Account)
resp, err := s.client.Do(req)
if err != nil {
logger.Error("登录失败:%s", err.Error())
return err
}
if strings.ToLower(resp.Header.Get("Content-Type")) != "application/json" {
logger.Error("服务器没有返回 json 数据")
return fmt.Errorf("未知的 Content-Type: %s", resp.Header.Get("Content-Type"))
}
defer resp.Body.Close()
result := loginResult{}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Error("读取响应内容失败:%s", err.Error())
}
logger.Info("登录响应内容:%s", strings.Replace(string(content), "\n", "", -1))
err = json.Unmarshal(content, &result)
if err != nil {
logger.Error("JSON 解析失败:%s", err.Error())
return err
}
if result.R == 0 {
logger.Success("登录成功!")
s.client.Jar.(*cookiejar.Jar).Save()
return nil
}
if result.R == 1 {
logger.Warn("登录失败!原因:%s", result.Msg)
return fmt.Errorf("登录失败!原因:%s", result.Msg)
}
logger.Error("登录出现未知错误:%s", string(content))
return fmt.Errorf("登录失败,未知错误:%s", string(content))
}
// Get 发起一个 GET 请求,自动处理 cookies
func (s *Session) Get(url string) (*http.Response, error) {
logger.Info("GET %s", url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
logger.Error("NewRequest failed with URL: %s", url)
return nil, err
}
req.Header = newHTTPHeaders(false)
return s.client.Do(req)
}
// Post 发起一个 POST 请求,自动处理 cookies
func (s *Session) Post(url string, bodyType string, body io.Reader) (*http.Response, error) {
logger.Info("POST %s, %s", url, bodyType)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
headers := newHTTPHeaders(false)
headers.Set("Content-Type", bodyType)
req.Header = headers
return s.client.Do(req)
}
// Ajax 发起一个 Ajax 请求,自动处理 cookies
func (s *Session) Ajax(url string, body io.Reader, referer string) (*http.Response, error) {
logger.Info("AJAX %s, referrer %s", url, referer)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
headers := newHTTPHeaders(true)
headers.Set("Content-Type", "application/x-www-form-urlencoded")
headers.Set("Referer", referer)
req.Header = headers
return s.client.Do(req)
}
// authenticated 检查是否已经登录(cookies 没有失效)
func (s *Session) authenticated() bool {
originURL := makeZhihuLink("/settings/profile")
resp, err := s.Get(originURL)
if err != nil {
logger.Error("访问 profile 页面出错: %s", err.Error())
return false
}
// 如果没有登录,会跳转到 http://www.zhihu.com/?next=%2Fsettings%2Fprofile
lastURL := resp.Request.URL.String()
logger.Info("获取 profile 的请求,跳转到了:%s", lastURL)
return lastURL == originURL
}
func (s *Session) buildLoginForm() url.Values {
values := s.auth.toForm()
values.Set("_xsrf", s.searchXSRF())
values.Set("captcha", s.downloadCaptcha())
return values
}
// 从 cookies 获取 _xsrf 用于 POST 请求
func (s *Session) searchXSRF() string {
resp, err := s.Get(baseZhihuURL)
if err != nil {
panic("获取 _xsrf 失败:" + err.Error())
}
// retrieve from cookies
for _, cookie := range resp.Cookies() {
if cookie.Name == "_xsrf" {
return cookie.Value
}
}
return ""
}
// downloadCaptcha 获取验证码,用于登录
func (s *Session) downloadCaptcha() string {
url := makeZhihuLink(fmt.Sprintf("/captcha.gif?r=%d&type=login", 1000*time.Now().Unix()))
logger.Info("获取验证码:%s", url)
resp, err := s.Get(url)
if err != nil {
panic("获取验证码失败:" + err.Error())
}
if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("获取验证码失败,StatusCode = %d", resp.StatusCode))
}
defer resp.Body.Close()
fileExt := strings.Split(resp.Header.Get("Content-Type"), "/")[1]
verifyImg := filepath.Join(getCwd(), "verify."+fileExt)
fd, err := os.OpenFile(verifyImg, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic("打开验证码文件失败:" + err.Error())
}
defer fd.Close()
io.Copy(fd, resp.Body) // 保存验证码文件
openCaptchaFile(verifyImg) // 调用外部程序打开
captcha := readCaptchaInput() // 读取用户输入
return captcha
}
var (
gSession = NewSession() // 全局的 Session,调用 Init() 初始化
)
// Init 用于传入配置文件,配置全局的 Session
func Init(cfgFile string) {
// 配置账号信息
gSession.LoadConfig(cfgFile)
// 登录
gSession.Login()
}
// SetSession 用于替换默认的 session
func SetSession(s *Session) {
gSession = s
}