1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
// Copyright (c) 2022-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package sftp
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"time"
"golang.org/x/crypto/ssh"
)
type (
// LogType defined various types of logs and errors
// that can happen within the SFTP implementation
LogType string
)
var (
// ErrMissingConnectionHandlerFunction ...
ErrMissingConnectionHandlerFunction = errors.New("new connection handler is not defined")
// ErrMissingSSHConfig ...
ErrMissingSSHConfig = errors.New("ssh Config is not defined")
// ErrMissingLoggerInterface ...
ErrMissingLoggerInterface = errors.New("logger interface is not defined")
// ErrInvalidPort ...
ErrInvalidPort = errors.New("port must not be 0 or bigger then 65535")
)
const (
// ServerStarted is logged when the SFTP server is first launched.
ServerStarted LogType = "server-started"
// ChannelNotSession is logged when the SFTP receives a request for a new channel which is NOT of type 'session'.
ChannelNotSession LogType = "channel-not-session"
// AcceptNetworkError is logged when there is an error accepting network connections within the listener.
AcceptNetworkError LogType = "accept-network-error"
// SSHKeyExchangeError is logged when there is an error performing a key exchange between the SFTP client and server.
SSHKeyExchangeError LogType = "ssh-key-exchange-error"
// AcceptChannelError is logged when there is an error while trying to accept the new request channel.
AcceptChannelError LogType = "accept-channel-error"
)
// Logger implements a basic logging interface
// for the SFTP server.
type Logger interface {
Info(tag LogType, msg string)
Error(tag LogType, err error)
}
// Server implements a composable SFTP Server.
type Server struct {
quit chan struct{}
port int
publicIP string
sshConfig ssh.ServerConfig
sshHandshakeDeadline time.Duration
logger Logger
beforeHandle func(conn net.Conn, err error) (acceptConn bool)
handleSFTPSession func(channel ssh.Channel, sconn *ssh.ServerConn)
listener net.Listener
}
// ShutDown calls the cancel context and shuts
// down the SFTP server.
func (s *Server) ShutDown() (err error) {
close(s.quit)
err = s.listener.Close()
return
}
// Options defines required configurations
// used when calling NewServer().
type Options struct {
Port int
PublicIP string
Logger Logger
SSHConfig *ssh.ServerConfig
// ConnectionKeepAlive controls how long the connection keep-alive duration is set to.
ConnectionKeepAlive time.Duration
// SSHHandshakeDeadline controls the time.Duration which ssh session
// have to complete their handshake. This option is not a part of the
// ssh.ServerConfig so we had to implement it separately.
SSHHandshakeDeadline time.Duration
// BeforeHandle will be executed before `HandleSFTPSession` and before
// error checking happens during the socket listener.Accept().
//
// if acceptConn is true the connection will be accepted, if not
// the .Close() method is called and the connection dropped.
BeforeHandle func(conn net.Conn, err error) (acceptConn bool)
// HandleSFTPSession is executed when a new SFTP session is requested.
HandleSFTPSession func(channel ssh.Channel, sconn *ssh.ServerConn)
}
// NewServer composes a new Server{} object from the options given.
//
// It is recommended to use (2*time.Minute) as the SSHHandshakeDeadline.
// 2 minutes is the default deadline for OpenSSH servers/clients.
func NewServer(options *Options) (sftpServer *Server, err error) {
if options.HandleSFTPSession == nil {
return nil, ErrMissingConnectionHandlerFunction
}
if options.SSHConfig == nil {
return nil, ErrMissingSSHConfig
}
if options.Logger == nil {
return nil, ErrMissingLoggerInterface
}
if options.Port < 1 || options.Port > 65535 {
return nil, ErrInvalidPort
}
// It is recommended to use (2*time.Minute) as the SSHHandshakeDeadline.
// 2 minutes is the default deadline for OpenSSH servers/clients.
if options.SSHHandshakeDeadline == 0 {
options.SSHHandshakeDeadline = time.Minute * 2
}
lc := new(net.ListenConfig)
if options.ConnectionKeepAlive != 0 {
lc.KeepAlive = options.ConnectionKeepAlive
}
sftpServer = new(Server)
// net.Listener does not respect the context cancelFunc.
// Hence we just pass it a normal context.Background()
sftpServer.listener, err = lc.Listen(
context.Background(),
"tcp",
net.JoinHostPort(options.PublicIP, strconv.Itoa(options.Port)),
)
if err != nil {
return
}
sftpServer.publicIP = options.PublicIP
sftpServer.port = options.Port
sftpServer.sshConfig = *options.SSHConfig
sftpServer.sshHandshakeDeadline = options.SSHHandshakeDeadline
sftpServer.beforeHandle = options.BeforeHandle
sftpServer.handleSFTPSession = options.HandleSFTPSession
sftpServer.logger = options.Logger
sftpServer.quit = make(chan struct{})
return
}
// Listen starts the SFTP server
func (s *Server) Listen() (err error) {
s.logger.Info(ServerStarted,
"SFTP Server listening on "+
net.JoinHostPort(s.publicIP, strconv.Itoa(s.port)),
)
for {
conn, err := s.listener.Accept()
if s.beforeHandle != nil && !s.beforeHandle(conn, err) {
if conn != nil {
conn.Close()
}
continue
}
if err != nil {
select {
case <-s.quit:
return nil
default:
}
// Temporary() is deprecated but since it's been deployed to
// current production builds I do not want to simply switch it out.
// ISSUE: https://github.com/golang/go/issues/45729
// According to golang updates the Temporary() functionality was
// not changed but the method simply marked deprecated, hence
// it is alright to keep it implemented for consistency.
// UPDATE: https://go-review.googlesource.com/c/go/+/340261
ne, ok := err.(net.Error)
if ok && ne.Timeout() {
s.logger.Error(
AcceptNetworkError,
fmt.Errorf("error accepting connections: %w", err),
)
continue
}
return err
}
go s.handleConnection(conn)
}
}
func (s *Server) handleConnection(conn net.Conn) {
// Before use, a handshake must be performed on the incoming net.Conn.
conn.SetDeadline(time.Now().Add(s.sshHandshakeDeadline))
sconn, chans, reqs, err := ssh.NewServerConn(conn, &s.sshConfig)
if err != nil {
s.logger.Error(SSHKeyExchangeError, err)
return
}
// Once we are done with SSH handshake, remove deadline.
conn.SetDeadline(time.Time{})
// The incoming Request channel must be serviced.
go ssh.DiscardRequests(reqs)
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of an SFTP session, this is "subsystem"
// with a payload string of "<length=4>sftp"
if newChannel.ChannelType() != "session" {
s.logger.Info(
ChannelNotSession,
"Channel type is not a session",
)
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
s.logger.Error(
AcceptChannelError,
fmt.Errorf("unable to accept request from channel: %w", err),
)
continue
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "subsystem" request.
go func(in <-chan *ssh.Request) {
for req := range in {
ok := false
if req.Type == "subsystem" {
if len(req.Payload) > 4 && string(req.Payload[4:]) == "sftp" {
ok = true
go s.handleSFTPSession(channel, sconn)
}
}
if req.WantReply {
// We only reply to SSH packets that have `sftp` payload, all other
// packets are rejected
req.Reply(ok, nil)
}
}
}(requests)
}
}
|