mirror of
https://github.com/cedar2025/hysteria.git
synced 2025-06-08 21:39:56 +00:00
feat: client config URI support
This commit is contained in:
parent
0dbd6af683
commit
07b7f14bef
@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -193,8 +194,79 @@ func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URI generates a URI for sharing the config with others.
|
||||||
|
// Note that only the bare minimum of information required to
|
||||||
|
// connect to the server is included in the URI, specifically:
|
||||||
|
// - server address
|
||||||
|
// - authentication
|
||||||
|
// - obfuscation type
|
||||||
|
// - obfuscation password
|
||||||
|
// - TLS SNI
|
||||||
|
// - TLS insecure
|
||||||
|
func (c *clientConfig) URI() string {
|
||||||
|
q := url.Values{}
|
||||||
|
switch strings.ToLower(c.Obfs.Type) {
|
||||||
|
case "salamander":
|
||||||
|
q.Set("obfs", "salamander")
|
||||||
|
q.Set("obfs-password", c.Obfs.Salamander.Password)
|
||||||
|
}
|
||||||
|
if c.TLS.SNI != "" {
|
||||||
|
q.Set("sni", c.TLS.SNI)
|
||||||
|
}
|
||||||
|
if c.TLS.Insecure {
|
||||||
|
q.Set("insecure", "1")
|
||||||
|
}
|
||||||
|
var user *url.Userinfo
|
||||||
|
if c.Auth != "" {
|
||||||
|
user = url.User(c.Auth)
|
||||||
|
}
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "hysteria2",
|
||||||
|
User: user,
|
||||||
|
Host: c.Server,
|
||||||
|
Path: "/",
|
||||||
|
RawQuery: q.Encode(),
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseURI tries to parse the server address field as a URI,
|
||||||
|
// and fills the config with the information contained in the URI.
|
||||||
|
// Returns whether the server address field is a valid URI.
|
||||||
|
// This allows a user to use put a URI as the server address and
|
||||||
|
// omit the fields that are already contained in the URI.
|
||||||
|
func (c *clientConfig) parseURI() bool {
|
||||||
|
u, err := url.Parse(c.Server)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.Scheme != "hysteria2" && u.Scheme != "hy2" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
c.Auth = u.User.String()
|
||||||
|
}
|
||||||
|
c.Server = u.Host
|
||||||
|
q := u.Query()
|
||||||
|
if obfsType := q.Get("obfs"); obfsType != "" {
|
||||||
|
c.Obfs.Type = obfsType
|
||||||
|
switch strings.ToLower(obfsType) {
|
||||||
|
case "salamander":
|
||||||
|
c.Obfs.Salamander.Password = q.Get("obfs-password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sni := q.Get("sni"); sni != "" {
|
||||||
|
c.TLS.SNI = sni
|
||||||
|
}
|
||||||
|
if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil {
|
||||||
|
c.TLS.Insecure = insecure
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Config validates the fields and returns a ready-to-use Hysteria client config
|
// Config validates the fields and returns a ready-to-use Hysteria client config
|
||||||
func (c *clientConfig) Config() (*client.Config, error) {
|
func (c *clientConfig) Config() (*client.Config, error) {
|
||||||
|
c.parseURI()
|
||||||
hyConfig := &client.Config{}
|
hyConfig := &client.Config{}
|
||||||
fillers := []func(*client.Config) error{
|
fillers := []func(*client.Config) error{
|
||||||
c.fillConnFactory,
|
c.fillConnFactory,
|
||||||
@ -213,32 +285,6 @@ func (c *clientConfig) Config() (*client.Config, error) {
|
|||||||
return hyConfig, nil
|
return hyConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShareURI generates a URI for sharing the config with others.
|
|
||||||
// Note that only the fields necessary for a client to connect to the server are included.
|
|
||||||
// It doesn't include local modes, for example.
|
|
||||||
func (c *clientConfig) ShareURI() string {
|
|
||||||
q := url.Values{}
|
|
||||||
switch strings.ToLower(c.Obfs.Type) {
|
|
||||||
case "salamander":
|
|
||||||
q.Set("obfs", "salamander")
|
|
||||||
q.Set("obfs-password", c.Obfs.Salamander.Password)
|
|
||||||
}
|
|
||||||
if c.TLS.SNI != "" {
|
|
||||||
q.Set("sni", c.TLS.SNI)
|
|
||||||
}
|
|
||||||
if c.TLS.Insecure {
|
|
||||||
q.Set("insecure", "1")
|
|
||||||
}
|
|
||||||
u := url.URL{
|
|
||||||
Scheme: "hysteria2",
|
|
||||||
User: url.User(c.Auth),
|
|
||||||
Host: c.Server,
|
|
||||||
Path: "/",
|
|
||||||
RawQuery: q.Encode(),
|
|
||||||
}
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClient(cmd *cobra.Command, args []string) {
|
func runClient(cmd *cobra.Command, args []string) {
|
||||||
logger.Info("client mode")
|
logger.Info("client mode")
|
||||||
|
|
||||||
@ -260,7 +306,7 @@ func runClient(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
uri := config.ShareURI()
|
uri := config.URI()
|
||||||
logger.Info("use this URI to share your server", zap.String("uri", uri))
|
logger.Info("use this URI to share your server", zap.String("uri", uri))
|
||||||
if showQR {
|
if showQR {
|
||||||
printQR(uri)
|
printQR(uri)
|
||||||
|
@ -98,3 +98,80 @@ func TestClientConfig(t *testing.T) {
|
|||||||
t.Fatal("parsed client config is not equal to expected")
|
t.Fatal("parsed client config is not equal to expected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClientConfigURI tests URI-related functions of clientConfig
|
||||||
|
func TestClientConfigURI(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
uri string
|
||||||
|
uriOK bool
|
||||||
|
config *clientConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
uri: "hysteria2://god@zilla.jp/",
|
||||||
|
uriOK: true,
|
||||||
|
config: &clientConfig{
|
||||||
|
Server: "zilla.jp",
|
||||||
|
Auth: "god",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&sni=crap.cc",
|
||||||
|
uriOK: true,
|
||||||
|
config: &clientConfig{
|
||||||
|
Server: "noauth.com",
|
||||||
|
Auth: "",
|
||||||
|
Obfs: struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Salamander struct {
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
} `mapstructure:"salamander"`
|
||||||
|
}{
|
||||||
|
Type: "salamander",
|
||||||
|
Salamander: struct {
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}{
|
||||||
|
Password: "66ccff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: struct {
|
||||||
|
SNI string `mapstructure:"sni"`
|
||||||
|
Insecure bool `mapstructure:"insecure"`
|
||||||
|
CA string `mapstructure:"ca"`
|
||||||
|
}{
|
||||||
|
SNI: "crap.cc",
|
||||||
|
Insecure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "invalid.bs",
|
||||||
|
uriOK: false,
|
||||||
|
config: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "https://www.google.com/search?q=test",
|
||||||
|
uriOK: false,
|
||||||
|
config: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.uri, func(t *testing.T) {
|
||||||
|
// Test parseURI
|
||||||
|
nc := &clientConfig{Server: test.uri}
|
||||||
|
if ok := nc.parseURI(); ok != test.uriOK {
|
||||||
|
t.Fatal("unexpected parseURI ok result")
|
||||||
|
}
|
||||||
|
if test.uriOK && !reflect.DeepEqual(nc, test.config) {
|
||||||
|
t.Fatal("unexpected parsed client config from URI")
|
||||||
|
}
|
||||||
|
// Test URI generation
|
||||||
|
if test.config == nil {
|
||||||
|
// config is nil if parseURI is expected to fail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if uri := test.config.URI(); uri != test.uri {
|
||||||
|
t.Fatalf("generated URI mismatch: %s != %s", uri, test.uri)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user