diff --git a/filebrowser-q/Makefile b/filebrowser-q/Makefile index 0406ef6fe..3f4d22b5e 100644 --- a/filebrowser-q/Makefile +++ b/filebrowser-q/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser-q PKG_VERSION:=2.0.2-stable -PKG_RELEASE:=13 +PKG_RELEASE:=17 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)? diff --git a/libopenssl-gost/Makefile b/libopenssl-gost/Makefile index f2022045a..82444175b 100644 --- a/libopenssl-gost/Makefile +++ b/libopenssl-gost/Makefile @@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=libopenssl-gost PKG_VERSION:=3.0.5 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/gost-engine/engine.git diff --git a/libopenssl-gost/patches/001-add-tests-option.patch b/libopenssl-gost/patches/0001-add-tests-option.patch similarity index 100% rename from libopenssl-gost/patches/001-add-tests-option.patch rename to libopenssl-gost/patches/0001-add-tests-option.patch diff --git a/libopenssl-gost/patches/0002-Add-digest-stub-with-MAC-for-TLS-1.2.patch b/libopenssl-gost/patches/0002-Add-digest-stub-with-MAC-for-TLS-1.2.patch new file mode 100644 index 000000000..681e7586c --- /dev/null +++ b/libopenssl-gost/patches/0002-Add-digest-stub-with-MAC-for-TLS-1.2.patch @@ -0,0 +1,86 @@ +From d7932b33a779c08551d6cd44bf0eae3e250db91c Mon Sep 17 00:00:00 2001 +From: Kirill Ermoshin +Date: Tue, 10 Nov 2026 16:27:14 +0300 +Subject: [PATCH 1/4] Add digest stub with MAC for TLS 1.2 + +--- + gost_prov_digest.c | 20 +++++++++++++++++++- + test_digest.c | 18 +++++++++++++++--- + 2 files changed, 34 insertions(+), 4 deletions(-) + +diff --git a/gost_prov_digest.c b/gost_prov_digest.c +index 7a237e5..4ce22fb 100644 +--- a/gost_prov_digest.c ++++ b/gost_prov_digest.c +@@ -158,6 +158,11 @@ MAKE_FUNCTIONS(GostR3411_94_digest); + MAKE_FUNCTIONS(GostR3411_2012_256_digest); + MAKE_FUNCTIONS(GostR3411_2012_512_digest); + ++MAKE_FUNCTIONS(Gost28147_89_mac); ++MAKE_FUNCTIONS(Gost28147_89_mac_12); ++MAKE_FUNCTIONS(magma_omac_mac); ++MAKE_FUNCTIONS(grasshopper_omac_mac); ++ + /* The OSSL_ALGORITHM for the provider's operation query function */ + const OSSL_ALGORITHM GOST_prov_digests[] = { + /* +@@ -184,13 +189,26 @@ const OSSL_ALGORITHM GOST_prov_digests[] = { + { SN_id_GostR3411_94":id-GostR3411-94:1.2.643.2.2.9:" + LN_id_GostR3411_94, NULL, + GostR3411_94_digest_functions, "GOST R 34.11-94" }, +- { NULL , NULL, NULL } ++ ++ { SN_id_Gost28147_89_MAC ":" LN_id_Gost28147_89_MAC ":1.2.643.2.2.22", NULL, ++ Gost28147_89_mac_functions, "gost mac stub for TLS 1.2" }, ++ { SN_gost_mac_12, NULL, ++ Gost28147_89_mac_12_functions, "gost12 mac stub for TLS 1.2" }, ++ { SN_magma_mac, NULL, ++ magma_omac_mac_functions, "magma mac stub for TLS 1.2" }, ++ { SN_kuznyechik_mac, NULL, ++ grasshopper_omac_mac_functions, "kuznyechik mac stub for TLS 1.2" }, ++ { NULL, NULL, NULL } + }; + + static GOST_digest *digests[] = { + &GostR3411_94_digest, + &GostR3411_2012_256_digest, + &GostR3411_2012_512_digest, ++ &Gost28147_89_mac, ++ &Gost28147_89_mac_12, ++ &magma_omac_mac, ++ &grasshopper_omac_mac, + }; + + #define arraysize(l) (sizeof(l) / sizeof(l[0])) +diff --git a/test_digest.c b/test_digest.c +index 89f73d9..46aaa94 100644 +--- a/test_digest.c ++++ b/test_digest.c +@@ -824,9 +824,21 @@ static int do_test(const struct hash_testvec *tv) + EVP_MAC *mac = NULL; + + ERR_set_mark(); +- T((md = EVP_MD_fetch(NULL, tv->algname, NULL)) +- || (mac = EVP_MAC_fetch(NULL, tv->algname, NULL)) +- || (md = (EVP_MD *)EVP_get_digestbyname(tv->algname))); ++ if (tv->key && !tv->hmac) { ++ /* ++ * The provider may expose a digest stub under the same name ++ * (needed for TLS 1.2) which cannot accept a key through ++ * EVP_MD_CTX_ctrl. Prefer the MAC implementation for keyed ++ * non-HMAC vectors. ++ */ ++ T((mac = EVP_MAC_fetch(NULL, tv->algname, NULL)) ++ || (md = EVP_MD_fetch(NULL, tv->algname, NULL)) ++ || (md = (EVP_MD *)EVP_get_digestbyname(tv->algname))); ++ } else { ++ T((md = EVP_MD_fetch(NULL, tv->algname, NULL)) ++ || (mac = EVP_MAC_fetch(NULL, tv->algname, NULL)) ++ || (md = (EVP_MD *)EVP_get_digestbyname(tv->algname))); ++ } + ERR_pop_to_mark(); + + printf(cBLUE "Test %s: %s: " cNORM, tv->algname, tv->name); +-- +2.43.0 + diff --git a/libopenssl-gost/patches/0003-Add-transport-keys-for-provider.patch b/libopenssl-gost/patches/0003-Add-transport-keys-for-provider.patch new file mode 100644 index 000000000..6eb5432c6 --- /dev/null +++ b/libopenssl-gost/patches/0003-Add-transport-keys-for-provider.patch @@ -0,0 +1,1197 @@ +From b12f3c3cc4863c1e9555970efbed0f0ff75e4db5 Mon Sep 17 00:00:00 2001 +From: Kirill Ermoshin +Date: Tue, 10 Nov 2026 16:27:29 +0300 +Subject: [PATCH 2/4] Add transport keys for provider + +--- + cmake/provider.cmake | 1 + + gost_ec_keyx.c | 10 +- + gost_lcl.h | 4 + + gost_prov.c | 2 + + gost_prov.h | 1 + + gost_prov_asym_cipher.c | 1020 +++++++++++++++++++++++++++++++++++++++ + test/04-pkey.t | 48 +- + 7 files changed, 1084 insertions(+), 2 deletions(-) + create mode 100644 gost_prov_asym_cipher.c + +diff --git a/cmake/provider.cmake b/cmake/provider.cmake +index 23b6636..68c92d4 100644 +--- a/cmake/provider.cmake ++++ b/cmake/provider.cmake +@@ -9,6 +9,7 @@ set(GOST_PROV_SOURCE_FILES + gost_prov_signature.c + gost_prov_decoder.c + gost_prov_keyexch.c ++ gost_prov_asym_cipher.c + gost_prov_tls.c + gost_prov_tls.h + gost_cipher_ctx.c +diff --git a/gost_ec_keyx.c b/gost_ec_keyx.c +index 5f741bb..afb0fac 100644 +--- a/gost_ec_keyx.c ++++ b/gost_ec_keyx.c +@@ -93,9 +93,16 @@ int VKO_compute_key(unsigned char *shared_key, + size_t secret_len = 0; + EVP_MD_CTX *mdctx = NULL; + const EVP_MD *md = NULL; ++ EVP_MD *fetched_md = NULL; + int ret = 0; + + md = EVP_get_digestbynid(vko_dgst_nid); ++ if (!md) { ++ /* Digest may live in a provider (e.g. when called from ++ * the GOST provider itself) */ ++ fetched_md = EVP_MD_fetch(NULL, OBJ_nid2sn(vko_dgst_nid), NULL); ++ md = fetched_md; ++ } + if (!md) { + GOSTerr(GOST_F_VKO_COMPUTE_KEY, GOST_R_INVALID_DIGEST_TYPE); + goto exit; +@@ -129,6 +136,7 @@ int VKO_compute_key(unsigned char *shared_key, + ret = (EVP_MD_size(md) > 0) ? EVP_MD_size(md) : 0; + exit: + ++ EVP_MD_free(fetched_md); + EVP_MD_CTX_free(mdctx); + OPENSSL_free(ecdh_secret); + +@@ -139,7 +147,7 @@ exit: + * KEG Algorithm described in R 1323565.1.020-2018 6.4.5.1. + * keyout expected to be 64 bytes + * */ +-static int gost_keg(const unsigned char *ukm_source, int pkey_nid, ++int gost_keg(const unsigned char *ukm_source, int pkey_nid, + const EC_POINT *pub_key, const EC_KEY *priv_key, + unsigned char *keyout) + { +diff --git a/gost_lcl.h b/gost_lcl.h +index bbed75f..c6ba7d4 100644 +--- a/gost_lcl.h ++++ b/gost_lcl.h +@@ -290,6 +290,10 @@ int VKO_compute_key(unsigned char *shared_key, + const EC_POINT *pub_key, const EC_KEY *priv_key, + const unsigned char *ukm, const size_t ukm_size, + const int vko_dgst_nid); ++/* KEG algorithm, keyout expected to be 64 bytes */ ++int gost_keg(const unsigned char *ukm_source, int pkey_nid, ++ const EC_POINT *pub_key, const EC_KEY *priv_key, ++ unsigned char *keyout); + + /*============== miscellaneous functions============================= */ + /* +diff --git a/gost_prov.c b/gost_prov.c +index 53f2bb9..94d0ec8 100644 +--- a/gost_prov.c ++++ b/gost_prov.c +@@ -104,6 +104,8 @@ static const OSSL_ALGORITHM *gost_operation(void *vprovctx, + return GOST_prov_decoder; + case OSSL_OP_KEYEXCH: + return GOST_prov_keyexch; ++ case OSSL_OP_ASYM_CIPHER: ++ return GOST_prov_asym_cipher; + } + return NULL; + } +diff --git a/gost_prov.h b/gost_prov.h +index 861eac7..069fed7 100644 +--- a/gost_prov.h ++++ b/gost_prov.h +@@ -73,3 +73,4 @@ extern const OSSL_ALGORITHM GOST_prov_encoder[]; + extern const OSSL_ALGORITHM GOST_prov_signature[]; + extern const OSSL_ALGORITHM GOST_prov_decoder[]; + extern const OSSL_ALGORITHM GOST_prov_keyexch[]; ++extern const OSSL_ALGORITHM GOST_prov_asym_cipher[]; +diff --git a/gost_prov_asym_cipher.c b/gost_prov_asym_cipher.c +new file mode 100644 +index 0000000..27891d2 +--- /dev/null ++++ b/gost_prov_asym_cipher.c +@@ -0,0 +1,1020 @@ ++/********************************************************************** ++ * gost_prov_asym_cipher.c * ++ * * ++ * Provider key transport (OSSL_OP_ASYM_CIPHER) for GOST * ++ * * ++ * Implements the CryptoPro key transport (RFC 4357 6.3/6.4 via * ++ * GOST_KEY_TRANSPORT) and the KEG/KExp15 transport (RFC 9189 / * ++ * R 1323565.1.020-2018 via PSKeyTransport_gost), mirroring the * ++ * engine pmeth callbacks in gost_ec_keyx.c * ++ * * ++ * This file is distributed under the same license as OpenSSL * ++ **********************************************************************/ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "gost_prov.h" ++#include "gost_lcl.h" ++#include "gost_keywrap.h" ++#include "gost_tls12_additional.h" ++#include "e_gost_err.h" ++ ++/* ++ * Parameter name used by newer OpenSSL versions. Define it here for ++ * builds against OpenSSL without it (same string as upstream). ++ */ ++#ifndef OSSL_ASYM_CIPHER_PARAM_KEY_ENC_ALGORITHM_ID ++# define OSSL_ASYM_CIPHER_PARAM_KEY_ENC_ALGORITHM_ID "key-enc-algorithm-id" ++#endif ++ ++#define GKT_UKM_SIZE 8 ++#define KEG_UKM_SIZE 32 ++#define GKT_KEY_LEN 32 ++ ++/* ++ * Forward declarations of all generic OSSL_DISPATCH functions, to make sure ++ * they are correctly defined further down. ++ */ ++static OSSL_FUNC_asym_cipher_newctx_fn gost_asym_cipher_newctx; ++static OSSL_FUNC_asym_cipher_freectx_fn gost_asym_cipher_freectx; ++static OSSL_FUNC_asym_cipher_dupctx_fn gost_asym_cipher_dupctx; ++static OSSL_FUNC_asym_cipher_encrypt_init_fn gost_asym_cipher_encrypt_init; ++static OSSL_FUNC_asym_cipher_decrypt_init_fn gost_asym_cipher_decrypt_init; ++static OSSL_FUNC_asym_cipher_encrypt_fn gost_asym_cipher_encrypt; ++static OSSL_FUNC_asym_cipher_decrypt_fn gost_asym_cipher_decrypt; ++static OSSL_FUNC_asym_cipher_get_ctx_params_fn gost_asym_cipher_get_ctx_params; ++static OSSL_FUNC_asym_cipher_set_ctx_params_fn gost_asym_cipher_set_ctx_params; ++static OSSL_FUNC_asym_cipher_gettable_ctx_params_fn ++ gost_asym_cipher_gettable_ctx_params; ++static OSSL_FUNC_asym_cipher_settable_ctx_params_fn ++ gost_asym_cipher_settable_ctx_params; ++ ++typedef struct gost_asym_cipher_ctx_st { ++ GOST_KEY_DATA *key_data; ++ int is_encrypt; ++ int cipher_nid; /* NID_id_Gost28147_89 or NID_{magma,kuznyechik}_ctr */ ++ unsigned char ukm[KEG_UKM_SIZE]; ++ size_t ukm_size; ++} GOST_ASYM_CIPHER_CTX; ++ ++static void *gost_asym_cipher_newctx(void *provctx) ++{ ++ (void)provctx; ++ return OPENSSL_zalloc(sizeof(GOST_ASYM_CIPHER_CTX)); ++} ++ ++static void gost_asym_cipher_freectx(void *vctx) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ ++ if (!ctx) ++ return; ++ ++ keymgmt_free(ctx->key_data); ++ OPENSSL_cleanse(ctx->ukm, sizeof(ctx->ukm)); ++ OPENSSL_free(ctx); ++} ++ ++static void *gost_asym_cipher_dupctx(void *vctx) ++{ ++ GOST_ASYM_CIPHER_CTX *src = vctx; ++ GOST_ASYM_CIPHER_CTX *dst; ++ ++ if (!src) ++ return NULL; ++ ++ dst = OPENSSL_zalloc(sizeof(*dst)); ++ if (dst == NULL) ++ return NULL; ++ ++ dst->is_encrypt = src->is_encrypt; ++ dst->cipher_nid = src->cipher_nid; ++ memcpy(dst->ukm, src->ukm, sizeof(dst->ukm)); ++ dst->ukm_size = src->ukm_size; ++ ++ if (src->key_data != NULL) { ++ dst->key_data = keymgmt_dup(src->key_data, ++ OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS ++ | OSSL_KEYMGMT_SELECT_KEYPAIR); ++ if (dst->key_data == NULL) { ++ OPENSSL_free(dst); ++ return NULL; ++ } ++ } ++ ++ return dst; ++} ++ ++static int gost_asym_cipher_init(void *vctx, void *vkey_data, ++ const OSSL_PARAM params[], int is_encrypt) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ GOST_KEY_DATA *key_data = vkey_data; ++ int selection = OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS; ++ ++ if (!ctx || !key_data || !key_data->ec) ++ return 0; ++ ++ if (EC_KEY_get0_private_key(key_data->ec) != NULL) ++ selection |= OSSL_KEYMGMT_SELECT_PRIVATE_KEY; ++ if (EC_KEY_get0_public_key(key_data->ec) != NULL) ++ selection |= OSSL_KEYMGMT_SELECT_PUBLIC_KEY; ++ ++ keymgmt_free(ctx->key_data); ++ ctx->key_data = keymgmt_dup(key_data, selection); ++ if (!ctx->key_data) ++ return 0; ++ ++ ctx->is_encrypt = is_encrypt; ++ ctx->cipher_nid = NID_id_Gost28147_89; ++ ctx->ukm_size = 0; ++ ++ return gost_asym_cipher_set_ctx_params(ctx, params); ++} ++ ++static int gost_asym_cipher_encrypt_init(void *vctx, void *vkey_data, ++ const OSSL_PARAM params[]) ++{ ++ return gost_asym_cipher_init(vctx, vkey_data, params, 1); ++} ++ ++static int gost_asym_cipher_decrypt_init(void *vctx, void *vkey_data, ++ const OSSL_PARAM params[]) ++{ ++ return gost_asym_cipher_init(vctx, vkey_data, params, 0); ++} ++ ++static int vko_dgst_nid_from_key_type(int key_type) ++{ ++ switch (key_type) { ++ case NID_id_GostR3410_2001: ++ case NID_id_GostR3410_2001DH: ++ return NID_id_GostR3411_94; ++ case NID_id_GostR3410_2012_256: ++ return NID_id_GostR3411_2012_256; ++ case NID_id_GostR3410_2012_512: ++ /* VKO 256 for 512 */ ++ return NID_id_GostR3411_2012_256; ++ default: ++ return NID_undef; ++ } ++} ++ ++/* ++ * Decode the ephemeral public key of the transport structure into a ++ * plain EC_KEY (no engine/provider decoding machinery involved). ++ */ ++static EC_KEY *decode_ephemeral_key(const X509_PUBKEY *pub) ++{ ++ const unsigned char *pubkey_buf = NULL; ++ int pubkey_len = 0; ++ X509_ALGOR *palg = NULL; ++ int key_type = NID_undef; ++ EC_KEY *ec = NULL; ++ ++ if (pub == NULL) ++ return NULL; ++ ++ ec = EC_KEY_new(); ++ if (ec == NULL) ++ return NULL; ++ ++ if (!X509_PUBKEY_get0_param(NULL, &pubkey_buf, &pubkey_len, &palg, pub) ++ || !internal_pub_decode_ec(ec, &key_type, palg, pubkey_buf, pubkey_len)) { ++ EC_KEY_free(ec); ++ return NULL; ++ } ++ ++ return ec; ++} ++ ++/* Generate ephemeral key pair on the same curve as key_data->ec */ ++static EC_KEY *generate_ephemeral_key(const EC_KEY *recipient) ++{ ++ EC_KEY *eph = EC_KEY_new(); ++ ++ if (eph == NULL) ++ return NULL; ++ ++ if (!EC_KEY_set_group(eph, EC_KEY_get0_group(recipient)) ++ || !gost_ec_keygen(eph)) { ++ EC_KEY_free(eph); ++ return NULL; ++ } ++ ++ return eph; ++} ++ ++/* Fill in the ephem_key field of a transport structure */ ++static int set_ephemeral_pubkey(X509_PUBKEY **pub, EC_KEY *ec, int key_type) ++{ ++ if (ec == NULL || pub == NULL) ++ return 0; ++ ++ if (*pub == NULL && (*pub = X509_PUBKEY_new()) == NULL) ++ return 0; ++ ++ return internal_pub_encode_ec(*pub, ec, key_type); ++} ++ ++/********************************************************************* ++ * ++ * CryptoPro key transport (RFC 4357 6.3/6.4, GOST_KEY_TRANSPORT) ++ * ++ *****/ ++ ++static int gost_asym_cipher_gkt_encrypt(void *vctx, unsigned char *out, ++ size_t *out_len, size_t out_size, ++ const unsigned char *key, ++ size_t key_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ GOST_KEY_TRANSPORT *gkt = NULL; ++ unsigned char ukm[GKT_UKM_SIZE], shared_key[32], crypted_key[44]; ++ gost_ctx cctx; ++ const struct gost_cipher_info *param = NULL; ++ ASN1_OBJECT *crypt_params_obj = NULL; ++ EC_KEY *eph_ec = NULL; ++ int pkey_nid = ctx->key_data->type; ++ int res_len = 0; ++ int ret = 0; ++ ++ if (ctx->ukm_size == GKT_UKM_SIZE) { ++ memcpy(ukm, ctx->ukm, GKT_UKM_SIZE); ++ } else if (ctx->ukm_size == 0) { ++ if (RAND_bytes(ukm, GKT_UKM_SIZE) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, GOST_R_RNG_ERROR); ++ return 0; ++ } ++ memcpy(ctx->ukm, ukm, GKT_UKM_SIZE); ++ ctx->ukm_size = GKT_UKM_SIZE; ++ } else { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, GOST_R_INVALID_IV_LENGTH); ++ return 0; ++ } ++ ++ crypt_params_obj = ++ (pkey_nid == NID_id_GostR3410_2001 || pkey_nid == NID_id_GostR3410_2001DH) ? ++ OBJ_nid2obj(NID_id_Gost28147_89_CryptoPro_A_ParamSet) : ++ OBJ_nid2obj(NID_id_tc26_gost_28147_param_Z); ++ param = get_encryption_params(crypt_params_obj); ++ if (!param) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, GOST_R_INVALID_CIPHER); ++ goto err; ++ } ++ ++ if (out != NULL) { ++ int dgst_nid = vko_dgst_nid_from_key_type(pkey_nid); ++ ++ if (key == NULL || key_len != GKT_KEY_LEN) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ++ GOST_R_KEY_IS_NOT_INITIALIZED); ++ goto err; ++ } ++ ++ eph_ec = generate_ephemeral_key(ctx->key_data->ec); ++ if (eph_ec == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ ++ if (!VKO_compute_key(shared_key, ++ EC_KEY_get0_public_key(ctx->key_data->ec), ++ eph_ec, ukm, GKT_UKM_SIZE, dgst_nid)) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ ++ gost_init(&cctx, param->sblock); ++ if (!keyWrapCryptoPro(&cctx, shared_key, ukm, key, crypted_key)) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ } ++ ++ gkt = GOST_KEY_TRANSPORT_new(); ++ if (gkt == NULL) ++ goto err; ++ ++ if (!ASN1_OCTET_STRING_set(gkt->key_agreement_info->eph_iv, ukm, ++ GKT_UKM_SIZE) ++ || !ASN1_OCTET_STRING_set(gkt->key_info->imit, crypted_key + 40, 4) ++ || !ASN1_OCTET_STRING_set(gkt->key_info->encrypted_key, ++ crypted_key + 8, 32)) ++ goto err; ++ ++ if (!set_ephemeral_pubkey(&gkt->key_agreement_info->ephem_key, ++ out ? eph_ec : ctx->key_data->ec, pkey_nid)) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ++ GOST_R_CANNOT_PACK_EPHEMERAL_KEY); ++ goto err; ++ } ++ ++ ASN1_OBJECT_free(gkt->key_agreement_info->cipher); ++ gkt->key_agreement_info->cipher = OBJ_nid2obj(param->nid); ++ ++ res_len = i2d_GOST_KEY_TRANSPORT(gkt, NULL); ++ if (res_len <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ERR_R_ASN1_LIB); ++ goto err; ++ } ++ ++ if (out == NULL) { ++ *out_len = res_len; ++ ret = 1; ++ } else { ++ if ((size_t)res_len > out_size) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, GOST_R_INVALID_BUFFER_SIZE); ++ goto err; ++ } ++ if ((*out_len = i2d_GOST_KEY_TRANSPORT(gkt, &out)) > 0) ++ ret = 1; ++ else ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_ENCRYPT, ERR_R_ASN1_LIB); ++ } ++ ++ err: ++ OPENSSL_cleanse(shared_key, sizeof(shared_key)); ++ EC_KEY_free(eph_ec); ++ GOST_KEY_TRANSPORT_free(gkt); ++ return ret; ++} ++ ++static int gost_asym_cipher_gkt_decrypt(void *vctx, unsigned char *key, ++ size_t *key_len, ++ const unsigned char *in, size_t in_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ const unsigned char *p = in; ++ GOST_KEY_TRANSPORT *gkt = NULL; ++ unsigned char wrappedKey[44]; ++ unsigned char sharedKey[32]; ++ gost_ctx cctx; ++ const struct gost_cipher_info *param = NULL; ++ EC_KEY *eph_ec = NULL; ++ int dgst_nid = NID_undef; ++ int ret = 0; ++ ++ gkt = d2i_GOST_KEY_TRANSPORT(NULL, (const unsigned char **)&p, in_len); ++ if (gkt == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, ++ GOST_R_ERROR_PARSING_KEY_TRANSPORT_INFO); ++ return 0; ++ } ++ ++ eph_ec = decode_ephemeral_key(gkt->key_agreement_info->ephem_key); ++ if (eph_ec == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, GOST_R_INCOMPATIBLE_PEER_KEY); ++ goto err; ++ } ++ ++ param = get_encryption_params(gkt->key_agreement_info->cipher); ++ if (param == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, GOST_R_INVALID_CIPHER); ++ goto err; ++ } ++ ++ if (ASN1_STRING_length(gkt->key_agreement_info->eph_iv) != 8 ++ || ASN1_STRING_length(gkt->key_info->encrypted_key) != 32 ++ || ASN1_STRING_length(gkt->key_info->imit) != 4) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, ++ GOST_R_ERROR_PARSING_KEY_TRANSPORT_INFO); ++ goto err; ++ } ++ ++ memcpy(wrappedKey, ASN1_STRING_get0_data(gkt->key_agreement_info->eph_iv), 8); ++ memcpy(wrappedKey + 8, ++ ASN1_STRING_get0_data(gkt->key_info->encrypted_key), 32); ++ memcpy(wrappedKey + 40, ASN1_STRING_get0_data(gkt->key_info->imit), 4); ++ ++ dgst_nid = vko_dgst_nid_from_key_type(ctx->key_data->type); ++ ++ if (!VKO_compute_key(sharedKey, ++ EC_KEY_get0_public_key(eph_ec), ++ ctx->key_data->ec, wrappedKey, GKT_UKM_SIZE, ++ dgst_nid)) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ ++ gost_init(&cctx, param->sblock); ++ if (!keyUnwrapCryptoPro(&cctx, sharedKey, wrappedKey, key)) { ++ GOSTerr(GOST_F_PKEY_GOST_ECCP_DECRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ ++ *key_len = GKT_KEY_LEN; ++ ret = 1; ++ err: ++ OPENSSL_cleanse(sharedKey, sizeof(sharedKey)); ++ EC_KEY_free(eph_ec); ++ GOST_KEY_TRANSPORT_free(gkt); ++ return ret; ++} ++ ++/********************************************************************* ++ * ++ * KEG / KExp15 key transport (R 1323565.1.020-2018, PSKeyTransport) ++ * ++ *****/ ++ ++static int keg_mac_and_iv(int cipher_nid, int *mac_nid, int *iv_len) ++{ ++ switch (cipher_nid) { ++ case NID_magma_ctr: ++ *mac_nid = NID_magma_mac; ++ *iv_len = 4; ++ return 1; ++ case NID_kuznyechik_ctr: ++ *mac_nid = NID_grasshopper_mac; ++ *iv_len = 8; ++ return 1; ++ default: ++ return 0; ++ } ++} ++ ++static int gost_asym_cipher_keg_encrypt(void *vctx, unsigned char *out, ++ size_t *out_len, size_t out_size, ++ const unsigned char *key, ++ size_t key_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ PSKeyTransport_gost *pst = NULL; ++ unsigned char expkeys[64]; ++ unsigned char *exp_buf = NULL; ++ int exp_len = 0; ++ int mac_nid = NID_undef; ++ int iv_len = 0; ++ EC_KEY *eph_ec = NULL; ++ int pkey_nid = ctx->key_data->type; ++ int res_len = 0; ++ int ret = 0; ++ ++ if (ctx->ukm_size == 0) { ++ if (RAND_bytes(ctx->ukm, KEG_UKM_SIZE) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_INTERNAL_ERROR); ++ return 0; ++ } ++ ctx->ukm_size = KEG_UKM_SIZE; ++ } else if (ctx->ukm_size != KEG_UKM_SIZE) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, GOST_R_INVALID_IV_LENGTH); ++ return 0; ++ } ++ ++ if (!keg_mac_and_iv(ctx->cipher_nid, &mac_nid, &iv_len)) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++ ++ exp_len = key_len + (mac_nid == NID_magma_mac ? 8 : 16); ++ exp_buf = OPENSSL_malloc(exp_len); ++ if (exp_buf == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_MALLOC_FAILURE); ++ return 0; ++ } ++ ++ eph_ec = generate_ephemeral_key(ctx->key_data->ec); ++ if (eph_ec == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ++ GOST_R_ERROR_COMPUTING_SHARED_KEY); ++ goto err; ++ } ++ ++ if (gost_keg(ctx->ukm, pkey_nid, ++ EC_KEY_get0_public_key(ctx->key_data->ec), ++ eph_ec, expkeys) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ++ GOST_R_ERROR_COMPUTING_EXPORT_KEYS); ++ goto err; ++ } ++ ++ if (gost_kexp15(key, key_len, ctx->cipher_nid, expkeys + 32, ++ mac_nid, expkeys + 0, ctx->ukm + 24, iv_len, ++ exp_buf, &exp_len) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ++ GOST_R_CANNOT_PACK_EPHEMERAL_KEY); ++ goto err; ++ } ++ ++ pst = PSKeyTransport_gost_new(); ++ if (pst == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_MALLOC_FAILURE); ++ goto err; ++ } ++ ++ pst->ukm = ASN1_OCTET_STRING_new(); ++ if (pst->ukm == NULL ++ || !ASN1_OCTET_STRING_set(pst->ukm, ctx->ukm, ctx->ukm_size)) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_MALLOC_FAILURE); ++ goto err; ++ } ++ ++ if (!ASN1_OCTET_STRING_set(pst->psexp, exp_buf, exp_len)) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_MALLOC_FAILURE); ++ goto err; ++ } ++ ++ if (!set_ephemeral_pubkey(&pst->ephem_key, eph_ec, pkey_nid)) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ++ GOST_R_CANNOT_PACK_EPHEMERAL_KEY); ++ goto err; ++ } ++ ++ res_len = i2d_PSKeyTransport_gost(pst, NULL); ++ if (res_len <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_ASN1_LIB); ++ goto err; ++ } ++ ++ if (out == NULL) { ++ *out_len = res_len; ++ ret = 1; ++ } else { ++ if ((size_t)res_len > out_size) { ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, GOST_R_INVALID_BUFFER_SIZE); ++ goto err; ++ } ++ if ((*out_len = i2d_PSKeyTransport_gost(pst, &out)) > 0) ++ ret = 1; ++ else ++ GOSTerr(GOST_F_PKEY_GOST2018_ENCRYPT, ERR_R_ASN1_LIB); ++ } ++ ++ err: ++ OPENSSL_cleanse(expkeys, sizeof(expkeys)); ++ if (exp_buf != NULL) ++ OPENSSL_cleanse(exp_buf, exp_len); ++ OPENSSL_free(exp_buf); ++ EC_KEY_free(eph_ec); ++ PSKeyTransport_gost_free(pst); ++ return ret; ++} ++ ++static int gost_asym_cipher_keg_decrypt(void *vctx, unsigned char *key, ++ size_t *key_len, ++ const unsigned char *in, size_t in_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ const unsigned char *p = in; ++ PSKeyTransport_gost *pst = NULL; ++ unsigned char expkeys[64]; ++ int mac_nid = NID_undef; ++ int iv_len = 0; ++ EC_KEY *eph_ec = NULL; ++ int pkey_nid = ctx->key_data->type; ++ int ret = 0; ++ ++ pst = d2i_PSKeyTransport_gost(NULL, (const unsigned char **)&p, in_len); ++ if (pst == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, ++ GOST_R_ERROR_PARSING_KEY_TRANSPORT_INFO); ++ return 0; ++ } ++ ++ if (ctx->ukm_size == 0 && pst->ukm != NULL) { ++ int ukm_len = ASN1_STRING_length(pst->ukm); ++ ++ if (ukm_len != KEG_UKM_SIZE) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, GOST_R_UKM_NOT_SET); ++ goto err; ++ } ++ memcpy(ctx->ukm, ASN1_STRING_get0_data(pst->ukm), KEG_UKM_SIZE); ++ ctx->ukm_size = KEG_UKM_SIZE; ++ } ++ ++ if (!keg_mac_and_iv(ctx->cipher_nid, &mac_nid, &iv_len)) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, GOST_R_INVALID_CIPHER); ++ goto err; ++ } ++ ++ eph_ec = decode_ephemeral_key(pst->ephem_key); ++ if (eph_ec == NULL) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, ++ GOST_R_ERROR_COMPUTING_EXPORT_KEYS); ++ goto err; ++ } ++ ++ if (gost_keg(ctx->ukm, pkey_nid, ++ EC_KEY_get0_public_key(eph_ec), ++ ctx->key_data->ec, expkeys) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, ++ GOST_R_ERROR_COMPUTING_EXPORT_KEYS); ++ goto err; ++ } ++ ++ if (gost_kimp15(ASN1_STRING_get0_data(pst->psexp), ++ ASN1_STRING_length(pst->psexp), ctx->cipher_nid, ++ expkeys + 32, mac_nid, expkeys + 0, ctx->ukm + 24, ++ iv_len, key) <= 0) { ++ GOSTerr(GOST_F_PKEY_GOST2018_DECRYPT, ++ GOST_R_CANNOT_UNPACK_EPHEMERAL_KEY); ++ goto err; ++ } ++ ++ *key_len = GKT_KEY_LEN; ++ ret = 1; ++ err: ++ OPENSSL_cleanse(expkeys, sizeof(expkeys)); ++ EC_KEY_free(eph_ec); ++ PSKeyTransport_gost_free(pst); ++ return ret; ++} ++ ++/********************************************************************* ++ * ++ * Dispatch ++ * ++ *****/ ++ ++static int gost_asym_cipher_encrypt(void *vctx, unsigned char *out, ++ size_t *out_len, size_t out_size, ++ const unsigned char *key, size_t key_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ ++ if (!ctx || !ctx->key_data || !ctx->key_data->ec) ++ return 0; ++ ++ switch (ctx->cipher_nid) { ++ case NID_id_Gost28147_89: ++ return gost_asym_cipher_gkt_encrypt(ctx, out, out_len, out_size, ++ key, key_len); ++ case NID_magma_ctr: ++ case NID_kuznyechik_ctr: ++ if (ctx->key_data->type == NID_id_GostR3410_2001 ++ || ctx->key_data->type == NID_id_GostR3410_2001DH) { ++ GOSTerr(GOST_F_PKEY_GOST_ENCRYPT, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++ return gost_asym_cipher_keg_encrypt(ctx, out, out_len, out_size, ++ key, key_len); ++ default: ++ GOSTerr(GOST_F_PKEY_GOST_ENCRYPT, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++} ++ ++static int gost_asym_cipher_decrypt(void *vctx, unsigned char *out, ++ size_t *out_len, size_t out_size, ++ const unsigned char *in, size_t in_len) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ ++ if (!ctx || !ctx->key_data || !ctx->key_data->ec ++ || EC_KEY_get0_private_key(ctx->key_data->ec) == NULL) ++ return 0; ++ ++ if (out == NULL) { ++ *out_len = GKT_KEY_LEN; ++ return 1; ++ } ++ ++ if (out_size < GKT_KEY_LEN) { ++ GOSTerr(GOST_F_PKEY_GOST_DECRYPT, GOST_R_INVALID_BUFFER_SIZE); ++ return 0; ++ } ++ ++ switch (ctx->cipher_nid) { ++ case NID_id_Gost28147_89: ++ return gost_asym_cipher_gkt_decrypt(ctx, out, out_len, in, in_len); ++ case NID_magma_ctr: ++ case NID_kuznyechik_ctr: ++ if (ctx->key_data->type == NID_id_GostR3410_2001 ++ || ctx->key_data->type == NID_id_GostR3410_2001DH) { ++ GOSTerr(GOST_F_PKEY_GOST_DECRYPT, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++ return gost_asym_cipher_keg_decrypt(ctx, out, out_len, in, in_len); ++ default: ++ GOSTerr(GOST_F_PKEY_GOST_DECRYPT, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++} ++ ++/********************************************************************* ++ * ++ * Parameters ++ * ++ *****/ ++ ++static int can_contain_hash_alg(int paramset_nid) ++{ ++ switch (paramset_nid) { ++ case NID_id_GostR3410_2001_CryptoPro_A_ParamSet: ++ case NID_id_GostR3410_2001_CryptoPro_B_ParamSet: ++ case NID_id_GostR3410_2001_CryptoPro_C_ParamSet: ++ case NID_id_GostR3410_2001_CryptoPro_XchA_ParamSet: ++ case NID_id_GostR3410_2001_CryptoPro_XchB_ParamSet: ++ case NID_id_tc26_gost_3410_2012_256_paramSetA: ++ case NID_id_tc26_gost_3410_2012_512_paramSetA: ++ case NID_id_tc26_gost_3410_2012_512_paramSetB: ++ case NID_id_tc26_gost_3410_2012_512_paramSetC: ++ return 1; ++ default: ++ return 0; ++ } ++} ++ ++static int hash_nid_for_alg(int alg_nid) ++{ ++ switch (alg_nid) { ++ case NID_id_GostR3410_2001: ++ return NID_id_GostR3411_94_CryptoProParamSet; ++ case NID_id_GostR3410_2012_256: ++ return NID_id_GostR3411_2012_256; ++ case NID_id_GostR3410_2012_512: ++ return NID_id_GostR3411_2012_512; ++ default: ++ return NID_undef; ++ } ++} ++ ++static ASN1_STRING *encode_gost_key_params(int alg_nid, int param_nid) ++{ ++ GOST_KEY_PARAMS *gkp = GOST_KEY_PARAMS_new(); ++ ASN1_STRING *params = ASN1_STRING_type_new(V_ASN1_SEQUENCE); ++ unsigned char *der = NULL; ++ int der_len; ++ ++ if (gkp == NULL || params == NULL) ++ goto err; ++ ++ gkp->key_params = OBJ_nid2obj(param_nid); ++ ++ if (can_contain_hash_alg(param_nid)) { ++ int hash_nid = hash_nid_for_alg(alg_nid); ++ if (hash_nid == NID_undef) ++ goto err; ++ gkp->hash_params = OBJ_nid2obj(hash_nid); ++ } ++ ++ der_len = i2d_GOST_KEY_PARAMS(gkp, &der); ++ if (der_len <= 0) ++ goto err; ++ ++ ASN1_STRING_set0(params, der, der_len); ++ GOST_KEY_PARAMS_free(gkp); ++ return params; ++ ++ err: ++ GOST_KEY_PARAMS_free(gkp); ++ ASN1_STRING_free(params); ++ return NULL; ++} ++ ++static ASN1_STRING *encode_cms_params(int ka_nid) ++{ ++ /* We have only one OID here, so GOST_KEY_PARAMS (a sequence of ++ * 3 OIDs, the 1st one mandatory, the rest optional) can be used */ ++ GOST_KEY_PARAMS *gkp = GOST_KEY_PARAMS_new(); ++ ASN1_STRING *params = ASN1_STRING_type_new(V_ASN1_SEQUENCE); ++ unsigned char *der = NULL; ++ int der_len; ++ ++ if (gkp == NULL || params == NULL) ++ goto err; ++ ++ gkp->key_params = OBJ_nid2obj(ka_nid); ++ ++ der_len = i2d_GOST_KEY_PARAMS(gkp, &der); ++ if (der_len <= 0) ++ goto err; ++ ++ ASN1_STRING_set0(params, der, der_len); ++ GOST_KEY_PARAMS_free(gkp); ++ return params; ++ ++ err: ++ GOST_KEY_PARAMS_free(gkp); ++ ASN1_STRING_free(params); ++ return NULL; ++} ++ ++static int gost_asym_cipher_get_ctx_params(void *vctx, OSSL_PARAM params[]) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ OSSL_PARAM *p; ++ int alg_nid; ++ int ka_nid = NID_undef; ++ ASN1_STRING *cms_params = NULL; ++ X509_ALGOR *alg = NULL; ++ unsigned char *aid = NULL; ++ int aid_len; ++ int ret = 0; ++ ++ if (!ctx || !ctx->key_data) ++ return 0; ++ ++ p = OSSL_PARAM_locate(params, OSSL_ASYM_CIPHER_PARAM_KEY_ENC_ALGORITHM_ID); ++ if (p == NULL) ++ return 1; ++ ++ switch (ctx->cipher_nid) { ++ case NID_magma_ctr: ++ alg_nid = NID_magma_kexp15; ++ ka_nid = (ctx->key_data->type == NID_id_GostR3410_2012_256) ? ++ NID_id_tc26_agreement_gost_3410_2012_256 : ++ NID_id_tc26_agreement_gost_3410_2012_512; ++ cms_params = encode_cms_params(ka_nid); ++ break; ++ case NID_kuznyechik_ctr: ++ alg_nid = NID_kuznyechik_kexp15; ++ ka_nid = (ctx->key_data->type == NID_id_GostR3410_2012_256) ? ++ NID_id_tc26_agreement_gost_3410_2012_256 : ++ NID_id_tc26_agreement_gost_3410_2012_512; ++ cms_params = encode_cms_params(ka_nid); ++ break; ++ case NID_id_Gost28147_89: ++ default: ++ alg_nid = ctx->key_data->type; ++ cms_params = encode_gost_key_params(alg_nid, ctx->key_data->param_nid); ++ break; ++ } ++ ++ if (cms_params == NULL) ++ return 0; ++ ++ alg = X509_ALGOR_new(); ++ if (alg == NULL || !X509_ALGOR_set0(alg, OBJ_nid2obj(alg_nid), ++ V_ASN1_SEQUENCE, cms_params)) ++ goto err; ++ cms_params = NULL; /* ownership transferred to alg */ ++ ++ aid_len = i2d_X509_ALGOR(alg, &aid); ++ if (aid_len <= 0) ++ goto err; ++ ++ ret = OSSL_PARAM_set_octet_string(p, aid, aid_len); ++ OPENSSL_free(aid); ++ ++ err: ++ X509_ALGOR_free(alg); ++ ASN1_STRING_free(cms_params); ++ return ret; ++} ++ ++static int gost_asym_cipher_set_ctx_params(void *vctx, ++ const OSSL_PARAM params[]) ++{ ++ GOST_ASYM_CIPHER_CTX *ctx = vctx; ++ const OSSL_PARAM *p; ++ ++ if (!ctx || !params) ++ return 1; ++ ++ if ((p = OSSL_PARAM_locate_const(params, ++ OSSL_EXCHANGE_PARAM_KDF_UKM)) != NULL) { ++ const void *data = NULL; ++ size_t data_len; ++ ++ if (OSSL_PARAM_get_octet_string_ptr(p, &data, &data_len) != 1 ++ || (data_len != KEG_UKM_SIZE && data_len != GKT_UKM_SIZE)) { ++ GOSTerr(GOST_F_PKEY_GOST_CTRL, GOST_R_INVALID_IV_LENGTH); ++ return 0; ++ } ++ ++ memcpy(ctx->ukm, data, data_len); ++ ctx->ukm_size = data_len; ++ } ++ ++ if ((p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_CIPHER)) != NULL) { ++ const char *data = NULL; ++ int cipher_nid; ++ ++ if (OSSL_PARAM_get_utf8_string_ptr(p, &data) != 1) { ++ GOSTerr(GOST_F_PKEY_GOST_CTRL, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++ ++ cipher_nid = OBJ_sn2nid(data); ++ switch (cipher_nid) { ++ case NID_magma_ctr: ++ case NID_magma_ctr_acpkm: ++ case NID_magma_ctr_acpkm_omac: ++ case NID_magma_kexp15: ++ ctx->cipher_nid = NID_magma_ctr; ++ break; ++ case NID_kuznyechik_ctr: ++ case NID_kuznyechik_ctr_acpkm: ++ case NID_kuznyechik_ctr_acpkm_omac: ++ case NID_kuznyechik_kexp15: ++ ctx->cipher_nid = NID_kuznyechik_ctr; ++ break; ++ case NID_id_Gost28147_89: ++ ctx->cipher_nid = NID_id_Gost28147_89; ++ break; ++ default: ++ GOSTerr(GOST_F_PKEY_GOST_CTRL, GOST_R_INVALID_CIPHER); ++ return 0; ++ } ++ } ++ ++ /* keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier */ ++ if ((p = OSSL_PARAM_locate_const(params, ++ OSSL_ASYM_CIPHER_PARAM_KEY_ENC_ALGORITHM_ID)) != NULL) { ++ unsigned char *aid = NULL; ++ size_t aid_len; ++ X509_ALGOR *alg = NULL; ++ int ret = 0; ++ ++ if (!OSSL_PARAM_get_octet_string(p, (void **)&aid, 0, &aid_len)) ++ return 0; ++ ++ alg = d2i_X509_ALGOR(NULL, (const unsigned char **)&aid, aid_len); ++ if (alg == NULL) ++ goto out; ++ ++ switch (OBJ_obj2nid(alg->algorithm)) { ++ case NID_kuznyechik_kexp15: ++ ctx->cipher_nid = NID_kuznyechik_ctr; ++ ret = 1; ++ break; ++ case NID_magma_kexp15: ++ ctx->cipher_nid = NID_magma_ctr; ++ ret = 1; ++ break; ++ case NID_id_GostR3410_2001: ++ case NID_id_GostR3410_2001DH: ++ case NID_id_GostR3410_2012_256: ++ case NID_id_GostR3410_2012_512: ++ ctx->cipher_nid = NID_id_Gost28147_89; ++ ret = 1; ++ break; ++ default: ++ GOSTerr(GOST_F_PKEY_GOST_CTRL, GOST_R_INVALID_CIPHER); ++ break; ++ } ++ ++ out: ++ X509_ALGOR_free(alg); ++ OPENSSL_free(aid); ++ return ret; ++ } ++ ++ return 1; ++} ++ ++static const OSSL_PARAM gost_asym_cipher_known_ctx_params[] = { ++ OSSL_PARAM_octet_string(OSSL_EXCHANGE_PARAM_KDF_UKM, NULL, 0), ++ OSSL_PARAM_utf8_string(OSSL_PKEY_PARAM_CIPHER, NULL, 0), ++ OSSL_PARAM_octet_string(OSSL_ASYM_CIPHER_PARAM_KEY_ENC_ALGORITHM_ID, NULL, 0), ++ OSSL_PARAM_END ++}; ++ ++static const OSSL_PARAM * ++gost_asym_cipher_settable_ctx_params(void *vctx, void *provctx) ++{ ++ (void)vctx; ++ (void)provctx; ++ return gost_asym_cipher_known_ctx_params; ++} ++ ++static const OSSL_PARAM * ++gost_asym_cipher_gettable_ctx_params(void *vctx, void *provctx) ++{ ++ (void)vctx; ++ (void)provctx; ++ return gost_asym_cipher_known_ctx_params; ++} ++ ++static const OSSL_DISPATCH gost_asym_cipher_functions[] = { ++ { OSSL_FUNC_ASYM_CIPHER_NEWCTX, (void (*)(void))gost_asym_cipher_newctx }, ++ { OSSL_FUNC_ASYM_CIPHER_ENCRYPT_INIT, ++ (void (*)(void))gost_asym_cipher_encrypt_init }, ++ { OSSL_FUNC_ASYM_CIPHER_ENCRYPT, (void (*)(void))gost_asym_cipher_encrypt }, ++ { OSSL_FUNC_ASYM_CIPHER_DECRYPT_INIT, ++ (void (*)(void))gost_asym_cipher_decrypt_init }, ++ { OSSL_FUNC_ASYM_CIPHER_DECRYPT, (void (*)(void))gost_asym_cipher_decrypt }, ++ { OSSL_FUNC_ASYM_CIPHER_FREECTX, (void (*)(void))gost_asym_cipher_freectx }, ++ { OSSL_FUNC_ASYM_CIPHER_DUPCTX, (void (*)(void))gost_asym_cipher_dupctx }, ++ { OSSL_FUNC_ASYM_CIPHER_GET_CTX_PARAMS, ++ (void (*)(void))gost_asym_cipher_get_ctx_params }, ++ { OSSL_FUNC_ASYM_CIPHER_SET_CTX_PARAMS, ++ (void (*)(void))gost_asym_cipher_set_ctx_params }, ++ { OSSL_FUNC_ASYM_CIPHER_GETTABLE_CTX_PARAMS, ++ (void (*)(void))gost_asym_cipher_gettable_ctx_params }, ++ { OSSL_FUNC_ASYM_CIPHER_SETTABLE_CTX_PARAMS, ++ (void (*)(void))gost_asym_cipher_settable_ctx_params }, ++ OSSL_DISPATCH_END ++}; ++ ++const OSSL_ALGORITHM GOST_prov_asym_cipher[] = { ++ { ALG_NAME_GOST2001, NULL, gost_asym_cipher_functions }, ++ { ALG_NAME_GOST2012_256, NULL, gost_asym_cipher_functions }, ++ { ALG_NAME_GOST2012_512, NULL, gost_asym_cipher_functions }, ++ { NULL, NULL, NULL } ++}; ++/* vim: set expandtab cinoptions=\:0,l1,t0,g0,(0 sw=4 : */ +diff --git a/test/04-pkey.t b/test/04-pkey.t +index 679e2e9..3143e5d 100644 +--- a/test/04-pkey.t ++++ b/test/04-pkey.t +@@ -6,7 +6,7 @@ my $target = $ARGV[0]; + unless (grep { $_ eq $target } @valid_target) { + skip_all("Unknown test mode '$target' – expected one of @valid_target"); + } +-plan(2); ++plan(3); + use Cwd 'abs_path'; + + # prepare data for +@@ -381,5 +381,51 @@ MF4CAQAwFwYIKoUDBwEBAQIwCwYJKoUDBwECAQIDBEDsI/BH7zxilCahaafnqe3ILFBHUf+pM0wAqwZl + + # 11. Сгенерирвоать encrypted key и его разобрать. + ++# 12. Key transport: wrap and unwrap a session key ++subtest 'key transport' => sub { ++ my @cases = ( ['gost2012_256', 'A'], ['gost2012_512', 'A'], ['gost2001', 'A'] ); ++ # 4 checks per GKT case + 6 skipped KEG checks on engine + 1 negative ++ plan(4 * scalar(@cases) + 6 + 1); ++ ++ # Exactly 32 bytes: GOST session key size ++ open $F, ">", "session.bin"; ++ print $F "0123456789abcdef0123456789abcdef"; ++ close $F; ++ my $session_hash = `$openssl_bin dgst -sha256 -r session.bin`; ++ # Keep the digest only: 'dgst -r' output contains the file name too ++ ($session_hash) = $session_hash =~ /^([0-9a-f]+)/; ++ $session_hash //= ''; ++ ++ foreach my $case (@cases) { ++ my ($alg, $paramset) = @$case; ++ # Recipient keygen ++ ok(system("$openssl_bin genpkey -algorithm $alg -pkeyopt paramset:$paramset -out recip.prv.pem") == 0, "Keygen:$alg"); ++ # CryptoPro key transport (GKT) roundtrip ++ ok(system("$openssl_bin pkeyutl -encrypt -inkey recip.prv.pem -in session.bin -out wrapped.bin") == 0, "GKT encrypt:$alg"); ++ ok(system("$openssl_bin pkeyutl -decrypt -inkey recip.prv.pem -in wrapped.bin -out unwrapped.bin") == 0, "GKT decrypt:$alg"); ++ like(`$openssl_bin dgst -sha256 -r unwrapped.bin`, qr/^\Q$session_hash\E/, "GKT roundtrip:$alg"); ++ } ++ ++ SKIP: { ++ skip "Engine has no cipher string control for KEG", 6 if $target eq 'engine'; ++ # KEG/KExp15 transport roundtrip ++ ok(system("$openssl_bin pkeyutl -encrypt -inkey recip.prv.pem -in session.bin -out wrapped.bin -pkeyopt cipher:kuznyechik-ctr") == 0, "KEG encrypt:kuznyechik-ctr"); ++ ok(system("$openssl_bin pkeyutl -decrypt -inkey recip.prv.pem -in wrapped.bin -out unwrapped.bin -pkeyopt cipher:kuznyechik-ctr") == 0, "KEG decrypt:kuznyechik-ctr"); ++ like(`$openssl_bin dgst -sha256 -r unwrapped.bin`, qr/^\Q$session_hash\E/, "KEG roundtrip:kuznyechik-ctr"); ++ ++ ok(system("$openssl_bin pkeyutl -encrypt -inkey recip.prv.pem -in session.bin -out wrapped.bin -pkeyopt cipher:magma-ctr") == 0, "KEG encrypt:magma-ctr"); ++ ok(system("$openssl_bin pkeyutl -decrypt -inkey recip.prv.pem -in wrapped.bin -out unwrapped.bin -pkeyopt cipher:magma-ctr") == 0, "KEG decrypt:magma-ctr"); ++ like(`$openssl_bin dgst -sha256 -r unwrapped.bin`, qr/^\Q$session_hash\E/, "KEG roundtrip:magma-ctr"); ++ } ++ ++ # KEG is impossible with GOST R 34.10-2001 keys ++ ok(system("$openssl_bin pkeyutl -encrypt -inkey recip.prv.pem -in session.bin -out wrapped.bin -pkeyopt cipher:kuznyechik-ctr") != 0, "KEG rejected for GOST2001"); ++ ++ unlink "recip.prv.pem"; ++ unlink "wrapped.bin"; ++ unlink "unwrapped.bin"; ++ unlink "session.bin"; ++}; ++ + unlink "test.cnf"; + +-- +2.43.0 + diff --git a/libopenssl-gost/patches/0004-Add-keymgmt-for-secret-keys-and-MAC-with-sign-provid.patch b/libopenssl-gost/patches/0004-Add-keymgmt-for-secret-keys-and-MAC-with-sign-provid.patch new file mode 100644 index 000000000..64b5c7838 --- /dev/null +++ b/libopenssl-gost/patches/0004-Add-keymgmt-for-secret-keys-and-MAC-with-sign-provid.patch @@ -0,0 +1,617 @@ +From aedda74bf7f09bcb520fe9f92b8f8b696b93c9f4 Mon Sep 17 00:00:00 2001 +From: Kirill Ermoshin +Date: Tue, 10 Nov 2026 14:02:42 +0300 +Subject: [PATCH 3/4] Add keymgmt for secret keys and MAC with sign provider + interface + +--- + cmake/provider.cmake | 2 + + gost_prov.h | 31 +++++ + gost_prov_keymgmt.c | 8 ++ + gost_prov_keymgmt_mac.c | 186 ++++++++++++++++++++++++++ + gost_prov_signature.c | 24 ++++ + gost_prov_signature_mac.c | 273 ++++++++++++++++++++++++++++++++++++++ + 6 files changed, 524 insertions(+) + create mode 100644 gost_prov_keymgmt_mac.c + create mode 100644 gost_prov_signature_mac.c + +diff --git a/cmake/provider.cmake b/cmake/provider.cmake +index 68c92d4..cac8a13 100644 +--- a/cmake/provider.cmake ++++ b/cmake/provider.cmake +@@ -5,6 +5,8 @@ set(GOST_PROV_SOURCE_FILES + gost_prov_digest.c + gost_prov_mac.c + gost_prov_keymgmt.c ++ gost_prov_keymgmt_mac.c ++ gost_prov_signature_mac.c + gost_prov_encoder.c + gost_prov_signature.c + gost_prov_decoder.c +diff --git a/gost_prov.h b/gost_prov.h +index 069fed7..f229dc2 100644 +--- a/gost_prov.h ++++ b/gost_prov.h +@@ -18,6 +18,7 @@ + #define OID_id_GostR3410_2012_256 "1.2.643.7.1.1.1.1" + #define OID_id_GostR3410_2012_512 "1.2.643.7.1.1.1.2" + #define OID_id_GostR3411_94_with_GostR3410_2001 "1.2.643.2.2.3" ++#define OID_id_Gost28147_89_MAC "1.2.643.2.2.22" + #define OID_id_tc26_signwithdigest_gost3410_2012_256 "1.2.643.7.1.1.3.2" + #define OID_id_tc26_signwithdigest_gost3410_2012_512 "1.2.643.7.1.1.3.3" + +@@ -42,6 +43,16 @@ + LN_id_GostR3410_2012_512 ":" \ + OID_id_GostR3410_2012_512 + ++/* Names for GOST MAC keymgmt and signature operations */ ++#define ALG_NAME_GOST_MAC \ ++ SN_id_Gost28147_89_MAC ":" \ ++ LN_id_Gost28147_89_MAC ":" \ ++ OID_id_Gost28147_89_MAC ++ ++#define ALG_NAME_GOST12_MAC SN_gost_mac_12 ++#define ALG_NAME_MAGMA_MAC SN_magma_mac ++#define ALG_NAME_KUZNYECHIK_MAC SN_kuznyechik_mac ++ + /* Utilities for checking and working with bit flags */ + #define FLAGS_CONTAIN(flags, subset) (((flags)&(subset)) == (subset)) + #define FLAGS_INTERSECT(flags, subset) (((flags)&(subset)) != 0) +@@ -64,6 +75,12 @@ typedef struct gost_key_data_st + int param_nid; + } GOST_KEY_DATA; + ++/* Secret key data for GOST MAC operations (gost_prov_keymgmt_mac.c) */ ++struct gost_mac_key_data_st { ++ unsigned char key[32]; ++}; ++typedef struct gost_mac_key_data_st GOST_MAC_KEY_DATA; ++ + int gost_get_max_keyexch_size(const GOST_KEY_DATA *); + int gost_get_max_signature_size(const GOST_KEY_DATA *); + +@@ -74,3 +91,17 @@ extern const OSSL_ALGORITHM GOST_prov_signature[]; + extern const OSSL_ALGORITHM GOST_prov_decoder[]; + extern const OSSL_ALGORITHM GOST_prov_keyexch[]; + extern const OSSL_ALGORITHM GOST_prov_asym_cipher[]; ++ ++/* keymgmt for GOST MAC secret keys (gost_prov_keymgmt_mac.c) */ ++extern const OSSL_DISPATCH id_gost_mac_keymgmt_mac_functions[]; ++extern const OSSL_DISPATCH id_gost_mac_12_keymgmt_mac_functions[]; ++extern const OSSL_DISPATCH id_magma_mac_keymgmt_mac_functions[]; ++extern const OSSL_DISPATCH id_kuznyechik_mac_keymgmt_mac_functions[]; ++ ++/* MAC over signature API (gost_prov_signature_mac.c) */ ++extern const OSSL_DISPATCH id_Gost28147_89_mac_signature_mac_functions[]; ++extern const OSSL_DISPATCH id_Gost28147_89_mac_12_signature_mac_functions[]; ++extern const OSSL_DISPATCH id_magma_omac_mac_signature_mac_functions[]; ++extern const OSSL_DISPATCH id_grasshopper_omac_mac_signature_mac_functions[]; ++ ++int keymgmt_mac_gen_set_params(void *genctx, const OSSL_PARAM params[]); +diff --git a/gost_prov_keymgmt.c b/gost_prov_keymgmt.c +index f8ef3d0..b182db9 100644 +--- a/gost_prov_keymgmt.c ++++ b/gost_prov_keymgmt.c +@@ -801,5 +801,13 @@ const OSSL_ALGORITHM GOST_prov_keymgmt[] = { + { ALG_NAME_GOST2001DH, NULL, id_gost2001dh_keymgmt_functions }, + { ALG_NAME_GOST2012_256, NULL, id_gost2012_256_keymgmt_functions }, + { ALG_NAME_GOST2012_512, NULL, id_gost2012_512_keymgmt_functions }, ++ { ALG_NAME_GOST_MAC, NULL, id_gost_mac_keymgmt_mac_functions, ++ "Keymgmt for gost-mac key" }, ++ { ALG_NAME_GOST12_MAC, NULL, id_gost_mac_12_keymgmt_mac_functions, ++ "Keymgmt for gost-mac-12 key" }, ++ { ALG_NAME_MAGMA_MAC, NULL, id_magma_mac_keymgmt_mac_functions, ++ "Keymgmt for magma-mac key" }, ++ { ALG_NAME_KUZNYECHIK_MAC, NULL, id_kuznyechik_mac_keymgmt_mac_functions, ++ "Keymgmt for kuznyechik-mac key" }, + { NULL, NULL, NULL } + }; +\ No newline at end of file +diff --git a/gost_prov_keymgmt_mac.c b/gost_prov_keymgmt_mac.c +new file mode 100644 +index 0000000..3d8f154 +--- /dev/null ++++ b/gost_prov_keymgmt_mac.c +@@ -0,0 +1,186 @@ ++/********************************************************************** ++ * gost_prov_keymgmt_mac.c - keymgmt for MAC keys * ++ * * ++ * This file is distributed under the same license as OpenSSL * ++ * * ++ * Provider keymgmt for GOST MAC secret keys (gost-mac, * ++ * gost-mac-12, magma-mac, kuznyechik-mac), requires OpenSSL 3.0 * ++ **********************************************************************/ ++ ++#include ++#include ++#include ++#include ++ ++#include "gost_prov.h" ++#include "gost_lcl.h" ++ ++#define MAC_KEY_SIZE 32 ++ ++/* ++ * Key data for GOST MAC operations is a 32-byte secret key (see ++ * GOST_MAC_KEY_DATA in gost_prov.h). ++ * There is no public part and no domain parameters, so most of the ++ * generic keymgmt functions are stubs, like in the reference ++ * rutoken-ossl-provider (src/secret/keymgmt_mac.cpp). ++ */ ++ ++typedef struct gost_mac_gen_ctx_st { ++ unsigned char key[MAC_KEY_SIZE]; ++ int key_set; ++} GOST_MAC_GEN_CTX; ++ ++static void *keymgmt_mac_new(void *provctx) ++{ ++ (void)provctx; ++ /* Key data can't be constructed without a key value */ ++ return NULL; ++} ++ ++static void *keymgmt_mac_load(const void *reference, size_t reference_sz) ++{ ++ (void)reference; ++ (void)reference_sz; ++ return NULL; ++} ++ ++static int keymgmt_mac_has(const void *keydata, int selection) ++{ ++ (void)keydata; ++ (void)selection; ++ return 0; ++} ++ ++static void keymgmt_mac_free(void *keydata) ++{ ++ GOST_MAC_KEY_DATA *key = keydata; ++ ++ if (!key) ++ return; ++ ++ OPENSSL_cleanse(key->key, sizeof(key->key)); ++ OPENSSL_free(key); ++} ++ ++static void keymgmt_mac_gen_cleanup(void *genctx) ++{ ++ GOST_MAC_GEN_CTX *gctx = genctx; ++ ++ if (!gctx) ++ return; ++ ++ OPENSSL_cleanse(gctx->key, sizeof(gctx->key)); ++ OPENSSL_free(gctx); ++} ++ ++static const OSSL_PARAM keymgmt_mac_gen_settable_params_list[] = { ++ OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), ++ OSSL_PARAM_END ++}; ++ ++static const OSSL_PARAM *keymgmt_mac_gen_settable_params(void *genctx, ++ void *provctx) ++{ ++ (void)genctx; ++ (void)provctx; ++ return keymgmt_mac_gen_settable_params_list; ++} ++ ++int keymgmt_mac_gen_set_params(void *genctx, const OSSL_PARAM params[]) ++{ ++ GOST_MAC_GEN_CTX *gctx = genctx; ++ const OSSL_PARAM *p = NULL; ++ ++ if (!gctx) ++ return 0; ++ ++ p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PRIV_KEY); ++ if (p == NULL) ++ return 1; ++ ++ if (p->data_type != OSSL_PARAM_OCTET_STRING || p->data_size != MAC_KEY_SIZE) ++ return 0; ++ ++ memcpy(gctx->key, p->data, MAC_KEY_SIZE); ++ gctx->key_set = 1; ++ ++ return 1; ++} ++ ++static void *keymgmt_mac_gen_init(void *provctx, int selection, ++ const OSSL_PARAM params[]) ++{ ++ (void)provctx; ++ ++ if (selection != OSSL_KEYMGMT_SELECT_KEYPAIR) ++ return NULL; ++ ++ GOST_MAC_GEN_CTX *gctx = OPENSSL_zalloc(sizeof(GOST_MAC_GEN_CTX)); ++ if (!gctx) ++ return NULL; ++ ++ if (params && !keymgmt_mac_gen_set_params(gctx, params)) { ++ keymgmt_mac_gen_cleanup(gctx); ++ return NULL; ++ } ++ ++ return gctx; ++} ++ ++static void *keymgmt_mac_gen(void *genctx, OSSL_CALLBACK *osslcb, void *cbarg) ++{ ++ GOST_MAC_GEN_CTX *gctx = genctx; ++ GOST_MAC_KEY_DATA *key = NULL; ++ ++ (void)osslcb; ++ (void)cbarg; ++ ++ if (!gctx) ++ return NULL; ++ ++ key = OPENSSL_zalloc(sizeof(GOST_MAC_KEY_DATA)); ++ if (!key) ++ return NULL; ++ ++ if (gctx->key_set) { ++ memcpy(key->key, gctx->key, MAC_KEY_SIZE); ++ } else if (RAND_bytes(key->key, MAC_KEY_SIZE) <= 0) { ++ keymgmt_mac_free(key); ++ return NULL; ++ } ++ ++ return key; ++} ++ ++typedef void (*fptr_t)(void); ++#define MAKE_KEYMGMT_MAC_FUNCTIONS(alg, alg_sn) \ ++ static OSSL_FUNC_keymgmt_query_operation_name_fn alg##_query_operation_name; \ ++ static const char *alg##_query_operation_name(int operation_id) \ ++ { \ ++ switch (operation_id) { \ ++ case OSSL_OP_SIGNATURE: \ ++ return alg_sn; \ ++ default: \ ++ return NULL; \ ++ } \ ++ } \ ++ const OSSL_DISPATCH id_##alg##_keymgmt_mac_functions[] = { \ ++ { OSSL_FUNC_KEYMGMT_NEW, (fptr_t)keymgmt_mac_new }, \ ++ { OSSL_FUNC_KEYMGMT_FREE, (fptr_t)keymgmt_mac_free }, \ ++ { OSSL_FUNC_KEYMGMT_HAS, (fptr_t)keymgmt_mac_has }, \ ++ { OSSL_FUNC_KEYMGMT_LOAD, (fptr_t)keymgmt_mac_load }, \ ++ { OSSL_FUNC_KEYMGMT_GEN_INIT, (fptr_t)keymgmt_mac_gen_init }, \ ++ { OSSL_FUNC_KEYMGMT_GEN, (fptr_t)keymgmt_mac_gen }, \ ++ { OSSL_FUNC_KEYMGMT_GEN_CLEANUP, (fptr_t)keymgmt_mac_gen_cleanup }, \ ++ { OSSL_FUNC_KEYMGMT_GEN_SET_PARAMS, (fptr_t)keymgmt_mac_gen_set_params }, \ ++ { OSSL_FUNC_KEYMGMT_GEN_SETTABLE_PARAMS, \ ++ (fptr_t)keymgmt_mac_gen_settable_params }, \ ++ { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ ++ (fptr_t)alg##_query_operation_name }, \ ++ OSSL_DISPATCH_END \ ++ }; ++ ++MAKE_KEYMGMT_MAC_FUNCTIONS(gost_mac, SN_id_Gost28147_89_MAC); ++MAKE_KEYMGMT_MAC_FUNCTIONS(gost_mac_12, SN_gost_mac_12); ++MAKE_KEYMGMT_MAC_FUNCTIONS(magma_mac, SN_magma_mac); ++MAKE_KEYMGMT_MAC_FUNCTIONS(kuznyechik_mac, SN_kuznyechik_mac); +diff --git a/gost_prov_signature.c b/gost_prov_signature.c +index 1546cbb..69b0f70 100644 +--- a/gost_prov_signature.c ++++ b/gost_prov_signature.c +@@ -546,5 +546,29 @@ const OSSL_ALGORITHM GOST_prov_signature[] = { + NULL, + id_signature_functions + }, ++ { ++ ALG_NAME_GOST_MAC, ++ NULL, ++ id_Gost28147_89_mac_signature_mac_functions, ++ "gost mac with sign interface" ++ }, ++ { ++ ALG_NAME_GOST12_MAC, ++ NULL, ++ id_Gost28147_89_mac_12_signature_mac_functions, ++ "gost 12 mac with sign interface" ++ }, ++ { ++ ALG_NAME_MAGMA_MAC, ++ NULL, ++ id_magma_omac_mac_signature_mac_functions, ++ "magma mac with sign interface" ++ }, ++ { ++ ALG_NAME_KUZNYECHIK_MAC, ++ NULL, ++ id_grasshopper_omac_mac_signature_mac_functions, ++ "kuznyechik mac with sign interface" ++ }, + { NULL, NULL, NULL } + }; +diff --git a/gost_prov_signature_mac.c b/gost_prov_signature_mac.c +new file mode 100644 +index 0000000..719192f +--- /dev/null ++++ b/gost_prov_signature_mac.c +@@ -0,0 +1,273 @@ ++/********************************************************************** ++ * gost_prov_signature_mac.c - MAC over signature API * ++ * * ++ * This file is distributed under the same license as OpenSSL * ++ * * ++ * Provider signature interface for GOST MAC secret keys * ++ * (gost-mac, gost-mac-12, magma-mac, kuznyechik-mac), * ++ * requires OpenSSL 3.0 * ++ **********************************************************************/ ++ ++#include ++#include ++#include ++ ++#include "gost_prov.h" ++#include "gost_lcl.h" ++ ++#define MAC_KEY_SIZE 32 ++ ++/* ++ * MAC computation over the OSSL_OP_SIGNATURE interface. ++ * The underlying MAC implementations are the same as ++ * used in gost_prov_mac.c, so we reuse the ++ * GOST_digest / GOST_digest_ctx wrappers. ++ */ ++struct gost_sig_mac_desc_st { ++ GOST_digest *digest; ++ size_t initial_mac_size; ++}; ++typedef struct gost_sig_mac_desc_st GOST_SIG_MAC_DESC; ++ ++struct gost_sig_mac_ctx_st { ++ PROV_CTX *provctx; ++ const GOST_SIG_MAC_DESC *descriptor; ++ /* Output MAC size */ ++ size_t mac_size; ++ /* Saved key, needed to reset the MAC state for one-shot sign */ ++ unsigned char key[MAC_KEY_SIZE]; ++ GOST_digest_ctx *dctx; ++}; ++typedef struct gost_sig_mac_ctx_st GOST_SIG_MAC_CTX; ++ ++static void sig_mac_freectx(void *vsigctx) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ ++ if (!sigctx) ++ return; ++ ++ /* ++ * We don't free sigctx->descriptor->digest here. ++ * That will be done by the provider teardown, via ++ * GOST_prov_deinit_macs() (defined in gost_prov_mac.c). ++ */ ++ OPENSSL_cleanse(sigctx->key, sizeof(sigctx->key)); ++ GOST_digest_ctx_free(sigctx->dctx); ++ OPENSSL_free(sigctx); ++} ++ ++static GOST_SIG_MAC_CTX *sig_mac_newctx(void *provctx, ++ const GOST_SIG_MAC_DESC *descriptor) ++{ ++ GOST_SIG_MAC_CTX *sigctx = NULL; ++ const GOST_digest *digest = NULL; ++ ++ if ((sigctx = OPENSSL_zalloc(sizeof(*sigctx))) != NULL) { ++ sigctx->provctx = provctx; ++ sigctx->descriptor = descriptor; ++ sigctx->mac_size = descriptor->initial_mac_size; ++ digest = GOST_digest_init(descriptor->digest); ++ sigctx->dctx = GOST_digest_ctx_new(); ++ ++ if (digest == NULL ++ || sigctx->dctx == NULL ++ || GOST_digest_ctx_init(sigctx->dctx, digest) <= 0) { ++ sig_mac_freectx(sigctx); ++ sigctx = NULL; ++ } ++ } ++ return sigctx; ++} ++ ++static void *sig_mac_dupctx(void *vsigctx) ++{ ++ GOST_SIG_MAC_CTX *src = vsigctx; ++ GOST_SIG_MAC_CTX *dst = NULL; ++ ++ if (!src) ++ return NULL; ++ ++ dst = sig_mac_newctx(src->provctx, src->descriptor); ++ if (!dst) ++ return NULL; ++ ++ if (GOST_digest_ctx_copy(dst->dctx, src->dctx) <= 0) { ++ sig_mac_freectx(dst); ++ return NULL; ++ } ++ ++ dst->mac_size = src->mac_size; ++ ++ return dst; ++} ++ ++static int sig_mac_digest_sign_init(void *vsigctx, const char *mdname, ++ void *provkey, const OSSL_PARAM params[]) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ GOST_MAC_KEY_DATA *key = provkey; ++ ++ (void)mdname; ++ (void)params; ++ ++ if (!sigctx || !key) ++ return 0; ++ ++ memcpy(sigctx->key, key->key, MAC_KEY_SIZE); ++ ++ return GOST_digest_ctx_ctrl(sigctx->dctx, EVP_MD_CTRL_SET_KEY, ++ MAC_KEY_SIZE, sigctx->key) > 0; ++} ++ ++static int sig_mac_digest_sign_update(void *vsigctx, const unsigned char *data, ++ size_t datalen) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ ++ if (!sigctx) ++ return 0; ++ ++ return GOST_digest_ctx_update(sigctx->dctx, data, datalen) > 0; ++} ++ ++static int sig_mac_final(GOST_SIG_MAC_CTX *sigctx, unsigned char *sig, ++ size_t *siglen, size_t sigsize) ++{ ++ int ret = 0; ++ ++ if (siglen == NULL) ++ return 0; ++ ++ if (sig == NULL) { ++ *siglen = sigctx->mac_size; ++ return 1; ++ } ++ ++ if (sigsize < sigctx->mac_size) ++ return 0; ++ ++ /* We ignore the error for GOST MDs that don't support setting the size */ ++ GOST_digest_ctx_ctrl(sigctx->dctx, EVP_MD_CTRL_XOF_LEN, ++ (int)sigctx->mac_size, NULL); ++ ret = GOST_digest_ctx_final(sigctx->dctx, sig); ++ if (ret > 0) { ++ *siglen = sigctx->mac_size; ++ } ++ ++ return ret; ++} ++ ++static int sig_mac_digest_sign_final(void *vsigctx, unsigned char *sig, ++ size_t *siglen, size_t sigsize) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ ++ if (!sigctx) ++ return 0; ++ ++ return sig_mac_final(sigctx, sig, siglen, sigsize); ++} ++ ++static int sig_mac_digest_sign(void *vsigctx, unsigned char *sig, ++ size_t *siglen, size_t sigsize, ++ const unsigned char *tbs, size_t tbslen) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ ++ if (!sigctx) ++ return 0; ++ ++ /* ++ * One-shot sign is independent of any previously processed data. ++ * EVP_DigestSign() may be called twice (to query the size and to ++ * compute the signature), so reset the MAC state by setting the ++ * key again: the SET_KEY ctrl re-initializes the underlying ++ * digest context. ++ */ ++ if (GOST_digest_ctx_ctrl(sigctx->dctx, EVP_MD_CTRL_SET_KEY, ++ MAC_KEY_SIZE, sigctx->key) <= 0) ++ return 0; ++ ++ if (!sig_mac_digest_sign_update(sigctx, tbs, tbslen)) ++ return 0; ++ ++ return sig_mac_final(sigctx, sig, siglen, sigsize); ++} ++ ++/* https://github.com/openssl/openssl/pull/29921 */ ++#ifndef OSSL_DIGEST_PARAM_TLSTREE ++#define OSSL_DIGEST_PARAM_TLSTREE "tlstree" ++#endif ++ ++static int sig_mac_set_ctx_md_params(void *vsigctx, const OSSL_PARAM params[]) ++{ ++ GOST_SIG_MAC_CTX *sigctx = vsigctx; ++ const OSSL_PARAM *p = NULL; ++ ++ if (params == NULL) ++ return 1; ++ ++ p = OSSL_PARAM_locate_const(params, OSSL_DIGEST_PARAM_TLSTREE); ++ if (p != NULL) { ++ const void *tlstree = NULL; ++ size_t tlstree_len = 0; ++ ++ if (!OSSL_PARAM_get_octet_string_ptr(p, &tlstree, &tlstree_len)) ++ return 0; ++ ++ if (GOST_digest_ctx_ctrl(sigctx->dctx, EVP_MD_CTRL_TLSTREE, ++ 0, (void *)tlstree) <= 0) ++ return 0; ++ } ++ ++ return 1; ++} ++ ++static const OSSL_PARAM sig_mac_settable_ctx_md_params_list[] = { ++ OSSL_PARAM_octet_string(OSSL_DIGEST_PARAM_TLSTREE, NULL, 0), ++ OSSL_PARAM_END ++}; ++ ++static const OSSL_PARAM *sig_mac_settable_ctx_md_params(void *vsigctx, ++ void *provctx) ++{ ++ (void)vsigctx; ++ (void)provctx; ++ return sig_mac_settable_ctx_md_params_list; ++} ++ ++typedef void (*fptr_t)(void); ++#define MAKE_SIG_MAC_FUNCTIONS(name, macsize) \ ++ const GOST_SIG_MAC_DESC name##_sig_mac_desc = { \ ++ &name, \ ++ macsize, \ ++ }; \ ++ static OSSL_FUNC_signature_newctx_fn name##_newctx; \ ++ static void *name##_newctx(void *provctx, const char *propq) \ ++ { \ ++ (void)propq; \ ++ return sig_mac_newctx(provctx, &name##_sig_mac_desc); \ ++ } \ ++ const OSSL_DISPATCH id_##name##_signature_mac_functions[] = { \ ++ { OSSL_FUNC_SIGNATURE_NEWCTX, (fptr_t)name##_newctx }, \ ++ { OSSL_FUNC_SIGNATURE_FREECTX, (fptr_t)sig_mac_freectx }, \ ++ { OSSL_FUNC_SIGNATURE_DUPCTX, (fptr_t)sig_mac_dupctx }, \ ++ { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_INIT, \ ++ (fptr_t)sig_mac_digest_sign_init }, \ ++ { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_UPDATE, \ ++ (fptr_t)sig_mac_digest_sign_update }, \ ++ { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_FINAL, \ ++ (fptr_t)sig_mac_digest_sign_final }, \ ++ { OSSL_FUNC_SIGNATURE_DIGEST_SIGN, (fptr_t)sig_mac_digest_sign }, \ ++ { OSSL_FUNC_SIGNATURE_SET_CTX_MD_PARAMS, \ ++ (fptr_t)sig_mac_set_ctx_md_params }, \ ++ { OSSL_FUNC_SIGNATURE_SETTABLE_CTX_MD_PARAMS, \ ++ (fptr_t)sig_mac_settable_ctx_md_params }, \ ++ OSSL_DISPATCH_END \ ++ } ++ ++MAKE_SIG_MAC_FUNCTIONS(Gost28147_89_mac, 4); ++MAKE_SIG_MAC_FUNCTIONS(Gost28147_89_mac_12, 4); ++MAKE_SIG_MAC_FUNCTIONS(magma_omac_mac, 8); ++MAKE_SIG_MAC_FUNCTIONS(grasshopper_omac_mac, 16); +-- +2.43.0 + diff --git a/libopenssl-gost/patches/0005-Add-OSSL_CIPHER_PARAM_TLS_MAC-handler-for-ciphers.patch b/libopenssl-gost/patches/0005-Add-OSSL_CIPHER_PARAM_TLS_MAC-handler-for-ciphers.patch new file mode 100644 index 000000000..9e770edef --- /dev/null +++ b/libopenssl-gost/patches/0005-Add-OSSL_CIPHER_PARAM_TLS_MAC-handler-for-ciphers.patch @@ -0,0 +1,205 @@ +From c2b4ae50479d12a2f72914595a1cb3d24bd6e831 Mon Sep 17 00:00:00 2001 +From: Kirill Ermoshin +Date: Tue, 10 Nov 2026 16:09:05 +0300 +Subject: [PATCH 4/4] Add OSSL_CIPHER_PARAM_TLS_MAC handler for ciphers + +We need cut MAC during decryption for TLS 1.2 +--- + gost_prov_cipher.c | 54 +++++++++++++++++++++++++++++-- + test_ciphers.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 132 insertions(+), 2 deletions(-) + +diff --git a/gost_prov_cipher.c b/gost_prov_cipher.c +index 7a0a02d..0a7472c 100644 +--- a/gost_prov_cipher.c ++++ b/gost_prov_cipher.c +@@ -118,6 +118,13 @@ struct gost_prov_crypt_ctx_st { + GOST_cipher *cipher; + /* The context for the GOST_cipher functions */ + GOST_cipher_ctx *cctx; ++ /* ++ * TLS MAC extracted from the tail of decrypted records, it is ++ * requested by libssl through OSSL_CIPHER_PARAM_TLS_MAC (octet_ptr). ++ * The size is set by libssl via OSSL_CIPHER_PARAM_TLS_MAC_SIZE. ++ */ ++ unsigned char *tls_mac; ++ size_t tls_mac_size; + }; + typedef struct gost_prov_crypt_ctx_st GOST_CTX; + +@@ -147,6 +154,7 @@ static void cipher_freectx(void *vgctx) + if (gctx == NULL) + return; + GOST_cipher_ctx_free(gctx->cctx); ++ OPENSSL_clear_free(gctx->tls_mac, gctx->tls_mac_size); + OPENSSL_free(gctx); + } + +@@ -314,6 +322,14 @@ static int cipher_get_ctx_params(void *vgctx, OSSL_PARAM params[]) + if (!OSSL_PARAM_set_int(p, nid)) + return 0; + } ++ if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_TLS_MAC)) != NULL) { ++ /* ++ * libssl reads the MAC extracted from the decrypted record tail ++ * (see cipher_update) through the octet_ptr parameter. ++ */ ++ if (!OSSL_PARAM_set_octet_ptr(p, gctx->tls_mac, gctx->tls_mac_size)) ++ return 0; ++ } + return 1; + } + +@@ -383,6 +399,22 @@ static int cipher_set_ctx_params(void *vgctx, const OSSL_PARAM params[]) + (int)arg, (void *)val) <= 0) + return 0; + } ++ if ((p = OSSL_PARAM_locate_const(params, OSSL_CIPHER_PARAM_TLS_MAC_SIZE)) != NULL) { ++ size_t mac_size = 0; ++ if (!OSSL_PARAM_get_size_t(p, &mac_size)) ++ return 0; ++ if (mac_size > EVP_MAX_MD_SIZE) ++ return 0; ++ OPENSSL_clear_free(gctx->tls_mac, gctx->tls_mac_size); ++ gctx->tls_mac = NULL; ++ gctx->tls_mac_size = 0; ++ if (mac_size != 0) { ++ gctx->tls_mac = OPENSSL_malloc(mac_size); ++ if (gctx->tls_mac == NULL) ++ return 0; ++ gctx->tls_mac_size = mac_size; ++ } ++ } + return 1; + } + +@@ -422,8 +454,26 @@ static int cipher_update(void *vgctx, + int int_outl = outl != NULL ? *outl : 0; + int res = GOST_CipherUpdate(gctx->cctx, out, &int_outl, in, (int)inl); + +- if (res > 0 && outl != NULL) +- *outl = (size_t)int_outl; ++ if (res > 0) { ++ /* ++ * In TLS the MAC is encrypted together with the data, libssl ++ * expects the provider to cut the MAC off the tail of the ++ * decrypted record and to hand it over later through the ++ * OSSL_CIPHER_PARAM_TLS_MAC (octet_ptr) parameter. This works ++ * only for stream ciphers which don't buffer data internally, ++ * i.e. all GOST TLS 1.2 suites. ++ */ ++ if (!GOST_cipher_ctx_encrypting(gctx->cctx) ++ && gctx->tls_mac_size > 0) { ++ if (out == NULL || int_outl < 0 ++ || (size_t)int_outl < gctx->tls_mac_size) ++ return 0; ++ int_outl -= (int)gctx->tls_mac_size; ++ memcpy(gctx->tls_mac, out + int_outl, gctx->tls_mac_size); ++ } ++ if (outl != NULL) ++ *outl = (size_t)int_outl; ++ } + return res > 0; + } + +diff --git a/test_ciphers.c b/test_ciphers.c +index 7508a82..ed8e382 100644 +--- a/test_ciphers.c ++++ b/test_ciphers.c +@@ -858,6 +858,82 @@ static int test_provider_padded_decrypt(const EVP_CIPHER *type, const char *name + return ret; + } + ++/* Size of the MAC (imit) for the GOST TLS 1.2 stream cipher suites */ ++static size_t tls_mac_size_for_cipher(const char *name) ++{ ++ if (strcmp(name, SN_grasshopper_ctr) == 0 ++ || strcmp(name, SN_id_tc26_cipher_gostr3412_2015_kuznyechik_ctracpkm) == 0) ++ return 16; ++ return 8; ++} ++ ++/* ++ * Emulates the libssl record protection for Mac-then-encrypt stream ++ * ciphers: the MAC is encrypted as a tail of the record, on decryption ++ * the provider must cut it off the output and hand it over through the ++ * OSSL_CIPHER_PARAM_TLS_MAC (octet_ptr) parameter. ++ */ ++static int test_provider_tls_mac(const EVP_CIPHER *type, const char *name, ++ const unsigned char *key, ++ const unsigned char *iv, size_t iv_size, ++ size_t mac_size) ++{ ++ EVP_CIPHER_CTX *enc = NULL, *dec = NULL; ++ OSSL_PARAM params[2]; ++ unsigned char rec[128]; ++ unsigned char ct[128]; ++ unsigned char *tls_mac = NULL; ++ int outl = 0, part = 0; ++ int ret = 0, test; ++ size_t i; ++ ++ if (EVP_CIPHER_get0_provider(type) == NULL) ++ return 0; ++ ++ T(mac_size > 0 && mac_size <= EVP_MAX_MD_SIZE); ++ T(sizeof(rec) >= sizeof(P) + mac_size); ++ memcpy(rec, P, sizeof(P)); ++ for (i = 0; i < mac_size; i++) ++ rec[sizeof(P) + i] = (unsigned char)(0xa0 + i); ++ ++ printf("Provider tls-mac test [%s]: ", name); ++ ++ enc = EVP_CIPHER_CTX_new(); ++ dec = EVP_CIPHER_CTX_new(); ++ T(enc != NULL && dec != NULL); ++ ++ /* libssl sets tls-mac-size on both directions */ ++ params[0] = OSSL_PARAM_construct_size_t(OSSL_CIPHER_PARAM_TLS_MAC_SIZE, &mac_size); ++ params[1] = OSSL_PARAM_construct_end(); ++ ++ T(EVP_CipherInit_ex(enc, type, NULL, key, iv, 1)); ++ T(EVP_CIPHER_CTX_set_params(enc, params)); ++ /* Nothing must be cut off the output while encrypting */ ++ T(EVP_EncryptUpdate(enc, ct, &outl, rec, (int)(sizeof(P) + mac_size))); ++ TEST_ASSERT((size_t)outl != sizeof(P) + mac_size); ++ ret |= test; ++ ++ T(EVP_CipherInit_ex(dec, type, NULL, key, iv, 0)); ++ T(EVP_CIPHER_CTX_set_params(dec, params)); ++ T(EVP_DecryptUpdate(dec, ct, &part, ct, outl)); ++ /* The plaintext must come out without the MAC tail */ ++ TEST_ASSERT((size_t)part != sizeof(P) || memcmp(ct, P, sizeof(P)) != 0); ++ ret |= test; ++ ++ params[0] = OSSL_PARAM_construct_octet_ptr(OSSL_CIPHER_PARAM_TLS_MAC, ++ (void **)&tls_mac, mac_size); ++ params[1] = OSSL_PARAM_construct_end(); ++ T(EVP_CIPHER_CTX_get_params(dec, params)); ++ T(params[0].return_size == mac_size); ++ TEST_ASSERT(tls_mac == NULL ++ || memcmp(tls_mac, rec + sizeof(P), mac_size) != 0); ++ ret |= test; ++ ++ EVP_CIPHER_CTX_free(enc); ++ EVP_CIPHER_CTX_free(dec); ++ return ret; ++} ++ + int engine_is_available(const char *name) + { + #if HAVE_ENGINE +@@ -963,6 +1039,10 @@ int main(int argc, char **argv) + } + ret |= test_provider_padded_decrypt(ciph, t->algname, t->block, + t->key, t->iv); ++ if (t->stream || strcmp(t->algname, SN_magma_ctr) == 0) ++ ret |= test_provider_tls_mac(ciph, t->algname, t->key, ++ t->iv, t->iv_size, ++ tls_mac_size_for_cipher(t->algname)); + } + ret |= test_engine_asn1_acpkm_reinit(ciph, t->algname, t->key, t->iv); + +-- +2.43.0 + diff --git a/luci-app-crontab-wizard/Makefile b/luci-app-crontab-wizard/Makefile index 481f72b1a..587bebe58 100644 --- a/luci-app-crontab-wizard/Makefile +++ b/luci-app-crontab-wizard/Makefile @@ -12,7 +12,7 @@ MAINTAINER:=Rafał Wabik <4Rafal@gmail.com> LUCI_DESCRIPTION:=Super simple graphical configurator for crontab. LUCI_DEPENDS:= PKG_VERSION:=1.0.4 -PKG_RELEASE:=1 +PKG_RELEASE:=2 LUCI_PKGARCH:=all include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-crontab-wizard/po/zh_Hans/crontab-wizard.po b/luci-app-crontab-wizard/po/zh_Hans/crontab-wizard.po index 5f2379c98..bc07a1838 100644 --- a/luci-app-crontab-wizard/po/zh_Hans/crontab-wizard.po +++ b/luci-app-crontab-wizard/po/zh_Hans/crontab-wizard.po @@ -7,196 +7,280 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: luci-app-crontab-wizard/root/usr/share/luci/menu.d/luci-app-crontab-wizard.json +#: luci-app-crontab-wizard/root/usr/share/luci/menu.d/luci-app-crontab-wizard.json:3 +#: luci-app-crontab-wizard/root/usr/share/luci/menu.d/luci-app-crontab-wizard.json:15 msgid "Scheduled Tasks" msgstr "计划任务" +#: luci-app-crontab-wizard/root/usr/share/luci/menu.d/luci-app-crontab-wizard.json:27 msgid "Crontab Configurator" -msgstr "Crontab 配置向导" +msgstr "任务配置向导" -#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:559 msgid "Graphical Crontab Configurator" -msgstr "图形化 Crontab 配置向导" +msgstr "可视化计划任务配置器" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:561 msgid "A super simple graphical configurator for crontab." -msgstr "一个超级简单的图形化 Crontab 定时任务配置工具。" +msgstr "简单直观的 Crontab 计划任务可视化配置与生成工具。" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:567 msgid "Minute" msgstr "分钟" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:585 msgid "every specified minute period" -msgstr "每隔指定分钟周期" +msgstr "按所选分钟间隔循环 (*/N)" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:591 msgid "Hour" msgstr "小时" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:609 msgid "every specified hour period" -msgstr "每隔指定小时周期" +msgstr "按所选小时间隔循环 (*/N)" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:615 msgid "Day" msgstr "日期" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:633 msgid "every specified day period" -msgstr "每隔指定天数周期" +msgstr "按所选天数间隔循环 (*/N)" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:639 msgid "Month" msgstr "月份" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:662 msgid "Weekday" msgstr "星期" -msgid "Command to execute:" -msgstr "要执行的命令:" - -msgid "Command preview:" -msgstr "命令预览:" - -msgid "Description:" -msgstr "规则说明:" - -msgid "Generated cron command:" -msgstr "生成的 Cron 命令:" - -msgid "Cron command for copying" -msgstr "用于复制的 Cron 命令" - -msgid "Add to cron" -msgstr "添加到 Crontab" - -msgid "Reset changes" -msgstr "重置更改" - -msgid "Please enter the command" -msgstr "请输入命令" - -msgid "Check the cron entry, it must contain (time + command)" -msgstr "请检查 Cron 条目,必须包含(时间 + 命令)" - -msgid "This entry already exists in cron" -msgstr "此条目已存在于 Crontab 中" - -msgid "Added entry to cron" -msgstr "已将条目添加到 Crontab" - -msgid "Error: %s" -msgstr "错误:%s" - -msgid "Command \"%s\" " -msgstr "命令 \"%s\" " - -msgid "Command" -msgstr "命令" - -msgid "Command \"%s\" will run" -msgstr "命令 \"%s\" 将运行" - -msgid "Command will run" -msgstr "命令将运行" - -msgid "at time: %s" -msgstr "时间:%s" - -msgid "at times: %s" -msgstr "时间:%s" - -msgid "runs every %d %s" -msgstr "每 %d %s运行一次" - -msgid "in minutes: %s" -msgstr "在指定分钟:%s" - -msgid "in hours: %s" -msgstr "在指定小时:%s" - -msgid "runs every %d %s of the month" -msgstr "每月每 %d %s运行一次" - -msgid "on day(s): %s" -msgstr "在日期:%s" - -msgid "runs every %d month(s)" -msgstr "每 %d 个月运行一次" - -msgid "in month(s): %s" -msgstr "在月份:%s" - -msgid "runs every %d day of the week" -msgstr "每周每 %d 天运行一次" - -msgid "on weekday(s): %s" -msgstr "在星期:%s" - -msgid "minute" -msgstr "分钟" - -msgid "minutes" -msgstr "分钟" - -msgid "hour" -msgstr "小时" - -msgid "hours" -msgstr "小时" - -msgid "day" -msgstr "天" - -msgid "days" -msgstr "天" - +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:34 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:673 msgid "Sunday" msgstr "星期日" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:35 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:674 msgid "Monday" msgstr "星期一" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:36 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:675 msgid "Tuesday" msgstr "星期二" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:37 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:676 msgid "Wednesday" msgstr "星期三" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:38 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:677 msgid "Thursday" msgstr "星期四" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:39 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:678 msgid "Friday" msgstr "星期五" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:40 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:679 msgid "Saturday" msgstr "星期六" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:45 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:647 msgid "January" msgstr "一月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:46 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:648 msgid "February" msgstr "二月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:47 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:649 msgid "March" msgstr "三月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:48 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:650 msgid "April" msgstr "四月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:49 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:651 msgid "May" msgstr "五月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:50 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:652 msgid "June" msgstr "六月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:51 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:653 msgid "July" msgstr "七月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:52 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:654 msgid "August" msgstr "八月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:53 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:655 msgid "September" msgstr "九月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:54 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:656 msgid "October" msgstr "十月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:55 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:657 msgid "November" msgstr "十一月" +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:56 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:658 msgid "December" msgstr "十二月" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:97 +msgid "minute" +msgstr "分钟" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:97 +msgid "minutes" +msgstr "分钟" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:98 +msgid "hour" +msgstr "小时" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:98 +msgid "hours" +msgstr "小时" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:99 +msgid "day" +msgstr "天" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:99 +msgid "days" +msgstr "天" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:121 +msgid "at time: %s" +msgstr "时间点为 %s" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:122 +msgid "at times: %s" +msgstr "时间点为 %s" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:128 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:136 +msgid "runs every %d %s" +msgstr "每 %d %s执行一次" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:131 +msgid "in minutes: %s" +msgstr "在第 %s 分钟" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:139 +msgid "in hours: %s" +msgstr "在 %s 时" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:148 +msgid "runs every %d %s of the month" +msgstr "每月每隔 %d %s" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:152 +msgid "on day(s): %s" +msgstr "日期为每月 %s 日" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:157 +msgid "runs every %d month(s)" +msgstr "每 %d 个月" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:160 +msgid "in month(s): %s" +msgstr "月份为 %s" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:165 +msgid "runs every %d day of the week" +msgstr "每周每隔 %d 天" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:168 +msgid "on weekday(s): %s" +msgstr "星期为 %s" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:181 +msgid "Command \"%s\" " +msgstr "命令 \"%s\"(默认每分钟执行)" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:181 +msgid "Command" +msgstr "尚未设置时间规则或命令" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:200 +msgid "Command \"%s\" will run" +msgstr "命令 \"%s\" 将按以下规则执行:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:200 +msgid "Command will run" +msgstr "命令将按以下规则执行:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:686 +msgid "Command to execute:" +msgstr "要执行的命令:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:695 +msgid "Command preview:" +msgstr "表达式高亮预览:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:704 +msgid "Description:" +msgstr "执行规则说明:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:713 +msgid "Generated cron command:" +msgstr "生成的 Cron 表达式:" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:720 +msgid "Cron command for copying" +msgstr "可用于复制的完整 Cron 表达式" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:726 +msgid "Add to cron" +msgstr "添加到计划任务" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:742 +msgid "Reset changes" +msgstr "重置选项" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:500 +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:512 +msgid "Please enter the command" +msgstr "请输入要执行的命令" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:508 +msgid "Check the cron entry, it must contain (time + command)" +msgstr "请检查计划任务条目,必须同时包含时间规则与执行命令" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:521 +msgid "This entry already exists in cron" +msgstr "计划任务中已存在相同的条目" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:531 +msgid "Added entry to cron" +msgstr "已成功添加至计划任务" + +#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js:534 +msgid "Error: %s" +msgstr "错误:%s" diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile index 9a24956b9..4104341c1 100644 --- a/luci-app-trafficctl/Makefile +++ b/luci-app-trafficctl/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-trafficctl -PKG_VERSION:=1.30.5 -PKG_RELEASE:=32 +PKG_VERSION:=1.30.6 +PKG_RELEASE:=33 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Denis Iusupov diff --git a/luci-app-upnp/po/zh_Hans/upnp.po b/luci-app-upnp/po/zh_Hans/upnp.po index fefbe2a08..c82138a0c 100644 --- a/luci-app-upnp/po/zh_Hans/upnp.po +++ b/luci-app-upnp/po/zh_Hans/upnp.po @@ -1,443 +1,482 @@ msgid "" msgstr "" -"Content-Type: text/plain; charset=UTF-8\n" "Project-Id-Version: luci-app-upnp\n" +"POT-Creation-Date: 2026-11-13 00:00+0000\n" +"PO-Revision-Date: 2026-11-13 23:30+0800\n" +"Last-Translator: OpenWrt LuCI Localization\n" +"Language-Team: Chinese (Simplified)\n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "Unknown" -msgstr "未知" +#: applications/luci-app-upnp/root/usr/share/luci/menu.d/luci-app-upnp.json:3 +msgid "UPnP IGD & PCP" +msgstr "UPnP IGD 与 PCP" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "Delete" -msgstr "删除" +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:24 +msgid "Active UPnP IGD & PCP/NAT-PMP Port Maps" +msgstr "已生效的 UPnP IGD 与 PCP/NAT-PMP 端口映射" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "Not yet deletable; UPnP IGDv2 IPv6 port maps expire sooner, have no description" -msgstr "暂不可删除;UPnP IGDv2 IPv6 端口映射过期更快且无描述信息" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "No active IPv4/IPv6 port maps" -msgstr "没有活动的 IPv4/IPv6 端口映射" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgctxt "%s & %s/%s (%s = UPnP IGD, %s = PCP, %s = NAT-PMP)" -msgid "%s & %s/%s" -msgstr "%s & %s/%s" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "UPnP IGD & PCP/NAT-PMP Service" -msgstr "UPnP IGD & PCP/NAT-PMP 服务" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgctxt "The %s (%s = UPnP IGD & PCP/NAT-PMP) protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router." -msgid "The %s protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router." -msgstr "%s 协议/服务允许本地网络中受许可的设备在此路由器上自动建立 IPv4/IPv6 端口映射/转发。" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "No suitable configuration was found!" -msgstr "未找到合适的配置文件!" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "No suitable config (LuCI app %s) found in %s. A related package update (daemon or LuCI app) may be missing." -msgstr "未找到适配的配置(LuCI 应用 %s,路径 %s)。可能缺少相关的软件包更新(守护进程或 LuCI 应用)。" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "Use the software package manager, update lists, and install the related update. Config is migrated with the daemon package update." -msgstr "请使用软件包管理器更新列表并安装相关更新。配置文件将随守护进程软件包更新自动迁移。" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgid "Go to package manager…" -msgstr "前往软件包管理器…" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:35 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:76 msgid "Hostname" msgstr "主机名" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:36 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:77 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:322 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:330 msgid "IP address" msgstr "IP 地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:37 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:78 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:338 msgid "Port" -msgstr "端口" +msgstr "内部端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:38 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:79 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:345 msgid "External port" msgstr "外部端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:39 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:80 msgid "Protocol" msgstr "协议" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:40 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:81 msgid "Expires" -msgstr "剩余时间" +msgstr "剩余有效期" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:41 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:82 msgid "Added via / description" msgstr "添加方式 / 描述" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:62 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:37 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:90 +msgid "Unknown" +msgstr "未知" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:72 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:73 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:78 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:47 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:48 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:53 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:100 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:101 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:106 +msgid "Delete" +msgstr "删除" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:76 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:51 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:104 +msgid "Not yet deletable; UPnP IGDv2 IPv6 port maps expire sooner, have no description" +msgstr "暂不支持手动删除;UPnP IGDv2 的 IPv6 端口映射无描述信息且会较快自动过期" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js:82 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:57 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:110 +msgid "No active IPv4/IPv6 port maps" +msgstr "当前没有活动的 IPv4/IPv6 端口映射" + +#. %s & %s/%s (%s = UPnP IGD, %s = PCP, %s = NAT-PMP) +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:62 +msgid "%s & %s/%s" +msgstr "%s 与 %s/%s" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:63 +msgid "UPnP IGD & PCP/NAT-PMP Service" +msgstr "UPnP IGD 与 PCP/NAT-PMP 服务" + +#. The %s (%s = UPnP IGD & PCP/NAT-PMP) protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router. +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:64 +msgid "The %s protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router." +msgstr "%s 协议与服务允许局域网内获准的设备在此路由器上自动配置 IPv4/IPv6 端口映射与转发规则。" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:67 +msgid "No suitable configuration was found!" +msgstr "未找到适用的配置文件!" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:68 +msgid "No suitable config (LuCI app %s) found in %s. A related package update (daemon or LuCI app) may be missing." +msgstr "在 %2$s 中未找到适用于当前前端(LuCI 应用 %1$s)的配置,可能缺少相关软件包(后端守护进程或 LuCI 应用)更新。" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:69 +msgid "Use the software package manager, update lists, and install the related update. Config is migrated with the daemon package update." +msgstr "请前往软件包管理器刷新列表并安装相关更新。更新后端守护进程软件包时会自动迁移配置文件。" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:70 +msgid "Go to package manager…" +msgstr "前往软件包管理器…" + +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:112 msgid "Active Port Maps" -msgstr "活动端口映射" +msgstr "已生效的端口映射" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -msgid "Active UPnP IGD & PCP/NAT-PMP Port Maps" -msgstr "活动的 UPnP IGD & PCP/NAT-PMP 端口映射" - -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:116 msgid "Service Settings" msgstr "服务设置" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:119 msgid "Service Setup" msgstr "基本设置" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:120 msgid "Access Control" msgstr "访问控制" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:121 msgid "Advanced Settings" msgstr "高级设置" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:122 msgid "UPnP IGD Adjustments" -msgstr "UPnP IGD 调整" +msgstr "UPnP IGD 参数调整" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:124 msgid "Enable service" msgstr "启用服务" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:125 msgid "Enable the autonomous port mapping service" msgstr "启用自动端口映射服务" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:128 msgid "Enable protocols" msgstr "启用协议" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:129 msgid "All protocols" msgstr "所有协议" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:130 msgid "UPnP IGD" msgstr "UPnP IGD" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:131 msgid "PCP and NAT-PMP" -msgstr "PCP 和 NAT-PMP" +msgstr "PCP 与 NAT-PMP" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:135 msgid "Enable networks" -msgstr "启用网络" +msgstr "启用网络接口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:136 msgid "Select local/internal (LAN) network interfaces to enable the service for" -msgstr "选择要启用此服务的本地/内部(LAN)网络接口" +msgstr "选择要启用此服务的本地内网(LAN)接口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:146 msgid "UPnP IGD compatibility" -msgstr "UPnP IGD 兼容性" +msgstr "UPnP IGD 兼容模式" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:147 msgid "Set compatibility mode (act as device) to workaround IGDv2-incompatible clients; %s are known to only work with %s (or)
Emulate/report a specific/different device to workaround/support/handle/bypass/assist/mitigate... (Alternative text welcome)" -msgstr "设置兼容模式(作为特定设备运行)以兼容不支持 IGDv2 的客户端;已知 %s 仅适用于 %s" +msgstr "设置协议兼容模式以适配不支持 IGDv2 的客户端;已知 %s 仅在 %s 模式下正常工作" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:149 msgid "IGDv1 (IPv4 only)" -msgstr "IGDv1(仅 IPv4)" +msgstr "IGDv1(仅限 IPv4)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:150 msgid "IGDv2 (with workarounds)" -msgstr "IGDv2(含兼容处理)" +msgstr "IGDv2(启用兼容修正)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:155 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:173 msgid "Access defaults" msgstr "默认访问策略" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:156 msgid "Set access control defaults for ports that all devices can map" -msgstr "设置所有设备均可映射的端口默认访问控制策略" +msgstr "设置默认允许所有内网设备映射的端口范围" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:157 msgid "None / accept extra ports only" -msgstr "无 / 仅允许额外指定端口" +msgstr "无(仅允许下方额外指定的端口)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:158 msgid "Accept ports >= 1024" -msgstr "允许端口 >= 1024" +msgstr "允许 1024 及以上端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:159 msgid "Accept HTTP/HTTPS + ports >= 1024" -msgstr "允许 HTTP/HTTPS + 端口 >= 1024" +msgstr "允许 HTTP/HTTPS 及 1024 以上端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:160 msgid "Accept HTTP/HTTPS ports" -msgstr "允许 HTTP/HTTPS 端口" +msgstr "仅允许 HTTP/HTTPS 端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:161 msgid "Accept all ports" msgstr "允许所有端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:163 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:173 msgid "Accept extra ports" msgstr "允许额外端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:166 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:173 msgid "Reject ports" msgstr "拒绝端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:167 msgid "Reject unsafe/insecure/risky FTP/Telnet/DCE/NetBIOS/SMB/RDP ports by default; overrides other settings; use %s for none" -msgstr "默认拒绝不安全的 FTP/Telnet/DCE/NetBIOS/SMB/RDP 端口;此项优先于其他设置;填 %s 表示不拒绝任何端口" +msgstr "默认拒绝存在安全风险的 FTP/Telnet/DCE/NetBIOS/SMB/RDP 等高危端口;优先级高于其他设置;若不限制请填 %s" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:171 msgid "Check ACL" -msgstr "检查访问控制列表(ACL)" +msgstr "启用 ACL 检查" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:172 msgid "Extend or override access defaults by device-specific permissions using the access control list (ACL)" -msgstr "使用访问控制列表(ACL)按设备权限扩展或覆盖默认访问策略" +msgstr "使用访问控制列表(ACL)按设备细分权限,以扩展或覆盖默认访问策略" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:173 msgid "Sequence:" msgstr "匹配顺序:" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:173 msgid "ACL entries (if checked)" -msgstr "ACL 条目(若已勾选)" +msgstr "ACL 条目(若启用)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:179 msgid "Disable IPv6 mapping" -msgstr "禁用 IPv6 映射" +msgstr "禁用 IPv6 端口映射" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:180 msgid "IPv6 is currently always accepted unless disabled" -msgstr "除非禁用,否则当前默认始终允许 IPv6 映射" +msgstr "除非勾选禁用,否则当前默认允许所有 IPv6 映射请求" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgctxt "Allow %s/%s (%s = CGNAT, %s = STUN)" +#. Allow %s/%s (%s = CGNAT, %s = STUN) +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:183 msgid "Allow %s/%s" msgstr "允许 %s/%s" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:186 msgid "Allow use of unrestricted endpoint-independent (1:1) CGNATs and detect the public IPv4" -msgstr "允许在无限制端点无关型(1:1)CGNAT 环境下使用并检测公网 IPv4" +msgstr "允许在无限制的端点无关型(Full Cone / 1:1)CGNAT 网络下运行,并通过 STUN 探测公网 IPv4 地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:187 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:190 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:207 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:358 msgid "Disabled" -msgstr "禁用" +msgstr "已禁用" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:187 msgid "Manually override external IPv4 with public" -msgstr "手动指定外部公网 IPv4 地址" +msgstr "可手动指定公网 IPv4 地址以覆盖外部地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:188 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:189 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:208 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:209 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:210 msgid "Enabled" -msgstr "启用" +msgstr "已启用" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:188 msgid "Filtering test currently requires an extra firewall rule" -msgstr "过滤测试目前需要额外的防火墙规则" +msgstr "NAT 过滤类型检测目前需要额外配置防火墙规则" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:189 msgid "allow filtered" -msgstr "允许过滤型" +msgstr "忽略过滤检测" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:189 msgid "Workaround filtered IPv4 CGNAT test result" -msgstr "绕过过滤型 IPv4 CGNAT 检测限制" +msgstr "跳过或忽略受限型 IPv4 CGNAT 的过滤检测结果" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:190 msgid "report private IPv4, avoid" -msgstr "上报私有 IPv4,不推荐" +msgstr "直接通告私网 IPv4,不推荐" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:190 msgid "No STUN public IPv4 detection; client issues" -msgstr "不通过 STUN 检测公网 IPv4;可能导致客户端异常" +msgstr "不通过 STUN 探测公网 IPv4;可能导致客户端连接异常" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:193 msgid "STUN server" msgstr "STUN 服务器" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:200 msgid "Override external IPv4" -msgstr "覆盖外部 IPv4" +msgstr "指定外部 IPv4 地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:201 msgid "Report custom public/external (WAN) IPv4 address" -msgstr "通告自定义公网/外部(WAN)IPv4 地址" +msgstr "向客户端通告自定义的公网/外部(WAN)IPv4 地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:205 msgid "Allow third-party mapping" msgstr "允许第三方映射" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:206 msgid "Allow adding port maps for non-requesting IP addresses; use with care" -msgstr "允许为非请求方 IP 地址添加端口映射;请谨慎使用" +msgstr "允许设备为其他内网 IP 地址代为添加端口映射;请谨慎启用" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:207 msgid "recommended" msgstr "推荐" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:209 msgid "UPnP IGD only" -msgstr "仅 UPnP IGD" +msgstr "仅限 UPnP IGD" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:210 msgid "PCP only" -msgstr "仅 PCP" +msgstr "仅限 PCP" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:212 msgid "Report system instead of service uptime" -msgstr "报告系统运行时间而非服务运行时间" +msgstr "通告系统运行时间而非服务运行时间" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:217 msgid "Log level" msgstr "日志级别" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:218 msgid "Default" msgstr "默认" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:219 msgid "Info" -msgstr "信息" +msgstr "信息 (Info)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:220 msgid "Debug" -msgstr "调试" +msgstr "调试 (Debug)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:224 msgid "Service lease file" msgstr "服务租约文件" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:228 msgid "Download speed" -msgstr "下载速度" +msgstr "下行速率" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:229 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:237 msgid "Report maximum connection speed in kbit/s" -msgstr "报告最大连接速度(kbit/s)" +msgstr "向客户端通告的最大连接速率(单位:kbit/s)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:231 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:239 msgid "Default interface link speed" -msgstr "默认接口链路速度" +msgstr "默认使用接口协商速率" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:236 msgid "Upload speed" -msgstr "上传速度" +msgstr "上行速率" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:244 msgid "Router/friendly name" -msgstr "路由器/友好名称" +msgstr "路由器友好名称" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:250 msgid "Announced model number" -msgstr "通告的型号" +msgstr "通告设备型号" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:256 msgid "Announced serial number" -msgstr "通告的序列号" +msgstr "通告序列号" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:262 msgid "Router/presentation URL" -msgstr "路由器/管理页面 URL" +msgstr "路由器管理页面 URL" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:263 msgid "Report custom router web interface URL" -msgstr "通告自定义路由器 Web 管理界面 URL" +msgstr "向客户端通告自定义的路由器 Web 管理界面地址" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:269 msgid "Device UUID" msgstr "设备 UUID" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:275 msgid "SOAP/HTTP port" msgstr "SOAP/HTTP 端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:282 msgid "Notify interval" -msgstr "通知间隔" +msgstr "通告间隔" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -msgctxt "A 900 s interval sends %s (%s = SSDP) announcements with the minimum %s (%s = Cache-Control: max-age=1800) header" +#. A 900 s interval sends %s (%s = SSDP) announcements with the minimum %s (%s = Cache-Control: max-age=1800) header +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:283 msgid "A 900 s interval sends %s announcements with the minimum %s header" -msgstr "900 秒的间隔将发送 %s 通告,并附带最小 %s 标头" +msgstr "设为 900 秒间隔时,将以最小有效期标头(%2$s)发送 %1$s 服务通告" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:294 msgid "Access Control List" -msgstr "访问控制列表" +msgstr "访问控制列表 (ACL)" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:295 msgid "The access control list (ACL) specifies which IPv4 addresses and ports can be mapped." -msgstr "访问控制列表(ACL)用于指定允许映射的 IPv4 地址和端口。" +msgstr "访问控制列表(ACL)用于指定允许或禁止哪些内网 IPv4 地址及端口进行映射。" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:296 msgid "ACL entries are checked in order, then rejected if not matched and not accepted by access defaults. (To do: should be part of access control tab)" -msgstr "ACL 条目按顺序进行匹配;若未匹配且未被默认访问策略允许,则拒绝映射。" +msgstr "ACL 条目按顺序依次匹配;若均未命中且不符合默认访问策略,则拒绝该请求。" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:301 msgid "UPnP IGD & PCP/NAT-PMP" -msgstr "UPnP IGD & PCP/NAT-PMP" +msgstr "UPnP IGD 与 PCP/NAT-PMP" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:301 msgid "Edit ACL Entry" msgstr "编辑 ACL 条目" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:319 msgid "Comment" msgstr "备注" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:320 msgid "unspecified" msgstr "未指定" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:331 msgid "Enter device's IPv4 address, or an address and netmask" -msgstr "输入设备的 IPv4 地址,或带子网掩码的地址" +msgstr "输入设备的 IPv4 地址,或输入 IPv4 网段与子网掩码" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:340 +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:347 msgid "any port" msgstr "任意端口" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:351 msgid "Description filter" -msgstr "描述过滤器" +msgstr "描述过滤规则" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:352 msgid "Regular expression to check description of UPnP IGD IPv4 port maps" -msgstr "用于匹配 UPnP IGD IPv4 端口映射描述的正则表达式" +msgstr "用于匹配和检查 UPnP IGD IPv4 端口映射描述的正则表达式" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:353 msgid "any description" msgstr "任意描述" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:355 msgid "Action" msgstr "动作" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:356 msgid "Accept" msgstr "允许" -#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js +#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:357 msgid "Reject" msgstr "拒绝" + +#: applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json:3 +msgid "Grant access to UPnP IGD & PCP" +msgstr "授予访问 UPnP IGD 与 PCP 的权限" diff --git a/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/config.js b/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/config.js index 48819f4e7..74b59c38f 100644 --- a/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/config.js +++ b/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/config.js @@ -111,7 +111,6 @@ return view.extend({ o = s.taboption('advanced', form.Value, 'v2ray_assetsdir', _('Rule data directory')); o.placeholder = '/usr/share/v2raya'; - o.default = '/usr/share/v2raya'; o.description = _('Directory containing geoip.dat and geosite.dat. Leave empty for auto-detection.'); o = s.taboption('advanced', form.Value, 'v2ray_bin', _('Core binary (v2raya_core only)')); diff --git a/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/overview.js b/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/overview.js index 37a186b97..e4aee6dc1 100644 --- a/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/overview.js +++ b/luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/overview.js @@ -123,13 +123,23 @@ function runAction(action, button) { } function startService(button) { - uci.set('v2raya', 'config', 'enabled', '1'); + setBusy(button, true); - return L.resolveDefault(uci.save(), null).then(function () { - /* Commit before starting: the init script reads enabled from UCI. */ - return L.resolveDefault(uci.apply(), null); - }).then(function () { - return runAction('start', button); + return fs.exec('/usr/libexec/luci-app-v2raya/enable', []).then(function (res) { + if (!res || res.code !== 0) { + setBusy(button, false); + ui.addNotification(null, E('p', {}, + (res && (res.stderr || res.stdout)) || _('Could not enable the service.')), 'danger'); + return; + } + + uci.unload('v2raya'); + return uci.load('v2raya').then(function () { + return runAction('start', button); + }); + }).catch(function (err) { + setBusy(button, false); + ui.addNotification(null, E('p', {}, _('Could not enable the service: %s').format(err)), 'danger'); }); } diff --git a/luci-app-v2raya/po/zh-cn b/luci-app-v2raya/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-v2raya/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-v2raya/po/zh_Hans/v2raya.po b/luci-app-v2raya/po/zh_Hans/v2raya.po new file mode 100644 index 000000000..026009c51 --- /dev/null +++ b/luci-app-v2raya/po/zh_Hans/v2raya.po @@ -0,0 +1,366 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: luci-app-v2raya\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: luci-app-v2raya/root/usr/share/luci/menu.d/luci-app-v2raya.json +msgid "v2rayA" +msgstr "v2rayA" + +msgid "Overview" +msgstr "概览" + +msgid "Settings" +msgstr "设置" + +msgid "Log" +msgstr "日志" + +#: luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/overview.js +msgid "A web client for its bundled v2raya_core, with transparent proxy support." +msgstr "专为内置 v2raya_core 打造的 Web 客户端,支持透明代理。" + +msgid "Service" +msgstr "运行状态" + +msgid "Checking…" +msgstr "正在检查…" + +msgid "Running" +msgstr "运行中" + +msgid "Stopped" +msgstr "已停止" + +msgid "v2rayA version" +msgstr "v2rayA 版本" + +msgid "v2raya_core version" +msgstr "v2raya_core 版本" + +msgid "not detected" +msgstr "未检测到" + +msgid "Listening address" +msgstr "监听地址" + +msgid "Web interface" +msgstr "Web 管理界面" + +msgid "Open v2rayA" +msgstr "打开 v2rayA" + +msgid "Detected cores and proxy services" +msgstr "已检测到的内核与代理服务" + +msgid "v2rayA 2.5.x runs only its bundled v2raya_core. Existing Xray-core, v2ray and sing-box installations are detected here so they are not duplicated or replaced; they are never used as the v2rayA backend." +msgstr "v2rayA 2.5.x 仅运行其内置的 v2raya_core。此处检测系统中已有的 Xray-core、v2ray 和 sing-box 仅用于避免重复安装或覆盖,它们绝不会被用作 v2rayA 的后端内核。" + +msgid "v2raya_core (required)" +msgstr "v2raya_core(必需)" + +msgid "This is the only core v2rayA 2.5.x supports." +msgstr "这是 v2rayA 2.5.x 唯一支持的内核。" + +msgid "Xray-core" +msgstr "Xray-core" + +msgid "Left untouched. Do not point the core binary setting at this." +msgstr "保持原样,请勿将核心二进制文件路径指向此处。" + +msgid "v2ray-core" +msgstr "v2ray-core" + +msgid "sing-box" +msgstr "sing-box" + +msgid "Left untouched. sing-box uses a different configuration format and cannot run v2rayA configs." +msgstr "保持原样。sing-box 使用不同的配置格式,无法运行 v2rayA 生成的配置。" + +msgid "Detected" +msgstr "已检测到" + +msgid "Not installed" +msgstr "未安装" + +msgid "Package installed" +msgstr "已通过软件包安装" + +msgid "Rule data" +msgstr "规则数据" + +msgid "geoip.dat and geosite.dat are installed" +msgstr "已安装 geoip.dat 和 geosite.dat" + +msgid "Missing. v2rayA downloads the data on first start and needs internet access; install v2raya-geoip and v2raya-geosite for offline use." +msgstr "缺失。v2rayA 首次启动时需联网自动下载这些数据;如需离线使用,请提前安装 v2raya-geoip 和 v2raya-geosite 软件包。" + +msgid "Other proxy services" +msgstr "其他代理服务" + +msgid "None detected" +msgstr "未检测到" + +msgid "Core and service detection is unavailable. The installed v2raya package does not provide the detection helper; version 2.5.x and later install it." +msgstr "内核与服务检测不可用。当前安装的 v2raya 软件包未提供检测辅助工具(2.5.x 及更高版本已内置)。" + +msgid "v2raya_core is missing. Install the v2raya-core package." +msgstr "缺少 v2raya_core,请安装 v2raya-core 软件包。" + +msgid "An older v2raya package is still installed. Remove it to avoid duplicate services." +msgstr "系统中仍安装有旧版 v2raya 软件包,请将其卸载以避免服务冲突。" + +msgid "These proxy services are enabled at boot: %s. Run only one transparent-proxy stack, otherwise duplicate firewall rules will conflict." +msgstr "以下代理服务已设为开机自启:%s。请仅运行一套透明代理方案,否则重复的防火墙规则将产生冲突。" + +msgid "Actions" +msgstr "服务控制" + +msgid "Start" +msgstr "启动" + +msgid "Stop" +msgstr "停止" + +msgid "Restart" +msgstr "重启" + +msgid "These buttons control the background service. Connecting the proxy is done in the v2rayA web UI with the Connect button." +msgstr "以上按钮仅用于控制后台服务进程;如需连接节点并开启代理,请进入 v2rayA Web 管理界面点击连接按钮(Connect)。" + +msgid "This independent, community-run packaging project is not affiliated with the upstream v2rayA authors. It is AI-assisted and human-maintained." +msgstr "本软件包为独立的社区维护项目,与上游 v2rayA 官方作者无关联。项目由 AI 辅助构建并由人工维护。" + +msgid "The service command failed." +msgstr "服务命令执行失败。" + +msgid "Service action \"%s\" completed." +msgstr "服务操作“%s”已完成。" + +msgid "Unable to run the service command: %s" +msgstr "无法执行服务命令:%s" + +msgid "Could not enable the service." +msgstr "无法启用服务。" + +msgid "Could not enable the service: %s" +msgstr "无法启用服务:%s" + +#: luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/config.js +msgid "Settings for the v2rayA service. Save & Apply restarts the service so the new values take effect. v2rayA 2.5.x uses only its bundled v2raya_core; Xray-core and sing-box are not supported backends." +msgstr "v2rayA 服务参数设置。“保存并应用”后将自动重启服务以使新配置生效。v2rayA 2.5.x 仅支持其内置的 v2raya_core,不支持将 Xray-core 或 sing-box 作为后端内核。" + +msgid "General" +msgstr "常规设置" + +msgid "Transparent proxy" +msgstr "透明代理" + +msgid "The transparent proxy mode, routing rules and nodes are configured in the v2rayA web interface. These options control core start-up and hooks." +msgstr "透明代理模式、分流规则及代理节点均在 v2rayA Web 管理界面中配置。此处的选项用于控制内核启动参数及生命周期钩子脚本。" + +msgid "Logging" +msgstr "日志设置" + +msgid "Advanced" +msgstr "高级设置" + +msgid "Enable service" +msgstr "启用服务" + +msgid "Start v2rayA at boot. You can also start or stop it from the overview page." +msgstr "开机自动启动 v2rayA 服务。你也可以在“概览”页面手动启动或停止服务。" + +msgid "An address is required." +msgstr "必须填写监听地址。" + +msgid "Enter an address and port, for example 0.0.0.0:2017 or [::]:2017." +msgstr "请输入有效的地址和端口,例如 0.0.0.0:2017 或 [::]:2017。" + +msgid "The web interface listens on this address. Use 127.0.0.1:2017 to keep it off the LAN." +msgstr "Web 管理界面监听的 IP 地址与端口。若设为 127.0.0.1:2017,则仅允许本机访问(禁止局域网其他设备访问)。" + +msgid "Base URL" +msgstr "基础路径 (Base URL)" + +msgid "URL prefix when v2rayA runs behind a reverse proxy." +msgstr "当 v2rayA 运行在反向代理后端时使用的 URL 前缀。" + +msgid "The base URL must start with /." +msgstr "基础路径必须以 / 开头。" + +msgid "IPv6 support" +msgstr "IPv6 支持" + +msgid "Auto" +msgstr "自动" + +msgid "On" +msgstr "开启" + +msgid "Off" +msgstr "关闭" + +msgid "Make sure IPv6 connectivity works before forcing it on." +msgstr "强制开启前,请确保路由器及网络环境已具备正常的 IPv6 连通性。" + +msgid "nftables support" +msgstr "nftables 支持" + +msgid "nftables is the default firewall backend on OpenWrt 22.03 and later." +msgstr "nftables 是 OpenWrt 22.03 及更高版本的默认防火墙后端。" + +msgid "Core startup timeout" +msgstr "内核启动超时" + +msgid "Seconds to wait for v2raya_core to start. Raise this on slow MIPS routers." +msgstr "等待 v2raya_core 启动的最长时间(秒)。性能较弱的 MIPS 架构路由器可适当调高此值。" + +msgid "Respect bound devices in redirect mode" +msgstr "Redirect 模式下放行绑定网卡的流量" + +msgid "Let sockets bound to a device bypass the proxy. Requires Linux 5.14 or later and cgroup v2." +msgstr "允许已绑定到特定网络接口(SO_BINDTODEVICE)的套接字绕过透明代理。需要 Linux 5.14 及以上内核并启用 cgroup v2。" + +msgid "Transparent proxy hook" +msgstr "透明代理钩子脚本" + +msgid "Executable called around transparent proxy setup and teardown." +msgstr "在配置或清理透明代理规则前后调用的可执行脚本路径。" + +msgid "Core life-cycle hook" +msgstr "内核生命周期钩子脚本" + +msgid "Executable called around v2raya_core start and stop." +msgstr "在启动或停止 v2raya_core 前后调用的可执行脚本路径。" + +msgid "Plugin manager" +msgstr "插件管理器" + +msgid "Executable used to parse protocols that v2rayA does not know natively." +msgstr "用于解析 v2rayA 原生不支持的代理协议的可执行文件路径。" + +msgid "Log level" +msgstr "日志级别" + +msgid "Trace" +msgstr "追踪 (Trace)" + +msgid "Debug" +msgstr "调试 (Debug)" + +msgid "Info" +msgstr "信息 (Info)" + +msgid "Warning" +msgstr "警告 (Warning)" + +msgid "Error" +msgstr "错误 (Error)" + +msgid "Warning keeps flash writes down. Use Info while debugging." +msgstr "设为“警告 (Warning)”可减少对路由器闪存的写入损耗;排查故障时可临时改为“信息 (Info)”。" + +msgid "Keep logs for" +msgstr "日志保留天数" + +msgid "Number of days to keep rotated log files." +msgstr "轮转日志文件的保留天数。" + +msgid "Disable colours in the log" +msgstr "禁用日志颜色代码" + +msgid "The log file is plain text, so colours are not useful there." +msgstr "日志文件为纯文本格式,禁用 ANSI 颜色代码可避免出现乱码字符。" + +msgid "Disable timestamps in the log" +msgstr "禁用日志时间戳" + +msgid "Only relevant when v2rayA writes to syslog or journald." +msgstr "仅在 v2rayA 将日志输出至系统日志(syslog 或 journald)时生效。" + +msgid "Rule data directory" +msgstr "规则数据目录" + +msgid "Directory containing geoip.dat and geosite.dat. Leave empty for auto-detection." +msgstr "存放 geoip.dat 和 geosite.dat 规则文件的目录路径,留空则自动检测。" + +msgid "Core binary (v2raya_core only)" +msgstr "内核二进制文件(仅限 v2raya_core)" + +msgid "Auto-detect" +msgstr "自动检测" + +msgid "Path to v2raya_core. Leave empty to use the binary next to v2raya or in PATH. Only the matching v2raya_core is supported; Xray-core and sing-box use incompatible configuration formats." +msgstr "v2raya_core 可执行文件的路径。留空则自动使用与 v2raya 同目录或系统 PATH 中的程序。仅支持版本匹配的 v2raya_core,Xray-core 和 sing-box 因配置格式不兼容而无法使用。" + +msgid "Extra configuration directory" +msgstr "额外配置目录" + +msgid "None" +msgstr "无" + +msgid "Additional v2ray configuration files merged into the generated configuration." +msgstr "存放额外 V2Ray 配置文件的目录,其中的配置将合并到自动生成的最终配置中。" + +msgid "Go memory limit" +msgstr "Go 内存限制 (GOMEMLIMIT)" + +msgid "Soft memory limit for both v2raya and v2raya_core (GOMEMLIMIT). Use 0 to disable, or 96MiB on a larger router." +msgstr "v2raya 与 v2raya_core 各自的 Go 运行时软内存上限(GOMEMLIMIT)。设为 0 表示不限制,大内存路由器可设为 96MiB。" + +msgid "Go scheduler threads" +msgstr "Go 调度线程数 (GOMAXPROCS)" + +msgid "GOMAXPROCS. 0 uses every CPU core; 2 is enough for most routers." +msgstr "设置 Go 运行时最大并发线程数(GOMAXPROCS)。设为 0 表示使用全部 CPU 核心;大多数路由器保持默认 2 即可。" + +msgid "Go GC percent" +msgstr "Go 垃圾回收阈值 (GOGC)" + +msgid "GOGC. 0 keeps the Go default; lower values use less memory and more CPU." +msgstr "设置 Go 垃圾回收触发百分比(GOGC)。设为 0 表示保持 Go 默认值;调低数值可降低内存占用,但会增加 CPU 开销。" + +msgid "Open file limit" +msgstr "最大打开文件数 (nofile)" + +msgid "Maximum open file descriptors. 16384 is plenty for a home router and bounds a connection flood." +msgstr "进程允许打开的最大文件描述符数量(ulimit -n)。16384 足以满足家用路由器需求,并能防止异常连接洪水耗尽资源。" + +#: luci-app-v2raya/htdocs/luci-static/resources/view/v2raya/log.js +msgid "v2rayA log" +msgstr "v2rayA 运行日志" + +msgid "Showing the last %d lines of %s. Refreshes every %d seconds." +msgstr "显示最新 %d 行日志(%s),每 %d 秒自动刷新。" + +msgid "Loading…" +msgstr "正在加载…" + +msgid "The log file does not exist yet. Start the service and try again." +msgstr "日志文件尚未生成,请先启动服务后再试。" + +msgid "The log is empty." +msgstr "日志为空。" + +msgid "Unable to read the log: %s" +msgstr "无法读取日志:%s" + +msgid "Refresh" +msgstr "刷新" + +msgid "Clear log" +msgstr "清空日志" + +msgid "Clear the v2rayA log file?" +msgstr "确定要清空 v2rayA 日志文件吗?" + +msgid "Unable to clear the log file." +msgstr "无法清空日志文件。" + +msgid "Log file cleared." +msgstr "日志文件已清空。" diff --git a/luci-app-v2raya/root/usr/libexec/luci-app-v2raya/enable b/luci-app-v2raya/root/usr/libexec/luci-app-v2raya/enable new file mode 100755 index 000000000..41b34fe04 --- /dev/null +++ b/luci-app-v2raya/root/usr/libexec/luci-app-v2raya/enable @@ -0,0 +1,13 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2026 Amr Fateem +# +# Commit only the enabled option for the v2rayA service. The LuCI overview +# calls this before starting the service; uci.apply() would commit every +# staged change in the session. + +set -eu + +uci -q set v2raya.config.enabled='1' +uci -q commit v2raya diff --git a/luci-app-v2raya/root/usr/share/rpcd/acl.d/luci-app-v2raya.json b/luci-app-v2raya/root/usr/share/rpcd/acl.d/luci-app-v2raya.json index a4ff9ff3d..b237b0379 100644 --- a/luci-app-v2raya/root/usr/share/rpcd/acl.d/luci-app-v2raya.json +++ b/luci-app-v2raya/root/usr/share/rpcd/acl.d/luci-app-v2raya.json @@ -3,8 +3,8 @@ "description": "Grant access to the v2rayA service and configuration", "read": { "file": { - "/usr/bin/v2raya": [ "exec" ], - "/usr/bin/v2raya_core": [ "exec" ], + "/usr/bin/v2raya --version": [ "exec" ], + "/usr/bin/v2raya_core version": [ "exec" ], "/usr/libexec/v2raya-detect": [ "exec" ], "/var/log/v2raya/v2raya.log": [ "read" ] }, @@ -18,6 +18,7 @@ "/etc/init.d/v2raya start": [ "exec" ], "/etc/init.d/v2raya stop": [ "exec" ], "/etc/init.d/v2raya restart": [ "exec" ], + "/usr/libexec/luci-app-v2raya/enable": [ "exec" ], "/var/log/v2raya/v2raya.log": [ "write" ] }, "uci": [ "v2raya" ] diff --git a/luci-theme-argon/Makefile b/luci-theme-argon/Makefile index 839441c1e..2eda16e46 100644 --- a/luci-theme-argon/Makefile +++ b/luci-theme-argon/Makefile @@ -12,8 +12,8 @@ LUCI_TITLE:=Argon Theme LUCI_DEPENDS:=+luci-base +wget-ssl +ca-bundle +jsonfilter +ucode-mod-math +flock +coreutils +coreutils-timeout # Keep existing translation package names and the argon-config message domain. LUCI_BASENAME:=argon-config -PKG_VERSION:=3.1.3 -PKG_RELEASE:=42 +PKG_VERSION:=3.1.4 +PKG_RELEASE:=43 PKG_LICENSE:=Apache-2.0 MIT # LuCI derives translations independently of the theme version. Keep release diff --git a/luci-theme-argon/RELEASE.md b/luci-theme-argon/RELEASE.md index fcd081f1f..183726f3f 100644 --- a/luci-theme-argon/RELEASE.md +++ b/luci-theme-argon/RELEASE.md @@ -1,5 +1,12 @@ # Update log for master branch +## v3.1.4 [ 2026.11.13 ] + +- Fix inline alerts obscuring dropdowns and clipped dropdown options in tables (#740, #741). +- Support newer LuCI dashboard layouts and configurable widget interactions (#742). +- Improve Firefox ESR appearance controls, joined inputs, dropdowns and port statistics (#739). +- Add background video playback controls and a keyboard skip link, and reduce realtime chart layout work. + ## v3.1.3 [ 2026.11.06 ] - Preserve bundled Argon translations during upgrades and synchronize language packages in subsequent online updates (#737). diff --git a/luci-theme-argon/RELEASE_ZH.md b/luci-theme-argon/RELEASE_ZH.md index 9220c76ce..bddc02e44 100644 --- a/luci-theme-argon/RELEASE_ZH.md +++ b/luci-theme-argon/RELEASE_ZH.md @@ -1,5 +1,12 @@ # Master 分支的更新日志 +## v3.1.4 [ 2026.11.13 ] + +- 修复提示条遮挡下拉菜单、表格下拉选项显示不完整的问题(#740、#741)。 +- 适配新版 LuCI 仪表盘,修复布局及组件配置交互(#742)。 +- 改善 Firefox ESR 的主题切换、组合控件、下拉菜单及端口统计显示(#739)。 +- 增加视频背景播放/暂停与键盘跳转导航,优化实时图表更新性能。 + ## v3.1.3 [ 2026.11.06 ] - 修复部分固件升级后 Argon 翻译丢失,保留已有翻译,并在后续在线升级时同步语言包(#737)。 diff --git a/luci-theme-argon/docs/DEVELOPMENT.md b/luci-theme-argon/docs/DEVELOPMENT.md index 29c6daec2..9920c660b 100644 --- a/luci-theme-argon/docs/DEVELOPMENT.md +++ b/luci-theme-argon/docs/DEVELOPMENT.md @@ -144,3 +144,5 @@ UCODE_BIN=/path/to/ucode sh .github/scripts/check_theme.sh The script checks JavaScript, shell and Ucode syntax, verifies compiled CSS against LESS, and runs the Node/Python tests. CI's Ucode build is defined in [check.yml](../.github/workflows/check.yml). For UI changes, check the actual affected LuCI pages in light/dark modes and desktop/mobile widths, including keyboard focus, long text and reduced motion where relevant. Network and hardware behavior still needs a suitable OpenWrt environment. + +Include Firefox ESR 115, 128, 140 and 153 when checking login and theme settings. Leave browser feature preferences at their defaults. `argon-compat.js` supplies DOM markers for joined controls and dropdown table sections only when native `:has()` is unavailable; use forgiving `:is()` lists to keep those fallback selectors valid. Keep new browser API use feature-detected, and avoid unsupported selectors in JavaScript queries. The fallback covers common theme controls; plugin-specific layouts need their own checks. diff --git a/luci-theme-argon/docs/DEVELOPMENT_ZH.md b/luci-theme-argon/docs/DEVELOPMENT_ZH.md index ec402ae76..294401488 100644 --- a/luci-theme-argon/docs/DEVELOPMENT_ZH.md +++ b/luci-theme-argon/docs/DEVELOPMENT_ZH.md @@ -144,3 +144,5 @@ UCODE_BIN=/path/to/ucode sh .github/scripts/check_theme.sh 脚本检查 JavaScript、Shell、Ucode 语法,核对 LESS 与 CSS 产物,并运行 Node / Python 测试。CI 的 Ucode 构建步骤见 [check.yml](../.github/workflows/check.yml)。 UI 改动应检查实际受影响页面的亮暗色、桌面与窄屏,以及相关的键盘焦点、长文本和减少动画场景。网络和硬件行为仍需相应 OpenWrt 环境验证。 + +登录和主题设置的检查应包含 Firefox ESR 115、128、140、153,保留浏览器默认的特性设置。`argon-compat.js` 仅在缺少原生 `:has()` 时为组合控件和下拉表格添加 DOM 标记,通过可容错的 `:is()` 选择器提供样式。新增浏览器 API 应检测能力,JavaScript 查询应避免不受支持的选择器。此兼容处理覆盖主题常用控件,插件专属布局仍需单独验证。 diff --git a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css index 02e9baa1b..7ae4d9c00 100644 --- a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css +++ b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css @@ -510,6 +510,23 @@ white-space: nowrap; border: 0; } +.skip-link { + position: fixed; + z-index: 1200; + inset-block-start: var(--control-gap); + inset-inline-start: var(--page-gutter); + padding: 0.5rem 0.75rem; + color: var(--text-strong); + background: var(--surface-panel); + border: 1px solid var(--border-color); + border-radius: var(--radius-control); + transform: translateY(calc(-100% - var(--control-gap))); +} +.skip-link:focus { + transform: none; + outline: 2px solid var(--focus-ring); + outline-offset: 2px; +} @media (prefers-reduced-motion: reduce) { .main-left, .main-right, @@ -1128,23 +1145,53 @@ select[multiple] { height: 100%; object-fit: cover; } -.login-page .volume-control { +.login-page .background-controls { position: fixed; - top: 1rem; - right: 1rem; - width: 1.5rem; - height: 1.5rem; + inset-block-start: var(--field-gap); + inset-inline-end: var(--field-gap); + display: flex; + gap: var(--control-gap); z-index: 5000; - cursor: pointer; - border: 0 !important; - background: url(../img/volume_high.svg) no-repeat center / contain; } -.login-page .volume-control:focus-visible { +.login-page .background-controls > button { + display: grid; + place-items: center; + min-width: 2.5rem; + min-height: 2.5rem; + padding: 0.5rem; + cursor: pointer; + background: var(--surface-panel); + color: var(--text-strong); +} +.login-page .background-controls > button::before { + content: ''; + width: 1rem; + height: 1rem; + background-color: currentColor; + mask: var(--background-control-icon) center / contain no-repeat; + -webkit-mask: var(--background-control-icon) center / contain no-repeat; +} +.login-page .background-controls > button:focus-visible { outline: 2px solid var(--focus-ring); - outline-offset: 4px; + outline-offset: 2px; +} +@media (forced-colors: active) { + .login-page .background-controls > button::before { + forced-color-adjust: none; + background-color: ButtonText; + } +} +.login-page .playback-control { + --background-control-icon: url(../icon/play.svg); +} +.login-page .playback-control.playing { + --background-control-icon: url(../icon/pause.svg); +} +.login-page .volume-control { + --background-control-icon: url(../img/volume_high.svg); } .login-page .volume-control.mute { - background-image: url(../img/volume_off.svg); + --background-control-icon: url(../img/volume_off.svg); } .login-page .login-container { --login-inset: 3.125rem; @@ -1795,7 +1842,7 @@ footer > a { background: var(--surface-color); box-shadow: var(--section-shadow); } -#view > div:first-child:not(.cbi-map) { +#view > div:first-child:not(.cbi-map, .Dashboard) { display: flex; flex-direction: column; min-width: 0; @@ -1991,6 +2038,7 @@ span.spinning { .alert-message { transform: none; position: static; + z-index: auto; transition: opacity 0.4s ease; } .alert-message.fade-out { @@ -2316,6 +2364,10 @@ fieldset > fieldset, .cbi-tblsection { overflow-x: auto; } +.cbi-tblsection:is(:has(.cbi-dropdown), .argon-dropdown-section), +.cbi-tblsection .table:is(:has(.cbi-dropdown), .argon-dropdown-section) { + overflow: visible; +} table { border-spacing: 0; border-collapse: collapse; @@ -2865,18 +2917,11 @@ header .header-actions { min-width: 0; overflow-wrap: anywhere; } -#argon-appearance .argon-appearance-panel .argon-appearance-option:has(input:checked) { - color: var(--dropdown-selected-text); - background: var(--dropdown-selected-bg); -} -#argon-appearance .argon-appearance-panel .argon-appearance-option:has(input:checked)::after { - visibility: visible; -} -#argon-appearance .argon-appearance-panel .argon-appearance-option:has(input:focus-visible) { +#argon-appearance .argon-appearance-panel .argon-appearance-option:focus-within { outline: 2px solid var(--focus-ring); outline-offset: -2px; } -#argon-appearance .argon-appearance-panel .argon-appearance-option:has(input:disabled) { +#argon-appearance .argon-appearance-panel .argon-appearance-option[aria-disabled="true"] { cursor: default; opacity: 0.6; } @@ -2894,7 +2939,7 @@ header .header-actions { #argon-appearance .argon-appearance-trigger:hover { background-color: var(--surface-active); } - #argon-appearance .argon-appearance-option:has(input:not(:disabled)):not(:has(input:checked)):hover { + #argon-appearance .argon-appearance-option:not([aria-disabled="true"]):hover { background: var(--dropdown-hover-bg); } } @@ -2914,6 +2959,17 @@ header .header-actions { background-color: CanvasText; } } +#argon-appearance[data-mode="light"] .argon-appearance-option[data-mode="light"], +#argon-appearance[data-mode="dark"] .argon-appearance-option[data-mode="dark"], +#argon-appearance[data-mode="normal"] .argon-appearance-option[data-mode="normal"] { + color: var(--dropdown-selected-text); + background: var(--dropdown-selected-bg); +} +#argon-appearance[data-mode="light"] .argon-appearance-option[data-mode="light"]::after, +#argon-appearance[data-mode="dark"] .argon-appearance-option[data-mode="dark"]::after, +#argon-appearance[data-mode="normal"] .argon-appearance-option[data-mode="normal"]::after { + visibility: visible; +} @starting-style { #argon-appearance .argon-appearance-panel { opacity: 0; @@ -3216,11 +3272,11 @@ header .header-actions { .control-group > * { max-width: 100%; } -.control-group:has(> input:first-child + .cbi-button) { +.control-group:is(:has(> input:first-child + .cbi-button), .argon-joined-control) { flex-wrap: nowrap; gap: 0; } -.control-group:has(> input:first-child + .cbi-button) > input { +.control-group:is(:has(> input:first-child + .cbi-button), .argon-joined-control) > input { flex: 0 1 15.5rem; min-width: 0; border-inline-end-width: 0; @@ -3228,14 +3284,14 @@ header .header-actions { border-end-end-radius: 0; margin-inline-end: 0; } -.control-group:has(> input:first-child + .cbi-button) > input + .cbi-button { +.control-group:is(:has(> input:first-child + .cbi-button), .argon-joined-control) > input + .cbi-button { flex: 0 0 auto; border-inline-start-width: 0; border-start-start-radius: 0; border-end-start-radius: 0; margin-inline-start: 0; } -.control-group:has(> input:first-child + .cbi-button) > .cbi-input-password + .cbi-button { +.control-group:is(:has(> input:first-child + .cbi-button), .argon-joined-control) > .cbi-input-password + .cbi-button { flex-basis: 2.5rem; padding-inline: 0; } @@ -6591,6 +6647,9 @@ pre.command-output { min-width: min(100%, 18rem); padding-block-end: var(--field-gap); } +#view > .Dashboard .dashboard-charts > .cbi-section > h3:first-child { + padding: var(--field-gap) var(--section-inset); +} #view > .Dashboard .dashboard-charts > .cbi-section > :not(h3:first-child) { margin-inline: var(--section-inset); } @@ -6628,6 +6687,47 @@ pre.command-output { #view > .Dashboard [data-tab="layout"] .cbi-page-actions { padding: var(--field-gap) var(--section-inset); } +#view > .Dashboard .dashboard-widget-list > .item { + align-items: flex-start; + pointer-events: auto; +} +#view > .Dashboard .dashboard-widget-list > .item > span { + flex: 1 1 auto; + width: auto; +} +#view > .Dashboard .dashboard-widget-settings { + flex: 1 1 0; + min-width: 0; + border: 1px solid var(--border-color); + border-radius: var(--radius-control) 0 0 var(--radius-control); +} +#view > .Dashboard .dashboard-widget-settings > summary { + box-sizing: border-box; + min-height: calc(2.5rem - 2px); + padding: 0.5rem 0.75rem; + line-height: 1.5; + list-style-position: inside; + overflow-wrap: anywhere; +} +#view > .Dashboard .dashboard-widget-settings > summary:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: -2px; + border-radius: inherit; +} +[dir="rtl"] #view > .Dashboard .dashboard-widget-settings { + direction: rtl; +} +#view > .Dashboard .dashboard-widget-options { + margin: 0; + padding: var(--control-gap) 0.75rem; +} +#view > .Dashboard .dashboard-widget-options .cbi-checkbox { + height: auto; + min-height: 2.5rem; +} +#view > .Dashboard .dashboard-widget-options .cbi-checkbox + br { + display: none; +} [data-page="admin-status-processes"] #view > .table { min-width: 0; border-radius: var(--radius-card); @@ -6737,8 +6837,8 @@ pre.command-output { [data-page="admin-status-overview"] #view > div:first-child { gap: 0 !important; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]), -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid), +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr)) !important; align-items: stretch !important; @@ -6747,40 +6847,40 @@ pre.command-output { margin: 0 !important; padding: var(--field-gap) var(--section-inset); } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox, -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox, +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox { width: auto !important; min-width: 0 !important; max-width: 100% !important; margin: 0 !important; text-align: center; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-head:first-child, -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-head:first-child { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-head:first-child, +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-head:first-child { overflow-wrap: anywhere; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body, -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body, +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body { font-size: 0.75rem; padding: 0.75rem 0.5rem; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body .cbi-tooltip-container, -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body .cbi-tooltip-container { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body .cbi-tooltip-container, +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body .cbi-tooltip-container { font-size: inherit !important; direction: ltr; unicode-bidi: isolate; } -[dir="rtl"] [data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip, -[dir="rtl"] [data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip { +[dir="rtl"] [data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip, +[dir="rtl"] [data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip { direction: rtl; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-body:nth-child(2), -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-body:nth-child(2) { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-body:nth-child(2), +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-body:nth-child(2) { flex-grow: 1; overflow-wrap: anywhere; } -[data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-body:last-child, -[data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox > .ifacebox-body:last-child { +[data-page="admin-status"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-body:last-child, +[data-page="admin-status-overview"] #view div:is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid) > .ifacebox > .ifacebox-body:last-child { padding: 0.5rem 0.625rem; } [data-page="admin-status"] .argon-port-traffic, @@ -9157,6 +9257,12 @@ pre.command-output { } } @media screen and (min-width: 600px) { + @supports (-moz-appearance: none) { + * { + scrollbar-width: thin; + scrollbar-color: var(--primary) transparent; + } + } ::-webkit-scrollbar { width: 10px; height: 10px; diff --git a/luci-theme-argon/htdocs/luci-static/argon/icon/pause.svg b/luci-theme-argon/htdocs/luci-static/argon/icon/pause.svg new file mode 100644 index 000000000..436af379c --- /dev/null +++ b/luci-theme-argon/htdocs/luci-static/argon/icon/pause.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-argon/htdocs/luci-static/argon/icon/play.svg b/luci-theme-argon/htdocs/luci-static/argon/icon/play.svg new file mode 100644 index 000000000..d1180029e --- /dev/null +++ b/luci-theme-argon/htdocs/luci-static/argon/icon/play.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-argon/htdocs/luci-static/resources/argon-appearance.js b/luci-theme-argon/htdocs/luci-static/resources/argon-appearance.js index f3cc43776..235bf6938 100644 --- a/luci-theme-argon/htdocs/luci-static/resources/argon-appearance.js +++ b/luci-theme-argon/htdocs/luci-static/resources/argon-appearance.js @@ -60,7 +60,10 @@ return baseclass.extend({ this.panel = E('fieldset', { id: 'argon-appearance-options', 'class': 'argon-appearance-panel', hidden: true }, [ E('legend', { 'class': 'visually-hidden' }, _('Theme mode')), ...this.radios.map(function(radio) { - return E('label', { 'class': 'argon-appearance-option', 'data-mode': radio.value }, [ + return E('label', { + 'class': 'argon-appearance-option', 'data-mode': radio.value, + 'aria-disabled': !this.writable ? 'true' : null + }, [ radio, E('span', {}, this.labels[radio.value]) ]); }.bind(this)), diff --git a/luci-theme-argon/htdocs/luci-static/resources/argon-compat.js b/luci-theme-argon/htdocs/luci-static/resources/argon-compat.js new file mode 100644 index 000000000..0fe9f5cf0 --- /dev/null +++ b/luci-theme-argon/htdocs/luci-static/resources/argon-compat.js @@ -0,0 +1,54 @@ +'use strict'; +'require baseclass'; + +// Firefox ESR 115 predates :has(). Keep common form controls usable without +// changing browser preferences or running a selector polyfill on every page. +return baseclass.extend({ + __init__: function() { + if (CSS.supports('selector(:has(*))')) + return; + var root = document.body; + if (!root) + return; + var sectionSelector = '.cbi-tblsection, .cbi-tblsection .table'; + var update = function(records) { + var groups = new Set(), sections = new Set(); + var collect = function(node) { + if (node.nodeType !== 1) + return; + if (node.matches('.control-group')) + groups.add(node); + if (node.matches(sectionSelector)) + sections.add(node); + node.querySelectorAll('.control-group').forEach(function(group) { groups.add(group); }); + node.querySelectorAll(sectionSelector).forEach(function(section) { sections.add(section); }); + }; + if (!records) + collect(root); + else records.forEach(function(record) { + // Counter/text updates cannot change these structural relationships. + if (!Array.from(record.addedNodes).concat(Array.from(record.removedNodes)) + .some(function(node) { return node.nodeType === 1; })) + return; + for (var parent = record.target; parent && parent !== root; parent = parent.parentElement) { + if (parent.matches('.control-group')) + groups.add(parent); + if (parent.matches(sectionSelector)) + sections.add(parent); + } + record.addedNodes.forEach(collect); + }); + groups.forEach(function(group) { + var input = group.firstElementChild; + group.classList.toggle('argon-joined-control', !!(input && input.matches('input') && + input.nextElementSibling && input.nextElementSibling.matches('.cbi-button'))); + }); + sections.forEach(function(section) { + section.classList.toggle('argon-dropdown-section', !!section.querySelector('.cbi-dropdown')); + }); + }; + // Observe native LuCI renders and modal contents, including dependent fields. + new MutationObserver(update).observe(root, { childList: true, subtree: true }); + update(); + } +}); diff --git a/luci-theme-argon/htdocs/luci-static/resources/argon-graphs.js b/luci-theme-argon/htdocs/luci-static/resources/argon-graphs.js index 1807abdbb..afcb97071 100644 --- a/luci-theme-argon/htdocs/luci-static/resources/argon-graphs.js +++ b/luci-theme-argon/htdocs/luci-static/resources/argon-graphs.js @@ -13,6 +13,7 @@ return baseclass.extend({ var frame = null; var update = function() { frame = null; + var pending = []; view.querySelectorAll('svg').forEach(function(svg) { if (graphs.has(svg) || svg.hasAttribute('viewBox')) return; @@ -28,11 +29,16 @@ return baseclass.extend({ if (width <= 0 || !(height > 0)) return; - graphs.set(svg, width); - svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height); - svg.setAttribute('preserveAspectRatio', 'none'); + pending.push({ svg: svg, width: width, height: height }); }); + pending.forEach(function(graph) { + graphs.set(graph.svg, graph.width); + graph.svg.setAttribute('viewBox', '0 0 ' + graph.width + ' ' + graph.height); + graph.svg.setAttribute('preserveAspectRatio', 'none'); + }); + + var labels = []; graphs.forEach(function(width, svg) { if (!svg.isConnected) { graphs.delete(svg); @@ -47,18 +53,26 @@ return baseclass.extend({ svg.querySelectorAll('text').forEach(function(text) { var x = parseFloat(text.getAttribute('x')) || 0; var offset = x * (1 - inverse); - var transform = function(shift) { - text.style.transform = 'translateX(' + shift + 'px) scaleX(' + inverse + ')'; - }; - text.style.transformOrigin = '0 0'; - transform(offset); - var label = text.getBoundingClientRect(); - var shift = Math.max(0, bounds.left + 4 - label.left) - - Math.max(0, label.right - bounds.right + 4); - if (shift) - transform(offset + shift / ratio); + labels.push({ text: text, bounds: bounds, ratio: ratio, inverse: inverse, offset: offset }); }); }); + + // Apply every provisional transform before measuring any label. Alternating + // a style write and a bounds read forces layout once per chart label. + labels.forEach(function(label) { + label.text.style.transformOrigin = '0 0'; + label.text.style.transform = 'translateX(' + label.offset + 'px) scaleX(' + label.inverse + ')'; + }); + labels.forEach(function(label) { + var bounds = label.text.getBoundingClientRect(); + label.shift = Math.max(0, label.bounds.left + 4 - bounds.left) - + Math.max(0, bounds.right - label.bounds.right + 4); + }); + labels.forEach(function(label) { + if (label.shift) + label.text.style.transform = 'translateX(' + (label.offset + label.shift / label.ratio) + + 'px) scaleX(' + label.inverse + ')'; + }); }; var schedule = function() { if (frame === null) diff --git a/luci-theme-argon/htdocs/luci-static/resources/argon-overview.js b/luci-theme-argon/htdocs/luci-static/resources/argon-overview.js index ce385309f..302627384 100644 --- a/luci-theme-argon/htdocs/luci-static/resources/argon-overview.js +++ b/luci-theme-argon/htdocs/luci-static/resources/argon-overview.js @@ -11,7 +11,12 @@ return baseclass.extend({ var frame = null; var update = function() { frame = null; - view.querySelectorAll('.ifacebox:has(> .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox-body:last-child > .cbi-tooltip-container').forEach(function(container) { + view.querySelectorAll('.ifacebox > .ifacebox-body > img[src*="/icons/port_"]').forEach(function(icon) { + var box = icon.parentNode.parentNode; + box.parentNode.classList.add('argon-port-grid'); + var container = box.lastElementChild.querySelector('.cbi-tooltip-container'); + if (!container) + return; if (container.querySelector('.argon-port-traffic')) return; diff --git a/luci-theme-argon/htdocs/luci-static/resources/view/argon-config.js b/luci-theme-argon/htdocs/luci-static/resources/view/argon-config.js index 2e9f1b9b1..d8370b20f 100644 --- a/luci-theme-argon/htdocs/luci-static/resources/view/argon-config.js +++ b/luci-theme-argon/htdocs/luci-static/resources/view/argon-config.js @@ -113,7 +113,7 @@ return view.extend({ var m, s, o; m = new form.Map('argon', _('Argon theme configuration'), - _('Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended.')); + _('Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos.')); s = m.section(form.TypedSection); s.render = function() { return updates.render(data[4], this.map); }; diff --git a/luci-theme-argon/less/components/accessibility.less b/luci-theme-argon/less/components/accessibility.less index de16f70d4..59f0f039d 100644 --- a/luci-theme-argon/less/components/accessibility.less +++ b/luci-theme-argon/less/components/accessibility.less @@ -12,6 +12,25 @@ border: 0; } +.skip-link { + position: fixed; + z-index: 1200; + inset-block-start: var(--control-gap); + inset-inline-start: var(--page-gutter); + padding: .5rem .75rem; + color: var(--text-strong); + background: var(--surface-panel); + border: 1px solid var(--border-color); + border-radius: var(--radius-control); + transform: translateY(calc(-100% - var(--control-gap))); + + &:focus { + transform: none; + outline: 2px solid var(--focus-ring); + outline-offset: 2px; + } +} + @media (prefers-reduced-motion: reduce) { .main-left, .main-right, diff --git a/luci-theme-argon/less/components/appearance.less b/luci-theme-argon/less/components/appearance.less index dfc61df2f..29aef9f16 100644 --- a/luci-theme-argon/less/components/appearance.less +++ b/luci-theme-argon/less/components/appearance.less @@ -162,21 +162,12 @@ header .header-actions { overflow-wrap: anywhere; } - &:has(input:checked) { - color: var(--dropdown-selected-text); - background: var(--dropdown-selected-bg); - - &::after { - visibility: visible; - } - } - - &:has(input:focus-visible) { + &:focus-within { outline: 2px solid var(--focus-ring); outline-offset: -2px; } - &:has(input:disabled) { + &[aria-disabled="true"] { cursor: default; opacity: .6; } @@ -201,7 +192,7 @@ header .header-actions { background-color: var(--surface-active); } - .argon-appearance-option:has(input:not(:disabled)):not(:has(input:checked)):hover { + .argon-appearance-option:not([aria-disabled="true"]):hover { background: var(--dropdown-hover-bg); } } @@ -226,6 +217,19 @@ header .header-actions { } } +// The existing mode attribute tracks the checked radio, including save rollback. +// Explicit selectors also preserve selection and its checkmark on Firefox ESR 115. +#argon-appearance[data-mode="light"] .argon-appearance-option[data-mode="light"], +#argon-appearance[data-mode="dark"] .argon-appearance-option[data-mode="dark"], +#argon-appearance[data-mode="normal"] .argon-appearance-option[data-mode="normal"] { + color: var(--dropdown-selected-text); + background: var(--dropdown-selected-bg); + + &::after { + visibility: visible; + } +} + // Explicit selectors keep these rules scoped when compiled with Less 4. // Native hidden semantics still remove the closed menu from focus and hit testing. @starting-style { diff --git a/luci-theme-argon/less/components/feedback.less b/luci-theme-argon/less/components/feedback.less index d89d89f76..3822e8ae9 100644 --- a/luci-theme-argon/less/components/feedback.less +++ b/luci-theme-argon/less/components/feedback.less @@ -152,6 +152,9 @@ .alert-message { transform: none; position: static; + // Inline banners can be flex items, where z-index applies even without positioning. + // Keep them below dropdowns instead of inheriting the fixed alert's overlay layer. + z-index: auto; transition: opacity .4s ease; &.fade-out { diff --git a/luci-theme-argon/less/components/forms.less b/luci-theme-argon/less/components/forms.less index eeffec562..fe505bf8c 100644 --- a/luci-theme-argon/less/components/forms.less +++ b/luci-theme-argon/less/components/forms.less @@ -212,7 +212,9 @@ } } -.control-group:has(> input:first-child + .cbi-button) { +// Interpolation preserves the forgiving selector list with the pinned Less 4 parser. +@joined-control-selector: ~":is(:has(> input:first-child + .cbi-button), .argon-joined-control)"; +.control-group@{joined-control-selector} { flex-wrap: nowrap; gap: 0; diff --git a/luci-theme-argon/less/components/tables.less b/luci-theme-argon/less/components/tables.less index 33c0eae19..dc096bf39 100644 --- a/luci-theme-argon/less/components/tables.less +++ b/luci-theme-argon/less/components/tables.less @@ -122,6 +122,15 @@ .cbi-tblsection { overflow-x: auto; + + // A horizontal scroller also computes overflow-y:auto. LuCI then sizes + // dropdowns against this short section instead of the page or modal. + // Native responsive tables wrap their fields when the viewport narrows. + @dropdown-section-selector: ~":is(:has(.cbi-dropdown), .argon-dropdown-section)"; + &@{dropdown-section-selector}, + .table@{dropdown-section-selector} { + overflow: visible; + } } table { diff --git a/luci-theme-argon/less/layout.less b/luci-theme-argon/less/layout.less index b5ba9a29a..efa65a0b6 100644 --- a/luci-theme-argon/less/layout.less +++ b/luci-theme-argon/less/layout.less @@ -517,7 +517,8 @@ footer { box-shadow: var(--section-shadow); } - &>div:first-child:not(.cbi-map) { + // The native dashboard supplies its own grid layout. + &>div:first-child:not(.cbi-map, .Dashboard) { display: flex; flex-direction: column; min-width: 0; diff --git a/luci-theme-argon/less/pages/dashboard.less b/luci-theme-argon/less/pages/dashboard.less index da19bf75a..963db26fa 100644 --- a/luci-theme-argon/less/pages/dashboard.less +++ b/luci-theme-argon/less/pages/dashboard.less @@ -25,6 +25,11 @@ min-width: ~"min(100%, 18rem)"; padding-block-end: var(--field-gap); + // Newer dashboard styles reset chart headings after the theme loads. + > h3:first-child { + padding: var(--field-gap) var(--section-inset); + } + // Keep the native full-width heading; inset its chart and legend. > :not(h3:first-child) { margin-inline: var(--section-inset); @@ -77,4 +82,58 @@ [data-tab="layout"] .cbi-page-actions { padding: var(--field-gap) var(--section-inset); } + + .dashboard-widget-list > .item { + align-items: flex-start; + // Dashboard entries support dragging and contain native form controls. + pointer-events: auto; + + // Fill the row so the delete button stays at LuCI's right-edge hit area. + > span { + flex: 1 1 auto; + width: auto; + } + } + + .dashboard-widget-settings { + flex: 1 1 0; + min-width: 0; + border: 1px solid var(--border-color); + border-radius: var(--radius-control) 0 0 var(--radius-control); + + > summary { + box-sizing: border-box; + min-height: calc(2.5rem - 2px); + padding: .5rem .75rem; + line-height: 1.5; + list-style-position: inside; + overflow-wrap: anywhere; + + &:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: -2px; + border-radius: inherit; + } + } + + // The item remains LTR because its native delete hit area is physical. + [dir="rtl"] & { + direction: rtl; + } + } + + .dashboard-widget-options { + margin: 0; + padding: var(--control-gap) .75rem; + + .cbi-checkbox { + height: auto; + min-height: 2.5rem; + } + + // Each checkbox is already a flex row; native
nodes add empty rows. + .cbi-checkbox + br { + display: none; + } + } } diff --git a/luci-theme-argon/less/pages/overview.less b/luci-theme-argon/less/pages/overview.less index 23ceaab86..7c8ac6b09 100644 --- a/luci-theme-argon/less/pages/overview.less +++ b/luci-theme-argon/less/pages/overview.less @@ -76,7 +76,8 @@ } // Match native port cards by their icons, independently of inline CSS formatting. - #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) { + @port-grid-selector: ~":is(:has(> .ifacebox > .ifacebox-body > img[src*=\"/icons/port_\"]), .argon-port-grid)"; + #view div@{port-grid-selector} { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(~"min(100%, 8.5rem)", 1fr)) !important; // ImmortalWrt centers fixed-width cards; let both native variants fill a row. diff --git a/luci-theme-argon/less/responsive.less b/luci-theme-argon/less/responsive.less index a276a89b9..a0ba6a438 100644 --- a/luci-theme-argon/less/responsive.less +++ b/luci-theme-argon/less/responsive.less @@ -556,6 +556,14 @@ // ========================================================================== @media screen and (min-width: 600px) { + // Firefox uses the standard scrollbar API rather than WebKit pseudo-elements. + @supports (-moz-appearance: none) { + * { + scrollbar-width: thin; + scrollbar-color: var(--primary) transparent; + } + } + ::-webkit-scrollbar { width: 10px; height: 10px; diff --git a/luci-theme-argon/less/sysauth.less b/luci-theme-argon/less/sysauth.less index b5b6c4b6f..57e0b913a 100644 --- a/luci-theme-argon/less/sysauth.less +++ b/luci-theme-argon/less/sysauth.less @@ -42,24 +42,60 @@ } } - .volume-control { + .background-controls { position: fixed; - top: 1rem; - right: 1rem; - width: 1.5rem; - height: 1.5rem; + inset-block-start: var(--field-gap); + inset-inline-end: var(--field-gap); + display: flex; + gap: var(--control-gap); z-index: 5000; - cursor: pointer; - border: 0 !important; - background: url(../img/volume_high.svg) no-repeat center/contain; - &:focus-visible { - outline: 2px solid var(--focus-ring); - outline-offset: 4px; + > button { + display: grid; + place-items: center; + min-width: 2.5rem; + min-height: 2.5rem; + padding: .5rem; + cursor: pointer; + background: var(--surface-panel); + color: var(--text-strong); + + &::before { + content: ''; + width: 1rem; + height: 1rem; + background-color: currentColor; + mask: var(--background-control-icon) center / contain no-repeat; + -webkit-mask: var(--background-control-icon) center / contain no-repeat; + } + + &:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; + } + + @media (forced-colors: active) { + &::before { + forced-color-adjust: none; + background-color: ButtonText; + } + } } + } + + .playback-control { + --background-control-icon: url(../icon/play.svg); + + &.playing { + --background-control-icon: url(../icon/pause.svg); + } + } + + .volume-control { + --background-control-icon: url(../img/volume_high.svg); &.mute { - background-image: url(../img/volume_off.svg); + --background-control-icon: url(../img/volume_off.svg); } } diff --git a/luci-theme-argon/po/ar/argon-config.po b/luci-theme-argon/po/ar/argon-config.po index 341ce732c..2e2e0bdd2 100644 --- a/luci-theme-argon/po/ar/argon-config.po +++ b/luci-theme-argon/po/ar/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "السماح للسمة luci-theme-argon بالوصول إلى UCI" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "يمكنك هنا ضبط التمويه والشفافية لصفحة تسجيل الدخول في سمة Argon، وإدارة صور الخلفية ومقاطع الفيديو. يُنصح باستخدام Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "يمكنك هنا ضبط التمويه والشفافية لصفحة تسجيل الدخول في سمة Argon، وإدارة صور الخلفية ومقاطع الفيديو." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "إلغاء" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "حاول مجددًا بعد %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "تشغيل فيديو الخلفية" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "إيقاف فيديو الخلفية مؤقتًا" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "تخطي إلى المحتوى" diff --git a/luci-theme-argon/po/de/argon-config.po b/luci-theme-argon/po/de/argon-config.po index d60215972..90962d331 100644 --- a/luci-theme-argon/po/de/argon-config.po +++ b/luci-theme-argon/po/de/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "UCI-Zugriff für luci-theme-argon erlauben" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Hier können Sie die Unschärfe und Transparenz der Anmeldeseite des Argon-Designs einstellen sowie Hintergrundbilder und -videos verwalten. Chrome wird empfohlen." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Hier können Sie die Unschärfe und Transparenz der Anmeldeseite des Argon-Designs einstellen sowie Hintergrundbilder und -videos verwalten." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Abbrechen" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Versuchen Sie es nach %s erneut." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Hintergrundvideo abspielen" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Hintergrundvideo pausieren" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Zum Inhalt springen" diff --git a/luci-theme-argon/po/es/argon-config.po b/luci-theme-argon/po/es/argon-config.po index 35d103c79..60834dc8b 100644 --- a/luci-theme-argon/po/es/argon-config.po +++ b/luci-theme-argon/po/es/argon-config.po @@ -165,8 +165,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Permitir el acceso a UCI a luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Aquí puede configurar el desenfoque y la transparencia de la página de inicio de sesión del tema Argon y administrar las imágenes y los vídeos de fondo. Se recomienda Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Aquí puede configurar el desenfoque y la transparencia de la página de inicio de sesión del tema Argon y administrar las imágenes y los vídeos de fondo." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -538,3 +538,15 @@ msgstr "Cancelar" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Vuelva a intentarlo después de las %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Reproducir el vídeo de fondo" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Pausar el vídeo de fondo" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Saltar al contenido" diff --git a/luci-theme-argon/po/fr/argon-config.po b/luci-theme-argon/po/fr/argon-config.po index 612024bd4..2e10bfc11 100644 --- a/luci-theme-argon/po/fr/argon-config.po +++ b/luci-theme-argon/po/fr/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Autoriser l’accès à UCI pour luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Vous pouvez régler ici le flou et la transparence de la page de connexion du thème Argon, ainsi que gérer les images et les vidéos d’arrière-plan. Chrome est recommandé." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Vous pouvez régler ici le flou et la transparence de la page de connexion du thème Argon, ainsi que gérer les images et les vidéos d’arrière-plan." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Annuler" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Réessayez après %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Lire la vidéo d’arrière-plan" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Mettre la vidéo d’arrière-plan en pause" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Aller au contenu" diff --git a/luci-theme-argon/po/it/argon-config.po b/luci-theme-argon/po/it/argon-config.po index 330a74715..b0499db39 100644 --- a/luci-theme-argon/po/it/argon-config.po +++ b/luci-theme-argon/po/it/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Consenti l’accesso UCI a luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Qui puoi impostare la sfocatura e la trasparenza della pagina di accesso del tema Argon e gestire le immagini e i video di sfondo. Si consiglia Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Qui puoi impostare la sfocatura e la trasparenza della pagina di accesso del tema Argon e gestire le immagini e i video di sfondo." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Annulla" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Riprovare dopo le %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Riproduci il video di sfondo" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Metti in pausa il video di sfondo" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Vai al contenuto" diff --git a/luci-theme-argon/po/ja/argon-config.po b/luci-theme-argon/po/ja/argon-config.po index 561cd0852..6c72d7c79 100644 --- a/luci-theme-argon/po/ja/argon-config.po +++ b/luci-theme-argon/po/ja/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "luci-theme-argon に UCI へのアクセスを許可" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Argon テーマのログインページのぼかしと透明度を設定し、背景画像や動画を管理できます。Chrome の使用を推奨します。" +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Argon テーマのログインページのぼかしと透明度を設定し、背景画像や動画を管理できます。" #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "キャンセル" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "%s 以降にもう一度お試しください。" + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "背景動画を再生" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "背景動画を一時停止" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "コンテンツへスキップ" diff --git a/luci-theme-argon/po/ko/argon-config.po b/luci-theme-argon/po/ko/argon-config.po index c23e058a6..6940b615e 100644 --- a/luci-theme-argon/po/ko/argon-config.po +++ b/luci-theme-argon/po/ko/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "luci-theme-argon에 UCI 접근 허용" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Argon 테마 로그인 페이지의 흐림 효과와 투명도를 설정하고 배경 이미지와 동영상을 관리할 수 있습니다. Chrome 사용을 권장합니다." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Argon 테마 로그인 페이지의 흐림 효과와 투명도를 설정하고 배경 이미지와 동영상을 관리할 수 있습니다." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "취소" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "%s 이후에 다시 시도하세요." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "배경 동영상 재생" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "배경 동영상 일시 중지" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "본문으로 건너뛰기" diff --git a/luci-theme-argon/po/nl/argon-config.po b/luci-theme-argon/po/nl/argon-config.po index 9627dd3c6..3d8059a10 100644 --- a/luci-theme-argon/po/nl/argon-config.po +++ b/luci-theme-argon/po/nl/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "UCI-toegang verlenen aan luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Hier kunt u de vervaging en transparantie van de aanmeldpagina van het Argon-thema instellen en achtergrondafbeeldingen en -video’s beheren. Chrome wordt aanbevolen." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Hier kunt u de vervaging en transparantie van de aanmeldpagina van het Argon-thema instellen en achtergrondafbeeldingen en -video’s beheren." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Annuleren" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Probeer het na %s opnieuw." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Achtergrondvideo afspelen" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Achtergrondvideo pauzeren" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Naar inhoud springen" diff --git a/luci-theme-argon/po/pl/argon-config.po b/luci-theme-argon/po/pl/argon-config.po index 681e4f01e..935dd1119 100644 --- a/luci-theme-argon/po/pl/argon-config.po +++ b/luci-theme-argon/po/pl/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Przyznaj dostęp do UCI dla luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Tutaj możesz ustawić rozmycie i przezroczystość strony logowania motywu Argon, oraz zarządzać obrazami i filmami tła. Zalecana jest przeglądarka Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Tutaj możesz ustawić rozmycie i przezroczystość strony logowania motywu Argon, oraz zarządzać obrazami i filmami tła." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Anuluj" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Spróbuj ponownie po %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Odtwórz wideo w tle" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Wstrzymaj wideo w tle" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Przejdź do treści" diff --git a/luci-theme-argon/po/pt_BR/argon-config.po b/luci-theme-argon/po/pt_BR/argon-config.po index 48254e3d0..7c433474e 100644 --- a/luci-theme-argon/po/pt_BR/argon-config.po +++ b/luci-theme-argon/po/pt_BR/argon-config.po @@ -165,8 +165,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Permitir acesso à UCI para luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Aqui você pode configurar o desfoque e a transparência da página de login do tema Argon e gerenciar as imagens e os vídeos de fundo. Recomenda-se o Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Aqui você pode configurar o desfoque e a transparência da página de login do tema Argon e gerenciar as imagens e os vídeos de fundo." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -538,3 +538,15 @@ msgstr "Cancelar" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Tente novamente após %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Reproduzir vídeo de fundo" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Pausar vídeo de fundo" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Ir para o conteúdo" diff --git a/luci-theme-argon/po/ru/argon-config.po b/luci-theme-argon/po/ru/argon-config.po index 88800279f..1e65ee752 100644 --- a/luci-theme-argon/po/ru/argon-config.po +++ b/luci-theme-argon/po/ru/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Разрешить luci-theme-argon доступ к UCI" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Здесь можно настроить размытие и прозрачность страницы логина темы Argon, управлять фоновыми изображениями и видео. Рекомендуется использовать Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Здесь можно настроить размытие и прозрачность страницы логина темы Argon, управлять фоновыми изображениями и видео." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Отмена" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Повторите попытку после %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Воспроизвести фоновое видео" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Приостановить фоновое видео" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Перейти к содержимому" diff --git a/luci-theme-argon/po/templates/argon-config.pot b/luci-theme-argon/po/templates/argon-config.pot index 2b8b87b18..e40132b31 100644 --- a/luci-theme-argon/po/templates/argon-config.pot +++ b/luci-theme-argon/po/templates/argon-config.pot @@ -160,7 +160,7 @@ msgstr "" #: htdocs/luci-static/resources/view/argon-config.js:114 msgid "" "Here you can set the blur and transparency of the login page of argon theme, " -"and manage the background pictures and videos. Chrome is recommended." +"and manage the background pictures and videos." msgstr "" #: htdocs/luci-static/resources/argon/branding.js:60 @@ -543,3 +543,15 @@ msgstr "" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "" + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "" diff --git a/luci-theme-argon/po/tr/argon-config.po b/luci-theme-argon/po/tr/argon-config.po index 8f0f317ac..ea32d7283 100644 --- a/luci-theme-argon/po/tr/argon-config.po +++ b/luci-theme-argon/po/tr/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "luci-theme-argon için UCI erişimine izin ver" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Burada Argon temasının giriş sayfasının bulanıklığını ve saydamlığını ayarlayabilir, arka plan resimlerini ve videolarını yönetebilirsiniz. Chrome kullanılması önerilir." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Burada Argon temasının giriş sayfasının bulanıklığını ve saydamlığını ayarlayabilir, arka plan resimlerini ve videolarını yönetebilirsiniz." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "İptal" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "%s sonrasında yeniden deneyin." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Arka plan videosunu oynat" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Arka plan videosunu duraklat" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "İçeriğe atla" diff --git a/luci-theme-argon/po/uk/argon-config.po b/luci-theme-argon/po/uk/argon-config.po index 98d3d913b..5f81bc25c 100644 --- a/luci-theme-argon/po/uk/argon-config.po +++ b/luci-theme-argon/po/uk/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Надати luci-theme-argon доступ до UCI" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Тут можна налаштувати розмиття та прозорість сторінки входу теми Argon, а також керувати фоновими зображеннями та відео. Рекомендовано використовувати Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Тут можна налаштувати розмиття та прозорість сторінки входу теми Argon, а також керувати фоновими зображеннями та відео." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Скасувати" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Спробуйте ще раз після %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Відтворити фонове відео" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Призупинити фонове відео" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Перейти до вмісту" diff --git a/luci-theme-argon/po/vi/argon-config.po b/luci-theme-argon/po/vi/argon-config.po index 29b7920cb..32ca1c393 100644 --- a/luci-theme-argon/po/vi/argon-config.po +++ b/luci-theme-argon/po/vi/argon-config.po @@ -163,8 +163,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "Cấp quyền truy cập UCI cho luci-theme-argon" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "Tại đây, bạn có thể thiết lập độ mờ và độ trong suốt của trang đăng nhập giao diện Argon, đồng thời quản lý hình ảnh và video nền. Khuyến nghị sử dụng Chrome." +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "Tại đây, bạn có thể thiết lập độ mờ và độ trong suốt của trang đăng nhập giao diện Argon, đồng thời quản lý hình ảnh và video nền." #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -536,3 +536,15 @@ msgstr "Hủy" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "Thử lại sau %s." + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "Phát video nền" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "Tạm dừng video nền" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "Chuyển đến nội dung" diff --git a/luci-theme-argon/po/zh_Hans/argon-config.po b/luci-theme-argon/po/zh_Hans/argon-config.po index 793407c4d..5390cc7c9 100644 --- a/luci-theme-argon/po/zh_Hans/argon-config.po +++ b/luci-theme-argon/po/zh_Hans/argon-config.po @@ -165,8 +165,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "授予 luci-theme-argon 访问 UCI 配置的权限" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "设置 Argon 主题登录页的模糊程度和透明度,管理背景图片与视频。建议使用 Chrome 浏览器。" +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "设置 Argon 主题登录页的模糊程度和透明度,管理背景图片与视频。" #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -538,3 +538,15 @@ msgstr "取消" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "请在 %s 后重试。" + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "播放背景视频" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "暂停背景视频" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "跳至主要内容" diff --git a/luci-theme-argon/po/zh_Hant/argon-config.po b/luci-theme-argon/po/zh_Hant/argon-config.po index 52b0af020..6141c28ad 100644 --- a/luci-theme-argon/po/zh_Hant/argon-config.po +++ b/luci-theme-argon/po/zh_Hant/argon-config.po @@ -165,8 +165,8 @@ msgid "Grant UCI access for luci-theme-argon" msgstr "授予 luci-theme-argon 存取 UCI 設定的權限" #: htdocs/luci-static/resources/view/argon-config.js:114 -msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended." -msgstr "您可以在此設定登入畫面的模糊度、透明度、以及管理背景圖片與影片(推薦使用 Chrome)" +msgid "Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos." +msgstr "您可以在此設定登入畫面的模糊度、透明度、以及管理背景圖片與影片。" #: htdocs/luci-static/resources/argon/branding.js:60 msgid "Invalid PNG image or dimensions." @@ -538,3 +538,15 @@ msgstr "取消" #: htdocs/luci-static/resources/argon/updates.js msgid "Try again after %s." msgstr "請在 %s 後重試。" + +#: ucode/template/themes/argon/login_background.ut +msgid "Play background video" +msgstr "播放背景影片" + +#: ucode/template/themes/argon/login_background.ut +msgid "Pause background video" +msgstr "暫停背景影片" + +#: ucode/template/themes/argon/header.ut +msgid "Skip to content" +msgstr "跳至主要內容" diff --git a/luci-theme-argon/tests/appearance.test.js b/luci-theme-argon/tests/appearance.test.js index 6e98cc3a3..bf0886ba8 100644 --- a/luci-theme-argon/tests/appearance.test.js +++ b/luci-theme-argon/tests/appearance.test.js @@ -75,6 +75,7 @@ test('failed writes reconcile stored state and restore usable controls', async ( test('read-only, unavailable, invalid, and repeated mode changes never write', async () => { const readonly = fixture({ writable: false }); await readonly.start(); await readonly.api.saveMode('dark'); assert.ok(readonly.api.radios.every(r => r.disabled)); assert.equal(readonly.calls.filter(c => c[0] === 'set').length, 0); + assert.ok(readonly.api.panel.children.filter(node => node.tag === 'label').every(node => node.attrs['aria-disabled'] === 'true')); const offline = fixture({ getError: true }); await offline.start(); assert.equal(offline.root.hidden, true); const state = fixture(); await state.start(); await state.api.saveMode('bogus'); await state.api.saveMode('normal'); assert.equal(state.calls.filter(c => c[0] === 'set').length, 0); diff --git a/luci-theme-argon/tests/background-playback.test.js b/luci-theme-argon/tests/background-playback.test.js new file mode 100644 index 000000000..f249824da --- /dev/null +++ b/luci-theme-argon/tests/background-playback.test.js @@ -0,0 +1,45 @@ +'use strict'; +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const template = fs.readFileSync(path.join(__dirname, '../ucode/template/themes/argon/login_background.ut'), 'utf8'); +const script = template.match(/