mirror of https://github.com/cppla/ServerStatus
				
				
				
			add ssl check
							parent
							
								
									830938eac9
								
							
						
					
					
						commit
						360d8d008d
					
				|  | @ -1,17 +1,38 @@ | ||||||
| OUT = sergate | OUT = sergate | ||||||
| 
 | 
 | ||||||
| #CC = clang
 | # Build mode: make (默认 release) 或 make BUILD=debug
 | ||||||
| CC = gcc | BUILD ?= release | ||||||
| CFLAGS = -Wall -O2 |  | ||||||
| 
 | 
 | ||||||
| #CXX = clang++
 | # 自动检测 ccache
 | ||||||
| CXX = g++ | CCACHE_BIN:=$(shell which ccache 2>/dev/null) | ||||||
| CXXFLAGS = -Wall -O2 -std=c++11 | ifeq ($(CCACHE_BIN),) | ||||||
|  | 	CC := gcc | ||||||
|  | 	CXX := g++ | ||||||
|  | else | ||||||
|  | 	CC := ccache gcc | ||||||
|  | 	CXX := ccache g++ | ||||||
|  | endif | ||||||
|  | 
 | ||||||
|  | COMMON_WARN=-Wall | ||||||
|  | COMMON_INC=-Iinclude | ||||||
|  | COMMON_PIPE=-pipe | ||||||
|  | 
 | ||||||
|  | ifeq ($(BUILD),debug) | ||||||
|  | 	CFLAGS   = $(COMMON_WARN) -O0 -g $(COMMON_PIPE) | ||||||
|  | 	CXXFLAGS = $(COMMON_WARN) -O0 -g -std=c++11 $(COMMON_PIPE) | ||||||
|  | else | ||||||
|  | 	CFLAGS   = $(COMMON_WARN) -O2 $(COMMON_PIPE) | ||||||
|  | 	CXXFLAGS = $(COMMON_WARN) -O2 -std=c++11 $(COMMON_PIPE) | ||||||
|  | endif | ||||||
| 
 | 
 | ||||||
| ODIR = obj | ODIR = obj | ||||||
| SDIR = src | SDIR = src | ||||||
| LIBS = -pthread -lm | LIBS = -pthread -lm -lcurl | ||||||
| INC = -Iinclude | INC = $(COMMON_INC) -Isrc | ||||||
|  | 
 | ||||||
|  | # 预编译头(主要加速包含 exprtk.hpp 的 C++ 编译)
 | ||||||
|  | PCH_HDR = $(SDIR)/pch.hpp | ||||||
|  | PCH = $(ODIR)/pch.hpp.gch | ||||||
| 
 | 
 | ||||||
