Compare commits

..

4 Commits

Author SHA1 Message Date
y a t s c7ccfc65bf refactoring 2026-06-19 10:42:39 -04:00
y a t s 74b923f06f Fix IsLoggedIn() and some other error checking 2026-06-19 10:21:21 -04:00
y a t s 92438691f1 Add cookie convenience funcs
Fix test helper func
2026-06-18 11:44:25 -04:00
y a t s 72840af90e Fix overwrite of existing cookiejar. 2026-06-18 10:02:16 -04:00
7 changed files with 112 additions and 67 deletions
+30 -26
View File
@@ -29,6 +29,16 @@ func loginForm(xfToken string, creds Credentials) url.Values {
} }
} }
func newFormRequest(ctx context.Context, reqURL string, form url.Values) (*http.Request, error) {
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) newLoginRequest(ctx context.Context, creds Credentials) (*http.Request, error) { func (kf *KF) newLoginRequest(ctx context.Context, creds Credentials) (*http.Request, error) {
u := kf.urlFromPath("login") u := kf.urlFromPath("login")
@@ -43,17 +53,8 @@ func (kf *KF) newLoginRequest(ctx context.Context, creds Credentials) (*http.Req
return nil, err return nil, err
} }
form := loginForm(xfToken, creds) // e.g. https://kiwifarms.net/login/login
return newFormRequest(ctx, fmt.Sprintf("%s/login", u.String()), loginForm(xfToken, creds))
// i.e. https://kiwifarms.net/login/login
reqURL := fmt.Sprintf("%s/login", u.String())
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) Login(ctx context.Context, creds Credentials) (*http.Response, error) { func (kf *KF) Login(ctx context.Context, creds Credentials) (*http.Response, error) {
@@ -101,16 +102,7 @@ func (kf *KF) newTwoFactorRequest(ctx context.Context, resp *http.Response, code
return nil, err return nil, err
} }
form := twoFactorForm(xfToken, code, u) return newFormRequest(ctx, fmt.Sprintf("https://%s/login/two-step", u.Hostname()), 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) { func (kf *KF) TwoFactorAuth(ctx context.Context, resp *http.Response, code uint32) (*http.Response, error) {
@@ -122,18 +114,30 @@ func (kf *KF) TwoFactorAuth(ctx context.Context, resp *http.Response, code uint3
return kf.Do(req) return kf.Do(req)
} }
func (kf *KF) RefreshSession(ctx context.Context) error { func (kf *KF) IsLoggedIn() bool {
cookie := kf.Cookies.GetCookie(kf.domain, "xf_user")
return !(cookie == nil || cookie.Value == "")
}
func (kf *KF) RefreshSession(ctx context.Context) (string, error) {
const COOKIE_NAME = "xf_session"
// Clear any existing session token to request a new one. // Clear any existing session token to request a new one.
setCookie(kf.domain, kf.client.Jar, &http.Cookie{ kf.Cookies.SetCookie(kf.domain, &http.Cookie{
Name: "xf_session", Name: COOKIE_NAME,
Value: "", Value: "",
}) })
resp, err := kf.Get(ctx, kf.domain) resp, err := kf.Get(ctx, kf.domain)
if err != nil { if err != nil {
return err return "", err
} }
resp.Body.Close() resp.Body.Close()
return nil session := kf.Cookies.GetCookie(kf.domain, COOKIE_NAME)
if session == nil || session.Value == "" {
return "", fmt.Errorf("Failed to get new %s cookie", COOKIE_NAME)
}
return session.Value, nil
} }
+14
View File
@@ -63,6 +63,20 @@ func TestLogin(t *testing.T) {
} }
t.Logf("2FA response: %+v\n", lr) t.Logf("2FA response: %+v\n", lr)
} }
t.Logf("Checking RefreshSession()")
session, err := kf.RefreshSession(ctx)
if err != nil {
t.Error(err)
return
}
t.Logf("xf_session: %s\n", session)
t.Logf("Checking IsLoggedIn()\n")
if !kf.IsLoggedIn() {
t.Error("IsLoggedIn() test returned false.")
return
}
} }
func genTest2FACode() (uint32, error) { func genTest2FACode() (uint32, error) {
+40
View File
@@ -0,0 +1,40 @@
package libkiwi
import (
"net/http"
"net/url"
)
// http.CookieJar interface wrapper to add convenience functions.
type cookieJar struct {
jar *http.CookieJar
}
func (j *cookieJar) GetCookie(u *url.URL, name string) *http.Cookie {
jar := (*j.jar)
cookies := jar.Cookies(u)
for _, c := range cookies {
if c.Name == name {
return c
}
}
return nil
}
func (j *cookieJar) SetCookie(u *url.URL, newCookie *http.Cookie) {
jar := (*j.jar)
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)
}
+1 -1
View File
@@ -1,6 +1,6 @@
module gitgud.io/yats/libkiwi module gitgud.io/yats/libkiwi
go 1.26.1 go 1.26.4
require ( require (
gitgud.io/yats/cerberus v0.0.0-20260214165307-66e6f74a4be9 gitgud.io/yats/cerberus v0.0.0-20260214165307-66e6f74a4be9
+8 -22
View File
@@ -19,12 +19,14 @@ func XFToken(page io.Reader) (string, error) {
z := html.NewTokenizer(page) z := html.NewTokenizer(page)
for i := z.Next(); i != html.ErrorToken; i = z.Next() { for i := z.Next(); i != html.ErrorToken; i = z.Next() {
tk := z.Token() tk := z.Token()
if tk.DataAtom == atom.Html { if tk.DataAtom != atom.Html {
for _, a := range tk.Attr { continue
switch a.Key { }
case "data-csrf":
return a.Val, nil for _, a := range tk.Attr {
} switch a.Key {
case "data-csrf":
return a.Val, nil
} }
} }
} }
@@ -32,22 +34,6 @@ func XFToken(page io.Reader) (string, error) {
return "", ErrNoXFToken return "", ErrNoXFToken
} }
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) Do(req *http.Request) (*http.Response, error) { func (kf *KF) Do(req *http.Request) (*http.Response, error) {
var ( var (
ctx = req.Context() ctx = req.Context()
+16 -15
View File
@@ -13,33 +13,34 @@ import (
gq "github.com/PuerkitoBio/goquery" gq "github.com/PuerkitoBio/goquery"
) )
const _USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0"
type KF struct { type KF struct {
client http.Client client http.Client
domain *url.URL domain *url.URL
Cookies cookieJar
} }
// Supply your own http.Client to route through any proxies. // Supply your own http.Client to route through any proxies.
func NewKF(hc http.Client, host *url.URL) (*KF, error) { func NewKF(hc http.Client, host *url.URL) (KF, error) {
u, err := url.Parse(fmt.Sprintf("https://%s", host.Hostname())) u, err := url.Parse(fmt.Sprintf("https://%s", host.Hostname()))
if err != nil { if err != nil {
return nil, err return KF{}, err
} }
jar, err := cookiejar.New(nil) if hc.Jar == nil {
if err != nil { jar, err := cookiejar.New(nil)
return nil, err if err != nil {
return KF{}, err
}
hc.Jar = jar
} }
hc.Jar = jar return KF{
client: hc,
kf := &KF{ domain: u,
client: hc, Cookies: cookieJar{&hc.Jar},
domain: u, }, nil
}
return kf, nil
} }
type User struct { type User struct {
+3 -3
View File
@@ -58,7 +58,7 @@ func TestGetPost(t *testing.T) {
t.Logf("Post text: %s\n", post.TextContent()) t.Logf("Post text: %s\n", post.TextContent())
} }
func newTestKF() (*KF, error) { func newTestKF() (KF, error) {
host := os.Getenv("TEST_HOST") host := os.Getenv("TEST_HOST")
if host == "" { if host == "" {
host = _TEST_HOST host = _TEST_HOST
@@ -66,7 +66,7 @@ func newTestKF() (*KF, error) {
u, err := url.Parse("https://" + host) u, err := url.Parse("https://" + host)
if err != nil { if err != nil {
return nil, err return KF{}, err
} }
p := proxy.FromEnvironment() p := proxy.FromEnvironment()
@@ -76,7 +76,7 @@ func newTestKF() (*KF, error) {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
return nil, err return KF{}, err
} }
hc := http.Client{ hc := http.Client{