@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""
Webhook Receiver Server
接收 webhook事件,每次请求动态读取配置文件。
配置文件路径:~/.webhook/config.yaml
"""
import json
import os
import hmac
import hashlib
import sys
import subprocess
from http . server import HTTPServer , BaseHTTPRequestHandler
from urllib . parse import urlparse , parse_qs
import datetime
import yaml
CONFIG_PATH = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " config.yaml " )
LOG_DIR = os . path . expanduser ( " ~/.webhook/logs " )
os . makedirs ( LOG_DIR , exist_ok = True )
# ============ 每次请求重新加载配置 ============
def load_config ( ) - > dict :
""" 动态加载配置文件,修改配置无需重启服务。 """
with open ( CONFIG_PATH , " r " , encoding = " utf-8 " ) as f :
return yaml . safe_load ( f )
def get_project_config ( config : dict , project_name : str ) :
""" 获取指定项目的配置,不存在或未启用则返回 None。 """
projects = config . get ( " projects " , { } )
proj = projects . get ( project_name )
if not proj :
return None
if not proj . get ( " enabled " , True ) :
return None
return proj
# ============ 日志 ============
def log_event ( event_type : str , data : dict , status : str = " ok " ) :
""" 结构化记录 webhook 事件到每日日志文件。 """
timestamp = datetime . datetime . now ( ) . strftime ( " % Y- % m- %d % H: % M: % S " )
log_file = os . path . join ( LOG_DIR , f " webhook_ { datetime . date . today ( ) . isoformat ( ) } .log " )
entry = {
" timestamp " : timestamp ,
" event " : event_type ,
" status " : status ,
* * data
}
with open ( log_file , " a " , encoding = " utf-8 " ) as f :
f . write ( json . dumps ( entry , ensure_ascii = False ) + " \n " )
def log_script_output ( project : str , client_ip : str , user_agent : str , referer : str ,
stdout : str , stderr : str , exit_code : int ) :
""" 记录项目脚本执行输出到独立日志文件(如 xsinfo.log)。 """
timestamp = datetime . datetime . now ( ) . strftime ( " % Y- % m- %d % H: % M: % S " )
log_file = os . path . join ( LOG_DIR , f " { project } .log " )
lines = [
f " [ { timestamp } ] { project } | ip= { client_ip } ua= { user_agent } referer= { referer } " ,
f " exit= { exit_code } " ,
]
if stdout :
for line in stdout . strip ( ) . split ( " \n " ) :
lines . append ( f " stdout: { line } " )
if stderr :
for line in stderr . strip ( ) . split ( " \n " ) :
lines . append ( f " stderr: { line } " )
with open ( log_file , " a " , encoding = " utf-8 " ) as f :
f . write ( " \n " . join ( lines ) + " \n " )
# ============ HTTP Handler ============
class WebhookHandler ( BaseHTTPRequestHandler ) :
def log_message ( self , format , * args ) :
""" 抑制默认日志输出。 """
pass
def send_json ( self , code : int , data : dict ) :
self . send_response ( code )
self . send_header ( " Content-Type " , " application/json " )
self . end_headers ( )
self . wfile . write ( json . dumps ( data , ensure_ascii = False ) . encode ( ) )
def do_GET ( self ) :
self . _handle_request ( )
def do_POST ( self ) :
self . _handle_request ( )
def _handle_request ( self ) :
""" 统一处理 GET/POST 请求,key 对了就执行脚本 """
parsed = urlparse ( self . path )
params = parse_qs ( parsed . query )
if parsed . path == " /health " :
self . send_json ( 200 , { " status " : " ok " , " service " : " webhook-receiver " } )
return
access_key = params . get ( " access_key " , [ None ] ) [ 0 ]
param_val = params . get ( " param " , [ None ] ) [ 0 ]
config = load_config ( )
global_key = config . get ( " access_key " , " " )
# 优先项目级 key,否则用全局 key
proj = get_project_config ( config , param_val ) if param_val else None
if param_val and proj and proj . get ( " api_key " ) :
expected_key = proj [ " api_key " ]
else :
expected_key = global_key
if access_key != expected_key :
log_event ( " unauthorized " , { " ip " : self . client_address [ 0 ] , " param " : param_val } , " denied " )
self . send_json ( 403 , { " error " : " invalid access_key " } )
return
client_ip = self . client_address [ 0 ]
user_agent = self . headers . get ( " User-Agent " , " N/A " )
referer = self . headers . get ( " Referer " , " N/A " )
# 执行脚本
result , status_code = self . _execute_script ( proj , param_val , client_ip , user_agent , referer )
script_res = result . get ( " script " , { } )
log_event (
" script_executed " ,
{
" param " : param_val ,
" project " : param_val or " unknown " ,
" ip " : client_ip ,
" ua " : user_agent ,
" referer " : referer ,
" executed " : script_res . get ( " executed " , False ) ,
" status " : script_res . get ( " status " , " unknown " ) ,
" exit_code " : script_res . get ( " exit_code " ) ,
} ,
status = script_res . get ( " status " , " unknown " ) ,
)
self . send_json ( status_code , { " ok " : status_code == 200 , " param " : param_val , * * result } )
def _execute_script ( self , proj : dict , param : str , client_ip : str , user_agent : str , referer : str ) - > tuple [ dict , int ] :
""" 执行脚本,key 对了就执行,不管什么请求 """
print ( f " \n { ' = ' * 60 } " )
print ( f " 🚀 执行脚本 | param= { param } " )
print ( f " { ' = ' * 60 } \n " )
script_result = {
" executed " : False ,
" status " : " skipped " ,
" exit_code " : None ,
" stdout " : None ,
" stderr " : None ,
" error " : None ,
}
if proj and proj . get ( " script " ) :
script_path = os . path . expanduser ( proj [ " script " ] )
script_cwd = os . path . dirname ( os . path . abspath ( __file__ ) )
if os . path . exists ( script_path ) :
os . chmod ( script_path , 0o755 )
try :
result = subprocess . run (
[ script_path ] ,
cwd = script_cwd ,
capture_output = True ,
text = True ,
timeout = 300 ,
)
script_result [ " executed " ] = True
script_result [ " exit_code " ] = result . returncode
script_result [ " stdout " ] = [ l for l in result . stdout . strip ( ) . split ( " \n " ) if l ] if result . stdout . strip ( ) else [ ]
script_result [ " stderr " ] = [ l for l in result . stderr . strip ( ) . split ( " \n " ) if l ] if result . stderr . strip ( ) else [ ]
script_result [ " status " ] = " success " if result . returncode == 0 else " failed "
print ( f " 📜 { proj [ ' script ' ] } : exit= { result . returncode } " )
if result . stdout :
print ( result . stdout )
if result . stderr :
print ( f " STDERR: { result . stderr } " , file = sys . stderr )
except subprocess . TimeoutExpired :
script_result [ " executed " ] = True
script_result [ " status " ] = " timeout "
script_result [ " error " ] = " script execution timed out after 300 seconds "
print ( f " ⚠️ 执行超时(300s) " )
except Exception as e :
script_result [ " executed " ] = True
script_result [ " status " ] = " error "
script_result [ " error " ] = str ( e )
print ( f " ⚠️ 执行失败 - { e } " )
else :
script_result [ " executed " ] = True
script_result [ " status " ] = " not_found "
script_result [ " error " ] = f " script not found: { script_path } "
print ( f " ⚠️ 脚本不存在: { script_path } " )
else :
print ( f " ⚠️ 项目 ' { param } ' 未配置 script " )
# 脚本失败返回 500
status_code = 500 if script_result [ " executed " ] and script_result [ " status " ] in ( " failed " , " error " , " timeout " , " not_found " ) else 200
log_script_output (
project = param or " unknown " ,
client_ip = client_ip ,
user_agent = user_agent ,
referer = referer ,
stdout = result . stdout if script_result [ " executed " ] else " " ,
stderr = result . stderr if script_result [ " executed " ] else " " ,
exit_code = script_result [ " exit_code " ] or - 1 ,
)
return { " script " : script_result } , status_code
def main ( ) :
# 启动前检查配置文件
if not os . path . exists ( CONFIG_PATH ) :
print ( f " ❌ 配置文件不存在: { CONFIG_PATH } " )
sys . exit ( 1 )
server = HTTPServer ( ( " 0.0.0.0 " , 8099 ) , WebhookHandler )
print ( f """
╔══════════════════════════════════════════════╗
║ 🌐 Webhook Receiver Started ║
╚══════════════════════════════════════════════╝
监听: http://0.0.0.0:8099
健康检查: http://<host>:8099/health
手动测试: http://<host>:8099/test
配置文件: { CONFIG_PATH }
配置变更实时生效(无需重启)
""" )
try :
server . serve_forever ( )
except KeyboardInterrupt :
print ( " \n 👋 Server stopped " )
sys . exit ( 0 )
if __name__ == " __main__ " :
main ( )