| C_SRCS := $(wildcard $(SDIR)/*.c) | C_SRCS := $(wildcard $(SDIR)/*.c) | ||||||
| CXX_SRCS := $(wildcard $(SDIR)/*.cpp) | CXX_SRCS := $(wildcard $(SDIR)/*.cpp) | ||||||
|  | @ -19,16 +40,33 @@ C_OBJS := $(patsubst $(SDIR)/%.c,$(ODIR)/%.o,$(C_SRCS)) | ||||||
| CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS)) | CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS)) | ||||||
| OBJS := $(C_OBJS) $(CXX_OBJS) | OBJS := $(C_OBJS) $(CXX_OBJS) | ||||||
| 
 | 
 | ||||||
| $(ODIR)/%.o: $(SDIR)/%.c |  | ||||||
| 	$(CC) -c $(INC) $(CFLAGS) $< -o $@ |  | ||||||
| 
 | 
 | ||||||
| $(ODIR)/%.o: $(SDIR)/%.cpp | all: $(OUT) | ||||||
| 	$(CXX) -c $(INC) $(CXXFLAGS) $< -o $@ | 
 | ||||||
|  | $(ODIR): | ||||||
|  | 	mkdir -p $(ODIR) | ||||||
|  | 
 | ||||||
|  | $(PCH): $(PCH_HDR) | $(ODIR) | ||||||
|  | 	$(CXX) $(INC) $(CXXFLAGS) -MMD -MP -x c++-header $< -o $@ | ||||||
|  | 
 | ||||||
|  | $(ODIR)/%.o: $(SDIR)/%.cpp $(PCH) | $(ODIR) | ||||||
|  | 	$(CXX) -c $(INC) $(CXXFLAGS) -MMD -MP -include src/pch.hpp $< -o $@ | ||||||
|  | 
 | ||||||
|  | $(ODIR)/%.o: $(SDIR)/%.c | $(ODIR) | ||||||
|  | 	$(CC) -c $(INC) $(CFLAGS) -MMD -MP $< -o $@ | ||||||
| 
 | 
 | ||||||
| $(OUT): $(OBJS) | $(OUT): $(OBJS) | ||||||
| 	$(CXX) $(LIBS) $^ -o $(OUT) -lcurl | 	$(CXX) $(CXXFLAGS) $^ -o $(OUT) $(LIBS) | ||||||
| 
 | 
 | ||||||
| .PHONY: clean | .PHONY: clean all | ||||||
| 
 | 
 | ||||||
| clean: | clean: | ||||||
| 	rm -f $(ODIR)/*.o $(OUT) | 	rm -f $(ODIR)/*.o $(ODIR)/*.d $(OUT) $(PCH) | ||||||
|  | 
 | ||||||
|  | .PHONY: debug release | ||||||
|  | debug: | ||||||
|  | 	$(MAKE) BUILD=debug | ||||||
|  | release: | ||||||
|  | 	$(MAKE) BUILD=release | ||||||
|  | 
 | ||||||
|  | -include $(ODIR)/*.d | ||||||
|  |  | ||||||
|  | @ -52,6 +52,43 @@ | ||||||
| 			"type": "tcp" | 			"type": "tcp" | ||||||
| 		} | 		} | ||||||
| 	], | 	], | ||||||
|  | 	"sslcerts": [ | ||||||
|  | 		{ | ||||||
|  | 			"name": "cpp.la", | ||||||
|  | 			"domain": "https://cpp.la", | ||||||
|  | 			"port": 443, | ||||||
|  | 			"interval": 600, | ||||||
|  | 			"callback": "https://yourSMSurl"  | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "my.cloudcpp.com", | ||||||
|  | 			"domain": "https://my.cloudcpp.com", | ||||||
|  | 			"port": 443, | ||||||
|  | 			"interval": 600, | ||||||
|  | 			"callback": "https://yourSMSurl"  | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "tz.cloudcpp.com", | ||||||
|  | 			"domain": "https://tz.cloudcpp.com", | ||||||
|  | 			"port": 443, | ||||||
|  | 			"interval": 600, | ||||||
|  | 			"callback": "https://yourSMSurl"  | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "3.0.2.1", | ||||||
|  | 			"domain": "https://3.0.2.1", | ||||||
|  | 			"port": 443, | ||||||
|  | 			"interval": 600, | ||||||
|  | 			"callback": "https://sctapi.ftqq.com/SCT51005TMjvDPWwgSXjgYODht6BnQmed.send?title=ServerStatus&desp=" | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "3.0.2.9", | ||||||
|  | 			"domain": "https://3.0.2.9", | ||||||
|  | 			"port": 443, | ||||||
|  | 			"interval": 600, | ||||||
|  | 			"callback": "https://sctapi.ftqq.com/SCT51005TMjvDPWwgSXjgYODht6BnQmed.send?title=ServerStatus&desp=" | ||||||
|  | 		} | ||||||
|  | 	], | ||||||
| 	"watchdog": [ | 	"watchdog": [ | ||||||
| 		{ | 		{ | ||||||
| 			"name": "cpu high warning,exclude username s01", | 			"name": "cpu high warning,exclude username s01", | ||||||
|  |  | ||||||
|  | @ -131,13 +131,15 @@ static int new_value | ||||||
| 
 | 
 | ||||||
|             values_size = sizeof (*value->u.object.values) * value->u.object.length; |             values_size = sizeof (*value->u.object.values) * value->u.object.length; | ||||||
| 
 | 
 | ||||||
|             if (! ((*(void **) &value->u.object.values) = json_alloc |             void *tmp_alloc = json_alloc(state, values_size + ((unsigned long) value->u.object.values), 0); | ||||||
|                   (state, values_size + ((unsigned long) value->u.object.values), 0)) ) |             if (!tmp_alloc) | ||||||
|             { |             { | ||||||
|                return 0; |                return 0; | ||||||
|             } |             } | ||||||
| 
 |             /* 避免违反严格别名:通过中间变量复制 */ | ||||||
|             value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size; |             memcpy(&value->u.object.values, &tmp_alloc, sizeof(void*)); | ||||||
|  |             char *obj_mem = (char*)value->u.object.values + values_size; | ||||||
|  |             memcpy(&value->_reserved.object_mem, &obj_mem, sizeof(char*)); | ||||||
| 
 | 
 | ||||||
|             value->u.object.length = 0; |             value->u.object.length = 0; | ||||||
|             break; |             break; | ||||||
|  | @ -361,16 +363,18 @@ json_value * json_parse_ex (json_settings * settings, | ||||||
|                   case json_object: |                   case json_object: | ||||||
| 
 | 
 | ||||||
|                      if (state.first_pass) |                      if (state.first_pass) | ||||||
|                         (*(json_char **) &top->u.object.values) += string_length + 1; |                      { | ||||||
|  |                         json_char *adv = (json_char*)top->u.object.values; | ||||||
|  |                         adv += string_length + 1; | ||||||
|  |                         memcpy(&top->u.object.values, &adv, sizeof(json_char*)); | ||||||
|  |                      } | ||||||
|                      else |                      else | ||||||
|                      {   |                      { | ||||||
|                         top->u.object.values [top->u.object.length].name |                         top->u.object.values[top->u.object.length].name = (json_char *)top->_reserved.object_mem; | ||||||
|                            = (json_char *) top->_reserved.object_mem; |                         top->u.object.values[top->u.object.length].name_length = string_length; | ||||||
| 
 |                         json_char *adv2 = (json_char*)top->_reserved.object_mem; | ||||||
|                         top->u.object.values [top->u.object.length].name_length |                         adv2 += string_length + 1; | ||||||
|                            = string_length; |                         memcpy(&top->_reserved.object_mem, &adv2, sizeof(json_char*)); | ||||||
| 
 |  | ||||||
|                         (*(json_char **) &top->_reserved.object_mem) += string_length + 1; |  | ||||||
|                      } |                      } | ||||||
| 
 | 
 | ||||||
|                      flags |= flag_seek_value | flag_need_colon; |                      flags |= flag_seek_value | flag_need_colon; | ||||||
|  |  | ||||||
|  | @ -9,6 +9,184 @@ | ||||||
| #include "main.h" | #include "main.h" | ||||||
| #include "exprtk.hpp" | #include "exprtk.hpp" | ||||||
| #include "curl/curl.h" | #include "curl/curl.h" | ||||||
|  | #include <stdio.h> | ||||||
|  | #include <stdlib.h> | ||||||
|  | #include <string> | ||||||
|  | 
 | ||||||
|  | // 全局运行标志(需在 SSLCheckThread 定义前初始化)
 | ||||||
|  | static volatile int gs_Running = 1; | ||||||
|  | static volatile int gs_ReloadConfig = 0; | ||||||
|  | 
 | ||||||
|  | static int64_t ParseOpenSSLEnddate(const char *line) | ||||||
|  | { | ||||||
|  | 	// line format: notAfter=Aug 12 23:59:59 2025 GMT
 | ||||||
|  | 	const char *p = strstr(line, "notAfter="); | ||||||
|  | 	if(!p) return 0; | ||||||
|  | 	p += 9; | ||||||
|  | 	struct tm tmv; memset(&tmv,0,sizeof(tmv)); | ||||||
|  | 	char month[4]={0}; | ||||||
|  | 	int day, hour, min, sec, year; | ||||||
|  | 	if(sscanf(p, "%3s %d %d:%d:%d %d GMT", month, &day, &hour, &min, &sec, &year)!=6) return 0; | ||||||
|  | 	const char *months="JanFebMarAprMayJunJulAugSepOctNovDec"; | ||||||
|  | 	const char *mpos = strstr(months, month); | ||||||
|  | 	if(!mpos) return 0; | ||||||
|  | 	int mon = (int)((mpos - months)/3); | ||||||
|  | 	tmv.tm_year = year - 1900; | ||||||
|  | 	tmv.tm_mon = mon; | ||||||
|  | 	tmv.tm_mday = day; | ||||||
|  | 	tmv.tm_hour = hour; tmv.tm_min = min; tmv.tm_sec = sec; | ||||||
|  | 	time_t t = timegm(&tmv); | ||||||
|  | 	return (int64_t)t; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct SSLCheckThreadData { CMain *pMain; }; | ||||||
|  | static void SSLCheckThread(void *pUser) | ||||||
|  | { | ||||||
|  | 	SSLCheckThreadData *pData = (SSLCheckThreadData*)pUser; | ||||||
|  | 	while(gs_Running){ | ||||||
|  | 		for(int i=0;i<NET_MAX_CLIENTS;i++){ | ||||||
|  | 			if(!pData->pMain->SSLCert(i) || !strcmp(pData->pMain->SSLCert(i)->m_aName, "NULL")) break; | ||||||
|  | 			CMain::CSSLCerts *cert = pData->pMain->SSLCert(i); | ||||||
|  | 			time_t nowt = time(0); | ||||||
|  | 			if(cert->m_aLastCheck !=0 && (nowt - cert->m_aLastCheck) < cert->m_aInterval) continue; | ||||||
|  | 			cert->m_aLastCheck = nowt; | ||||||
|  | 			char cmd[1024]; | ||||||
|  | 			// 说明: 通过 s_client 获取证书,再用 x509 解析到期时间;统一屏蔽 stderr 以防握手失败/非 TLS 端口时刷屏。
 | ||||||
|  | 			// 若配置中写成 https://domain/path 则需要清洗。
 | ||||||
|  | 			char cleanHost[256]; | ||||||
|  | 			str_copy(cleanHost, cert->m_aDomain, sizeof(cleanHost)); | ||||||
|  | 			// 去协议
 | ||||||
|  | 			if(!strncasecmp(cleanHost, "https://", 8)) memmove(cleanHost, cleanHost+8, strlen(cleanHost+8)+1); | ||||||
|  | 			else if(!strncasecmp(cleanHost, "http://", 7)) memmove(cleanHost, cleanHost+7, strlen(cleanHost+7)+1); | ||||||
|  | 			// 去路径
 | ||||||
|  | 			char *slash = strchr(cleanHost, '/'); if(slash) *slash='\0'; | ||||||
|  | 			// 若含 :port 再截取主机部分(端口由配置提供)
 | ||||||
|  | 			char *colon = strchr(cleanHost, ':'); if(colon) *colon='\0'; | ||||||
|  | 			int n = snprintf(cmd,sizeof(cmd),"echo | openssl s_client -servername %s -connect %s:%d </dev/null 2>/dev/null | openssl x509 -noout -enddate -text 2>/dev/null", cleanHost, cleanHost, cert->m_aPort); | ||||||
|  | 			if(n <= 0 || n >= (int)sizeof(cmd)) continue; // 避免截断执行
 | ||||||
|  | 			FILE *fp = popen(cmd, "r"); | ||||||
|  | 			if(!fp) continue; | ||||||
|  | 			char line[1024]={0}; | ||||||
|  | 			int foundEnddate=0; | ||||||
|  | 			int mismatch = 1; // 默认视为不匹配,发现任一匹配域名再置0
 | ||||||
|  | 			int haveNames = 0; | ||||||
|  | 			// 将目标域名转为小写
 | ||||||
|  | 			char target[256]; str_copy(target, cleanHost, sizeof(target)); | ||||||
|  | 			for(char *p=target; *p; ++p) *p=tolower(*p); | ||||||
|  | 			while(fgets(line,sizeof(line),fp)){ | ||||||
|  | 				if(!foundEnddate){ | ||||||
|  | 					int64_t expire = ParseOpenSSLEnddate(line); | ||||||
|  | 					if(expire>0){ cert->m_aExpireTS = expire; foundEnddate=1; } | ||||||
|  | 				} | ||||||
|  | 				// 解析 subjectAltName
 | ||||||
|  | 				// 解析 Subject 中的 CN(备用)
 | ||||||
|  | 				char *subj = strstr(line, "Subject:"); | ||||||
|  | 				if(subj){ | ||||||
|  | 					char *cn = strstr(subj, " CN="); | ||||||
|  | 					if(cn){ | ||||||
|  | 						cn += 4; // 跳过 ' CN='
 | ||||||
|  | 						char name[256]={0}; int ni=0; | ||||||
|  | 						while(*cn && *cn!='/' && *cn!=',' && *cn!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*cn++; } | ||||||
|  | 						name[ni]='\0'; | ||||||
|  | 						while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; } | ||||||
|  | 						for(char *q=name; *q; ++q) *q=tolower(*q); | ||||||
|  | 						if(ni>0){ | ||||||
|  | 							haveNames=1; | ||||||
|  | 							int match=0; | ||||||
|  | 							if(name[0]=='*' && name[1]=='.'){ | ||||||
|  | 								const char *sub = strchr(target,'.'); | ||||||
|  | 								if(sub && !strcmp(sub+1, name+2)) match=1; | ||||||
|  | 							}else if(!strcmp(name,target)) match=1; | ||||||
|  | 							if(match){ mismatch=0; } | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if(strstr(line, "DNS:")){ | ||||||
|  | 					char *p = line; | ||||||
|  | 					while((p = strstr(p, "DNS:"))){ | ||||||
|  | 						p += 4; while(*p==' '){p++;} | ||||||
|  | 						char name[256]={0}; int ni=0; | ||||||
|  | 						while(*p && *p!=',' && *p!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*p++; } | ||||||
|  | 						name[ni]='\0'; | ||||||
|  | 						// 去空白
 | ||||||
|  | 						while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; } | ||||||
|  | 						for(char *q=name; *q; ++q) *q=tolower(*q); | ||||||
|  | 						haveNames=1; | ||||||
|  | 						// 通配符匹配 *.example.com
 | ||||||
|  | 						int match=0; | ||||||
|  | 						if(name[0]=='*' && name[1]=='.'){ | ||||||
|  | 							const char *sub = strchr(target,'.'); | ||||||
|  | 							if(sub && !strcmp(sub+1, name+2)) match=1; | ||||||
|  | 						}else if(!strcmp(name,target)) match=1; | ||||||
|  | 						if(match){ mismatch=0; goto names_done; } | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | names_done: | ||||||
|  | 			pclose(fp); | ||||||
|  | 			if(haveNames){ cert->m_aHostnameMismatch = mismatch ? 1 : 0; } | ||||||
|  | 			else { /* 未能提取任何域名,保留原状态,不触发误报 */ } | ||||||
|  | 			// 告警: 仅在不匹配且 24h 冷却
 | ||||||
|  | 			if(cert->m_aHostnameMismatch==1){ | ||||||
|  | 				if(cert->m_aLastAlarmMismatch==0 || nowt - cert->m_aLastAlarmMismatch > 24*3600){ | ||||||
|  | 					if(strlen(cert->m_aCallback)>0){ | ||||||
|  | 						CURL *curl = curl_easy_init(); | ||||||
|  | 						if(curl){ | ||||||
|  | 							char msg[1024]; | ||||||
|  | 							snprintf(msg,sizeof(msg),"【SSL证书域名不匹配】%s(%s) 证书域名与配置不一致", cert->m_aName, cert->m_aDomain); | ||||||
|  | 							char *enc = curl_easy_escape(curl,msg,0); | ||||||
|  | 							char url[1500]; snprintf(url,sizeof(url),"%s%s", cert->m_aCallback, enc?enc:""); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_POST, 1L); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_URL, url); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL"); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); | ||||||
|  | 							curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L); | ||||||
|  | 							curl_easy_perform(curl); | ||||||
|  | 							if(enc) curl_free(enc); | ||||||
|  | 							curl_easy_cleanup(curl); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					cert->m_aLastAlarmMismatch = nowt; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			// alarm logic
 | ||||||
|  | 			if(cert->m_aExpireTS>0){ | ||||||
|  | 				int days = (int)((cert->m_aExpireTS - nowt)/86400); | ||||||
|  | 				int64_t *lastAlarm = NULL; int need=0; int target=0; | ||||||
|  | 				if(days <=7 && days >3){ lastAlarm=&cert->m_aLastAlarm7; target=7; } | ||||||
|  | 				else if(days <=3 && days >1){ lastAlarm=&cert->m_aLastAlarm3; target=3; } | ||||||
|  | 				else if(days <=1){ lastAlarm=&cert->m_aLastAlarm1; target=1; } | ||||||
|  | 				if(lastAlarm && (*lastAlarm==0 || nowt - *lastAlarm > 20*3600)) need=1; // avoid spam, 20h
 | ||||||
|  | 				if(need && strlen(cert->m_aCallback)>0){ | ||||||
|  | 					CURL *curl = curl_easy_init(); | ||||||
|  | 					if(curl){ | ||||||
|  | 						char msg[1024]; | ||||||
|  | 						char timebuf[32]; | ||||||
|  | 						time_t expt = (time_t)cert->m_aExpireTS; | ||||||
|  | 						strftime(timebuf,sizeof(timebuf),"%Y-%m-%d %H:%M:%S", gmtime(&expt)); | ||||||
|  | 						snprintf(msg,sizeof(msg),"【SSL证书提醒】%s(%s) 将在 %d 天后(%s UTC) 到期", cert->m_aName, cert->m_aDomain, target, timebuf); | ||||||
|  | 						char *enc = curl_easy_escape(curl,msg,0); | ||||||
|  | 						char url[1500]; snprintf(url,sizeof(url),"%s%s", cert->m_aCallback, enc?enc:""); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_POST, 1L); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_URL, url); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL"); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); | ||||||
|  | 						curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L); | ||||||
|  | 						curl_easy_perform(curl); | ||||||
|  | 						if(enc) curl_free(enc); | ||||||
|  | 						curl_easy_cleanup(curl); | ||||||
|  | 					} | ||||||
|  | 					*lastAlarm = nowt; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		thread_sleep(5000); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| #if defined(CONF_FAMILY_UNIX) | #if defined(CONF_FAMILY_UNIX) | ||||||
| 	#include <signal.h> | 	#include <signal.h> | ||||||
|  | @ -18,9 +196,6 @@ | ||||||
| 	#define PRId64 "I64d" | 	#define PRId64 "I64d" | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| static volatile int gs_Running = 1; |  | ||||||
| static volatile int gs_ReloadConfig = 0; |  | ||||||
| 
 |  | ||||||
| static void ExitFunc(int Signal) | static void ExitFunc(int Signal) | ||||||
| { | { | ||||||
| 	printf("[EXIT] Caught signal %d\n", Signal); | 	printf("[EXIT] Caught signal %d\n", Signal); | ||||||
|  | @ -297,12 +472,16 @@ void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, doubl | ||||||
|         typedef exprtk::expression<double>   expression_t; |         typedef exprtk::expression<double>   expression_t; | ||||||
|         typedef exprtk::parser<double>       parser_t; |         typedef exprtk::parser<double>       parser_t; | ||||||
|         const std::string expression_string = Watchdog(ID)->m_aRule; |         const std::string expression_string = Watchdog(ID)->m_aRule; | ||||||
|         int ClientID = ClientNetToClient(ClientNetID); | 		int ClientID = ClientNetToClient(ClientNetID); | ||||||
|         std::string username = Client(ClientID)->m_aUsername; | 		if(ClientID < 0 || ClientID >= NET_MAX_CLIENTS) { | ||||||
|         std::string name = Client(ClientID)->m_aName; | 			ID++; | ||||||
|         std::string type = Client(ClientID)->m_aType; | 			continue; // 无效客户端,跳过当前 watchdog 规则
 | ||||||
|         std::string host = Client(ClientID)->m_aHost; | 		} | ||||||
|         std::string location = Client(ClientID)->m_aLocation; | 		std::string username = Client(ClientID)->m_aUsername; | ||||||
|  | 		std::string name = Client(ClientID)->m_aName; | ||||||
|  | 		std::string type = Client(ClientID)->m_aType; | ||||||
|  | 		std::string host = Client(ClientID)->m_aHost; | ||||||
|  | 		std::string location = Client(ClientID)->m_aLocation; | ||||||
| 
 | 
 | ||||||
|         symbol_table_t symbol_table; |         symbol_table_t symbol_table; | ||||||
|         symbol_table.add_stringvar("username", username); |         symbol_table.add_stringvar("username", username); | ||||||
|  | @ -473,13 +652,23 @@ void CMain::JSONUpdateThread(void *pUser) | ||||||
| 				pBuf += strlen(pBuf); | 				pBuf += strlen(pBuf); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if(!m_pJSONUpdateThreadData->m_ReloadRequired) | 		// append ssl certs data
 | ||||||
| 			str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"\n}", (long long)time(/*ago*/0)); | 		str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"sslcerts\": [\n"); | ||||||
| 		else | 		pBuf += strlen(pBuf); | ||||||
|  | 		for(int si = 0; si < NET_MAX_CLIENTS; si++) | ||||||
| 		{ | 		{ | ||||||
| 			str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\",\n\"reload\": true\n}", (long long)time(/*ago*/0)); | 			if(!m_pJSONUpdateThreadData->pMain->SSLCert(si) || !strcmp(m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, "NULL")) break; | ||||||
| 			m_pJSONUpdateThreadData->m_ReloadRequired--; | 			int64_t expire_ts = m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aExpireTS; | ||||||
|  | 			int expire_days = 0; | ||||||
|  | 			if(expire_ts>0){ | ||||||
|  | 				int64_t nowts = (long long)time(/*ago*/0); | ||||||
|  | 				expire_days = (int)((expire_ts - nowts)/86400); | ||||||
|  | 			} | ||||||
|  | 			str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"domain\": \"%s\", \"port\": %d, \"expire_ts\": %lld, \"expire_days\": %d, \"mismatch\": %s },\n", m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aDomain, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aPort, (long long)expire_ts, expire_days, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aHostnameMismatch?"true":"false"); | ||||||
|  | 			pBuf += strlen(pBuf); | ||||||
| 		} | 		} | ||||||
|  | 		if(pBuf - aFileBuf >= 2) str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"%s\n}", (long long)time(/*ago*/0), m_pJSONUpdateThreadData->m_ReloadRequired?",\n\"reload\": true":""); | ||||||
|  | 		if(m_pJSONUpdateThreadData->m_ReloadRequired) m_pJSONUpdateThreadData->m_ReloadRequired--; | ||||||
| 		pBuf += strlen(pBuf); | 		pBuf += strlen(pBuf); | ||||||
| 
 | 
 | ||||||
| 		char aJSONFileTmp[1024]; | 		char aJSONFileTmp[1024]; | ||||||
|  | @ -706,8 +895,11 @@ int CMain::ReadConfig() | ||||||
|             ID++; |             ID++; | ||||||
|         } |         } | ||||||
|         str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName)); |         str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName)); | ||||||
|     } else | 	}  | ||||||
|         str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName)); | 	else | ||||||
|  | 	{ | ||||||
|  | 		str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName)); | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
|     // monitor
 |     // monitor
 | ||||||
