增加订阅快照备机同步链路
This commit is contained in:
@@ -21,7 +21,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
SendResponse.class, Message.class, com.pengrad.telegrambot.model.User.class,
|
SendResponse.class, Message.class, com.pengrad.telegrambot.model.User.class,
|
||||||
Chat.class, MessageEntity.class,
|
Chat.class, MessageEntity.class,
|
||||||
AbstractMethodError.class, DeleteGalleryMessage.class, DownloadPostMessage.class, DownloadStatusMessage.class,
|
AbstractMethodError.class, DeleteGalleryMessage.class, DownloadPostMessage.class, DownloadStatusMessage.class,
|
||||||
IdentityMessage.class, MaintainMessage.class, ResponseMessage.class, AvailableCheckMessage.class, LinkPreviewOptions.class})
|
IdentityMessage.class, MaintainMessage.class, ResponseMessage.class, AvailableCheckMessage.class,
|
||||||
|
SubscriptionSnapshotMessage.class, SubscriptionSnapshotPayload.class, SubscriptionAccountSnapshot.class,
|
||||||
|
SubscriptionBindingSnapshot.class, LinkPreviewOptions.class})
|
||||||
public class CustomBean {
|
public class CustomBean {
|
||||||
|
|
||||||
@Value("${bot.token:5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA}")
|
@Value("${bot.token:5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA}")
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ public class AbstractMessage {
|
|||||||
|
|
||||||
public static final byte AVAILABLE_CHECK_MESSAGE = 8;
|
public static final byte AVAILABLE_CHECK_MESSAGE = 8;
|
||||||
|
|
||||||
|
public static final byte SUBSCRIPTION_SNAPSHOT_MESSAGE = 9;
|
||||||
|
|
||||||
|
public static final byte SUBSCRIPTION_SNAPSHOT_MESSAGE = 9;
|
||||||
|
|
||||||
public byte messageType;
|
public byte messageType;
|
||||||
|
|
||||||
public int messageId;
|
public int messageId;
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public class MessageCodec extends ByteToMessageCodec<AbstractMessage> {
|
|||||||
case AbstractMessage.IDENTITY_MESSAGE -> objectMapper.readValue(metadata, IdentityMessage.class);
|
case AbstractMessage.IDENTITY_MESSAGE -> objectMapper.readValue(metadata, IdentityMessage.class);
|
||||||
case AbstractMessage.MAINTAIN_MESSAGE -> objectMapper.readValue(metadata, MaintainMessage.class);
|
case AbstractMessage.MAINTAIN_MESSAGE -> objectMapper.readValue(metadata, MaintainMessage.class);
|
||||||
case AbstractMessage.AVAILABLE_CHECK_MESSAGE -> objectMapper.readValue(metadata, AvailableCheckMessage.class);
|
case AbstractMessage.AVAILABLE_CHECK_MESSAGE -> objectMapper.readValue(metadata, AvailableCheckMessage.class);
|
||||||
|
case AbstractMessage.SUBSCRIPTION_SNAPSHOT_MESSAGE -> objectMapper.readValue(metadata, SubscriptionSnapshotMessage.class);
|
||||||
|
case AbstractMessage.SUBSCRIPTION_SNAPSHOT_MESSAGE -> objectMapper.readValue(metadata, SubscriptionSnapshotMessage.class);
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.lion.lionwebsite.Message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SubscriptionAccountSnapshot {
|
||||||
|
private Integer accountId;
|
||||||
|
private boolean enabled;
|
||||||
|
private boolean filterHighMultiplier;
|
||||||
|
private String v2ContentBase64;
|
||||||
|
private String v2Sha256;
|
||||||
|
private String clashContentBase64;
|
||||||
|
private String clashSha256;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.lion.lionwebsite.Message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SubscriptionBindingSnapshot {
|
||||||
|
private String publicKeySha256;
|
||||||
|
private Integer accountId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.lion.lionwebsite.Message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SubscriptionSnapshotMessage extends AbstractMessage {
|
||||||
|
{
|
||||||
|
messageType = SUBSCRIPTION_SNAPSHOT_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int schemaVersion;
|
||||||
|
private String revision;
|
||||||
|
private long generatedAt;
|
||||||
|
private String payloadBase64;
|
||||||
|
private String payloadSha256;
|
||||||
|
private String signature;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.lion.lionwebsite.Message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SubscriptionSnapshotPayload {
|
||||||
|
private int schemaVersion;
|
||||||
|
private List<SubscriptionAccountSnapshot> accounts = new ArrayList<>();
|
||||||
|
private List<SubscriptionBindingSnapshot> bindings = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -118,6 +118,7 @@ public class LocalService{
|
|||||||
public boolean updateSub(boolean isManual) throws IOException {
|
public boolean updateSub(boolean isManual) throws IOException {
|
||||||
// 手动和定时入口均刷新全部启用子账号;全部成功后更新原有“上次更新时间”。
|
// 手动和定时入口均刷新全部启用子账号;全部成功后更新原有“上次更新时间”。
|
||||||
boolean success = subscriptionRefreshService.refreshAll();
|
boolean success = subscriptionRefreshService.refreshAll();
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
if (success)
|
if (success)
|
||||||
configurationMapper.updateConfiguration(CustomConfiguration.LAST_UPDATE_SUB_TIME,
|
configurationMapper.updateConfiguration(CustomConfiguration.LAST_UPDATE_SUB_TIME,
|
||||||
CustomUtil.dateTimeFormatter().format(LocalDateTime.now()));
|
CustomUtil.dateTimeFormatter().format(LocalDateTime.now()));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -34,6 +35,7 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Data
|
@Data
|
||||||
@@ -68,6 +70,19 @@ public class RemoteService {
|
|||||||
|
|
||||||
final WebSocketService webSocketService;
|
final WebSocketService webSocketService;
|
||||||
|
|
||||||
|
final SubscriptionStandbySnapshotService subscriptionStandbySnapshotService;
|
||||||
|
|
||||||
|
final ExecutorService subscriptionSyncExecutor = Executors.newSingleThreadExecutor(r -> {
|
||||||
|
Thread thread = new Thread(r, "subscription-standby-sync");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
final AtomicBoolean subscriptionSyncQueued = new AtomicBoolean();
|
||||||
|
|
||||||
|
@Value("${subscription.standby.sync-enabled:false}")
|
||||||
|
boolean subscriptionSyncEnabled;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void init() {
|
void init() {
|
||||||
if(!initChannel()){ //如果远程服务器连接失败,则开启本地监听
|
if(!initChannel()){ //如果远程服务器连接失败,则开启本地监听
|
||||||
@@ -113,6 +128,7 @@ public class RemoteService {
|
|||||||
|
|
||||||
//子节点上线时,发送未完成的任务
|
//子节点上线时,发送未完成的任务
|
||||||
resetUndone();
|
resetUndone();
|
||||||
|
requestSubscriptionSync();
|
||||||
return true;
|
return true;
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
log.error("connect node failed, wait for node back online", e);
|
log.error("connect node failed, wait for node back online", e);
|
||||||
@@ -156,6 +172,50 @@ public class RemoteService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 请求将当前全部订阅状态异步同步到存储节点,短时间内的多次请求会合并。 */
|
||||||
|
public void requestSubscriptionSync() {
|
||||||
|
if (!subscriptionSyncEnabled)
|
||||||
|
return;
|
||||||
|
if (!subscriptionSyncQueued.compareAndSet(false, true))
|
||||||
|
return;
|
||||||
|
subscriptionSyncExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
subscriptionSyncQueued.set(false);
|
||||||
|
if (isDead())
|
||||||
|
continue;
|
||||||
|
SubscriptionSnapshotMessage message = subscriptionStandbySnapshotService.build();
|
||||||
|
message.setMessageId(atomicInteger.getAndIncrement());
|
||||||
|
DefaultPromise<AbstractMessage> promise = new DefaultPromise<>(eventLoopGroup);
|
||||||
|
promiseHashMap.put(message.messageId, promise);
|
||||||
|
channel.writeAndFlush(message);
|
||||||
|
if (promise.await(30, TimeUnit.SECONDS)) {
|
||||||
|
AbstractMessage reply = promise.getNow();
|
||||||
|
if (reply instanceof ResponseMessage response && response.getResult() == 0)
|
||||||
|
log.info("订阅快照同步成功 revision={}", shortRevision(message.getRevision()));
|
||||||
|
else
|
||||||
|
log.warn("订阅快照同步失败 revision={} result={}", shortRevision(message.getRevision()),
|
||||||
|
reply instanceof ResponseMessage response ? response.getResult() : "timeout");
|
||||||
|
} else {
|
||||||
|
log.warn("订阅快照同步超时 revision={}", shortRevision(message.getRevision()));
|
||||||
|
}
|
||||||
|
promiseHashMap.remove(message.messageId, promise);
|
||||||
|
} while (subscriptionSyncQueued.getAndSet(false));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("生成或发送订阅快照失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortRevision(String revision) {
|
||||||
|
return revision == null ? null : revision.substring(0, Math.min(12, revision.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
void shutdownSubscriptionSync() {
|
||||||
|
subscriptionSyncExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isDead(){
|
public boolean isDead(){
|
||||||
return channelFuture == null || channelFuture.channel() == null || !channelFuture.channel().isActive();
|
return channelFuture == null || channelFuture.channel() == null || !channelFuture.channel().isActive();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class SubService {
|
|||||||
final SubMapper subMapper;
|
final SubMapper subMapper;
|
||||||
final UserMapper userMapper;
|
final UserMapper userMapper;
|
||||||
final SubscriptionRefreshService refreshService;
|
final SubscriptionRefreshService refreshService;
|
||||||
|
final RemoteService remoteService;
|
||||||
|
|
||||||
public String insertSubscriptionAccount(String name, String upstreamKey, boolean filterHighMultiplier, boolean enabled) {
|
public String insertSubscriptionAccount(String name, String upstreamKey, boolean filterHighMultiplier, boolean enabled) {
|
||||||
Response response = Response.generateResponse();
|
Response response = Response.generateResponse();
|
||||||
@@ -42,6 +43,7 @@ public class SubService {
|
|||||||
subMapper.insertSubscriptionAccount(account);
|
subMapper.insertSubscriptionAccount(account);
|
||||||
if (enabled)
|
if (enabled)
|
||||||
refreshService.refresh(account.getId());
|
refreshService.refresh(account.getId());
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success(accountJson(account)).toJSONString();
|
return response.success(accountJson(account)).toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ public class SubService {
|
|||||||
subMapper.updateSubscriptionAccount(account);
|
subMapper.updateSubscriptionAccount(account);
|
||||||
if (enabled && !refreshService.refresh(id))
|
if (enabled && !refreshService.refresh(id))
|
||||||
refreshService.invalidateCache(id);
|
refreshService.invalidateCache(id);
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success(accountJson(account)).toJSONString();
|
return response.success(accountJson(account)).toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +86,14 @@ public class SubService {
|
|||||||
if (account.getBoundUserCount() != null && account.getBoundUserCount() > 0)
|
if (account.getBoundUserCount() != null && account.getBoundUserCount() > 0)
|
||||||
return response.failure("子账号仍绑定用户,请先改绑").toJSONString();
|
return response.failure("子账号仍绑定用户,请先改绑").toJSONString();
|
||||||
subMapper.deleteSubscriptionAccount(id);
|
subMapper.deleteSubscriptionAccount(id);
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success("删除成功").toJSONString();
|
return response.success("删除成功").toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String refreshSubscriptionAccount(Integer id) {
|
public String refreshSubscriptionAccount(Integer id) {
|
||||||
return refreshService.refresh(id) ? Response._success("刷新成功") : Response._failure("刷新失败,请查看子账号错误状态");
|
boolean success = refreshService.refresh(id);
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
|
return success ? Response._success("刷新成功") : Response._failure("刷新失败,请查看子账号错误状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String insertSubBind(String user, Integer accountId) {
|
public String insertSubBind(String user, Integer accountId) {
|
||||||
@@ -105,6 +111,7 @@ public class SubService {
|
|||||||
while (subMapper.selectSubBindExist(key))
|
while (subMapper.selectSubBindExist(key))
|
||||||
key = RandomUtil.randomString(8);
|
key = RandomUtil.randomString(8);
|
||||||
subMapper.insertSubBind(new SubBind(key, user, account.getId(), account.getName(), account.isEnabled(), account.isFilterHighMultiplier()));
|
subMapper.insertSubBind(new SubBind(key, user, account.getId(), account.getName(), account.isEnabled(), account.isFilterHighMultiplier()));
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success("添加成功").toJSONString();
|
return response.success("添加成功").toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +124,7 @@ public class SubService {
|
|||||||
key = RandomUtil.randomString(8);
|
key = RandomUtil.randomString(8);
|
||||||
subMapper.updateSubBindKey(user, key);
|
subMapper.updateSubBindKey(user, key);
|
||||||
subMapper.deleteSubUpdateRecord(user);
|
subMapper.deleteSubUpdateRecord(user);
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success().toJSONString();
|
return response.success().toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +137,7 @@ public class SubService {
|
|||||||
return response.failure("子账号尚无有效缓存,请先刷新").toJSONString();
|
return response.failure("子账号尚无有效缓存,请先刷新").toJSONString();
|
||||||
if (subMapper.updateSubBindAccount(user, accountId) == 0)
|
if (subMapper.updateSubBindAccount(user, accountId) == 0)
|
||||||
return response.failure("绑定不存在").toJSONString();
|
return response.failure("绑定不存在").toJSONString();
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return response.success("改绑成功").toJSONString();
|
return response.success("改绑成功").toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +195,7 @@ public class SubService {
|
|||||||
public String deleteSubBind(String user) {
|
public String deleteSubBind(String user) {
|
||||||
subMapper.deleteSubBind(user);
|
subMapper.deleteSubBind(user);
|
||||||
subMapper.deleteSubUpdateRecord(user);
|
subMapper.deleteSubUpdateRecord(user);
|
||||||
|
remoteService.requestSubscriptionSync();
|
||||||
return Response._success("删除成功");
|
return Response._success("删除成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.lion.lionwebsite.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.lion.lionwebsite.Dao.normal.SubMapper;
|
||||||
|
import com.lion.lionwebsite.Domain.SubBind;
|
||||||
|
import com.lion.lionwebsite.Domain.SubscriptionAccount;
|
||||||
|
import com.lion.lionwebsite.Message.*;
|
||||||
|
import com.lion.lionwebsite.Util.CustomUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SubscriptionStandbySnapshotService {
|
||||||
|
private final SubMapper subMapper;
|
||||||
|
private final SubscriptionRefreshService refreshService;
|
||||||
|
private final ObjectMapper objectMapper = CustomUtil.objectMapper;
|
||||||
|
|
||||||
|
@Value("${subscription.standby.sync-secret:}")
|
||||||
|
String syncSecret;
|
||||||
|
|
||||||
|
public SubscriptionSnapshotMessage build() throws IOException {
|
||||||
|
Map<Integer, SubscriptionAccountSnapshot> accountMap = new HashMap<>();
|
||||||
|
for (SubscriptionAccount account : subMapper.selectAllSubscriptionAccounts()) {
|
||||||
|
if (!account.isEnabled() || !refreshService.hasCompleteCache(account.getId()))
|
||||||
|
continue;
|
||||||
|
Path v2Path = refreshService.cachedPath(account.getId(), "v2");
|
||||||
|
Path clashPath = refreshService.cachedPath(account.getId(), "cat");
|
||||||
|
byte[] v2 = Files.readAllBytes(v2Path);
|
||||||
|
byte[] clash = Files.readAllBytes(clashPath);
|
||||||
|
SubscriptionAccountSnapshot snapshot = new SubscriptionAccountSnapshot();
|
||||||
|
snapshot.setAccountId(account.getId());
|
||||||
|
snapshot.setEnabled(true);
|
||||||
|
snapshot.setFilterHighMultiplier(account.isFilterHighMultiplier());
|
||||||
|
snapshot.setV2ContentBase64(Base64.getEncoder().encodeToString(v2));
|
||||||
|
snapshot.setV2Sha256(sha256(v2));
|
||||||
|
snapshot.setClashContentBase64(Base64.getEncoder().encodeToString(clash));
|
||||||
|
snapshot.setClashSha256(sha256(clash));
|
||||||
|
accountMap.put(account.getId(), snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SubscriptionAccountSnapshot> accounts = new ArrayList<>(accountMap.values());
|
||||||
|
accounts.sort(Comparator.comparing(SubscriptionAccountSnapshot::getAccountId));
|
||||||
|
List<SubscriptionBindingSnapshot> bindings = new ArrayList<>();
|
||||||
|
for (SubBind bind : subMapper.selectAllSubBind()) {
|
||||||
|
if (bind.getSubscriptionAccountId() == null || !accountMap.containsKey(bind.getSubscriptionAccountId()))
|
||||||
|
continue;
|
||||||
|
SubscriptionBindingSnapshot snapshot = new SubscriptionBindingSnapshot();
|
||||||
|
snapshot.setPublicKeySha256(sha256(bind.getKey().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
snapshot.setAccountId(bind.getSubscriptionAccountId());
|
||||||
|
bindings.add(snapshot);
|
||||||
|
}
|
||||||
|
bindings.sort(Comparator.comparing(SubscriptionBindingSnapshot::getPublicKeySha256));
|
||||||
|
|
||||||
|
SubscriptionSnapshotPayload payload = new SubscriptionSnapshotPayload();
|
||||||
|
payload.setSchemaVersion(1);
|
||||||
|
payload.setAccounts(accounts);
|
||||||
|
payload.setBindings(bindings);
|
||||||
|
byte[] payloadJson = objectMapper.writeValueAsBytes(payload);
|
||||||
|
byte[] compressed = gzip(payloadJson);
|
||||||
|
String revision = sha256(payloadJson);
|
||||||
|
String payloadSha256 = sha256(compressed);
|
||||||
|
long generatedAt = System.currentTimeMillis();
|
||||||
|
String signatureInput = "1\n" + revision + "\n" + generatedAt + "\n" + payloadSha256;
|
||||||
|
|
||||||
|
SubscriptionSnapshotMessage message = new SubscriptionSnapshotMessage();
|
||||||
|
message.setSchemaVersion(1);
|
||||||
|
message.setRevision(revision);
|
||||||
|
message.setGeneratedAt(generatedAt);
|
||||||
|
message.setPayloadBase64(Base64.getEncoder().encodeToString(compressed));
|
||||||
|
message.setPayloadSha256(payloadSha256);
|
||||||
|
message.setSignature(hmac(signatureInput.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hmac(byte[] input) {
|
||||||
|
if (syncSecret == null || syncSecret.isBlank())
|
||||||
|
throw new IllegalStateException("订阅备机同步密钥未配置");
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(syncSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||||
|
return hex(mac.doFinal(input));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("生成订阅快照签名失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] gzip(byte[] input) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
|
||||||
|
gzip.write(input);
|
||||||
|
}
|
||||||
|
return output.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sha256(byte[] input) {
|
||||||
|
try { return hex(MessageDigest.getInstance("SHA-256").digest(input)); }
|
||||||
|
catch (Exception e) { throw new IllegalStateException(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hex(byte[] input) { return HexFormat.of().formatHex(input); }
|
||||||
|
}
|
||||||
@@ -43,6 +43,9 @@ subscription:
|
|||||||
clash-url-template: "https://aaaa.gay/link/{key}?client=clashmeta"
|
clash-url-template: "https://aaaa.gay/link/{key}?client=clashmeta"
|
||||||
high-multiplier-threshold: 2.0
|
high-multiplier-threshold: 2.0
|
||||||
refresh-interval-ms: 86400000
|
refresh-interval-ms: 86400000
|
||||||
|
standby:
|
||||||
|
sync-enabled: "${SUBSCRIPTION_STANDBY_SYNC_ENABLED:false}"
|
||||||
|
sync-secret: "${SUBSCRIPTION_SYNC_SECRET:}"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
token: "5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA"
|
token: "5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA"
|
||||||
|
|||||||
Reference in New Issue
Block a user