package main import ( "bytes" "crypto/sha256" "crypto/tls" "encoding/hex" "errors" "fmt" "net/url" "strings" ) type Identities struct { ByName map[string]*tls.Config ByDomain map[string]*tls.Config ByPage map[string]*tls.Config ByFolder map[string]*tls.Config } func findIdentity(state *BrowserState, prefix string) (string, error) { found := 0 value := "" for name := range state.Identities.ByName { if strings.HasPrefix(name, prefix) { found += 1 value = name } } switch found { case 0: return "", errors.New("no matching identity found") case 1: return value, nil default: return "", fmt.Errorf("too ambiguous - that name matched %d identities", found) } } func (ids Identities) Get(u *url.URL) *tls.Config { if conf, ok := ids.ByPage[u.String()]; ok { return conf } if conf, ok := ids.ByFolder[u.Hostname()+u.Path]; ok { return conf } pathsegments := strings.Split(strings.TrimLeft(u.Path, "/"), "/") for len(pathsegments) > 0 { pathsegments = pathsegments[0 : len(pathsegments)-1] if conf, ok := ids.ByFolder[u.Hostname()+"/"+strings.Join(pathsegments, "/")]; ok { return conf } } if conf, ok := ids.ByDomain[u.Hostname()]; ok { return conf } return nil } func IdentityCreate(state *BrowserState, name string) error { ident, err := createIdentity(state, name) if err != nil { return err } state.Identities.ByName[name] = ident if err := saveIdentities(state.Identities); err != nil { return err } state.Modal = []byte(fmt.Sprintf("Created new identity %s\n", name)) return Print(state) } func IdentityList(state *BrowserState) error { buf := &bytes.Buffer{} for name, ident := range state.Identities.ByName { if _, err := fmt.Fprintf(buf, "%s (%s):\n", name, showIdent(ident)); err != nil { return err } for domain, id := range state.Identities.ByDomain { if id == ident { if _, err := fmt.Fprintf(buf, " domain %s\n", domain); err != nil { return err } } } for folder, id := range state.Identities.ByFolder { if id == ident { if _, err := fmt.Fprintf(buf, " folder %s\n", folder); err != nil { return err } } } for page, id := range state.Identities.ByPage { if id == ident { if _, err := fmt.Fprintf(buf, " page %s\n", page); err != nil { return err } } } } state.Modal = buf.Bytes() if len(state.Modal) == 0 { state.Modal = []byte("(no identities)\n") } return Print(state) } func IdentityDelete(state *BrowserState, name string) error { name, err := findIdentity(state, name) if err != nil { return err } ident := state.Identities.ByName[name] delete(state.Identities.ByName, name) for domain, id := range state.Identities.ByDomain { if id == ident { delete(state.Identities.ByDomain, domain) } } for folder, id := range state.Identities.ByFolder { if id == ident { delete(state.Identities.ByFolder, folder) } } for page, id := range state.Identities.ByPage { if id == ident { delete(state.Identities.ByPage, page) } } if err := removeIdentity(name); err != nil { return err } if err := saveIdentities(state.Identities); err != nil { return err } state.Modal = []byte(fmt.Sprintf("Removed identity %s\n", name)) return Print(state) } func IdentityUseDomain(state *BrowserState, name string, domain string) error { name, err := findIdentity(state, name) if err != nil { return err } ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain } } else if err != nil { return err } state.Identities.ByDomain[u.Hostname()] = ident if err := saveIdentities(state.Identities); err != nil { return err } state.Modal = []byte(fmt.Sprintf("Identity %s will be used across domain %s\n", name, u.Hostname())) return Print(state) } func IdentityUseFolder(state *BrowserState, name string, domain string) error { name, err := findIdentity(state, name) if err != nil { return err } ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain } } else if err != nil { return err } u.Path = strings.TrimRight(u.Path, "/") state.Identities.ByFolder[u.Hostname()+u.Path] = ident if err := saveIdentities(state.Identities); err != nil { return err } state.Modal = []byte(fmt.Sprintf("Identity %s will be used within folder %s%s\n", name, u.Hostname(), u.Path)) return Print(state) } func IdentityUsePage(state *BrowserState, name string, domain string) error { name, err := findIdentity(state, name) if err != nil { return err } ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain } } else if err != nil { return err } state.Identities.ByPage[u.String()] = ident if err := saveIdentities(state.Identities); err != nil { return err } state.Modal = []byte(fmt.Sprintf("Identity %s will be used on page %s\n", name, u.String())) return Print(state) } func showIdent(ident *tls.Config) string { hash := sha256.Sum256(ident.Certificates[0].Certificate[0]) return strings.ToUpper(hex.EncodeToString(hash[:])[:10]) }