mirror of
https://gitgud.io/yats/libkiwi.git
synced 2026-06-20 01:55:23 -04:00
233 lines
5.0 KiB
Go
233 lines
5.0 KiB
Go
package libkiwi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
"golang.org/x/net/html/atom"
|
|
)
|
|
|
|
type LoginResp struct {
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Redirect *url.URL `json:"redirect"`
|
|
Visitor struct {
|
|
ConversationsUnread uint32 `json:"conversations_unread"`
|
|
AlertsUnviewed uint32 `json:"alerts_unviewed"`
|
|
TotalUnread uint32 `json:"total_unread"`
|
|
} `json:"visitor"`
|
|
}
|
|
|
|
func (lr *LoginResp) UnmarshalJSON(b []byte) error {
|
|
var jsonMap map[string]any
|
|
err := json.Unmarshal(b, &jsonMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range jsonMap {
|
|
switch k {
|
|
case "status":
|
|
lr.Status = v.(string)
|
|
case "message":
|
|
lr.Message = v.(string)
|
|
case "redirect":
|
|
u, err := url.Parse(v.(string))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lr.Redirect = u
|
|
case "visitor":
|
|
for k, v := range v.(map[string]any) {
|
|
switch k {
|
|
case "conversations_unread":
|
|
convos, err := strconv.Atoi(v.(string))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lr.Visitor.ConversationsUnread = uint32(convos)
|
|
case "alerts_unviewed":
|
|
alerts, err := strconv.Atoi(v.(string))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lr.Visitor.AlertsUnviewed = uint32(alerts)
|
|
case "total_unread":
|
|
unread, err := strconv.Atoi(v.(string))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lr.Visitor.ConversationsUnread = uint32(unread)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var ErrNoXFToken = errors.New("Failed to locate xfToken on page.")
|
|
|
|
func xfToken(page io.Reader) (string, error) {
|
|
z := html.NewTokenizer(page)
|
|
for i := z.Next(); i != html.ErrorToken; i = z.Next() {
|
|
tk := z.Token()
|
|
if tk.DataAtom == atom.Html {
|
|
for _, a := range tk.Attr {
|
|
switch a.Key {
|
|
case "data-csrf":
|
|
return a.Val, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", ErrNoXFToken
|
|
}
|
|
|
|
func loginURL(host *url.URL) *url.URL {
|
|
return &url.URL{
|
|
Scheme: "https",
|
|
Host: host.Hostname(),
|
|
Path: "login",
|
|
}
|
|
}
|
|
|
|
func (kf *KF) newLoginRequest(ctx context.Context, user string, pass string) (*http.Request, error) {
|
|
u := loginURL(kf.domain)
|
|
|
|
resp, err := kf.GetPage(ctx, u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
xfToken, err := xfToken(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
form := url.Values{
|
|
"_xfToken": {xfToken},
|
|
"login": {user},
|
|
"password": {pass},
|
|
"remember": {"1"},
|
|
}
|
|
|
|
// i.e. https://kiwifarms.net/login/login
|
|
postURL := fmt.Sprintf("%s/login", u.String())
|
|
req, err := http.NewRequestWithContext(ctx, "POST", postURL, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (kf *KF) Login(ctx context.Context, user string, pass string) (*http.Response, error) {
|
|
req, err := kf.newLoginRequest(ctx, user, pass)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := kf.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
// TODO
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func TwoFactorForm(xfToken string, code uint32, u *url.URL) url.Values {
|
|
return url.Values{
|
|
"code": {fmt.Sprint(code)},
|
|
"provider": {"totp"},
|
|
"trust": {"1"},
|
|
"confirm": {"1"},
|
|
"remember": {"1"},
|
|
"_xfRedirect": {fmt.Sprintf("https://%s/", u.Hostname())},
|
|
"_xfResponseType": {"json"},
|
|
"_xfToken": {xfToken},
|
|
"_xfWithData": {"1"},
|
|
"_xfRequestUri": {u.RequestURI()},
|
|
}
|
|
}
|
|
|
|
func (kf *KF) twoFactorAuthForm(ctx context.Context, resp *http.Response, code uint32) (*http.Request, error) {
|
|
u := resp.Request.URL
|
|
if !strings.Contains(u.Path, "two-step") {
|
|
// TODO: move err to proper type with url included.
|
|
return nil, errors.New("Did not redirect to two-step page.")
|
|
}
|
|
|
|
xfToken, err := xfToken(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
form := TwoFactorForm(xfToken, code, u)
|
|
reqURL := fmt.Sprintf("https://%s/login/two-step", u.Hostname())
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (kf *KF) TwoFactorAuth(ctx context.Context, resp *http.Response, code uint32) (*http.Response, error) {
|
|
req, err := kf.twoFactorAuthForm(ctx, resp, code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return kf.client.Do(req)
|
|
}
|
|
|
|
func setCookie(u *url.URL, jar http.CookieJar, newCookie *http.Cookie) {
|
|
cookies := jar.Cookies(u)
|
|
|
|
for i, c := range cookies {
|
|
if c.Name == newCookie.Name {
|
|
cookies[i] = newCookie
|
|
jar.SetCookies(u, cookies)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Append if not already existing.
|
|
cookies = append(cookies, newCookie)
|
|
jar.SetCookies(u, cookies)
|
|
}
|
|
|
|
func (kf *KF) RefreshSession(ctx context.Context) error {
|
|
// Clear any existing session token to request a new one.
|
|
setCookie(kf.domain, kf.client.Jar, &http.Cookie{
|
|
Name: "xf_session",
|
|
Value: "",
|
|
})
|
|
|
|
resp, err := kf.GetPage(ctx, kf.domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
return nil
|
|
}
|