|     // support by: https://cpp.la
 |     // support by: https://cpp.la
 | ||||||
|  | @ -728,8 +920,36 @@ int CMain::ReadConfig() | ||||||
|             ID++; |             ID++; | ||||||
|         } |         } | ||||||
|         str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName)); |         str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName)); | ||||||
|     } else | 	}  | ||||||
|         str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName)); | 	else | ||||||
|  | 	{ | ||||||
|  | 		str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sslcerts
 | ||||||
|  | 	ID = 0; | ||||||
|  | 	const json_value &sStart = (*pJsonData)["sslcerts"]; | ||||||
|  | 	if(sStart.type == json_array) | ||||||
|  | 	{ | ||||||
|  | 		for(unsigned i = 0; i < sStart.u.array.length; i++) | ||||||
|  | 		{ | ||||||
|  | 			if(ID < 0 || ID >= NET_MAX_CLIENTS) | ||||||
|  | 				continue; | ||||||
|  | 			str_copy(SSLCert(ID)->m_aName, sStart[i]["name"].u.string.ptr, sizeof(SSLCert(ID)->m_aName)); | ||||||
|  | 			str_copy(SSLCert(ID)->m_aDomain, sStart[i]["domain"].u.string.ptr, sizeof(SSLCert(ID)->m_aDomain)); | ||||||
|  | 			SSLCert(ID)->m_aPort = sStart[i]["port"].u.integer; | ||||||
|  | 			SSLCert(ID)->m_aInterval = sStart[i]["interval"].u.integer; | ||||||
|  | 			str_copy(SSLCert(ID)->m_aCallback, sStart[i]["callback"].u.string.ptr, sizeof(SSLCert(ID)->m_aCallback)); | ||||||
|  | 			SSLCert(ID)->m_aExpireTS = 0; // reset
 | ||||||
|  | 			SSLCert(ID)->m_aLastCheck = 0; | ||||||
|  | 			SSLCert(ID)->m_aLastAlarm7 = 0; | ||||||
|  | 			SSLCert(ID)->m_aLastAlarm3 = 0; | ||||||
|  | 			SSLCert(ID)->m_aLastAlarm1 = 0; | ||||||
|  | 			ID++; | ||||||
|  | 		} | ||||||
|  | 		str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName)); | ||||||
|  | 	}else | ||||||
|  | 		str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName)); | ||||||
| 
 | 
 | ||||||
