mirror of
https://github.com/alireza0/s-ui.git
synced 2026-10-21 11:22:26 +00:00
upgrade acme to ca provider
This commit is contained in:
parent
2951532c94
commit
91ff2e72d0
8 changed files with 576 additions and 10 deletions
24
core/testdata/clean/shared-cert-provider.json
vendored
Normal file
24
core/testdata/clean/shared-cert-provider.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"log": {"level": "error"},
|
||||
"certificate_providers": [{
|
||||
"type": "acme",
|
||||
"tag": "letsencrypt",
|
||||
"domain": ["example.com"],
|
||||
"email": "a@example.com",
|
||||
"data_directory": "__ACMEDIR__",
|
||||
"dns01_challenge": {"provider": "cloudflare", "api_token": "tok"}
|
||||
}],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless", "tag": "vless-in", "listen": "127.0.0.1", "listen_port": 42445,
|
||||
"users": [{"uuid": "b831381d-6324-4d53-ad4f-8cda48b30811"}],
|
||||
"tls": {"enabled": true, "server_name": "example.com", "certificate_provider": "letsencrypt"}
|
||||
},
|
||||
{
|
||||
"type": "trojan", "tag": "trojan-in", "listen": "127.0.0.1", "listen_port": 42446,
|
||||
"users": [{"password": "hunter2"}],
|
||||
"tls": {"enabled": true, "server_name": "example.com", "certificate_provider": "letsencrypt"}
|
||||
}
|
||||
],
|
||||
"outbounds": [{"type": "direct", "tag": "direct"}]
|
||||
}
|
||||
|
|
@ -128,6 +128,10 @@ func InitDB(dbPath string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = migrateCertificateProviders()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reportSingBox114Manual()
|
||||
|
||||
return nil
|
||||
|
|
|
|||
205
database/migration_cert_providers.go
Normal file
205
database/migration_cert_providers.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// migratedKeyCertProviders marks that inline certificate providers have already
|
||||
// been hoisted into the shared certificate_providers list.
|
||||
const migratedKeyCertProviders = "migratedCertProviders"
|
||||
|
||||
// migrateCertificateProviders turns the inline certificate_provider object a
|
||||
// TLS config may carry into an entry in the config's certificate_providers
|
||||
// list, leaving the TLS config with a tag reference to it.
|
||||
//
|
||||
// sing-box accepts both forms, so this is not a compatibility fix: it is what
|
||||
// lets the panel edit providers in one place and share them between TLS
|
||||
// configs, which an inline object cannot do.
|
||||
func migrateCertificateProviders() error {
|
||||
var flag model.Setting
|
||||
err := db.Where("key = ?", migratedKeyCertProviders).First(&flag).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
changed, err := hoistInlineCertificateProviders(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed > 0 {
|
||||
log.Printf("certificate providers: moved %d inline provider(s) into certificate_providers", changed)
|
||||
}
|
||||
return tx.Create(&model.Setting{Key: migratedKeyCertProviders, Value: "true"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func hoistInlineCertificateProviders(tx *gorm.DB) (int, error) {
|
||||
var tlsConfigs []model.Tls
|
||||
if err := tx.Find(&tlsConfigs).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", "config").First(&setting).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Without a base config there is nowhere to put shared providers, so
|
||||
// the inline ones are left as they are. They still work.
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err = json.Unmarshal([]byte(setting.Value), &root); err != nil {
|
||||
log.Printf("certificate providers: skipping, cannot parse config: %v", err)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
providers, takenTags, err := decodeCertificateProviders(root)
|
||||
if err != nil {
|
||||
log.Printf("certificate providers: skipping, cannot parse certificate_providers: %v", err)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
changed := 0
|
||||
for _, tlsConfig := range tlsConfigs {
|
||||
if len(tlsConfig.Server) == 0 {
|
||||
continue
|
||||
}
|
||||
var server map[string]json.RawMessage
|
||||
if err = json.Unmarshal(tlsConfig.Server, &server); err != nil {
|
||||
continue
|
||||
}
|
||||
inline, ok := inlineProviderObject(server["certificate_provider"])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := uniqueProviderTag(providerTagBase(tlsConfig), takenTags)
|
||||
takenTags[tag] = struct{}{}
|
||||
inline["tag"] = json.RawMessage(strconv.Quote(tag))
|
||||
encodedProvider, err := json.Marshal(inline)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
providers = append(providers, encodedProvider)
|
||||
|
||||
server["certificate_provider"] = json.RawMessage(strconv.Quote(tag))
|
||||
encodedServer, err := json.Marshal(server)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = tx.Model(&model.Tls{}).Where("id = ?", tlsConfig.Id).
|
||||
Update("server", json.RawMessage(encodedServer)).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed++
|
||||
}
|
||||
if changed == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
encodedProviders, err := json.Marshal(providers)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
root["certificate_providers"] = encodedProviders
|
||||
encodedRoot, err := json.MarshalIndent(root, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setting.Value = string(encodedRoot)
|
||||
if err = tx.Save(&setting).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// decodeCertificateProviders reads the providers already defined in the config
|
||||
// and the tags they occupy, so hoisted ones do not collide with them.
|
||||
func decodeCertificateProviders(root map[string]json.RawMessage) ([]json.RawMessage, map[string]struct{}, error) {
|
||||
taken := make(map[string]struct{})
|
||||
raw, ok := root["certificate_providers"]
|
||||
if !ok || len(raw) == 0 {
|
||||
return nil, taken, nil
|
||||
}
|
||||
var providers []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &providers); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, provider := range providers {
|
||||
var fields struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
if err := json.Unmarshal(provider, &fields); err != nil {
|
||||
continue
|
||||
}
|
||||
if fields.Tag != "" {
|
||||
taken[fields.Tag] = struct{}{}
|
||||
}
|
||||
}
|
||||
return providers, taken, nil
|
||||
}
|
||||
|
||||
// inlineProviderObject reports whether certificate_provider holds an inline
|
||||
// definition rather than a tag reference to a shared one.
|
||||
func inlineProviderObject(raw json.RawMessage) (map[string]json.RawMessage, bool) {
|
||||
trimmed := strings.TrimSpace(string(raw))
|
||||
if !strings.HasPrefix(trimmed, "{") {
|
||||
return nil, false
|
||||
}
|
||||
var inline map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &inline); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
// A provider with no type cannot be created by sing-box either way, so
|
||||
// there is nothing worth moving.
|
||||
if _, ok := inline["type"]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
return inline, true
|
||||
}
|
||||
|
||||
// providerTagBase names the hoisted provider after the TLS config it came
|
||||
// from, which is how the operator will recognise it in the list.
|
||||
func providerTagBase(tlsConfig model.Tls) string {
|
||||
slug := strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||||
return r
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r + ('a' - 'A')
|
||||
default:
|
||||
return '-'
|
||||
}
|
||||
}, tlsConfig.Name)
|
||||
slug = strings.Trim(slug, "-")
|
||||
for strings.Contains(slug, "--") {
|
||||
slug = strings.ReplaceAll(slug, "--", "-")
|
||||
}
|
||||
if slug == "" {
|
||||
return "cert-" + strconv.FormatUint(uint64(tlsConfig.Id), 10)
|
||||
}
|
||||
return slug
|
||||
}
|
||||
|
||||
func uniqueProviderTag(base string, taken map[string]struct{}) string {
|
||||
tag := base
|
||||
for i := 2; ; i++ {
|
||||
if _, exists := taken[tag]; !exists {
|
||||
return tag
|
||||
}
|
||||
tag = base + "-" + strconv.Itoa(i)
|
||||
}
|
||||
}
|
||||
176
database/migration_cert_providers_test.go
Normal file
176
database/migration_cert_providers_test.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
)
|
||||
|
||||
func certificateProviders(t *testing.T, root map[string]any) []map[string]any {
|
||||
t.Helper()
|
||||
raw, ok := root["certificate_providers"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing certificate_providers in %v", root)
|
||||
}
|
||||
providers := make([]map[string]any, 0, len(raw))
|
||||
for _, entry := range raw {
|
||||
provider, isObject := entry.(map[string]any)
|
||||
if !isObject {
|
||||
t.Fatalf("expected provider objects, got %v", entry)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func tlsServer(t *testing.T, id uint) map[string]any {
|
||||
t.Helper()
|
||||
var stored model.Tls
|
||||
if err := db.Where("id = ?", id).First(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var server map[string]any
|
||||
if err := json.Unmarshal(stored.Server, &server); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func TestMigrateCertificateProviders(t *testing.T) {
|
||||
openTestDB(t)
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: `{"log":{"level":"info"}}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.Tls{
|
||||
Name: "Main Site",
|
||||
Server: json.RawMessage(`{
|
||||
"enabled": true,
|
||||
"certificate_provider": {"type": "acme", "domain": ["example.com"], "email": "a@example.com"}
|
||||
}`),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateCertificateProviders(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
providers := certificateProviders(t, readConfig(t))
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("expected one hoisted provider, got %v", providers)
|
||||
}
|
||||
if providers[0]["type"] != "acme" || providers[0]["email"] != "a@example.com" {
|
||||
t.Errorf("the provider fields must carry over, got %v", providers[0])
|
||||
}
|
||||
tag, _ := providers[0]["tag"].(string)
|
||||
if tag != "main-site" {
|
||||
t.Errorf("the tag should be named after the TLS config, got %q", tag)
|
||||
}
|
||||
// The TLS config must now point at the shared provider rather than repeat it.
|
||||
if got := tlsServer(t, 1)["certificate_provider"]; got != tag {
|
||||
t.Errorf("expected the tag %q as the reference, got %v", tag, got)
|
||||
}
|
||||
}
|
||||
|
||||
// A tag reference is already the new form and must be left alone, as must a
|
||||
// provider that was defined in the config by hand.
|
||||
func TestMigrateCertificateProvidersLeavesReferences(t *testing.T) {
|
||||
openTestDB(t)
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: `{
|
||||
"certificate_providers": [{"type": "acme", "tag": "shared", "domain": ["example.com"]}]
|
||||
}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.Tls{
|
||||
Name: "site",
|
||||
Server: json.RawMessage(`{"enabled": true, "certificate_provider": "shared"}`),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateCertificateProviders(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
providers := certificateProviders(t, readConfig(t))
|
||||
if len(providers) != 1 {
|
||||
t.Errorf("nothing should have been added, got %v", providers)
|
||||
}
|
||||
if got := tlsServer(t, 1)["certificate_provider"]; got != "shared" {
|
||||
t.Errorf("the reference must be untouched, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Two TLS configs whose names slugify the same must not end up sharing a tag,
|
||||
// which would silently point one of them at the other's provider.
|
||||
func TestMigrateCertificateProvidersDeduplicatesTags(t *testing.T) {
|
||||
openTestDB(t)
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: `{
|
||||
"certificate_providers": [{"type": "acme", "tag": "my-site", "domain": ["taken.example"]}]
|
||||
}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"My Site", "my.site"} {
|
||||
if err := db.Create(&model.Tls{
|
||||
Name: name,
|
||||
Server: json.RawMessage(`{"enabled": true, "certificate_provider": {"type": "acme", "domain": ["example.com"]}}`),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := migrateCertificateProviders(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
providers := certificateProviders(t, readConfig(t))
|
||||
if len(providers) != 3 {
|
||||
t.Fatalf("expected the hand-written provider plus two hoisted ones, got %v", providers)
|
||||
}
|
||||
tags := make(map[string]bool)
|
||||
for _, provider := range providers {
|
||||
tag, _ := provider["tag"].(string)
|
||||
if tag == "" {
|
||||
t.Fatalf("every shared provider needs a tag, got %v", provider)
|
||||
}
|
||||
if tags[tag] {
|
||||
t.Fatalf("duplicate tag %q in %v", tag, providers)
|
||||
}
|
||||
tags[tag] = true
|
||||
}
|
||||
for id := uint(1); id <= 2; id++ {
|
||||
reference, _ := tlsServer(t, id)["certificate_provider"].(string)
|
||||
if reference == "my-site" {
|
||||
t.Errorf("tls %d must not claim the existing provider's tag", id)
|
||||
}
|
||||
if !tags[reference] {
|
||||
t.Errorf("tls %d references %q, which is not defined", id, reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The migration runs once; a provider added inline afterwards is the operator's
|
||||
// own doing and must not be moved out from under them on the next start.
|
||||
func TestMigrateCertificateProvidersRunsOnce(t *testing.T) {
|
||||
openTestDB(t)
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: `{"log":{"level":"info"}}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrateCertificateProviders(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.Tls{
|
||||
Name: "later",
|
||||
Server: json.RawMessage(`{"enabled": true, "certificate_provider": {"type": "acme", "domain": ["example.com"]}}`),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrateCertificateProviders(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, hoisted := readConfig(t)["certificate_providers"]; hoisted {
|
||||
t.Error("the migration should not run a second time")
|
||||
}
|
||||
}
|
||||
2
frontend
2
frontend
|
|
@ -1 +1 @@
|
|||
Subproject commit 353dea9d3667bbf4525ec8f4c8b0572beb07c98d
|
||||
Subproject commit 1da3ca9f92f227574c8f77797aa24803d7d913c8
|
||||
|
|
@ -33,15 +33,19 @@ type ConfigService struct {
|
|||
}
|
||||
|
||||
type SingBoxConfig struct {
|
||||
Log json.RawMessage `json:"log"`
|
||||
Dns json.RawMessage `json:"dns"`
|
||||
Ntp json.RawMessage `json:"ntp"`
|
||||
Inbounds []json.RawMessage `json:"inbounds"`
|
||||
Outbounds []json.RawMessage `json:"outbounds"`
|
||||
Services []json.RawMessage `json:"services"`
|
||||
Endpoints []json.RawMessage `json:"endpoints"`
|
||||
Route json.RawMessage `json:"route"`
|
||||
Experimental json.RawMessage `json:"experimental"`
|
||||
Log json.RawMessage `json:"log"`
|
||||
Dns json.RawMessage `json:"dns"`
|
||||
Ntp json.RawMessage `json:"ntp"`
|
||||
// Shared certificate providers, referenced by tag from a TLS config's
|
||||
// certificate_provider. They are edited alongside the TLS configs but
|
||||
// stored in the base config, so they are carried through unchanged.
|
||||
CertificateProviders []json.RawMessage `json:"certificate_providers,omitempty"`
|
||||
Inbounds []json.RawMessage `json:"inbounds"`
|
||||
Outbounds []json.RawMessage `json:"outbounds"`
|
||||
Services []json.RawMessage `json:"services"`
|
||||
Endpoints []json.RawMessage `json:"endpoints"`
|
||||
Route json.RawMessage `json:"route"`
|
||||
Experimental json.RawMessage `json:"experimental"`
|
||||
}
|
||||
|
||||
func NewConfigService(core *core.Core) *ConfigService {
|
||||
|
|
|
|||
90
service/config_providers_test.go
Normal file
90
service/config_providers_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/alireza0/s-ui/database"
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
)
|
||||
|
||||
// TestGetConfigEmitsCertificateProviders covers the whole path the TLS page
|
||||
// relies on: providers saved into the base config have to come out in the
|
||||
// config handed to sing-box, and a TLS config's tag reference has to survive
|
||||
// alongside them.
|
||||
func TestGetConfigEmitsCertificateProviders(t *testing.T) {
|
||||
if err := database.InitDB(filepath.Join(t.TempDir(), "test.db")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := database.GetDB()
|
||||
|
||||
base := `{
|
||||
"log": {"level": "info"},
|
||||
"certificate_providers": [
|
||||
{"type": "acme", "tag": "letsencrypt", "domain": ["example.com"], "email": "a@example.com"}
|
||||
],
|
||||
"route": {"rules": []}
|
||||
}`
|
||||
// The defaults are only materialised on demand, so the row may not exist yet.
|
||||
if err := db.Where("key = ?", "config").Delete(&model.Setting{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: base}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tlsConfig := model.Tls{
|
||||
Name: "site",
|
||||
Server: json.RawMessage(`{"enabled": true, "certificate_provider": "letsencrypt"}`),
|
||||
Client: json.RawMessage(`{}`),
|
||||
}
|
||||
if err := db.Create(&tlsConfig).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inbound := model.Inbound{
|
||||
Type: "vless",
|
||||
Tag: "vless-in",
|
||||
TlsId: tlsConfig.Id,
|
||||
Options: json.RawMessage(`{"listen":"127.0.0.1","listen_port":42447}`),
|
||||
}
|
||||
if err := db.Create(&inbound).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw, err := (&ConfigService{}).GetConfig("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var built struct {
|
||||
CertificateProviders []struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Domain []string `json:"domain"`
|
||||
} `json:"certificate_providers"`
|
||||
Inbounds []struct {
|
||||
Tag string `json:"tag"`
|
||||
TLS struct {
|
||||
CertificateProvider string `json:"certificate_provider"`
|
||||
} `json:"tls"`
|
||||
} `json:"inbounds"`
|
||||
}
|
||||
if err = json.Unmarshal(*raw, &built); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(built.CertificateProviders) != 1 {
|
||||
t.Fatalf("the provider must reach the generated config, got %s", *raw)
|
||||
}
|
||||
provider := built.CertificateProviders[0]
|
||||
if provider.Tag != "letsencrypt" || provider.Type != "acme" || len(provider.Domain) != 1 {
|
||||
t.Errorf("the provider must be emitted unchanged, got %+v", provider)
|
||||
}
|
||||
if len(built.Inbounds) != 1 {
|
||||
t.Fatalf("expected the inbound, got %s", *raw)
|
||||
}
|
||||
if built.Inbounds[0].TLS.CertificateProvider != "letsencrypt" {
|
||||
t.Errorf("the inbound TLS must reference the provider, got %s", *raw)
|
||||
}
|
||||
}
|
||||
63
service/config_test.go
Normal file
63
service/config_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// GetConfig rebuilds the sing-box config by decoding the stored base config
|
||||
// into SingBoxConfig and marshalling it back, so anything the struct does not
|
||||
// model is dropped on the way through. certificate_providers is edited by the
|
||||
// TLS page and has to survive that round trip.
|
||||
func TestSingBoxConfigKeepsCertificateProviders(t *testing.T) {
|
||||
base := `{
|
||||
"log": {"level": "info"},
|
||||
"certificate_providers": [
|
||||
{"type": "acme", "tag": "letsencrypt", "domain": ["example.com"]},
|
||||
{"type": "cloudflare-origin-ca", "tag": "cf", "domain": ["example.com"]}
|
||||
],
|
||||
"route": {"rules": []}
|
||||
}`
|
||||
|
||||
var config SingBoxConfig
|
||||
if err := json.Unmarshal([]byte(base), &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rebuilt, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded struct {
|
||||
CertificateProviders []struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
} `json:"certificate_providers"`
|
||||
}
|
||||
if err = json.Unmarshal(rebuilt, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded.CertificateProviders) != 2 {
|
||||
t.Fatalf("both providers must reach the final config, got %s", rebuilt)
|
||||
}
|
||||
if decoded.CertificateProviders[0].Tag != "letsencrypt" || decoded.CertificateProviders[1].Type != "cloudflare-origin-ca" {
|
||||
t.Errorf("providers must carry over unchanged, got %s", rebuilt)
|
||||
}
|
||||
}
|
||||
|
||||
// A config with no providers must not grow an empty key, which sing-box would
|
||||
// read as an explicit empty list.
|
||||
func TestSingBoxConfigOmitsEmptyCertificateProviders(t *testing.T) {
|
||||
var config SingBoxConfig
|
||||
if err := json.Unmarshal([]byte(`{"log": {"level": "info"}}`), &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rebuilt, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(rebuilt), "certificate_providers") {
|
||||
t.Errorf("expected no certificate_providers key, got %s", rebuilt)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue