fix(certificates): учитывать подтверждённый часовой пояс устройства
This commit is contained in:
1 parent
e78bc2d7cb
commit
0e86628d58
12 files changed
+176
-22
No files matched your search
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"
|
||||
import { appendEvent } from "../modules/events/service/events-service.js"
|
||||
import type { CertificatesRenewRunSnapshot } from "../types/scheduler-run-snapshot.js"
|
||||
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
|
||||
import { readCertificateServerState } from "./certificates-state.js"
|
||||
import { readCertificateServerState, readCertificateRowsWithClock } from "./certificates-state.js"
|
||||
import { runCertificateIssueJob } from "./certificate-issue-runner.js"
|
||||
import {
|
||||
createIssueJobRecord,
|
||||
@@ -21,7 +21,10 @@ const defaults = {
|
||||
getTargets: listManagedCertificateTargets,
|
||||
hasActiveJob: hasActiveIssueJob,
|
||||
getServer: getServerRowByIdString,
|
||||
read: (server: NonNullable<Awaited<ReturnType<typeof getServerRowByIdString>>>) => MikrotikClient.fromServer(server).getCertificates(),
|
||||
read: (server: NonNullable<Awaited<ReturnType<typeof getServerRowByIdString>>>) => {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
return readCertificateRowsWithClock(() => client.get<unknown>("/certificate"), () => client.get<unknown>("/system/clock"))
|
||||
},
|
||||
createJob: createIssueJobRecord,
|
||||
runJob: runCertificateIssueJob,
|
||||
getJob: getIssueJobRecord,
|
||||
|
||||
@@ -269,7 +269,7 @@ export async function hasActiveIssueJob(serverId: string, certName: string): Pro
|
||||
}
|
||||
|
||||
export async function listCertificatesState() {
|
||||
const { collectCertificatesState } = await import("./certificates-state.js")
|
||||
const { collectCertificatesState,readCertificateRowsWithClock } = await import("./certificates-state.js")
|
||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
return collectCertificatesState(enabledServers, server => { const client=MikrotikClient.fromServer(server); return () => client.get<unknown>("/certificate") })
|
||||
return collectCertificatesState(enabledServers, server => { const client=MikrotikClient.fromServer(server); return () => readCertificateRowsWithClock(()=>client.get<unknown>("/certificate"),()=>client.get<unknown>("/system/clock")) })
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {mapObservedCertificate,readCertificateServerState,collectCertificatesState} from "./certificates-state.js"
|
||||
import {mapObservedCertificate,readCertificateServerState,collectCertificatesState,readCertificateRowsWithClock} from "./certificates-state.js"
|
||||
import {certificatesStateSchema,certificateObservedSchema} from "@mmapp/contracts/certificates"
|
||||
const server={id:1,name:"Router"},now=new Date("2026-10-01T00:00:00Z")
|
||||
const raw={".id":"*A",name:"cert-live",flags:"KT","invalid-before":"2026-09-01T00:00:00Z","invalid-after":"2026-11-01T00:00:00Z","key-size":"2048",trusted:"true"}
|
||||
@@ -24,3 +24,19 @@ assert.equal(certificateObservedSchema.safeParse({...good,validUntil:null}).succ
|
||||
console.log("Certificate observed: unknown/date timezone/invalid calendar, flags, key size, known empty, malformed/duplicate/context, partial devices; no DB/RouterOS")
|
||||
|
||||
const setupFailure=await collectCertificatesState([server,{id:2,name:"bad client"}],srv=>{if(srv.id===2)throw new Error("client setup refused");return async()=>[raw]});assert.equal(setupFailure.servers[0].certificates.length,1);assert.equal(setupFailure.servers[1].error,"client setup refused")
|
||||
|
||||
const native={...raw,"invalid-before":"2026-10-14 15:37:47","invalid-after":"2027-01-12 15:37:46"}
|
||||
const calibrated=mapObservedCertificate(server,native,new Date("2026-10-14T14:37:23Z"),"+02:00")
|
||||
assert.equal(calibrated.validFrom,"2026-10-14T13:37:47.000Z");assert.equal(calibrated.validUntil,"2027-01-12T13:37:46.000Z");assert.equal(calibrated.status,"valid");assert.equal(calibrated.rawValidUntil,native["invalid-after"])
|
||||
for(const offset of [undefined,null,"bad","+14:01","+24:00","+02:60"])assert.equal(mapObservedCertificate(server,native,now,offset).validUntil,null)
|
||||
assert.equal(mapObservedCertificate(server,{...native,"invalid-after":"2026-02-30 12:00:00"},now,"+02:00").validUntil,null)
|
||||
assert.equal(mapObservedCertificate(server,raw,now,"+02:00").validUntil,"2026-11-01T00:00:00.000Z")
|
||||
for(const offsets of [["+02:00","+02:00"],["+02:00","+01:00"],[null,"+02:00"],["+02:00",null]]) {
|
||||
let index=0
|
||||
const rows=await readCertificateRowsWithClock(async()=>[native],async()=>({"gmt-offset":offsets[index++]}))
|
||||
const observed=await readCertificateServerState(server,async()=>rows)
|
||||
assert.equal(observed.error,null);assert.equal(observed.certificates[0].validUntil,offsets[0]!==null&&offsets[0]===offsets[1]?"2027-01-12T13:37:46.000Z":null)
|
||||
}
|
||||
const clockFailure=await readCertificateRowsWithClock(async()=>[native],async()=>{throw Error("clock denied")})
|
||||
assert.equal((await readCertificateServerState(server,async()=>clockFailure)).certificates[0].daysLeft,null)
|
||||
console.log("Native dates: observed stable clock offset, future winter expiry, missing/changed/failed clock, invalid calendar, explicit ISO preservation passed")
|
||||
@@ -4,14 +4,26 @@ import {mapRosCertificateRow,type RosCertificateRow} from "./certificate-parse.j
|
||||
type ServerContext={id:number;name:string}
|
||||
const rawSchema=z.array(z.record(z.string(),z.string().optional()).refine(row=>!!row.name?.trim(),"Отсутствует имя сертификата"))
|
||||
const dateSchema=z.string().datetime({offset:true})
|
||||
function observedDate(raw:string|undefined) {
|
||||
const value=raw?.trim()
|
||||
return value&&dateSchema.safeParse(value).success?new Date(value):null
|
||||
const nativeDate=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
|
||||
function validOffset(value:unknown):value is string {
|
||||
return typeof value==="string"&&/^[+-](?:0\d|1[0-4]):[0-5]\d$/.test(value)&&(!value.slice(1).startsWith("14:")||value.endsWith(":00"))
|
||||
}
|
||||
export function mapObservedCertificate(server:ServerContext,row:RosCertificateRow,now=new Date()) {
|
||||
export async function readCertificateRowsWithClock(read:()=>Promise<unknown>,clock:()=>Promise<unknown>):Promise<unknown> {
|
||||
const offset=async()=>{try {const row=await clock();const value=row&&typeof row==="object"?(row as Record<string,unknown>)["gmt-offset"]:null;return validOffset(value)?value:null}catch{return null}}
|
||||
const before=await offset(),rows=await read()
|
||||
if(!Array.isArray(rows)||!rows.some(row=>row&&[row["invalid-before"],row["invalid-after"]].some(value=>typeof value==="string"&&nativeDate.test(value.trim()))))return rows
|
||||
const after=await offset()
|
||||
return {rows,gmtOffset:before!==null&&before===after?before:null}
|
||||
}
|
||||
function observedDate(raw:string|undefined,gmtOffset?:string|null) {
|
||||
const value=raw?.trim()
|
||||
const qualified=value&&nativeDate.test(value)&&validOffset(gmtOffset)?value.replace(" ","T")+gmtOffset:value
|
||||
return qualified&&dateSchema.safeParse(qualified).success?new Date(qualified):null
|
||||
}
|
||||
export function mapObservedCertificate(server:ServerContext,row:RosCertificateRow,now=new Date(),gmtOffset?:string|null) {
|
||||
const base=mapRosCertificateRow(server.id,server.name,row)
|
||||
if(!base)throw new Error("Отсутствует сертификат")
|
||||
const from=observedDate(row["invalid-before"]),until=observedDate(row["invalid-after"])
|
||||
const from=observedDate(row["invalid-before"],gmtOffset),until=observedDate(row["invalid-after"],gmtOffset)
|
||||
const flags=row.flags?.toUpperCase()
|
||||
const revoked=flags?.includes("R")||row.revoked==="true"||row.revoked==="yes"
|
||||
const expired=flags?.includes("E")||row.expired==="true"||row.expired==="yes"||!!until&&until.getTime()<now.getTime()
|
||||
@@ -28,8 +40,9 @@ export function mapObservedCertificate(server:ServerContext,row:RosCertificateRo
|
||||
}
|
||||
export async function readCertificateServerState(server:ServerContext,read:()=>Promise<unknown>):Promise<CertificateServerState> {
|
||||
try{
|
||||
const rows=rawSchema.parse(await read()),now=new Date()
|
||||
const result={serverId:String(server.id),serverName:server.name,collectedAt:now.toISOString(),certificates:rows.map(row=>mapObservedCertificate(server,row,now)),error:null}
|
||||
const raw=await read(),envelope=Array.isArray(raw)?null:z.object({rows:rawSchema,gmtOffset:z.string().nullable()}).strict().parse(raw)
|
||||
const rows=rawSchema.parse(envelope?envelope.rows:raw),now=new Date()
|
||||
const result={serverId:String(server.id),serverName:server.name,collectedAt:now.toISOString(),certificates:rows.map(row=>mapObservedCertificate(server,row,now,envelope?.gmtOffset)),error:null}
|
||||
return certificateServerStateSchema.parse(result)
|
||||
}catch(error){
|
||||
return certificateServerStateSchema.parse({serverId:String(server.id),serverName:server.name,collectedAt:new Date().toISOString(),certificates:[],error:error instanceof z.ZodError?"Некорректный ответ каталога сертификатов":error instanceof Error?error.message.slice(0,500)||"Ошибка RouterOS":"Ошибка RouterOS"})
|
||||
|
||||
Reference in new issue
Block a user