| 	// if file exists, read last network traffic record,reset m_LastNetworkIN and m_LastNetworkOUT
 | 	// if file exists, read last network traffic record,reset m_LastNetworkIN and m_LastNetworkOUT
 | ||||||
| 	// support by: https://cpp.la
 | 	// support by: https://cpp.la
 | ||||||
|  | @ -805,7 +1025,10 @@ int CMain::Run() | ||||||
| 	m_JSONUpdateThreadData.pClients = m_aClients; | 	m_JSONUpdateThreadData.pClients = m_aClients; | ||||||
| 	m_JSONUpdateThreadData.pConfig = &m_Config; | 	m_JSONUpdateThreadData.pConfig = &m_Config; | ||||||
|     m_JSONUpdateThreadData.pWatchDogs = m_aCWatchDogs; |     m_JSONUpdateThreadData.pWatchDogs = m_aCWatchDogs; | ||||||
|  | 	m_JSONUpdateThreadData.pMain = this; | ||||||
| 	void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData); | 	void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData); | ||||||
|  | 	// Start SSL check thread
 | ||||||
|  | 	static SSLCheckThreadData sslData; sslData.pMain = this; thread_create(SSLCheckThread, &sslData); | ||||||
| 	//thread_detach(LoadThread);
 | 	//thread_detach(LoadThread);
 | ||||||
