mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
188 lines
4.6 KiB
Go
188 lines
4.6 KiB
Go
// Package prehook implements a connection prehook mechanism, to handle any
|
|
// pre-negotiation required by a remote node before the B2F protocol can
|
|
// commence (e.g. packet node traversal).
|
|
package prehook
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/la5nta/pat/internal/debug"
|
|
"github.com/la5nta/pat/internal/directories"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
var ErrConnNotWrapped = errors.New("connection not wrapped for prehook")
|
|
|
|
type Script struct {
|
|
File string
|
|
Dir string
|
|
Args []string
|
|
Env []string
|
|
}
|
|
|
|
// Execute executes the prehook script on a wrapped connection.
|
|
//
|
|
// ErrConnNotWrapped is returned if conn is not wrapped.
|
|
func (s Script) Execute(ctx context.Context, conn net.Conn) error {
|
|
if conn, ok := conn.(*Conn); ok {
|
|
return conn.Execute(ctx, s)
|
|
}
|
|
return ErrConnNotWrapped
|
|
}
|
|
|
|
type Conn struct {
|
|
net.Conn
|
|
br *bufio.Reader
|
|
}
|
|
|
|
// VerifyFile returns nil if the given script file is found and valid.
|
|
func (s Script) VerifyFile() error {
|
|
p, err := s.Path()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := os.Stat(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return fmt.Errorf("%s: is a directory", s.File)
|
|
}
|
|
if _, err := exec.LookPath(p); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s Script) Path() (string, error) {
|
|
p := filepath.Join(s.Dir, s.File)
|
|
if !directories.IsInPath(s.Dir, p) {
|
|
return "", fmt.Errorf("%s: escapes base path", p)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// Wrap returns a wrapped connection with the ability to execute a prehook.
|
|
//
|
|
// The returned Conn implements the net.Conn interface, and should be used in
|
|
// place of the original throughout the lifetime of the connection once the
|
|
// prehook script is executed.
|
|
func Wrap(conn net.Conn) *Conn {
|
|
return &Conn{
|
|
Conn: conn,
|
|
br: bufio.NewReader(conn),
|
|
}
|
|
}
|
|
|
|
func (p *Conn) Read(b []byte) (int, error) { return p.br.Read(b) }
|
|
|
|
// Execute executes the prehook script, returning nil if the process
|
|
// terminated successfully (exit code 0).
|
|
func (p *Conn) Execute(ctx context.Context, script Script) error {
|
|
name, err := script.Path()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.CommandContext(ctx, name, script.Args...)
|
|
cmd.Env = script.Env
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = p.Conn
|
|
cmdStdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
debugf("start cmd: %s", cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
g.Go(func() error { return forwardLines(ctx, cmdStdin, p.br) })
|
|
g.Go(func() error { defer cancel(); return cmd.Wait() })
|
|
return g.Wait()
|
|
}
|
|
|
|
// forwardLines forwards data from to the spawned process line by line.
|
|
//
|
|
// The line delimiter is CR or LF, but to facilitate scripting we forward
|
|
// each line with LF ending only.
|
|
func forwardLines(ctx context.Context, w io.Writer, r *bufio.Reader) error {
|
|
// Copy the lines to stdout so the user can see what's going on.
|
|
stdinBuffered := bufio.NewWriter(io.MultiWriter(w, os.Stdout))
|
|
defer stdinBuffered.Flush()
|
|
|
|
isDelimiter := func(b byte) bool { return b == '\n' || b == '\r' }
|
|
|
|
var isPrefix bool // true if we're in the middle of a line
|
|
for {
|
|
if !isPrefix {
|
|
// Peek until the next new line (discard empty lines).
|
|
debugf("wait next line")
|
|
switch peek, err := r.Peek(1); {
|
|
case err != nil:
|
|
// Connection lost.
|
|
debugf("connection lost while waiting for next line")
|
|
return err
|
|
case len(peek) > 0 && isDelimiter(peek[0]):
|
|
debugf("discard %q", peek)
|
|
r.Discard(1)
|
|
continue
|
|
case ctx.Err() != nil:
|
|
// Child process exited before the next line
|
|
// arrived. We're done.
|
|
debugf("cmd exited while waiting for next line")
|
|
return nil
|
|
default:
|
|
debugf("at next line")
|
|
}
|
|
}
|
|
|
|
// Read and forward the byte.
|
|
// Replace CR with LF for convenience.
|
|
b, err := r.ReadByte()
|
|
if err != nil {
|
|
// Connection lost.
|
|
debugf("connection lost while reading next byte")
|
|
return err
|
|
}
|
|
if b == '\r' {
|
|
b = '\n'
|
|
}
|
|
stdinBuffered.WriteByte(b)
|
|
|
|
isPrefix = !isDelimiter(b)
|
|
if isPrefix {
|
|
// Keep going. We're in the middle of a line.
|
|
continue
|
|
}
|
|
|
|
// A line was just terminated.
|
|
// Flush and wait a bit to check if the process exits.
|
|
if err := stdinBuffered.Flush(); err != nil {
|
|
return fmt.Errorf("child process exited prematurely: %w", err)
|
|
}
|
|
select {
|
|
case <-time.After(100 * time.Millisecond):
|
|
// Child process is still alive. Keep going.
|
|
case <-ctx.Done():
|
|
// Child process exited. We're done.
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func debugf(format string, args ...interface{}) {
|
|
debug.Printf("prehook: "+format, args...)
|
|
}
|