imap: add timeout to tcp connect functions
Extract the tcp connection details and timeout the tcp connect functions (net.ResolveTCPAddr and net.DialTCP). If timed out, ensure that the connection is properly closed. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
b92efe4cd9
commit
b29293d7b5
|
@ -0,0 +1,178 @@
|
||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connect establishes a new tcp connection to the imap server, logs in and
|
||||||
|
// selects the default inbox. If no error is returned, the imap client will be
|
||||||
|
// in the imap.SelectedState.
|
||||||
|
func (w *IMAPWorker) connect() (*client.Client, error) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
conn *net.TCPConn
|
||||||
|
err error
|
||||||
|
c *client.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
conn, err = newTCPConn(w.config.addr, w.config.connection_timeout)
|
||||||
|
if conn == nil || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.connection_timeout > 0 {
|
||||||
|
end := time.Now().Add(w.config.connection_timeout)
|
||||||
|
err = conn.SetDeadline(end)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.keepalive_period > 0 {
|
||||||
|
err = w.setKeepaliveParameters(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _, _ := net.SplitHostPort(w.config.addr)
|
||||||
|
tlsConfig := &tls.Config{ServerName: serverName}
|
||||||
|
|
||||||
|
switch w.config.scheme {
|
||||||
|
case "imap":
|
||||||
|
c, err = client.New(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !w.config.insecure {
|
||||||
|
if err = c.StartTLS(tlsConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "imaps":
|
||||||
|
tlsConn := tls.Client(conn, tlsConfig)
|
||||||
|
c, err = client.New(tlsConn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ErrorLog = w.worker.Logger
|
||||||
|
|
||||||
|
if w.config.user != nil {
|
||||||
|
username := w.config.user.Username()
|
||||||
|
password, hasPassword := w.config.user.Password()
|
||||||
|
if !hasPassword {
|
||||||
|
// TODO: ask password
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.oauthBearer.Enabled {
|
||||||
|
if err := w.config.oauthBearer.Authenticate(
|
||||||
|
username, password, c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err := c.Login(username, password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetDebug(w.worker.Logger.Writer())
|
||||||
|
|
||||||
|
if _, err := c.Select(imap.InboxName, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTCPConn establishes a new tcp connection. Timeout will ensure that the
|
||||||
|
// function does not hang when there is no connection. If there is a timeout,
|
||||||
|
// but a valid connection is eventually returned, ensure that it is properly
|
||||||
|
// closed.
|
||||||
|
func newTCPConn(addr string, timeout time.Duration) (*net.TCPConn, error) {
|
||||||
|
|
||||||
|
var errTCPTimeout = fmt.Errorf("tcp connection timeout")
|
||||||
|
|
||||||
|
type tcpConn struct {
|
||||||
|
conn *net.TCPConn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan tcpConn)
|
||||||
|
go func() {
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
done <- tcpConn{nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConn, err := net.DialTCP("tcp", nil, addr)
|
||||||
|
if err != nil {
|
||||||
|
done <- tcpConn{nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done <- tcpConn{newConn, nil}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(timeout):
|
||||||
|
go func() {
|
||||||
|
if tcpResult := <-done; tcpResult.conn != nil {
|
||||||
|
tcpResult.conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil, errTCPTimeout
|
||||||
|
case tcpResult := <-done:
|
||||||
|
if tcpResult.conn == nil || tcpResult.err != nil {
|
||||||
|
return nil, tcpResult.err
|
||||||
|
}
|
||||||
|
return tcpResult.conn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set additional keepalive parameters.
|
||||||
|
// Uses new interfaces introduced in Go1.11, which let us get connection's file
|
||||||
|
// descriptor, without blocking, and therefore without uncontrolled spawning of
|
||||||
|
// threads (not goroutines, actual threads).
|
||||||
|
func (w *IMAPWorker) setKeepaliveParameters(conn *net.TCPConn) error {
|
||||||
|
err := conn.SetKeepAlive(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Idle time before sending a keepalive probe
|
||||||
|
err = conn.SetKeepAlivePeriod(w.config.keepalive_period)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawConn, e := conn.SyscallConn()
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
err = rawConn.Control(func(fdPtr uintptr) {
|
||||||
|
fd := int(fdPtr)
|
||||||
|
// Max number of probes before failure
|
||||||
|
err := lib.SetTcpKeepaliveProbes(fd, w.config.keepalive_probes)
|
||||||
|
if err != nil {
|
||||||
|
w.worker.Logger.Printf(
|
||||||
|
"cannot set tcp keepalive probes: %v\n", err)
|
||||||
|
}
|
||||||
|
// Wait time after an unsuccessful probe
|
||||||
|
err = lib.SetTcpKeepaliveInterval(fd, w.config.keepalive_interval)
|
||||||
|
if err != nil {
|
||||||
|
w.worker.Logger.Printf(
|
||||||
|
"cannot set tcp keepalive interval: %v\n", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
package imap
|
package imap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,6 +23,7 @@ func init() {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errUnsupported = fmt.Errorf("unsupported command")
|
errUnsupported = fmt.Errorf("unsupported command")
|
||||||
|
errClientNotReady = fmt.Errorf("client not ready")
|
||||||
errNotConnected = fmt.Errorf("not connected")
|
errNotConnected = fmt.Errorf("not connected")
|
||||||
errAlreadyConnected = fmt.Errorf("already connected")
|
errAlreadyConnected = fmt.Errorf("already connected")
|
||||||
)
|
)
|
||||||
|
@ -93,6 +92,15 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
|
||||||
|
|
||||||
var reterr error // will be returned at the end, needed to support idle
|
var reterr error // will be returned at the end, needed to support idle
|
||||||
|
|
||||||
|
// when client is nil allow only certain messages to be handled
|
||||||
|
if w.client == nil {
|
||||||
|
switch msg.(type) {
|
||||||
|
case *types.Connect, *types.Reconnect, *types.Disconnect, *types.Configure:
|
||||||
|
default:
|
||||||
|
return errClientNotReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set connection timeout for calls to imap server
|
// set connection timeout for calls to imap server
|
||||||
if w.client != nil {
|
if w.client != nil {
|
||||||
w.client.Timeout = w.config.connection_timeout
|
w.client.Timeout = w.config.connection_timeout
|
||||||
|
@ -251,124 +259,6 @@ func (w *IMAPWorker) handleImapUpdate(update client.Update) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *IMAPWorker) connect() (*client.Client, error) {
|
|
||||||
var (
|
|
||||||
conn *net.TCPConn
|
|
||||||
c *client.Client
|
|
||||||
)
|
|
||||||
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", w.config.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err = net.DialTCP("tcp", nil, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.config.connection_timeout > 0 {
|
|
||||||
end := time.Now().Add(w.config.connection_timeout)
|
|
||||||
err = conn.SetDeadline(end)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if w.config.keepalive_period > 0 {
|
|
||||||
err = w.setKeepaliveParameters(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverName, _, _ := net.SplitHostPort(w.config.addr)
|
|
||||||
tlsConfig := &tls.Config{ServerName: serverName}
|
|
||||||
|
|
||||||
switch w.config.scheme {
|
|
||||||
case "imap":
|
|
||||||
c, err = client.New(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !w.config.insecure {
|
|
||||||
if err = c.StartTLS(tlsConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "imaps":
|
|
||||||
tlsConn := tls.Client(conn, tlsConfig)
|
|
||||||
c, err = client.New(tlsConn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.ErrorLog = w.worker.Logger
|
|
||||||
|
|
||||||
if w.config.user != nil {
|
|
||||||
username := w.config.user.Username()
|
|
||||||
password, hasPassword := w.config.user.Password()
|
|
||||||
if !hasPassword {
|
|
||||||
// TODO: ask password
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.config.oauthBearer.Enabled {
|
|
||||||
if err := w.config.oauthBearer.Authenticate(
|
|
||||||
username, password, c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if err := c.Login(username, password); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetDebug(w.worker.Logger.Writer())
|
|
||||||
|
|
||||||
if _, err := c.Select(imap.InboxName, false); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set additional keepalive parameters.
|
|
||||||
// Uses new interfaces introduced in Go1.11, which let us get connection's file
|
|
||||||
// descriptor, without blocking, and therefore without uncontrolled spawning of
|
|
||||||
// threads (not goroutines, actual threads).
|
|
||||||
func (w *IMAPWorker) setKeepaliveParameters(conn *net.TCPConn) error {
|
|
||||||
err := conn.SetKeepAlive(true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Idle time before sending a keepalive probe
|
|
||||||
err = conn.SetKeepAlivePeriod(w.config.keepalive_period)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rawConn, e := conn.SyscallConn()
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
err = rawConn.Control(func(fdPtr uintptr) {
|
|
||||||
fd := int(fdPtr)
|
|
||||||
// Max number of probes before failure
|
|
||||||
err := lib.SetTcpKeepaliveProbes(fd, w.config.keepalive_probes)
|
|
||||||
if err != nil {
|
|
||||||
w.worker.Logger.Printf(
|
|
||||||
"cannot set tcp keepalive probes: %v\n", err)
|
|
||||||
}
|
|
||||||
// Wait time after an unsuccessful probe
|
|
||||||
err = lib.SetTcpKeepaliveInterval(fd, w.config.keepalive_interval)
|
|
||||||
if err != nil {
|
|
||||||
w.worker.Logger.Printf(
|
|
||||||
"cannot set tcp keepalive interval: %v\n", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *IMAPWorker) Run() {
|
func (w *IMAPWorker) Run() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
Loading…
Reference in New Issue