| 
 | 
 | ||||||
| 	while(gs_Running) | 	while(gs_Running) | ||||||
|  |  | ||||||
|  | @ -96,12 +96,28 @@ class CMain | ||||||
|         int  m_aInterval; |         int  m_aInterval; | ||||||
|         char m_aType[128]; |         char m_aType[128]; | ||||||
|     } m_aCMonitors[NET_MAX_CLIENTS]; |     } m_aCMonitors[NET_MAX_CLIENTS]; | ||||||
|  | public: | ||||||
|  | 	struct CSSLCerts{ | ||||||
|  | 		char m_aName[128]; | ||||||
|  | 		char m_aDomain[256]; | ||||||
|  | 		int  m_aPort; | ||||||
|  | 		int  m_aInterval; // seconds
 | ||||||
|  | 		char m_aCallback[1024]; | ||||||
|  | 		int64_t m_aExpireTS; // epoch seconds cache
 | ||||||
|  | 		int64_t m_aLastCheck; // last check time
 | ||||||
|  | 		int64_t m_aLastAlarm7; | ||||||
|  | 		int64_t m_aLastAlarm3; | ||||||
|  | 		int64_t m_aLastAlarm1; | ||||||
|  | 		int m_aHostnameMismatch; // 1: 域名与证书不匹配
 | ||||||
|  | 		int64_t m_aLastAlarmMismatch; // 上次不匹配告警时间
 | ||||||
|  | 	} m_aCSSLCerts[NET_MAX_CLIENTS]; | ||||||
| 
 | 
 | ||||||
