1
1
mirror of https://github.com/therootcompany/sclient synced 2026-10-30 22:54:56 +00:00

feat: authenticate via HTTP/1.1 before establishing SSH tunnel

Add --http-auth and --auth-token flags to perform HTTP authentication
over TLS (with http/1.1 ALPN) before establishing the SSH tunnel.

Add --http-upgrade flag to use HTTP Upgrade header mechanism, allowing
the proxy to hijack/switch the connection to SSH on the same TLS
connection (similar to WebSocket upgrade but without websockets).

Two modes:
- Without --http-upgrade: Auth connection is closed after auth, then a
  second TLS+SSH connection is established (two connections)
- With --http-upgrade <proto>: After auth, if proxy responds with 101
  Switching Protocols + Upgrade: <proto>, the same connection is used
  for SSH (single connection, protocol switch)

No changes to existing behavior when --http-auth is not used.
This commit is contained in:
AJ ONeal 2026-04-30 23:42:19 -06:00
parent d806c1853c
commit 01501f4c60
No known key found for this signature in database
2 changed files with 161 additions and 25 deletions

View File

@ -51,6 +51,9 @@ func main() {
var insecure bool
var servername string
var silent bool
var httpAuthURL string
var authToken string
var httpUpgradeProtocol string
flag.Usage = usage
@ -60,6 +63,9 @@ func main() {
flag.StringVar(&servername, "servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do not use this option)")
flag.BoolVar(&insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates")
flag.BoolVar(&silent, "silent", false, "less verbose output")
flag.StringVar(&httpAuthURL, "http-auth", "", "authenticate via HTTP/1.1 before establishing tunnel (ex: 'proxy.example.com/auth')")
flag.StringVar(&authToken, "auth-token", "", "authentication token (sent as Bearer token in Authorization header)")
flag.StringVar(&httpUpgradeProtocol, "http-upgrade", "", "protocol to upgrade to after authentication (e.g., 'ssh') - uses HTTP Upgrade header to switch protocols on the same connection")
flag.Parse()
@ -78,6 +84,18 @@ func main() {
}
}
authTokenFromURL := ""
if httpAuthURL != "" {
if idx := strings.Index(httpAuthURL, "token="); idx != -1 {
authTokenFromURL = httpAuthURL[idx+6:]
if idx2 := strings.Index(authTokenFromURL, "&"); idx2 != -1 {
authTokenFromURL = authTokenFromURL[:idx2]
} else if idx2 := strings.Index(authTokenFromURL, " "); idx2 != -1 {
authTokenFromURL = authTokenFromURL[:idx2]
}
}
}
sclient := &sclient.Tunnel{
RemotePort: 443,
LocalAddress: "localhost",
@ -85,6 +103,13 @@ func main() {
ServerName: servername,
Silent: silent,
NextProtos: alpns,
HttpAuthURL: httpAuthURL,
AuthToken: authToken,
HttpUpgradeProtocol: httpUpgradeProtocol,
}
if sclient.AuthToken == "" {
sclient.AuthToken = authTokenFromURL
}
remote := strings.Split(remotestr, ":")

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
@ -20,16 +21,76 @@ type Tunnel struct {
NextProtos []string
ServerName string
Silent bool
HttpAuthURL string
AuthToken string
HttpUpgradeProtocol string
}
func (t *Tunnel) httpAuth(conn net.Conn) (bool, error) {
url := t.HttpAuthURL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
}
parts := strings.SplitN(url, "//", 2)
path := "/"
if idx := strings.Index(parts[1], "/"); idx != -1 {
path = parts[1][idx:]
}
host := parts[0]
req, _ := http.NewRequest("GET", path, nil)
req.Host = host
if t.AuthToken != "" {
req.Header.Set("Authorization", "Bearer "+t.AuthToken)
}
if t.HttpUpgradeProtocol != "" {
req.Header.Set("Upgrade", t.HttpUpgradeProtocol)
}
req.ContentLength = 0
req.Close = true
if err := req.Write(conn); nil != err {
return false, err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if nil != err {
return false, err
}
resp := string(buf[:n])
if t.HttpUpgradeProtocol != "" {
if strings.Contains(resp, "101") && strings.Contains(strings.ToLower(resp), "upgrade: "+strings.ToLower(t.HttpUpgradeProtocol)) {
return true, nil
}
if !strings.Contains(resp, "200") && !strings.Contains(resp, "201") && !strings.Contains(resp, "202") && !strings.Contains(resp, "204") {
return false, fmt.Errorf("authentication failed: %s", resp)
}
}
if !strings.Contains(resp, "200") && !strings.Contains(resp, "201") && !strings.Contains(resp, "202") && !strings.Contains(resp, "204") {
return false, fmt.Errorf("authentication failed: %s", resp)
}
return false, nil
}
// DialAndListen will create a test TLS connection to the remote address and then
// begin listening locally. Each local connection will result in a separate remote connection.
func (t *Tunnel) DialAndListen() error {
remote := t.RemoteAddress + ":" + strconv.Itoa(t.RemotePort)
var nextProtos []string = t.NextProtos
if t.HttpAuthURL != "" {
nextProtos = []string{"http/1.1"}
}
conn, err := tls.Dial("tcp", remote,
&tls.Config{
ServerName: t.ServerName,
NextProtos: t.NextProtos,
NextProtos: nextProtos,
InsecureSkipVerify: t.InsecureSkipVerify,
})
@ -142,10 +203,44 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
}
func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
sclient, err := tls.Dial("tcp", remote,
var sclient net.Conn
if t.HttpAuthURL != "" {
authConn, err := tls.Dial("tcp", remote,
&tls.Config{
ServerName: t.ServerName,
NextProtos: t.NextProtos,
NextProtos: []string{"http/1.1"},
InsecureSkipVerify: t.InsecureSkipVerify,
})
if err != nil {
fmt.Fprintf(os.Stderr, "[error] (auth connection) %s\n", err)
_ = conn.Close()
return
}
upgraded, err := t.httpAuth(authConn)
if nil != err {
fmt.Fprintf(os.Stderr, "[error] (authentication) %s\n", err)
_ = conn.Close()
_ = authConn.Close()
return
}
if t.HttpUpgradeProtocol != "" && upgraded {
sclient = authConn
} else {
_ = authConn.Close()
var nextProtos []string = t.NextProtos
if len(nextProtos) == 0 {
nextProtos = []string{"ssh"}
}
sclient, err = tls.Dial("tcp", remote,
&tls.Config{
ServerName: t.ServerName,
NextProtos: nextProtos,
InsecureSkipVerify: t.InsecureSkipVerify,
})
@ -154,6 +249,22 @@ func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
_ = conn.Close()
return
}
}
} else {
var dialErr error
sclient, dialErr = tls.Dial("tcp", remote,
&tls.Config{
ServerName: t.ServerName,
NextProtos: t.NextProtos,
InsecureSkipVerify: t.InsecureSkipVerify,
})
if dialErr != nil {
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", dialErr)
_ = conn.Close()
return
}
}
if !t.Silent {
if conn.RemoteAddr().Network() == "stdio" {