Move API login flow to its own file; add support for secondary verification challenges
This commit is contained in:
parent
12d10d4355
commit
73ffb90f63
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -219,7 +220,18 @@ func main() {
|
|||||||
func login(username string, password string) {
|
func login(username string, password string) {
|
||||||
// Skip the scraper.the_api variable, just use a local one since no scraping is happening
|
// Skip the scraper.the_api variable, just use a local one since no scraping is happening
|
||||||
api := scraper.NewGuestSession()
|
api := scraper.NewGuestSession()
|
||||||
api.LogIn(username, password)
|
challenge := api.LogIn(username, password)
|
||||||
|
if challenge != nil {
|
||||||
|
fmt.Printf("Secondary challenge issued:\n")
|
||||||
|
fmt.Printf(" >>> %s\n", challenge.PrimaryText)
|
||||||
|
fmt.Printf(" >>> %s\n", challenge.SecondaryText)
|
||||||
|
fmt.Printf("Response: ")
|
||||||
|
phone_number, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
api.LoginVerifyPhone(*challenge, phone_number)
|
||||||
|
}
|
||||||
|
|
||||||
profile.SaveSession(api)
|
profile.SaveSession(api)
|
||||||
happy_exit("Logged in as " + string(api.UserHandle))
|
happy_exit("Logged in as " + string(api.UserHandle))
|
||||||
|
168
pkg/scraper/api_login.go
Normal file
168
pkg/scraper/api_login.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package scraper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LOGIN_URL = "https://twitter.com/i/api/1.1/onboarding/task.json"
|
||||||
|
|
||||||
|
type flow_result struct {
|
||||||
|
FlowToken string `json:"flow_token"`
|
||||||
|
Subtasks []struct {
|
||||||
|
SubtaskID string `json:"subtask_id"`
|
||||||
|
|
||||||
|
// Login success
|
||||||
|
OpenAccount struct {
|
||||||
|
User struct {
|
||||||
|
ID int `json:"id_str,string"`
|
||||||
|
Name string
|
||||||
|
ScreenName string `json:"screen_name"`
|
||||||
|
}
|
||||||
|
} `json:"open_account"`
|
||||||
|
|
||||||
|
// Phone verification challenge
|
||||||
|
EnterText struct {
|
||||||
|
PrimaryText struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"primary_text"`
|
||||||
|
SecondaryText struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"secondary_text"`
|
||||||
|
} `json:"enter_text"`
|
||||||
|
} `json:"subtasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChallengeResult struct {
|
||||||
|
FlowToken string
|
||||||
|
SubtaskID string
|
||||||
|
Data map[string]string
|
||||||
|
PrimaryText string
|
||||||
|
SecondaryText string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) do_login_task(flow_token string, task_id string, data map[string]string) (result flow_result) {
|
||||||
|
var body string
|
||||||
|
switch task_id {
|
||||||
|
// Regular login flow
|
||||||
|
case "LoginJsInstrumentationSubtask":
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s", "subtask_inputs": [{"subtask_id": "LoginJsInstrumentationSubtask", "js_instrumentation": {"response": "{\"rf\":{\"a560cdc18ff70ce7662311eac0f2441dd3d3ed27c354f082f587e7a30d1a7d5f\":72,\"a8e890b5fec154e7af62d8f529fbec5942dfdd7ad41597245b71a3fbdc9a180d\":176,\"a3c24597ad4c862773c74b9194e675e96a7607708f6bbd0babcfdf8b109ed86d\":-161,\"af9847e2cd4e9a0ca23853da4b46bf00a2e801f98dc819ee0dd6ecc1032273fa\":-8},\"s\":\"hOai7h2KQi4RBGKSYLUhH0Y0fBm5KHIJgxD5AmNKtwP7N8gpVuAqP8o9n2FpCnNeR1d6XbB0QWkGAHiXkKao5PhaeXEZgPJU1neLcVgTnGuFzpjDnGutCUgYaxNiwUPfDX0eQkgr_q7GWmbB7yyYPt32dqSd5yt-KCpSt7MOG4aFmGf11xWE4MTpXfkefbnX4CwZeEFKQQYzJptOvmUWa7qI0A69BSOs7HZ_4Wry2TwB9k03Q_S-MDZAZ3yB_L7WoosVVb1e84YWgaLWWzqhz4C77jDy6isT8EKSWKWnVctsIcaqM_wMV8AiYa5lr0_WkN5TwK9h0vDOTS1obOZuhAAAAYTZan_3\"}", "link": "next_link"}}]}`, //nolint:lll // json body
|
||||||
|
flow_token)
|
||||||
|
case "LoginEnterUserIdentifierSSO":
|
||||||
|
username_json, is_ok := data["username_json"]
|
||||||
|
if !is_ok {
|
||||||
|
panic("No username provided")
|
||||||
|
}
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginEnterUserIdentifierSSO","settings_list":{"setting_responses":[{"key":"user_identifier","response_data":{"text_data":{"result":`+username_json+`}}}],"link":"next_link"}}]}`, //nolint:lll // json body
|
||||||
|
flow_token)
|
||||||
|
case "LoginEnterPassword":
|
||||||
|
password_json, is_ok := data["password_json"]
|
||||||
|
if !is_ok {
|
||||||
|
panic("No password provided")
|
||||||
|
}
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginEnterPassword","enter_password":{"password":`+password_json+`,"link":"next_link"}}]}`, //nolint:lll // json body
|
||||||
|
flow_token)
|
||||||
|
case "AccountDuplicationCheck":
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"AccountDuplicationCheck","check_logged_in_account":{"link":"AccountDuplicationCheck_false"}}]}`, //nolint:lll // json body
|
||||||
|
flow_token)
|
||||||
|
|
||||||
|
// Challenge flows
|
||||||
|
case "LoginAcid":
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginAcid","enter_text":{"text":"%s","link":"next_link"}}]}`,
|
||||||
|
flow_token,
|
||||||
|
data["phone"])
|
||||||
|
case "LoginEnterAlternateIdentifierSubtask":
|
||||||
|
body = fmt.Sprintf(
|
||||||
|
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginEnterAlternateIdentifierSubtask","enter_text":{"text":"%s","link":"next_link"}}]}`,
|
||||||
|
flow_token,
|
||||||
|
data["phone"])
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("Unknown task_id: " + task_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := api.do_http_POST(LOGIN_URL, body, &result)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("api.Client.Jar: %#v\n", api.Client.Jar)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conducts the "login flow".
|
||||||
|
//
|
||||||
|
// Logging in is implemented as a series of subtasks in which username and password are submitted in
|
||||||
|
// separate requests ("tasks"). Other possible tasks include answering challenges like "verify your
|
||||||
|
// phone number", etc.
|
||||||
|
//
|
||||||
|
// To log in, we do "flow tasks" in sequence until the result is "LoginSuccessSubtask"
|
||||||
|
func (api *API) LogIn(username string, password string) *ChallengeResult {
|
||||||
|
// Format username and password safely as JSON (escape quotes, etc)
|
||||||
|
username_json, err := json.Marshal(username)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
password_json, err := json.Marshal(password)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]string{
|
||||||
|
"username_json": string(username_json),
|
||||||
|
"password_json": string(password_json),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin flow
|
||||||
|
var result flow_result
|
||||||
|
err = api.do_http_POST(LOGIN_URL+"?flow_name=login", "", &result)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue login flow
|
||||||
|
return api.continue_login_flow(result, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue flow until finished or challenged
|
||||||
|
// This helper function lets you re-enter the login flow after a challenge disrupts it
|
||||||
|
func (api *API) continue_login_flow(result flow_result, data map[string]string) *ChallengeResult {
|
||||||
|
for result.Subtasks[0].SubtaskID != "LoginSuccessSubtask" {
|
||||||
|
if result.FlowToken == "" { // Sanity check
|
||||||
|
panic("No flow token.")
|
||||||
|
}
|
||||||
|
result = api.do_login_task(result.FlowToken, result.Subtasks[0].SubtaskID, data)
|
||||||
|
|
||||||
|
// Check for challenges
|
||||||
|
if result.Subtasks[0].EnterText.PrimaryText.Text != "" {
|
||||||
|
// Challenge issued
|
||||||
|
return &ChallengeResult{
|
||||||
|
FlowToken: result.FlowToken,
|
||||||
|
Data: data,
|
||||||
|
SubtaskID: result.Subtasks[0].SubtaskID,
|
||||||
|
PrimaryText: result.Subtasks[0].EnterText.PrimaryText.Text,
|
||||||
|
SecondaryText: result.Subtasks[0].EnterText.SecondaryText.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login successful
|
||||||
|
api.UserID = UserID(result.Subtasks[0].OpenAccount.User.ID)
|
||||||
|
api.UserHandle = UserHandle(result.Subtasks[0].OpenAccount.User.ScreenName)
|
||||||
|
api.update_csrf_token()
|
||||||
|
api.IsAuthenticated = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) LoginVerifyPhone(challenge ChallengeResult, phone_num string) {
|
||||||
|
data := challenge.Data
|
||||||
|
data["phone"] = strings.TrimSpace(phone_num)
|
||||||
|
result := api.do_login_task(challenge.FlowToken, challenge.SubtaskID, data)
|
||||||
|
api.continue_login_flow(result, data)
|
||||||
|
}
|
@ -127,77 +127,6 @@ func NewGuestSession() API {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) LogIn(username string, password string) {
|
|
||||||
loginURL := "https://twitter.com/i/api/1.1/onboarding/task.json"
|
|
||||||
|
|
||||||
// Format username and password safely as JSON (escape quotes, etc)
|
|
||||||
json_username, err := json.Marshal(username)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
json_password, err := json.Marshal(password)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
login_bodies := []string{
|
|
||||||
`{"flow_token":"%s", "subtask_inputs": [{"subtask_id": "LoginJsInstrumentationSubtask", "js_instrumentation": {"response": "{\"rf\":{\"a560cdc18ff70ce7662311eac0f2441dd3d3ed27c354f082f587e7a30d1a7d5f\":72,\"a8e890b5fec154e7af62d8f529fbec5942dfdd7ad41597245b71a3fbdc9a180d\":176,\"a3c24597ad4c862773c74b9194e675e96a7607708f6bbd0babcfdf8b109ed86d\":-161,\"af9847e2cd4e9a0ca23853da4b46bf00a2e801f98dc819ee0dd6ecc1032273fa\":-8},\"s\":\"hOai7h2KQi4RBGKSYLUhH0Y0fBm5KHIJgxD5AmNKtwP7N8gpVuAqP8o9n2FpCnNeR1d6XbB0QWkGAHiXkKao5PhaeXEZgPJU1neLcVgTnGuFzpjDnGutCUgYaxNiwUPfDX0eQkgr_q7GWmbB7yyYPt32dqSd5yt-KCpSt7MOG4aFmGf11xWE4MTpXfkefbnX4CwZeEFKQQYzJptOvmUWa7qI0A69BSOs7HZ_4Wry2TwB9k03Q_S-MDZAZ3yB_L7WoosVVb1e84YWgaLWWzqhz4C77jDy6isT8EKSWKWnVctsIcaqM_wMV8AiYa5lr0_WkN5TwK9h0vDOTS1obOZuhAAAAYTZan_3\"}", "link": "next_link"}}]}`, //nolint:lll // json body
|
|
||||||
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginEnterUserIdentifierSSO","settings_list":{"setting_responses":[{"key":"user_identifier","response_data":{"text_data":{"result":` + string(json_username) + `}}}],"link":"next_link"}}]}`, //nolint:lll // json body
|
|
||||||
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"LoginEnterPassword","enter_password":{"password":` + string(json_password) + `,"link":"next_link"}}]}`, //nolint:lll // json body
|
|
||||||
`{"flow_token":"%s","subtask_inputs":[{"subtask_id":"AccountDuplicationCheck","check_logged_in_account":{"link":"AccountDuplicationCheck_false"}}]}`, //nolint:lll // json body
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
|
||||||
err = api.do_http_POST(loginURL+"?flow_name=login", "", &result)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, body := range login_bodies {
|
|
||||||
flow_token, is_ok := result["flow_token"]
|
|
||||||
if !is_ok {
|
|
||||||
panic("No flow token.")
|
|
||||||
}
|
|
||||||
flow_token_string, is_ok := flow_token.(string)
|
|
||||||
if !is_ok {
|
|
||||||
panic("Flow token couldn't be turned into a string")
|
|
||||||
}
|
|
||||||
log.Debug(flow_token_string)
|
|
||||||
err = api.do_http_POST(loginURL, fmt.Sprintf(body, flow_token_string), &result)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%#v\n", api.Client.Jar)
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type final_result_struct struct {
|
|
||||||
Subtasks []struct {
|
|
||||||
OpenAccount struct {
|
|
||||||
User struct {
|
|
||||||
ID int `json:"id_str,string"`
|
|
||||||
Name string
|
|
||||||
ScreenName string `json:"screen_name"`
|
|
||||||
}
|
|
||||||
} `json:"open_account"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bytes, err := json.Marshal(result)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var final_result final_result_struct
|
|
||||||
err = json.Unmarshal(bytes, &final_result)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.UserID = UserID(final_result.Subtasks[0].OpenAccount.User.ID)
|
|
||||||
api.UserHandle = UserHandle(final_result.Subtasks[0].OpenAccount.User.ScreenName)
|
|
||||||
|
|
||||||
api.update_csrf_token()
|
|
||||||
api.IsAuthenticated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) update_csrf_token() {
|
func (api *API) update_csrf_token() {
|
||||||
dummyURL, err := url.Parse("https://twitter.com/i/api/1.1/onboarding/task.json")
|
dummyURL, err := url.Parse("https://twitter.com/i/api/1.1/onboarding/task.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user