| 	struct CJSONUpdateThreadData | 	struct CJSONUpdateThreadData | ||||||
| 	{ | 	{ | ||||||
| 		CClient *pClients; | 		CClient *pClients; | ||||||
| 		CConfig *pConfig; | 		CConfig *pConfig; | ||||||
|         CWatchDog *pWatchDogs; |         CWatchDog *pWatchDogs; | ||||||
|  | 		CMain *pMain; | ||||||
| 		volatile short m_ReloadRequired; | 		volatile short m_ReloadRequired; | ||||||
| 	} m_JSONUpdateThreadData, m_OfflineAlarmThreadData; | 	} m_JSONUpdateThreadData, m_OfflineAlarmThreadData; | ||||||
| 
 | 
 | ||||||
|  | @ -118,6 +134,7 @@ public: | ||||||
| 
 | 
 | ||||||
|     CWatchDog *Watchdog(int ruleID) { return &m_aCWatchDogs[ruleID]; } |     CWatchDog *Watchdog(int ruleID) { return &m_aCWatchDogs[ruleID]; } | ||||||
|     CMonitors *Monitors(int ruleID) { return &m_aCMonitors[ruleID]; } |     CMonitors *Monitors(int ruleID) { return &m_aCMonitors[ruleID]; } | ||||||
|  | 	CSSLCerts *SSLCert(int ruleID) { return &m_aCSSLCerts[ruleID]; } | ||||||
| 
 | 
 | ||||||
