package server import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "time" ) // storedCert is one issued certificate. With the local issuer, issuance is // synchronous, so a bound CSR always has its certificate populated. type storedCert struct { ID string `json:"id"` CSR string `json:"csr"` Certificate string `json:"certificate"` Chain string `json:"chain"` Expiry string `json:"expiry"` } type storedTunnel struct { ID string `json:"id"` Hostname string `json:"hostname"` TokenHash string `json:"tokenHash"` Certificate *storedCert `json:"certificate,omitempty"` } // storeData is the on-disk shape. The CA key is a secret, so the file is // written 0600. type storeData struct { CAKeyPEM string `json:"caKeyPem"` CACertPEM string `json:"caCertPEM"` ServerKey string `json:"serverKey"` ServerCrt string `json:"serverCrt"` Tunnels map[string]storedTunnel `json:"tunnels"` } // Store is a JSON-file-backed tunnel store: adequate for a single-node // self-hosted deployment, and the one writer this server has. type Store struct { mu sync.Mutex path string data storeData } // openStore loads path, or initializes it (with a fresh local CA) when it // does not exist. An empty path keeps state in memory only. func OpenStore(path string) (*Store, error) { s := &Store{path: path, data: storeData{Tunnels: map[string]storedTunnel{}}} if path != "" { if raw, err := os.ReadFile(path); err == nil { if err := json.Unmarshal(raw, &s.data); err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } if s.data.Tunnels == nil { s.data.Tunnels = map[string]storedTunnel{} } } else if !errors.Is(err, fs.ErrNotExist) { return nil, err } } if s.data.CAKeyPEM == "" { ca, err := newLocalCA() if err != nil { return nil, err } keyPEM, err := ca.keyPEM() if err != nil { return nil, err } s.data.CAKeyPEM = string(keyPEM) s.data.CACertPEM = string(ca.certPEM) if err := s.saveLocked(); err != nil { return nil, err } } return s, nil } // ca returns the store's local CA, loading it from the stored PEM. func (s *Store) ca() (*localCA, error) { s.mu.Lock() defer s.mu.Unlock() return loadLocalCA([]byte(s.data.CAKeyPEM), []byte(s.data.CACertPEM)) } // CACertPEM is the CA certificate clients should trust. func (s *Store) CACertPEM() string { s.mu.Lock() defer s.mu.Unlock() return s.data.CACertPEM } // serverCertificate returns the API domain's key and certificate PEM, // issuing one with the CA when missing. func (s *Store) ServerCertificate(domain string) (certPEM, keyPEM []byte, err error) { s.mu.Lock() defer s.mu.Unlock() if s.data.ServerCrt != "" { return []byte(s.data.ServerCrt), []byte(s.data.ServerKey), nil } ca, err := loadLocalCA([]byte(s.data.CAKeyPEM), []byte(s.data.CACertPEM)) if err != nil { return nil, nil, err } certPEM, keyPEM, err = ca.issue(domain) if err != nil { return nil, nil, err } s.data.ServerCrt = string(certPEM) s.data.ServerKey = string(keyPEM) if err := s.saveLocked(); err != nil { return nil, nil, err } return certPEM, keyPEM, nil } // createTunnel stores a new tunnel and returns it with its bearer token, the // only time the token is visible. func (s *Store) createTunnel(domain string) (storedTunnel, string, error) { token := newToken() t := storedTunnel{ ID: slug(), TokenHash: hashToken(token), } t.Hostname = t.ID + "." + domain s.mu.Lock() defer s.mu.Unlock() s.data.Tunnels[t.ID] = t return t, token, s.saveLocked() } // get returns the tunnel with id, or false. func (s *Store) get(id string) (storedTunnel, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.data.Tunnels[id] return t, ok } // bindCSR records the issued certificate for the tunnel. The CSR is kept so a // renewal (and a resumed provision) can be answered without a new bind. func (s *Store) bindCertificate(id string, cert storedCert) error { s.mu.Lock() defer s.mu.Unlock() t, ok := s.data.Tunnels[id] if !ok { return errors.New("tunnel not found") } t.Certificate = &cert s.data.Tunnels[id] = t return s.saveLocked() } // remove deletes the tunnel. func (s *Store) remove(id string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.data.Tunnels[id]; !ok { return errors.New("tunnel not found") } delete(s.data.Tunnels, id) return s.saveLocked() } // saveLocked writes the data file atomically (0600); a no-op for an // in-memory store. func (s *Store) saveLocked() error { if s.path == "" { return nil } raw, err := json.MarshalIndent(s.data, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil { return err } if err := os.WriteFile(tmp, raw, 0o600); err != nil { return err } return os.Rename(tmp, s.path) } // expiryString formats a time as the reference does: ISO 8601 UTC with // milliseconds, e.g. 2026-10-03T00:00:00.000Z. func expiryString(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }