Files
Jaime Pillora 69093be1d9 Bump to Go 1.21 (#440)
Co-authored-by: cmeng <cmenginnz@gmail.com>
2023-08-19 11:00:41 +10:00

245 lines
6.1 KiB
Go

package chserver
import (
"context"
"errors"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"time"
"github.com/gorilla/websocket"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/ccrypto"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/requestlog"
"golang.org/x/crypto/ssh"
)
// Config is the configuration for the chisel service
type Config struct {
KeySeed string
KeyFile string
AuthFile string
Auth string
Proxy string
Socks5 bool
Reverse bool
KeepAlive time.Duration
TLS TLSConfig
}
// Server respresent a chisel service
type Server struct {
*cio.Logger
config *Config
fingerprint string
httpServer *cnet.HTTPServer
reverseProxy *httputil.ReverseProxy
sessCount int32
sessions *settings.Users
sshConfig *ssh.ServerConfig
users *settings.UserIndex
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
}
// NewServer creates and returns a new chisel server
func NewServer(c *Config) (*Server, error) {
server := &Server{
config: c,
httpServer: cnet.NewHTTPServer(),
Logger: cio.NewLogger("server"),
sessions: settings.NewUsers(),
}
server.Info = true
server.users = settings.NewUserIndex(server.Logger)
if c.AuthFile != "" {
if err := server.users.LoadUsers(c.AuthFile); err != nil {
return nil, err
}
}
if c.Auth != "" {
u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}
u.Name, u.Pass = settings.ParseAuth(c.Auth)
if u.Name != "" {
server.users.AddUser(u)
}
}
var pemBytes []byte
var err error
if c.KeyFile != "" {
var key []byte
if ccrypto.IsChiselKey([]byte(c.KeyFile)) {
key = []byte(c.KeyFile)
} else {
key, err = os.ReadFile(c.KeyFile)
if err != nil {
log.Fatalf("Failed to read key file %s", c.KeyFile)
}
}
pemBytes = key
if ccrypto.IsChiselKey(key) {
pemBytes, err = ccrypto.ChiselKey2PEM(key)
if err != nil {
log.Fatalf("Invalid key %s", string(key))
}
}
} else {
//generate private key (optionally using seed)
pemBytes, err = ccrypto.Seed2PEM(c.KeySeed)
if err != nil {
log.Fatal("Failed to generate key")
}
}
//convert into ssh.PrivateKey
private, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
log.Fatal("Failed to parse key")
}
//fingerprint this key
server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
//create ssh config
server.sshConfig = &ssh.ServerConfig{
ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server",
PasswordCallback: server.authUser,
}
server.sshConfig.AddHostKey(private)
//setup reverse proxy
if c.Proxy != "" {
u, err := url.Parse(c.Proxy)
if err != nil {
return nil, err
}
if u.Host == "" {
return nil, server.Errorf("Missing protocol (%s)", u)
}
server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
//always use proxy host
server.reverseProxy.Director = func(r *http.Request) {
//enforce origin, keep path
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
r.Host = u.Host
}
}
//print when reverse tunnelling is enabled
if c.Reverse {
server.Infof("Reverse tunnelling enabled")
}
return server, nil
}
// Run is responsible for starting the chisel service.
// Internally this calls Start then Wait.
func (s *Server) Run(host, port string) error {
if err := s.Start(host, port); err != nil {
return err
}
return s.Wait()
}
// Start is responsible for kicking off the http server
func (s *Server) Start(host, port string) error {
return s.StartContext(context.Background(), host, port)
}
// StartContext is responsible for kicking off the http server,
// and can be closed by cancelling the provided context
func (s *Server) StartContext(ctx context.Context, host, port string) error {
s.Infof("Fingerprint %s", s.fingerprint)
if s.users.Len() > 0 {
s.Infof("User authentication enabled")
}
if s.reverseProxy != nil {
s.Infof("Reverse proxy enabled")
}
l, err := s.listener(host, port)
if err != nil {
return err
}
h := http.Handler(http.HandlerFunc(s.handleClientHandler))
if s.Debug {
o := requestlog.DefaultOptions
o.TrustProxy = true
h = requestlog.WrapWith(h, o)
}
return s.httpServer.GoServe(ctx, l, h)
}
// Wait waits for the http server to close
func (s *Server) Wait() error {
return s.httpServer.Wait()
}
// Close forcibly closes the http server
func (s *Server) Close() error {
return s.httpServer.Close()
}
// GetFingerprint is used to access the server fingerprint
func (s *Server) GetFingerprint() string {
return s.fingerprint
}
// authUser is responsible for validating the ssh user / password combination
func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
// check if user authentication is enabled and if not, allow all
if s.users.Len() == 0 {
return nil, nil
}
// check the user exists and has matching password
n := c.User()
user, found := s.users.Get(n)
if !found || user.Pass != string(password) {
s.Debugf("Login failed for user: %s", n)
return nil, errors.New("Invalid authentication for username: %s")
}
// insert the user session map
// TODO this should probably have a lock on it given the map isn't thread-safe
s.sessions.Set(string(c.SessionID()), user)
return nil, nil
}
// AddUser adds a new user into the server user index
func (s *Server) AddUser(user, pass string, addrs ...string) error {
authorizedAddrs := []*regexp.Regexp{}
for _, addr := range addrs {
authorizedAddr, err := regexp.Compile(addr)
if err != nil {
return err
}
authorizedAddrs = append(authorizedAddrs, authorizedAddr)
}
s.users.AddUser(&settings.User{
Name: user,
Pass: pass,
Addrs: authorizedAddrs,
})
return nil
}
// DeleteUser removes a user from the server user index
func (s *Server) DeleteUser(user string) {
s.users.Del(user)
}
// ResetUsers in the server user index.
// Use nil to remove all.
func (s *Server) ResetUsers(users []*settings.User) {
s.users.Reset(users)
}