|     void WatchdogMessage(int ClientNetID, |     void WatchdogMessage(int ClientNetID, | ||||||
|                          double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086, |                          double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | #ifndef PCH_HPP | ||||||
|  | #define PCH_HPP | ||||||
|  | // 预编译头: 尽量只放稳定且常用/体积大的头, 减少频繁改动触发全量重编译
 | ||||||
|  | #include <stdio.h> | ||||||
|  | #include <stdlib.h> | ||||||
|  | #include <string.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <inttypes.h> | ||||||
|  | #include <time.h> | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | #include <map> | ||||||
|  | #include <algorithm> | ||||||
|  | // 体积较大的表达式库
 | ||||||
|  | #include "exprtk.hpp" | ||||||
|  | #endif // PCH_HPP
 | ||||||
|  | @ -25,6 +25,9 @@ | ||||||
|                 <li class="nav-item"> |                 <li class="nav-item"> | ||||||
|                     <a class="nav-link" href="#monitor" data-bs-toggle="tab">服务</a> |                     <a class="nav-link" href="#monitor" data-bs-toggle="tab">服务</a> | ||||||
|                 </li> |                 </li> | ||||||
|  |                 <li class="nav-item"> | ||||||
|  |                     <a class="nav-link" href="#sslpanel" data-bs-toggle="tab">证书</a> | ||||||
|  |                 </li> | ||||||
|                 <li class="nav-item dropdown"> |                 <li class="nav-item dropdown"> | ||||||
|                     <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> |                     <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||||
|                         风格 |                         风格 | ||||||
|  | @ -93,6 +96,23 @@ | ||||||
|                 </tbody> |                 </tbody> | ||||||
|             </table> |             </table> | ||||||
|         </div> |         </div> | ||||||
|  |         <div class="tab-pane fade" id="sslpanel"> | ||||||
|  |             <!--SSL 证书--> | ||||||
|  |             <table class="table table-striped table-hover"> | ||||||
|  |                 <thead> | ||||||
|  |                 <tr> | ||||||
|  |                     <th style="text-align:center;">名称</th> | ||||||
|  |                     <th>域名</th> | ||||||
|  |                     <th style="text-align:center;">端口</th> | ||||||
|  |                     <th style="text-align:center;">剩余(天)</th> | ||||||
|  |                     <th style="text-align:center;">到期(UTC)</th> | ||||||
|  |                     <th style="text-align:center;">状态</th> | ||||||
|  |                 </tr> | ||||||
|  |                 </thead> | ||||||
|  |                 <tbody id="sslcerts"> | ||||||
|  |                 </tbody> | ||||||
|  |             </table> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <br/> |     <br/> | ||||||
|  |  | ||||||
|  | @ -30,6 +30,46 @@ function uptime() { | ||||||
|             document.getElementById("loading-notice")?.remove(); |             document.getElementById("loading-notice")?.remove(); | ||||||
|             if (result.reload) setTimeout(location.reload, 1000); |             if (result.reload) setTimeout(location.reload, 1000); | ||||||
| 
 | 
 | ||||||
|  |             // 构建 SSL 证书映射
 | ||||||
|  |             const sslMap = {}; | ||||||
|  |             if (Array.isArray(result.sslcerts)) { | ||||||
|  |                 result.sslcerts.forEach(c => { | ||||||
|  |                     if (c.domain) { | ||||||
|  |                         const d = c.domain.replace(/^https?:\/\//,'').replace(/[:/].*/,''); | ||||||
|  |                         sslMap[d] = {...c, domain_clean:d}; | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |                                 // 渲染独立 SSL 面板
 | ||||||
|  |                                 const tbody = document.getElementById('sslcerts'); | ||||||
|  |                                 if (tbody) { | ||||||
|  |                                         tbody.innerHTML = ''; | ||||||
|  |                                         result.sslcerts.forEach((raw, idx) => { | ||||||
|  |                                             const c = {...raw}; | ||||||
|  |                                             const clean = (c.domain||'').replace(/^https?:\/\//,'').replace(/[:/].*/,''); | ||||||
|  |                                             c.domain_clean = clean; | ||||||
|  |                                                 const days = c.expire_days; | ||||||
|  |                                                 let cls = 'text-success'; | ||||||
|  |                                                 let status = '正常'; | ||||||
|  |                                                 // 先判定过期
 | ||||||
|  |                                                 if (days <= 0) { cls = 'text-danger fw-bold'; status='已过期'; } | ||||||
|  |                                                 else if (c.mismatch) { cls = 'text-danger'; status='域名不匹配'; } | ||||||
|  |                                                 else if (days <= 1) { cls = 'text-danger'; status='紧急(≤1天)'; } | ||||||
|  |                                                 else if (days <= 3) { cls = 'text-danger'; status='紧急(≤3天)'; } | ||||||
|  |                                                 else if (days <= 7) { cls = 'text-warning'; status='将到期'; } | ||||||
|  |                                                 const expireDt = c.expire_ts ? new Date(c.expire_ts * 1000).toISOString().replace('T',' ').replace(/\.\d+Z/,'') : '-'; | ||||||
|  |                                                 tbody.insertAdjacentHTML('beforeend', ` | ||||||
|  |                                                     <tr id="ssl_${idx}"> | ||||||
|  |                                                         <td style="text-align:center;">${c.name||'-'}</td> | ||||||
|  |                                                         <td>${clean}</td> | ||||||
|  |                                                         <td style="text-align:center;">${c.port||443}</td> | ||||||
|  |                                                         <td style="text-align:center;" class="${cls}">${days ?? '-'}</td> | ||||||
|  |                                                         <td style="text-align:center;">${expireDt}</td> | ||||||
|  |                                                         <td style="text-align:center;" class="${cls}">${status}</td> | ||||||
|  |                                                     </tr>`); | ||||||
|  |                                         }); | ||||||
|  |                                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             result.servers.forEach((server, i) => { |             result.servers.forEach((server, i) => { | ||||||
|                 let TableRow = document.querySelector(`#servers tr#r${i}`); |                 let TableRow = document.querySelector(`#servers tr#r${i}`); | ||||||
|                 let MableRow = document.querySelector(`#monitors tr#r${i}`); |                 let MableRow = document.querySelector(`#monitors tr#r${i}`); | ||||||
|  | @ -151,6 +191,7 @@ function uptime() { | ||||||
|                                 pingBar.style.width = "100%"; |                                 pingBar.style.width = "100%"; | ||||||
|                                 pingBar.innerHTML = "<small>关闭</small>"; |                                 pingBar.innerHTML = "<small>关闭</small>"; | ||||||
|                             } |                             } | ||||||
|  |                             // SSL 列已移除
 | ||||||
|                         } |                         } | ||||||
|                         if (MableRow) { |                         if (MableRow) { | ||||||
|                             MableRow.querySelector("#monitor_text").innerHTML = "-"; |                             MableRow.querySelector("#monitor_text").innerHTML = "-"; | ||||||
|  | @ -238,6 +279,15 @@ function uptime() { | ||||||
|                     if (ExpandRow) ExpandRow.querySelector("#expand_ping").innerHTML = `CU/CT/CM: ${server.time_10010}ms (${PING_10010}%) / ${server.time_189}ms (${PING_189}%) / ${server.time_10086}ms (${PING_10086}%)`; |                     if (ExpandRow) ExpandRow.querySelector("#expand_ping").innerHTML = `CU/CT/CM: ${server.time_10010}ms (${PING_10010}%) / ${server.time_189}ms (${PING_189}%) / ${server.time_10086}ms (${PING_10086}%)`; | ||||||
| 
 | 
 | ||||||
|                     if (MableRow) MableRow.querySelector("#monitor_text").innerHTML = server.custom; |                     if (MableRow) MableRow.querySelector("#monitor_text").innerHTML = server.custom; | ||||||
|  | 
 | ||||||
|  |                     // SSL 匹配: 使用 host 的域名部分匹配 sslMap
 | ||||||
|  |                     const extractDomain = (h) => { | ||||||
|  |                         if(!h) return ''; | ||||||
|  |                         return h.replace(/^https?:\/\//,'').replace(/:.*/,''); | ||||||
|  |                     }; | ||||||
|  |                     const hostDomain = extractDomain(server.host || server.name || ''); | ||||||
|  |                     // 首页服务器表已取消 SSL 列
 | ||||||
|  |                     // 服务表移除 SSL 列
 | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 cppla
						cppla