Files
jpillora-chisel/server/server_handler.go

167 lines
4.5 KiB
Go

package chserver
import (
"net/http"
"strings"
"sync/atomic"
"time"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/chisel/share/tunnel"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
)
// handleClientHandler is the main http websocket handler for the chisel server
func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {
//websockets upgrade AND has chisel prefix
upgrade := strings.ToLower(r.Header.Get("Upgrade"))
protocol := r.Header.Get("Sec-WebSocket-Protocol")
if upgrade == "websocket" {
if protocol == chshare.ProtocolVersion {
s.handleWebsocket(w, r)
return
}
//print into server logs and silently fall-through
s.Infof("ignored client connection using protocol '%s', expected '%s'",
protocol, chshare.ProtocolVersion)
}
//proxy target was provided
if s.reverseProxy != nil {
s.reverseProxy.ServeHTTP(w, r)
return
}
//no proxy defined, provide access to health/version checks
switch r.URL.Path {
case "/health":
w.Write([]byte("OK\n"))
return
case "/version":
w.Write([]byte(chshare.BuildVersion))
return
}
//missing :O
w.WriteHeader(404)
w.Write([]byte("Not found"))
}
// handleWebsocket is responsible for handling the websocket connection
func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
id := atomic.AddInt32(&s.sessCount, 1)
l := s.Fork("session#%d", id)
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
l.Debugf("Failed to upgrade (%s)", err)
return
}
conn := cnet.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
l.Debugf("Handshaking with %s...", req.RemoteAddr)
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
if err != nil {
s.Debugf("Failed to handshake (%s)", err)
return
}
// pull the users from the session map
var user *settings.User
if s.users.Len() > 0 {
sid := string(sshConn.SessionID())
u, ok := s.sessions.Get(sid)
if !ok {
panic("bug in ssh auth handler")
}
user = u
s.sessions.Del(sid)
}
// chisel server handshake (reverse of client handshake)
// verify configuration
l.Debugf("Verifying configuration")
// wait for request, with timeout
var r *ssh.Request
select {
case r = <-reqs:
case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)):
l.Debugf("Timeout waiting for configuration")
sshConn.Close()
return
}
failed := func(err error) {
l.Debugf("Failed: %s", err)
r.Reply(false, []byte(err.Error()))
}
if r.Type != "config" {
failed(s.Errorf("expecting config request"))
return
}
c, err := settings.DecodeConfig(r.Payload)
if err != nil {
failed(s.Errorf("invalid config"))
return
}
//print if client and server versions dont match
cv := strings.TrimPrefix(c.Version, "v")
if cv == "" {
cv = "<unknown>"
}
sv := strings.TrimPrefix(chshare.BuildVersion, "v")
if cv != sv {
l.Infof("Client version (%s) differs from server version (%s)", cv, sv)
}
//validate remotes
for _, r := range c.Remotes {
//if user is provided, ensure they have
//access to the desired remotes
if user != nil {
addr := r.UserAddr()
if !user.HasAccess(addr) {
failed(s.Errorf("access to '%s' denied", addr))
return
}
}
//confirm reverse tunnels are allowed
if r.Reverse && !s.config.Reverse {
l.Debugf("Denied reverse port forwarding request, please enable --reverse")
failed(s.Errorf("Reverse port forwaring not enabled on server"))
return
}
//confirm reverse tunnel is available
if r.Reverse && !r.CanListen() {
failed(s.Errorf("Server cannot listen on %s", r.String()))
return
}
}
//successfuly validated config!
r.Reply(true, nil)
//tunnel per ssh connection
tunnel := tunnel.New(tunnel.Config{
Logger: l,
Inbound: s.config.Reverse,
Outbound: true, //server always accepts outbound
Socks: s.config.Socks5,
KeepAlive: s.config.KeepAlive,
})
//bind
eg, ctx := errgroup.WithContext(req.Context())
eg.Go(func() error {
//connected, handover ssh connection for tunnel to use, and block
return tunnel.BindSSH(ctx, sshConn, reqs, chans)
})
eg.Go(func() error {
//connected, setup reversed-remotes?
serverInbound := c.Remotes.Reversed(true)
if len(serverInbound) == 0 {
return nil
}
//block
return tunnel.BindRemotes(ctx, serverInbound)
})
err = eg.Wait()
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
l.Debugf("Closed connection (%s)", err)
} else {
l.Debugf("Closed connection")
}
}