package hook import ( "bufio" "crypto/md5" "crypto/rand" "errors" "fmt" "io" "os" "strings" "time" "tunnel/pkg/server/env" "tunnel/pkg/server/queue" ) const authTimeout = 5 * time.Second const saltSize = 16 const hashSize = md5.Size var hashNone = strings.Repeat("\x00", hashSize) type authHook struct { Passive bool } type auth struct { file *os.File user string salt string pass string peer struct { user string salt string } hash string passive bool stageHello chan struct{} stageHash chan struct{} ready chan struct{} recver chan struct{} sender chan struct{} tmr *time.Timer } var errAuthFail = errors.New("mismatch auth hash") var errAuthUnknown = errors.New("unknown auth peer") var errTimeout = errors.New("timeout") func hashsum(args ...string) string { h := md5.New() for _, s := range args { io.WriteString(h, s) } return string(h.Sum(nil)) } func getpass(f *os.File, salt string, user string) string { f.Seek(0, 0) for scanner := bufio.NewScanner(f); scanner.Scan(); { splitted := strings.SplitN(scanner.Text(), "#", 2) tokens := strings.Fields(splitted[0]) if len(tokens) > 1 { if salt == "" { if tokens[0] == user { return tokens[1] } } else { if hashsum(salt, tokens[0]) == user { return tokens[1] } } } } return "" } func (a *auth) Init(authfile string, user string) error { file, err := os.Open(authfile) if err != nil { return fmt.Errorf("authfile: %w", err) } a.file = file if user != "" { pass := getpass(file, "", user) if pass == "" { file.Close() return fmt.Errorf("authfile: no pass for user '%s'", user) } a.user = user a.pass = pass } b := make([]byte, saltSize) if _, err := rand.Read(b); err != nil { return err } a.salt = string(b) a.tmr = time.NewTimer(authTimeout) return nil } func (a *auth) sync(c chan struct{}) error { select { case <-a.tmr.C: return errTimeout case <-a.recver: return io.EOF case <-a.sender: return io.EOF case <-c: return nil } } func (a *auth) Send(rq, wq queue.Q) error { w := wq.Writer() io.WriteString(w, a.salt) if a.passive { io.WriteString(w, hashNone) } else { io.WriteString(w, hashsum(a.salt, a.user)) } if err := a.sync(a.stageHello); err != nil { return err } if a.passive && a.peer.user == hashNone { close(a.sender) return fmt.Errorf("can not be passive together") } var pass string if a.peer.user == hashNone { pass = hashsum(a.pass) } else { pass = getpass(a.file, a.peer.salt, a.peer.user) if pass == "" { close(a.sender) return fmt.Errorf("no pass for peer") } if a.passive { a.pass = hashsum(pass) } } io.WriteString(w, hashsum(a.peer.salt, pass)) if err := a.sync(a.stageHash); err != nil { return err } if a.hash != hashsum(a.salt, a.pass) { close(a.sender) return errAuthFail } close(a.ready) return queue.IoCopy(rq.Reader(), w) } func (a *auth) read(r io.Reader, n int, s *string) error { b := make([]byte, n) if _, err := io.ReadFull(r, b); err != nil { close(a.recver) return err } *s = string(b) return nil } func (a *auth) Recv(rq, wq queue.Q) error { r := rq.Reader() if err := a.read(r, saltSize, &a.peer.salt); err != nil { return err } if err := a.read(r, hashSize, &a.peer.user); err != nil { return err } close(a.stageHello) if err := a.read(r, hashSize, &a.hash); err != nil { return err } close(a.stageHash) if err := a.sync(a.ready); err != nil { return err } a.tmr.Stop() return queue.IoCopy(r, wq.Writer()) } func (a *auth) Close() { a.file.Close() } func (h *authHook) New(env env.Env) (interface{}, error) { file := env.Value("authfile") if file == "" { return nil, errors.New("no authfile configured") } user := "" if !h.Passive { user = env.Value("authuser") if user == "" { return nil, errors.New("no authuser configured") } } a := &auth{ passive: h.Passive, ready: make(chan struct{}), recver: make(chan struct{}), sender: make(chan struct{}), stageHello: make(chan struct{}), stageHash: make(chan struct{}), } if err := a.Init(file, user); err != nil { return nil, err } return a, nil } func init() { register("auth", "chap out/in", authHook{}) }