~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
159 lines · 3.8 KB Raw History
package irohdial

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/childproc"
)

const (
	MaxSocketPath  = 107
	MaxTicketBytes = 4096
	TicketTimeout  = 10 * time.Second
)

var ticketPattern = regexp.MustCompile(`^[a-z0-9]+$`)

type Config struct {
	Socket   string
	Dumbpipe string
	Log      *slog.Logger
}

func (c Config) Check() error {
	switch {
	case !filepath.IsAbs(c.Socket):
		return fmt.Errorf("irohdial: socket %q is not an absolute path", c.Socket)
	case len(c.Socket) > MaxSocketPath:
		return fmt.Errorf("irohdial: socket %s is %d bytes, over the %d a unix socket path takes", c.Socket, len(c.Socket), MaxSocketPath)
	case !filepath.IsAbs(c.Dumbpipe):
		return fmt.Errorf("irohdial: dumbpipe %q is not an absolute path", c.Dumbpipe)
	}
	st, err := os.Stat(c.Dumbpipe)
	if err != nil {
		return fmt.Errorf("irohdial: dumbpipe: %w", err)
	}
	if st.IsDir() || st.Mode().Perm()&0o111 == 0 {
		return fmt.Errorf("irohdial: dumbpipe %s is not an executable file", c.Dumbpipe)
	}
	return nil
}

func CheckTicket(ticket string) error {
	if len(ticket) == 0 || len(ticket) > MaxTicketBytes || !ticketPattern.MatchString(ticket) {
		return errors.New("irohdial: not an iroh ticket (lowercase base32, at most 4096 bytes)")
	}
	return nil
}

func Run(ctx context.Context, c Config) error {
	if err := c.Check(); err != nil {
		return err
	}
	log := c.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	if err := os.MkdirAll(filepath.Dir(c.Socket), 0o700); err != nil {
		return err
	}
	if err := os.Remove(c.Socket); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	ln, err := net.Listen("unix", c.Socket)
	if err != nil {
		return err
	}
	if err := os.Chmod(c.Socket, 0o600); err != nil {
		ln.Close()
		return err
	}
	log.Info("the iroh dialer listens; each connection names one guest's iroh ticket", "socket", c.Socket, "dumbpipe", c.Dumbpipe)
	var wg sync.WaitGroup
	defer wg.Wait()
	stop := make(chan struct{})
	defer close(stop)
	wg.Go(func() {
		select {
		case <-ctx.Done():
		case <-stop:
		}
		ln.Close()
	})
	for {
		conn, err := ln.Accept()
		if err != nil {
			if ctx.Err() != nil {
				return nil
			}
			return err
		}
		wg.Go(func() {
			serve(ctx, c, log, conn)
		})
	}
}

func serve(ctx context.Context, c Config, log *slog.Logger, conn net.Conn) {
	defer conn.Close()
	started := time.Now()
	conn.SetReadDeadline(started.Add(TicketTimeout))
	br := bufio.NewReaderSize(conn, MaxTicketBytes+2)
	line, err := br.ReadSlice('\n')
	if err != nil {
		log.Warn("a connection named no ticket", "err", err.Error())
		return
	}
	conn.SetReadDeadline(time.Time{})
	ticket := strings.TrimSpace(string(line))
	if err := CheckTicket(ticket); err != nil {
		log.Warn("refused a connection", "err", err.Error())
		return
	}
	dctx, cancel := context.WithCancel(ctx)
	defer cancel()
	cmd := exec.CommandContext(dctx, c.Dumbpipe, "connect", ticket)
	stdin, err := cmd.StdinPipe()
	if err != nil {
		log.Warn("dumbpipe stdin", "err", err.Error())
		return
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Warn("dumbpipe stdout", "err", err.Error())
		return
	}
	var tail childproc.Tail
	cmd.Stderr = childproc.FilterSecrets(&tail)
	if err := cmd.Start(); err != nil {
		log.Warn("starting dumbpipe connect", "err", err.Error())
		return
	}
	id := ticket[:min(16, len(ticket))]
	log.Info("dumbpipe connect started", "ticket", id, "pid", cmd.Process.Pid)
	var copyIn sync.WaitGroup
	copyIn.Go(func() {
		io.Copy(stdin, br)
		stdin.Close()
	})
	io.Copy(conn, stdout)
	cancel()
	err = cmd.Wait()
	log.Info("dumbpipe connect ended", "ticket", id, "seconds", time.Since(started).Seconds(), "exit", fmt.Sprint(err),
		"stderr", strings.TrimSpace(tail.String()))
	conn.Close()
	copyIn.Wait()
}