featurebase/pg/protocol.go

581 lines
16 KiB
Go

// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pg
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net"
"strings"
"sync"
"time"
"github.com/pilosa/pilosa/v2/pg/message"
"github.com/pkg/errors"
)
// Protocol is a Postgres protocol version.
type Protocol uint32
const (
// ProtocolPostgres30 is version 3.0 of the Postgres wire protocol.
ProtocolPostgres30 Protocol = (3 << 16)
// ProtocolCancel is the protocol used for query cancellation.
ProtocolCancel Protocol = (1234 << 16) | 5678
// ProtocolSSL is the protocol used for SSL upgrades.
ProtocolSSL Protocol = (1234 << 16) | 5679
// ProtocolSupported is the main protocol version supported by this package.
ProtocolSupported Protocol = ProtocolPostgres30
)
// Major returns the major revision of the protocol.
func (p Protocol) Major() uint16 {
return uint16(p >> 16)
}
// Minor returns the minor revision of the protocol.
func (p Protocol) Minor() uint16 {
return uint16(p)
}
func (p Protocol) String() string {
switch p {
case ProtocolCancel:
return "cancel"
case ProtocolSSL:
return "SSL"
}
return fmt.Sprintf("v%d.%d", p.Major(), p.Minor())
}
// handle reads the startup packet and dispatches an appropriate protocol handler for the connection.
func (s *Server) handle(ctx context.Context, conn net.Conn) (err error) {
var hasTLS bool
defer func() {
cerr := conn.Close()
if cerr != nil && err == nil {
if hasTLS {
if nerr, ok := cerr.(net.Error); ok && nerr.Timeout() {
// TLS does this sometimes.
return
}
}
err = errors.Wrap(cerr, "closing connection")
}
}()
if tcpconn, ok := conn.(*net.TCPConn); ok {
// Postgres does not have any real mechanism for confirming that a connection is still alive.
// Without this, a connection that breaks while idle would live indefinitely.
// With a TCP keepalive, this should return an error after approximately 2 hours (depending on OS configuration).
err := tcpconn.SetKeepAlive(true)
if err != nil {
return errors.Wrap(err, "enabling TCP keepalive")
}
}
var startupDeadline time.Time
if s.StartupTimeout > 0 {
// Set deadline for processing the startup.
startupDeadline = time.Now().Add(s.StartupTimeout)
err = conn.SetDeadline(startupDeadline)
if err != nil {
return errors.Wrap(err, "setting deadline on protocol startup")
}
}
startup:
// Read startup packet.
var buf [4]byte
_, err = io.ReadFull(conn, buf[:])
if err != nil {
return errors.Wrap(err, "reading startup message length")
}
size := binary.BigEndian.Uint32(buf[:])
if size < 4 {
return errors.Errorf("invalid startup packet length: %d bytes", size)
}
maxLen := s.MaxStartupSize
if maxLen == 0 {
maxLen = 1024 * 1024
}
if size > maxLen {
return errors.Errorf("oversized startup frame of %d bytes (max: %d bytes)", size, maxLen)
}
data := make([]byte, size-4)
_, err = io.ReadFull(conn, data)
if err != nil {
return errors.Wrap(err, "reading startup packet")
}
// Extract protocol ID.
if len(data) < 4 {
return errors.Errorf("startup packet is too small for protocol ID: %d bytes", len(data))
}
proto := Protocol(binary.BigEndian.Uint32(data))
data = data[4:]
if proto == ProtocolSSL {
if s.TLSConfig != nil {
// Upgrade the connection to TLS and renegotiate on the tunneled connection.
_, err = conn.Write([]byte{'S'})
if err != nil {
return errors.Wrap(err, "sending SSL support confirmation")
}
conn = tls.Server(conn, s.TLSConfig)
if s.StartupTimeout > 0 {
err := conn.SetDeadline(startupDeadline)
if err != nil {
return errors.Wrap(err, "transferring startup deadline to TLS connection")
}
}
hasTLS = true
goto startup
}
// Inform the client that SSL is not available and try again.
s.Logger.Debugf("client at %s requested a secure postgres connection but TLS is not configured", conn.RemoteAddr())
_, err = conn.Write([]byte{'N'})
if err != nil {
return errors.Wrap(err, "sending SSL unsupported notification")
}
goto startup
}
if s.TLSConfig != nil && !hasTLS {
// Reject the unsecured connection.
return errors.Errorf("client at %s attempted to initiate an unsecured postgres conenction", conn.RemoteAddr())
}
switch proto {
case ProtocolCancel:
// Handle cancellation.
return s.handleCancel(ctx, conn, data)
default:
// Handle regular postgres.
return s.handleStandard(ctx, proto, conn, data)
}
}
// parseParams parses a parameter list from a startup packet.
func parseParams(data []byte) (map[string]string, error) {
params := make(map[string]string)
for {
idx := bytes.IndexByte(data, 0)
switch idx {
case 0:
return params, nil
case -1:
return nil, errors.New("malformed startup parameter list")
}
key := string(data[:idx])
data = data[idx+1:]
idx = bytes.IndexByte(data, 0)
if idx == -1 {
return nil, errors.New("malformed startup parameter list")
}
val := string(data[:idx])
data = data[idx+1:]
params[key] = val
}
}
// handleCancel handles cancel request connections.
func (s *Server) handleCancel(ctx context.Context, conn net.Conn, data []byte) error {
if len(data) != 8 {
return errors.New("malformed cancellation packet")
}
if s.CancellationManager == nil {
return errors.New("cancellation is not configured")
}
pid := int32(binary.BigEndian.Uint32(data[:4]))
key := int32(binary.BigEndian.Uint32(data[4:]))
err := s.CancellationManager.Cancel(CancellationToken{PID: pid, Key: key})
switch err {
case nil:
case ErrCancelledMissingConnection:
// This is usually not a real error (race condition in the protocol).
// This can happen if a client cancels a request and shuts down.
s.Logger.Debugf("client at %v sent a mismatched cancellation token (is a load balancer misconfigured?)", conn.RemoteAddr())
default:
return err
}
return nil
}
// handleStandard handles a connection in the standard postgres wire protocol.
// The client is responsible for closing the connection when this finishes.
func (s *Server) handleStandard(ctx context.Context, proto Protocol, conn net.Conn, data []byte) error {
// Wait for helper goroutines to finish.
var wg sync.WaitGroup
defer wg.Wait()
// Set up context.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Check the major version.
if proto.Major() != ProtocolSupported.Major() {
return errors.Errorf("unsupported protocol %v", proto)
}
// Parse the parameters bundled in the startup packet.
params, err := parseParams(data)
if err != nil {
return errors.Wrap(err, "parsing parameters")
}
if user, ok := params["user"]; ok {
// Log the connection.
s.Logger.Debugf("new postgres connection from user %q at %v", user, conn.RemoteAddr())
} else {
// We do not use this much yet, but the wire protocol says that it is required.
return errors.New("missing username")
}
// Set up message input and output.
// Set up a reader that will preempt the connection when the context is canceled.
ir := idleReader{
conn: conn,
timeout: s.ReadTimeout,
}
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
ir.preempt() //nolint:errcheck
}()
// Clear the startup deadline.
err = conn.SetDeadline(time.Time{})
if err != nil {
return err
}
// Set up a message reader with buffering.
rbuf := bufio.NewReader(&ir)
r := message.NewWireReader(rbuf)
// Set up a writer on the connection.
var ww io.Writer = conn
if s.WriteTimeout != 0 {
// Apply the write timeout.
ww = &timeoutWriter{
conn: conn,
timeout: s.WriteTimeout,
}
}
// Set up a message writer with buffering.
w := message.NewWireWriter(bufio.NewWriter(ww))
var encoder message.Encoder
if proto.Minor() > ProtocolSupported.Minor() {
// Negotiate the version down.
s.Logger.Debugf("client requested unsupported protocol version %v; attempting to downgrade to %v", proto, ProtocolSupported)
msg, err := encoder.NegotiateProtocolVersion(int32(ProtocolSupported.Minor()))
if err != nil {
return errors.Wrap(err, "negotiating version")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "negotiating version")
}
}
// TODO: real auth
err = w.WriteMessage(message.AuthenticationOK)
if err != nil {
return errors.Wrap(err, "sending authentication confirmation")
}
var cancelNotify <-chan struct{}
if s.CancellationManager != nil {
notify, cancel, token, err := s.CancellationManager.Token()
if err != nil {
return errors.Wrap(err, "setting up cancellation")
}
defer cancel()
msg, err := encoder.BackendKeyData(token.PID, token.Key)
if err != nil {
return errors.Wrap(err, "encoding cancellation key data")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "sending cancellation key data")
}
cancelNotify = notify
}
var queryReady bool
for {
if !queryReady {
// Indicate that we are ready for a query.
// TODO: provide a valid transaction state.
msg, err := encoder.ReadyForQuery(message.TransactionStatusActive)
if err != nil {
return errors.Wrap(err, "sending query ready status")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "sending query ready status")
}
// Flush the write buffer so that the client can respond.
err = w.Flush()
if err != nil {
return errors.Wrap(err, "flushing status")
}
if rbuf.Buffered() == 0 {
// Put the connection into idle mode.
err = ir.setIdle()
if err != nil {
return errors.Wrap(err, "setting idle mode")
}
} else {
// If the client follows the spec, then it should not have sent anything more.
// However, it seems that no clients completely follow the spec, so we shouldn't rely on anything that isn't entirely straightforward.
s.Logger.Debugf("postgres client sent additional data without waiting for completion")
}
}
// Read the next packet.
msg, err := r.ReadMessage()
if err != nil {
if err == errPreempted {
// The server is shutting down.
return errors.Wrap(s.handleShutdown(
conn, w, &encoder,
message.NoticeField{
Type: message.NoticeFieldSeverity,
Data: "ERROR",
},
message.NoticeField{
Type: message.NoticeFieldMessage,
Data: "server shutting down",
},
message.NoticeField{
Type: message.NoticeFieldHint,
Data: "This is normal. This message is sent when a server is shutting down and terminating its connections.",
},
), "processing connection shutdown")
}
return err
}
switch msg.Type {
case message.TypeTermination:
// We are done.
return w.Flush()
case message.TypeSimpleQuery:
// Execute a simple query.
queryReady = false
// Parse the query message (a null-terminated string).
query := SimpleQuery(strings.TrimSuffix(string(msg.Data), "\x00"))
// Execute the query.
err := s.handleQuery(w, query, cancelNotify)
if err != nil {
return err
}
default:
// The message is not supported yet.
// Send an error.
s.Logger.Printf("unrecognized postgres packet %v", msg)
msg, err = encoder.Error(
message.NoticeField{
Type: message.NoticeFieldSeverity,
Data: "ERROR",
},
message.NoticeField{
Type: message.NoticeFieldMessage,
Data: fmt.Sprintf("unrecognized message type %q", msg.Type),
},
message.NoticeField{
Type: message.NoticeFieldDetail,
Data: "message body:" + hex.Dump(msg.Data),
},
)
if err != nil {
return errors.Wrap(err, "sending unrecognized message error")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "sending unrecognized message error")
}
err = w.Flush()
if err != nil {
return errors.Wrap(err, "sending unrecognized message error")
}
}
}
}
// handleQuery processes a single query on a connection.
func (s *Server) handleQuery(w message.Writer, query Query, cancelNotify <-chan struct{}) error {
// Configure cancellation.
// This is not the connection context, since we want the request to finish safely before connection shutdown.
ctx := context.Background()
if cancelNotify != nil {
defer func() {
// Flush any cancel notifications.
// This works on a best-effort basis.
// It is still entirely possible that the cancel notification may be delivered to the next request.
// Regardless of what we do, we either get false positives or false negatives.
// This code chooses false positives.
for len(cancelNotify) > 0 {
<-cancelNotify
}
}()
var wg sync.WaitGroup
defer wg.Add(1)
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-ctx.Done():
case <-cancelNotify:
cancel()
}
}()
}
// Set up a result writer.
// SELECT is used as a default tag, which seems to be handled decently by clients.
// The encoder is intentionally not re-used because its buffer may be huge.
qwriter := &queryResultWriter{
w: w,
te: s.TypeEngine,
tag: "SELECT",
}
// Dispatch the query handler.
qerr := s.QueryHandler.HandleQuery(ctx, qwriter, query)
if qerr != nil {
// There was an error in processing the query.
// Send the error back to the client and keep going.
s.Logger.Debugf("failed to execute query %q: %v", query, qerr)
msg, err := qwriter.enc.GoError(qerr)
if err != nil {
return errors.Wrap(err, "failed to send query error to client")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "failed to send query error to client")
}
} else {
if !qwriter.wroteHeaders {
// The handler did not write headers.
// Write back an empty set of headers.
err := qwriter.WriteHeader()
if err != nil {
return errors.Wrap(err, "sending empty column headers")
}
}
// The query completed normally.
// Notify the client of completion.
msg, err := qwriter.enc.CommandComplete(qwriter.tag)
if err != nil {
return errors.Wrap(err, "sending command completion notification")
}
err = w.WriteMessage(msg)
if err != nil {
return errors.Wrap(err, "sending command completion notification")
}
}
// The data will be flushed after we write back the "ready for query" state.
return nil
}
func (s *Server) handleShutdown(conn net.Conn, w message.Writer, encoder *message.Encoder, notice ...message.NoticeField) error {
var wg sync.WaitGroup
defer wg.Wait()
// Try to send a message to the client before closing the connection.
msg, err := encoder.Error(notice...)
if err != nil {
return errors.Wrap(err, "generating shutdown notification")
}
if s.WriteTimeout == 0 {
// The client is likely to not listen for incoming messages.
// Force a write timeout to ensure that this terminates.
err := conn.SetWriteDeadline(time.Now().Add(time.Second))
if err != nil {
return errors.Wrap(err, "setting shutdown write deadline")
}
}
// The client may be waiting on a write, so we need to drain the incoming data stream.
err = conn.SetReadDeadline(time.Time{})
if err != nil {
return errors.Wrap(err, "clearing read deadline for shutdown")
}
defer conn.SetReadDeadline(time.Now()) //nolint:errcheck
wg.Add(1)
go func() {
defer wg.Done()
io.Copy(ioutil.Discard, conn) //nolint:errcheck
}()
// Attempt to send the shutdown notification.
// This will fail under many scenarios, as the client is not necessarily reading.
err = w.WriteMessage(msg)
if err != nil {
return nil
}
w.Flush()
return nil
}