Довёл до прода интеграцию Hysteria2: безопасный URI, тесты, актуализация доков и hardening env
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"hy2xs-admin/model/bo"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/proxy"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -255,34 +256,54 @@ func Hysteria2Url(accountId int64) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
urlConfig := ""
|
||||
if hysteria2Config.Obfs != nil &&
|
||||
hysteria2Config.Obfs.Type != nil &&
|
||||
*hysteria2Config.Obfs.Type == "salamander" &&
|
||||
hysteria2Config.Obfs.Salamander != nil &&
|
||||
hysteria2Config.Obfs.Salamander.Password != nil &&
|
||||
*hysteria2Config.Obfs.Salamander.Password != "" {
|
||||
urlConfig += fmt.Sprintf("&obfs=salamander&obfs-password=%s", *hysteria2Config.Obfs.Salamander.Password)
|
||||
}
|
||||
|
||||
if hysteria2Config.ACME != nil &&
|
||||
hysteria2Config.ACME.Domains != nil &&
|
||||
len(hysteria2Config.ACME.Domains) > 0 {
|
||||
urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0])
|
||||
}
|
||||
|
||||
urlConfig += "&insecure=0"
|
||||
|
||||
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if *hysteria2ConfigRemark.Value != "" {
|
||||
urlConfig += fmt.Sprintf("#%s", *hysteria2ConfigRemark.Value)
|
||||
|
||||
remark := ""
|
||||
if hysteria2ConfigRemark.Value != nil {
|
||||
remark = *hysteria2ConfigRemark.Value
|
||||
}
|
||||
if urlConfig != "" {
|
||||
urlConfig = "/?" + strings.TrimPrefix(urlConfig, "&")
|
||||
|
||||
obfsType := ""
|
||||
obfsPassword := ""
|
||||
if hysteria2Config.Obfs != nil &&
|
||||
hysteria2Config.Obfs.Type != nil &&
|
||||
hysteria2Config.Obfs.Salamander != nil &&
|
||||
hysteria2Config.Obfs.Salamander.Password != nil {
|
||||
obfsType = *hysteria2Config.Obfs.Type
|
||||
obfsPassword = *hysteria2Config.Obfs.Salamander.Password
|
||||
}
|
||||
return fmt.Sprintf("hysteria2://%s@%s:%d", *account.ConPass, hostname, port) + urlConfig, nil
|
||||
|
||||
sni := ""
|
||||
if hysteria2Config.ACME != nil && len(hysteria2Config.ACME.Domains) > 0 {
|
||||
sni = hysteria2Config.ACME.Domains[0]
|
||||
}
|
||||
|
||||
return buildHysteria2Url(*account.ConPass, hostname, port, obfsType, obfsPassword, sni, remark), nil
|
||||
}
|
||||
|
||||
func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string {
|
||||
query := url.Values{}
|
||||
if obfsType == "salamander" && obfsPassword != "" {
|
||||
query.Set("obfs", "salamander")
|
||||
query.Set("obfs-password", obfsPassword)
|
||||
}
|
||||
if sni != "" {
|
||||
query.Set("sni", sni)
|
||||
}
|
||||
query.Set("insecure", "0")
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "hysteria2",
|
||||
User: url.User(conPass),
|
||||
Host: net.JoinHostPort(hostname, strconv.Itoa(port)),
|
||||
Path: "/",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
if strings.TrimSpace(remark) != "" {
|
||||
u.Fragment = remark
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildHysteria2Url_EncodesUserInfoQueryAndFragment(t *testing.T) {
|
||||
raw := buildHysteria2Url(
|
||||
"u@ser:#&=+ pass",
|
||||
"example.com",
|
||||
443,
|
||||
"salamander",
|
||||
"obf+s&pass=@x",
|
||||
"exa mple.com",
|
||||
"my remark #1",
|
||||
)
|
||||
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid url, got error: %v", err)
|
||||
}
|
||||
if parsed.Scheme != "hysteria2" {
|
||||
t.Fatalf("expected hysteria2 scheme, got %s", parsed.Scheme)
|
||||
}
|
||||
if parsed.User == nil {
|
||||
t.Fatal("expected userinfo to be present")
|
||||
}
|
||||
if parsed.User.Username() != "u@ser:#&=+ pass" {
|
||||
t.Fatalf("expected decoded userinfo to match source, got %q", parsed.User.Username())
|
||||
}
|
||||
|
||||
q := parsed.Query()
|
||||
if q.Get("obfs") != "salamander" {
|
||||
t.Fatalf("expected obfs=salamander, got %q", q.Get("obfs"))
|
||||
}
|
||||
if q.Get("obfs-password") != "obf+s&pass=@x" {
|
||||
t.Fatalf("expected decoded obfs-password, got %q", q.Get("obfs-password"))
|
||||
}
|
||||
if q.Get("sni") != "exa mple.com" {
|
||||
t.Fatalf("expected decoded sni, got %q", q.Get("sni"))
|
||||
}
|
||||
if q.Get("insecure") != "0" {
|
||||
t.Fatalf("expected insecure=0, got %q", q.Get("insecure"))
|
||||
}
|
||||
if parsed.Fragment != "my remark #1" {
|
||||
t.Fatalf("expected decoded fragment, got %q", parsed.Fragment)
|
||||
}
|
||||
|
||||
if strings.Contains(raw, "u@ser:#&=+ pass") {
|
||||
t.Fatalf("raw uri must not contain unescaped userinfo: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHysteria2Url_MinimalConfig(t *testing.T) {
|
||||
raw := buildHysteria2Url("pass", "example.com", 8443, "", "", "", "")
|
||||
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid url, got error: %v", err)
|
||||
}
|
||||
if parsed.Host != "example.com:8443" {
|
||||
t.Fatalf("unexpected host: %s", parsed.Host)
|
||||
}
|
||||
q := parsed.Query()
|
||||
if q.Get("insecure") != "0" {
|
||||
t.Fatalf("expected insecure=0, got %q", q.Get("insecure"))
|
||||
}
|
||||
if q.Get("obfs") != "" || q.Get("obfs-password") != "" || q.Get("sni") != "" {
|
||||
t.Fatalf("unexpected optional query params in minimal config: %s", parsed.RawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user