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) }