Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion client_v2/src/__tests__/common/ui/Banners.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@solidjs/testing-library';
import { fireEvent, render, screen } from '@solidjs/testing-library';
import userEvent from '@testing-library/user-event';
import { HashRouter, Route } from '@solidjs/router';

Expand Down Expand Up @@ -235,6 +235,29 @@ describe('Banners', () => {
expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument();
});

it('shows the update banner after dismissing the TLS expiring banner', () => {
mockEncryptionState.enabled = true;
mockEncryptionState.valid_cert = true;
mockEncryptionState.not_after = new Date(Date.now() + 2 * 86400000).toISOString(); // 2 days
// Update available too, but TLS expiring takes priority
mockDashboardState.isUpdateAvailable = true;
mockDashboardState.newVersion = 'v1.0.0';
mockDashboardState.canAutoUpdate = true;

renderBanners();

expect(screen.getByTestId('banner-tls-expiring')).toBeInTheDocument();
expect(screen.queryByTestId('banner-update-auto')).not.toBeInTheDocument();

// Dismiss the expiring warning while TLS is still within the warning window
const closeButton = screen.getByTestId('banner-tls-expiring-close');
fireEvent.click(closeButton);

// Lower-priority update banner should now appear even though TLS still takes priority
expect(screen.queryByTestId('banner-tls-expiring')).not.toBeInTheDocument();
expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
});

// ── forceBanner (dev test override) ──

