1
1
mirror of https://github.com/therootcompany/sclient synced 2025-10-07 09:28:19 +00:00

Compare commits

..

7 Commits

5 changed files with 109 additions and 42 deletions

View File

@ -3,25 +3,34 @@ before:
- go mod download - go mod download
- go generate ./... - go generate ./...
builds: builds:
- main: ./cmd/sclient/main.go - main: ./cmd/sclient/
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
- linux
- windows
- darwin - darwin
- linux
- freebsd
- windows
- js
goarch: goarch:
- 386
- amd64 - amd64
- arm - arm
- arm64 - arm64
- wasm
goarm: goarm:
- 6
- 7 - 7
goamd64:
- v2
ignore:
- goos: windows
goarch: 386
- goos: windows
goarm: 6
- goos: windows
goarm: 7
archives: archives:
- replacements: - id: sclient-binary
386: i386 format: tar.xz
amd64: x86_64
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip format: zip

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"singleQuote": false,
"bracketSpacing": true,
"proseWrap": "always",
"semi": true
}

View File

@ -1,6 +1,7 @@
# sclient # sclient
Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. Secure Client for exposing TLS (aka SSL) secured services as plain-text
connections locally.
Also ideal for multiplexing a single port with multiple protocols using SNI. Also ideal for multiplexing a single port with multiple protocols using SNI.
@ -30,7 +31,8 @@ cURL
curl http://localhost:3000 -H 'Host: whatever.com' curl http://localhost:3000 -H 'Host: whatever.com'
``` ```
A poor man's (or Windows user's) makeshift replacement for `openssl s_client`, `stunnel`, or `socat`. A poor man's (or Windows user's) makeshift replacement for `openssl s_client`,
`stunnel`, or `socat`.
# Table of Contents # Table of Contents
@ -53,9 +55,11 @@ curl.exe -A MS https://webinstall.dev/sclient | powershell
### Downloads ### Downloads
Check the [Github Releases](https://github.com/therootcompany/sclient/releases) for Check the [Github Releases](https://github.com/therootcompany/sclient/releases)
for
- macOS (x64) Apple Silicon [coming soon](https://github.com/golang/go/issues/39782) - macOS (x64) Apple Silicon
[coming soon](https://github.com/golang/go/issues/39782)
- Linux (x64, i386, arm64, arm6, arm7) - Linux (x64, i386, arm64, arm6, arm7)
- Windows 10 (x64, i386) - Windows 10 (x64, i386)
@ -66,8 +70,11 @@ sclient [flags] <remote> <local>
``` ```
- flags - flags
- -k, --insecure ignore invalid TLS (SSL/HTTPS) certificates - `-s`, `--silent` less verbose logging
- --servername <string> spoof SNI (to disable use IP as &lt;remote&gt; and do not use this option) - `-k`, `--insecure` ignore invalid TLS (SSL/HTTPS) certificates
- `--servername <domain>` spoof SNI (to disable use IP as &lt;remote&gt; and do
not use this option)
- `--alpn <protocol-list>`
- remote - remote
- must have servername (i.e. example.com) - must have servername (i.e. example.com)
- port is optional (default is 443) - port is optional (default is 443)
@ -75,6 +82,17 @@ sclient [flags] <remote> <local>
- address is optional (default is localhost) - address is optional (default is localhost)
- must have port (i.e. 3000) - must have port (i.e. 3000)
-alpn string
acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' (default) 'ssh' (default "http/1.1")
-insecure
ignore bad TLS/SSL/HTTPS certificates
-k alias for --insecure
-s alias of --silent
-servername string
specify a servername different from <remote> (to disable SNI use an IP as <remote> and do use this option)
-silent
less verbose output
# Examples # Examples
Bridge between `telebit.cloud` and local port `3000`. Bridge between `telebit.cloud` and local port `3000`.
@ -121,10 +139,14 @@ sclient --servername "Robert'); DROP TABLE Students;" -k example.com localhost:3
sclient --servername "../../../.hidden/private.txt" -k example.com localhost:3000 sclient --servername "../../../.hidden/private.txt" -k example.com localhost:3000
``` ```
# API
See [Go Docs](https://pkg.go.dev/github.com/therootcompany/sclient).
# Build from source # Build from source
You'll need to install [Go](https://golang.org). You'll need to install [Go](https://golang.org). See
See [webinstall.dev/golang](https://webinstall.dev/golang) for install instructions. [webinstall.dev/golang](https://webinstall.dev/golang) for install instructions.
```bash ```bash
curl -sS https://webinstall.dev/golang | bash curl -sS https://webinstall.dev/golang | bash

View File

@ -40,27 +40,37 @@ func usage() {
func main() { func main() {
if len(os.Args) >= 2 { if len(os.Args) >= 2 {
if "version" == strings.TrimLeft(os.Args[1], "-") { if os.Args[1] == "-V" || strings.TrimLeft(os.Args[1], "-") == "version" {
fmt.Printf("%s\n", ver()) fmt.Printf("%s\n", ver())
os.Exit(0) os.Exit(0)
return return
} }
} }
var alpnList string
var insecure bool
var servername string
var silent bool
flag.Usage = usage flag.Usage = usage
insecure := flag.Bool("k", false, "alias for --insecure")
silent := flag.Bool("s", false, "alias of --silent") flag.StringVar(&alpnList, "alpn", "", "acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' 'ssh'")
servername := flag.String("servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do use this option)") flag.BoolVar(&insecure, "k", false, "alias for --insecure")
flag.BoolVar(insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates") flag.BoolVar(&silent, "s", false, "alias of --silent")
flag.BoolVar(silent, "silent", false, "less verbose output") 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.Parse() flag.Parse()
alpns := parseOptionList(alpnList)
remotestr := flag.Arg(0) remotestr := flag.Arg(0)
localstr := flag.Arg(1) localstr := flag.Arg(1)
i := flag.NArg() i := flag.NArg()
if 2 != i { if i != 2 {
// We may omit the second argument if we're going straight to stdin // We may omit the second argument if we're going straight to stdin
if stat, _ := os.Stdin.Stat(); 1 == i && (stat.Mode()&os.ModeCharDevice) == 0 { if stat, _ := os.Stdin.Stat(); i == 1 && (stat.Mode()&os.ModeCharDevice) == 0 {
localstr = "|" localstr = "|"
} else { } else {
usage() usage()
@ -71,27 +81,28 @@ func main() {
sclient := &sclient.Tunnel{ sclient := &sclient.Tunnel{
RemotePort: 443, RemotePort: 443,
LocalAddress: "localhost", LocalAddress: "localhost",
InsecureSkipVerify: *insecure, InsecureSkipVerify: insecure,
ServerName: *servername, ServerName: servername,
Silent: *silent, Silent: silent,
NextProtos: alpns,
} }
remote := strings.Split(remotestr, ":") remote := strings.Split(remotestr, ":")
//remoteAddr, remotePort, err := net.SplitHostPort(remotestr) //remoteAddr, remotePort, err := net.SplitHostPort(remotestr)
if 2 == len(remote) { if len(remote) == 2 {
rport, err := strconv.Atoi(remote[1]) rport, err := strconv.Atoi(remote[1])
if nil != err { if nil != err {
usage() usage()
os.Exit(0) os.Exit(0)
} }
sclient.RemotePort = rport sclient.RemotePort = rport
} else if 1 != len(remote) { } else if len(remote) != 1 {
usage() usage()
os.Exit(0) os.Exit(0)
} }
sclient.RemoteAddress = remote[0] sclient.RemoteAddress = remote[0]
if "-" == localstr || "|" == localstr { if localstr == "-" || localstr == "|" {
// User may specify stdin/stdout instead of net // User may specify stdin/stdout instead of net
sclient.LocalAddress = localstr sclient.LocalAddress = localstr
sclient.LocalPort = -1 sclient.LocalPort = -1
@ -99,7 +110,7 @@ func main() {
// Test that argument is a local address // Test that argument is a local address
local := strings.Split(localstr, ":") local := strings.Split(localstr, ":")
if 1 == len(local) { if len(local) == 1 {
lport, err := strconv.Atoi(local[0]) lport, err := strconv.Atoi(local[0])
if nil != err { if nil != err {
usage() usage()
@ -124,3 +135,17 @@ func main() {
//os.Exit(6) //os.Exit(6)
} }
} }
// parsers "a,b,c" "a b c" and "a, b, c" all the same
func parseOptionList(optionList string) []string {
optionList = strings.TrimSpace(optionList)
if len(optionList) == 0 {
return nil
}
optionList = strings.ReplaceAll(optionList, ",", " ")
options := strings.Fields(optionList)
return options
}

View File

@ -17,6 +17,7 @@ type Tunnel struct {
LocalAddress string LocalAddress string
LocalPort int LocalPort int
InsecureSkipVerify bool InsecureSkipVerify bool
NextProtos []string
ServerName string ServerName string
Silent bool Silent bool
} }
@ -28,20 +29,21 @@ func (t *Tunnel) DialAndListen() error {
conn, err := tls.Dial("tcp", remote, conn, err := tls.Dial("tcp", remote,
&tls.Config{ &tls.Config{
ServerName: t.ServerName, ServerName: t.ServerName,
NextProtos: t.NextProtos,
InsecureSkipVerify: t.InsecureSkipVerify, InsecureSkipVerify: t.InsecureSkipVerify,
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err) fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err)
} else { } else {
conn.Close() _ = conn.Close()
} }
// use stdin/stdout // use stdin/stdout
if "-" == t.LocalAddress || "|" == t.LocalAddress { if t.LocalAddress == "-" || t.LocalAddress == "|" {
var name string var name string
network := "stdio" network := "stdio"
if "|" == t.LocalAddress { if t.LocalAddress == "|" {
name = "pipe" name = "pipe"
} else { } else {
name = "stdin" name = "stdin"
@ -59,7 +61,7 @@ func (t *Tunnel) DialAndListen() error {
} }
if !t.Silent { if !t.Silent {
fmt.Fprintf(os.Stdout, "[listening] %s:%d <= %s:%d\n", _, _ = fmt.Fprintf(os.Stdout, "[listening] %s:%d <= %s:%d\n",
t.RemoteAddress, t.RemotePort, t.LocalAddress, t.LocalPort) t.RemoteAddress, t.RemotePort, t.LocalAddress, t.LocalPort)
} }
@ -115,11 +117,11 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
if io.EOF != err { if io.EOF != err {
fmt.Fprintf(os.Stderr, "[read error] (%s:%d) %s\n", t, count, err) fmt.Fprintf(os.Stderr, "[read error] (%s:%d) %s\n", t, count, err)
} }
r.Close() _ = r.Close()
//w.Close() //w.Close()
done = true done = true
} }
if 0 == count { if count == 0 {
break break
} }
_, err = w.Write(buffer[:count]) _, err = w.Write(buffer[:count])
@ -129,7 +131,7 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
fmt.Fprintf(os.Stderr, "[write error] (%s) %s\n", t, err) fmt.Fprintf(os.Stderr, "[write error] (%s) %s\n", t, err)
} }
// TODO handle error closing? // TODO handle error closing?
r.Close() _ = r.Close()
//w.Close() //w.Close()
done = true done = true
} }
@ -143,21 +145,22 @@ func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
sclient, err := tls.Dial("tcp", remote, sclient, err := tls.Dial("tcp", remote,
&tls.Config{ &tls.Config{
ServerName: t.ServerName, ServerName: t.ServerName,
NextProtos: t.NextProtos,
InsecureSkipVerify: t.InsecureSkipVerify, InsecureSkipVerify: t.InsecureSkipVerify,
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err) fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
conn.Close() _ = conn.Close()
return return
} }
if !t.Silent { if !t.Silent {
if "stdio" == conn.RemoteAddr().Network() { if conn.RemoteAddr().Network() == "stdio" {
fmt.Fprintf(os.Stdout, "(connected to %s:%d and reading from %s)\n", _, _ = fmt.Fprintf(os.Stdout, "(connected to %s:%d and reading from %s)\n",
t.RemoteAddress, t.RemotePort, conn.RemoteAddr().String()) t.RemoteAddress, t.RemotePort, conn.RemoteAddr().String())
} else { } else {
fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n", _, _ = fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n",
strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), t.RemoteAddress, t.RemotePort) strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), t.RemoteAddress, t.RemotePort)
} }
} }