package server_test
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"encoding/json"
"fmt"
"html"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"misaki/db"
"misaki/server"
)
func code(secret string, offset int64) string {
key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ReplaceAll(secret, " ", ""))
if err != nil {
panic(err)
}
var msg [8]byte
binary.BigEndian.PutUint64(msg[:], uint64(time.Now().Unix()/30+offset))
m := hmac.New(sha1.New, key)
m.Write(msg[:])
sum := m.Sum(nil)
off := sum[len(sum)-1] & 0xf
return fmt.Sprintf("%06d", (binary.BigEndian.Uint32(sum[off:off+4])&0x7fffffff)%1000000)
}
func cookieNamed(rr *httptest.ResponseRecorder, name string) *http.Cookie {
for _, c := range rr.Result().Cookies() {
if c.Name == name && c.Value != "" {
return c
}
}
return nil
}
func newTOTPServer(t *testing.T, smtp server.SMTPConfig) (http.Handler, *db.DB) {
t.Helper()
d := newTestDB(t)
srv := server.New(d, server.ServiceURLs{}, nil, smtp, server.Config{
TOTPKeyFile: filepath.Join(t.TempDir(), "totp.key"),
PublicURL: "https://misaki.test",
})
return srv, d
}
var (
secretRe = regexp.MustCompile(`]*>([A-Z2-7 ]+)`)
blobRe = regexp.MustCompile(`name="blob" value="([^"]+)"`)
recoveryRe = regexp.MustCompile(`[a-z2-7]{4}-[a-z2-7]{4}-[a-z2-7]{4}-[a-z2-7]{4}`)
)
// enable2FA turns on TOTP from the dashboard and returns the secret and recovery codes.
func enable2FA(t *testing.T, srv http.Handler, c *http.Cookie) (string, []string) {
t.Helper()
rr := doPost(srv, "/dashboard/2fa/setup", nil, c)
assertStatus(t, rr, http.StatusOK)
body := rr.Body.String()
sm, bm := secretRe.FindStringSubmatch(body), blobRe.FindStringSubmatch(body)
if sm == nil || bm == nil {
t.Fatalf("setup page missing secret/blob:\n%s", body)
}
if !strings.Contains(body, "otpauth://totp/misaki:") {
t.Errorf("setup page has no otpauth link")
}
if !strings.Contains(body, `