it('renders forced TLS expired banner regardless of store state', () => {
Expand Down
38 changes: 20 additions & 18 deletions client_v2/src/common/ui/Banners/Banners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ export const Banners = (props: Props) => {
return null;
});

const computeActiveBanner = (): BannerSpec | null => {
const getActiveBanners = (): BannerSpec[] => {
const active: BannerSpec[] = [];

if (encryptionState.enabled && encryptionState.valid_cert && encryptionState.not_after) {
const expiry = new Date(encryptionState.not_after).getTime();
if (!Number.isNaN(expiry)) {
if (Date.now() > expiry) {
return { type: 'tls_expired' };
}
if (Date.now() > expiry - TLS_EXPIRY_WARNING_MS) {
return { type: 'tls_expiring' };
active.push({ type: 'tls_expired' });
} else if (Date.now() > expiry - TLS_EXPIRY_WARNING_MS) {
active.push({ type: 'tls_expiring' });
}
}
}
Expand All @@ -57,22 +58,24 @@ export const Banners = (props: Props) => {
version: dashboardState.newVersion,
announcementUrl: dashboardState.announcementUrl,
};
return dashboardState.canAutoUpdate
? { type: 'update_auto', ...spec }
: { type: 'update_manual', ...spec };
active.push(
dashboardState.canAutoUpdate
? { type: 'update_auto', ...spec }
: { type: 'update_manual', ...spec },
);
}

return null;
return active;
};

const banner = createMemo<BannerSpec | null>(() => {
const active = props.forceBanner ?? forceFromQuery() ?? computeActiveBanner();
if (!active) return null;
const forced = props.forceBanner ?? forceFromQuery();
const candidates = forced ? [forced] : getActiveBanners();
const dismissedValue = dismissed();
if (dismissedValue && bannerSpecsEqual(dismissedValue, active)) {
return null;
}
return active;
return (
candidates.find((b) => !(dismissedValue && bannerSpecsEqual(dismissedValue, b))) ??
null
);
});

const announcementLinkHandler = (announcementUrl: string) => (text: string) => (
Expand All @@ -82,9 +85,8 @@ export const Banners = (props: Props) => {
);

return (
<Show when={banner()}>
{(spec) => {
const current = spec();
<Show when={banner()} keyed>
{(current) => {

const renderBanner = (): JSX.Element => {
switch (current.type) {
Expand Down
38 changes: 12 additions & 26 deletions internal/aghtls/defaultmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,13 @@ type DefaultManager struct {
logger *slog.Logger

// mu protects tlsConf, extTLSConf, certLastMod, tlsCert, and pair.
mu *sync.Mutex
tlsConf *tls.Config
extTLSConf *ExtendedTLSConfig
tlsCert *tls.Certificate
rootCerts *x509.CertPool
updates chan UpdateSignal
pair TLSPair

// TODO(m.kazantsev): Add support for dynamic updates of custom ciphers.
mu *sync.Mutex
tlsConf *tls.Config
extTLSConf *ExtendedTLSConfig
tlsCert *tls.Certificate
rootCerts *x509.CertPool
updates chan UpdateSignal
pair TLSPair
customCipherIDs []uint16
}

Expand Down Expand Up @@ -435,7 +433,11 @@ func (mgr *DefaultManager) SetExtendedTLSConfig(
return false, err
}

updatePlainDNS(mgr.extTLSConf, newConf, servePlainDNS)
if servePlainDNS != aghalg.NBNull {
newConf.ServePlainDNS = servePlainDNS == aghalg.NBTrue
} else {
newConf.ServePlainDNS = mgr.extTLSConf.ServePlainDNS
}

if !setPrivateFieldsAndCompare(mgr.extTLSConf, newConf) {
mgr.logger.InfoContext(ctx, "config has changed, restarting https server")
Expand Down Expand Up @@ -533,22 +535,6 @@ func setPrivateFieldsAndCompare(
return cmp.Equal(currentTLSConf, newTLSConf)
}

// updatePlainDNS checks the old value of
// [aghtls.ExtendedTLSConfig.ServePlainDNS] in currentTLSConf and if it differs
// from servePlain, sets the value of servePlain in newTLSConf.ServePlainDNS.
// currentTLSConf and newTLSConf must not be nil.
func updatePlainDNS(
currentTLSConf *ExtendedTLSConfig,
newTLSConf *ExtendedTLSConfig,
servePlain aghalg.NullBool,
) {
if servePlain != aghalg.NBNull {
newTLSConf.ServePlainDNS = servePlain == aghalg.NBTrue
} else {
newTLSConf.ServePlainDNS = currentTLSConf.ServePlainDNS
}
}

// setCertFileTime sets [tlsManager.certLastMod] from the certificate. If there
// are errors, setCertFileTime logs them. m.mu is expected to be locked.
func (mgr *DefaultManager) setCertFileTime(ctx context.Context) {
Expand Down
48 changes: 28 additions & 20 deletions internal/home/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,9 @@
// hostsContainer is used for DNS initialization on updates.
hostsContainer *aghnet.HostsContainer

// httpsServer is the server that handles HTTPS traffic. If it is not nil,
// [Web.http3Server] must also not be nil.
//
// TODO(d.kolyshev): Make it a pointer.
httpsServer httpsServer
// httpsServer is the server that handles HTTPS traffic. It must not be
// nil.
httpsServer *httpsServer

// pidFilePath is used for cleanup.
pidFilePath string
Expand Down Expand Up @@ -284,9 +282,11 @@
w.registerControlHandlers()
}

w.httpsServer.logger = conf.baseLogger.With(slogutil.KeyPrefix, "https_server")
w.httpsServer.mu = &sync.Mutex{}
w.httpsServer.reconfigured = make(chan unit, 1)
w.httpsServer = &httpsServer{
logger: conf.baseLogger.With(slogutil.KeyPrefix, "https_server"),
mu: &sync.Mutex{},
reconfigured: make(chan unit, 1),
}

return w
}
Expand Down Expand Up @@ -513,6 +513,7 @@
}

// startPprof launches the debug and profiling server on the provided port.
// baseLogger must not be nil.
func startPprof(baseLogger *slog.Logger, port uint16) {
addr := netip.AddrPortFrom(netutil.IPv4Localhost(), port)

Expand Down Expand Up @@ -599,15 +600,16 @@
status,
)
resp := &tlsConfig{
tlsConfigSettingsExt: setts,
tlsConfigSettingsExt: *setts,
tlsConfigStatus: tlsConfigStatusFromConf(status),
}

marshalTLS(ctx, web.logger, w, r, resp)
}

// validateTLSSettings returns error if the setts are not valid.
func (web *webAPI) validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
// validateTLSSettings returns error if the setts are not valid. setts must not
// be nil.
func (web *webAPI) validateTLSSettings(setts *tlsConfigSettingsExt) (err error) {
if !setts.Enabled {
if setts.ServePlainDNS == aghalg.NBFalse {
// TODO(a.garipov): Support full disabling of all DNS.
Expand Down Expand Up @@ -793,7 +795,7 @@
err = aghtls.LoadTLSConfig(ctx, web.logger, web.tlsManager, conf, status)
if err != nil {
resp := &tlsConfig{
tlsConfigSettingsExt: req,
tlsConfigSettingsExt: *req,
tlsConfigStatus: tlsConfigStatusFromConf(status),
}

Expand Down Expand Up @@ -827,7 +829,7 @@
return
}

web.respondAndRestartTLS(ctx, w, r, req, status, restartHTTPS)

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (windows-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (macos-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (ubuntu-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / build_and_release

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (macos-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (ubuntu-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS

Check failure on line 832 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (windows-latest)

cannot use req (variable of type *tlsConfigSettingsExt) as tlsConfigSettingsExt value in argument to web.respondAndRestartTLS
}

// respondAndRestartTLS sends the TLS config response, flushes the response
Expand All @@ -842,7 +844,7 @@
restartHTTPS bool,
) {
resp := &tlsConfig{
tlsConfigSettingsExt: req,
tlsConfigSettingsExt: *req,

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (windows-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (macos-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (ubuntu-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / build_and_release

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (macos-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (ubuntu-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)

Check failure on line 847 in internal/home/web.go

View workflow job for this annotation

GitHub Actions / TestJob (windows-latest)

invalid operation: cannot indirect req (variable of struct type tlsConfigSettingsExt)
tlsConfigStatus: tlsConfigStatusFromConf(status),
}

Expand All @@ -860,7 +862,7 @@
}

// setServePlainDNS updates the ServePlainDNS field of [config.DNS].
func setServePlainDNS(req tlsConfigSettingsExt) {
func setServePlainDNS(req *tlsConfigSettingsExt) {
if req.ServePlainDNS == aghalg.NBNull {
return
}
Expand Down Expand Up @@ -955,12 +957,18 @@
aghhttp.WriteJSONResponseOK(ctx, logger, w, r, *data)
}

// unmarshalTLS handles base64-encoded certificates transparently.
func unmarshalTLS(r *http.Request) (data tlsConfigSettingsExt, err error) {
data = tlsConfigSettingsExt{}
// unmarshalTLS handles base64-encoded certificates transparently. r must not
// be nil.
func unmarshalTLS(r *http.Request) (data *tlsConfigSettingsExt, err error) {
data = &tlsConfigSettingsExt{}

err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
return data, fmt.Errorf("failed to parse new TLS config json: %w", err)
return data, fmt.Errorf("failed to parse new tls config json: %w", err)
}

if data == nil {
return &tlsConfigSettingsExt{}, nil
}

if data.tlsConfigSettings.CertificateChain != "" {
Expand All @@ -972,7 +980,7 @@

data.tlsConfigSettings.CertificateChain = string(cert)
if data.tlsConfigSettings.CertificatePath != "" {
return data, fmt.Errorf("certificate data and file can't be set together")
return data, errors.Error("certificate data and file can't be set together")
}
}

Expand All @@ -987,7 +995,7 @@

data.tlsConfigSettings.PrivateKey = string(key)
if data.tlsConfigSettings.PrivateKeyPath != "" {
return data, fmt.Errorf("private key data and file can't be set together")
return data, errors.Error("private key data and file can't be set together")
}

return data, nil
Expand Down
17 changes: 9 additions & 8 deletions internal/home/web_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ func TestWebAPI_HandleTLSConfigure(t *testing.T) {
})

res := &tlsConfig{
tlsConfigStatus: &tlsConfigStatus{},
tlsConfigStatus: &tlsConfigStatus{},
tlsConfigSettingsExt: tlsConfigSettingsExt{},
}

err = json.NewDecoder(w.Body).Decode(res)
Expand Down Expand Up @@ -223,21 +224,21 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) {
testCases := []struct {
name string
wantErr string
setts tlsConfigSettingsExt
setts *tlsConfigSettingsExt
}{{
name: "basic",
wantErr: "",
setts: tlsConfigSettingsExt{},
setts: &tlsConfigSettingsExt{},
}, {
name: "disabled_all",
wantErr: "plain DNS is required in case encryption protocols are disabled",
setts: tlsConfigSettingsExt{
setts: &tlsConfigSettingsExt{
ServePlainDNS: aghalg.NBFalse,
},
}, {
name: "busy_https_port",
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
setts: tlsConfigSettingsExt{
setts: &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortHTTPS: uint16(busyTCPPort),
Expand All @@ -246,7 +247,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) {
}, {
name: "busy_dot_port",
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort),
setts: tlsConfigSettingsExt{
setts: &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverTLS: uint16(busyTCPPort),
Expand All @@ -255,7 +256,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) {
}, {
name: "busy_doq_port",
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
setts: tlsConfigSettingsExt{
setts: &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverQUIC: uint16(busyUDPPort),
Expand All @@ -264,7 +265,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) {
}, {
name: "duplicate_port",
wantErr: "validating tcp ports: duplicated values: [4433]",
setts: tlsConfigSettingsExt{
setts: &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortHTTPS: 4433,
Expand Down
Loading