| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 | package nginximport (	"os"	"regexp"	"strings"	"sync"	"time"	"github.com/elliotchance/orderedmap/v3")const (	ModuleStream = "stream")type Module struct {	Name    string `json:"name"`	Params  string `json:"params,omitempty"`	Dynamic bool   `json:"dynamic"`	Loaded  bool   `json:"loaded"`}// modulesCache stores the cached modules list and related metadatavar (	modulesCache     = orderedmap.NewOrderedMap[string, *Module]()	modulesCacheLock sync.RWMutex	lastPIDPath      string	lastPIDModTime   time.Time	lastPIDSize      int64)// clearModulesCache clears the modules cachefunc clearModulesCache() {	modulesCacheLock.Lock()	defer modulesCacheLock.Unlock()	modulesCache = orderedmap.NewOrderedMap[string, *Module]()	lastPIDPath = ""	lastPIDModTime = time.Time{}	lastPIDSize = 0}// ClearModulesCache clears the modules cache (public version for external use)func ClearModulesCache() {	clearModulesCache()}// isPIDFileChanged checks if the PID file has changed since the last checkfunc isPIDFileChanged() bool {	pidPath := GetPIDPath()	// If PID path has changed, consider it changed	if pidPath != lastPIDPath {		return true	}	// If Nginx is not running, consider PID changed	if !IsRunning() {		return true	}	// Check if PID file has changed (modification time or size)	fileInfo, err := os.Stat(pidPath)	if err != nil {		return true	}	modTime := fileInfo.ModTime()	size := fileInfo.Size()	return modTime != lastPIDModTime || size != lastPIDSize}// updatePIDFileInfo updates the stored PID file informationfunc updatePIDFileInfo() {	pidPath := GetPIDPath()	if fileInfo, err := os.Stat(pidPath); err == nil {		modulesCacheLock.Lock()		defer modulesCacheLock.Unlock()		lastPIDPath = pidPath		lastPIDModTime = fileInfo.ModTime()		lastPIDSize = fileInfo.Size()	}}// addLoadedDynamicModules discovers modules loaded via load_module statements// that might not be present in the configure arguments (e.g., externally installed modules)func addLoadedDynamicModules() {	// Get nginx -T output to find load_module statements	out := getNginxT()	if out == "" {		return	}	// Use the shared regex function to find loaded dynamic modules	loadModuleRe := GetLoadModuleRegex()	matches := loadModuleRe.FindAllStringSubmatch(out, -1)	modulesCacheLock.Lock()	defer modulesCacheLock.Unlock()	for _, match := range matches {		if len(match) > 1 {			// Extract the module name from load_module statement and normalize it			loadModuleName := match[1]			normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)			// Check if this module is already in our cache			if _, exists := modulesCache.Get(normalizedName); !exists {				// This is a module that's loaded but not in configure args				// Add it as a dynamic module that's loaded				modulesCache.Set(normalizedName, &Module{					Name:    normalizedName,					Params:  "",					Dynamic: true, // Loaded via load_module, so it's dynamic					Loaded:  true, // We found it in load_module statements, so it's loaded				})			}		}	}}// updateDynamicModulesStatus checks which dynamic modules are actually loaded in the running Nginxfunc updateDynamicModulesStatus() {	modulesCacheLock.Lock()	defer modulesCacheLock.Unlock()	// If cache is empty, there's nothing to update	if modulesCache.Len() == 0 {		return	}	// Get nginx -T output to check for loaded modules	out := getNginxT()	if out == "" {		return	}	// Use the shared regex function to find loaded dynamic modules	loadModuleRe := GetLoadModuleRegex()	matches := loadModuleRe.FindAllStringSubmatch(out, -1)	for _, match := range matches {		if len(match) > 1 {			// Extract the module name from load_module statement and normalize it			loadModuleName := match[1]			normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)			// Try to find the module in our cache using the normalized name			module, ok := modulesCache.Get(normalizedName)			if ok {				module.Loaded = true			}		}	}}// GetLoadModuleRegex returns a compiled regular expression to match nginx load_module statements.// It matches both quoted and unquoted module paths://   - load_module "/usr/local/nginx/modules/ngx_stream_module.so";//   - load_module modules/ngx_http_upstream_fair_module.so;//// The regex captures the module name (without path and extension).func GetLoadModuleRegex() *regexp.Regexp {	// Pattern explanation:	// load_module\s+ - matches "load_module" followed by whitespace	// "? - optional opening quote	// (?:[^"\s]+/)? - non-capturing group for optional path (any non-quote, non-space chars ending with /)	// ([a-zA-Z0-9_-]+) - capturing group for module name	// \.so - matches ".so" extension	// "? - optional closing quote	// \s*; - optional whitespace followed by semicolon	return regexp.MustCompile(`load_module\s+"?(?:[^"\s]+/)?([a-zA-Z0-9_-]+)\.so"?\s*;`)}// normalizeModuleNameFromLoadModule converts a module name from load_module statement// to match the format used in configure arguments.// Examples://   - "ngx_stream_module" -> "stream"//   - "ngx_http_geoip_module" -> "http_geoip"//   - "ngx_stream_geoip_module" -> "stream_geoip"//   - "ngx_http_image_filter_module" -> "http_image_filter"func normalizeModuleNameFromLoadModule(moduleName string) string {	// Remove "ngx_" prefix if present	normalized := strings.TrimPrefix(moduleName, "ngx_")	// Remove "_module" suffix if present	normalized = strings.TrimSuffix(normalized, "_module")	return normalized}// normalizeModuleNameFromConfigure converts a module name from configure arguments// to a consistent format for internal use.// Examples://   - "stream" -> "stream"//   - "http_geoip_module" -> "http_geoip"//   - "http_image_filter_module" -> "http_image_filter"func normalizeModuleNameFromConfigure(moduleName string) string {	// Remove "_module" suffix if present to keep consistent format	normalized := strings.TrimSuffix(moduleName, "_module")	return normalized}// getExpectedLoadModuleName converts a configure argument module name// to the expected load_module statement module name.// Examples://   - "stream" -> "ngx_stream_module"//   - "http_geoip" -> "ngx_http_geoip_module"//   - "stream_geoip" -> "ngx_stream_geoip_module"func getExpectedLoadModuleName(configureModuleName string) string {	normalized := normalizeModuleNameFromConfigure(configureModuleName)	return "ngx_" + normalized + "_module"}// normalizeAddModuleName converts a module name from --add-module arguments// to a consistent format for internal use.// Examples://   - "ngx_devel_kit" -> "devel_kit"//   - "echo-nginx-module" -> "echo_nginx"//   - "headers-more-nginx-module" -> "headers_more_nginx"//   - "ngx_lua" -> "lua"//   - "set-misc-nginx-module" -> "set_misc_nginx"//   - "ngx_stream_lua" -> "stream_lua"func normalizeAddModuleName(addModuleName string) string {	// Convert dashes to underscores	normalized := strings.ReplaceAll(addModuleName, "-", "_")	// Remove common prefixes	normalized = strings.TrimPrefix(normalized, "ngx_")	// Remove common suffixes - prioritize longer suffixes first	if strings.HasSuffix(normalized, "_nginx_module") {		// For modules ending with "_nginx_module", remove only "_module" to keep "_nginx"		normalized = strings.TrimSuffix(normalized, "_module")	} else if strings.HasSuffix(normalized, "_module") {		normalized = strings.TrimSuffix(normalized, "_module")	}	return normalized}func GetModules() *orderedmap.OrderedMap[string, *Module] {	modulesCacheLock.RLock()	cachedModules := modulesCache	modulesCacheLock.RUnlock()	// If we have cached modules and PID file hasn't changed, return cached modules	if cachedModules.Len() > 0 && !isPIDFileChanged() {		return cachedModules	}	// If PID has changed or we don't have cached modules, get fresh modules	out := getNginxV()	// Update cache	modulesCacheLock.Lock()	modulesCache = orderedmap.NewOrderedMap[string, *Module]()	// Regular expression to find --with- module parameters with values	paramRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(?:_module)?(?:=([^"'\s]+|"[^"]*"|'[^']*'))?`)	paramMatches := paramRe.FindAllStringSubmatch(out, -1)	// Extract module names and parameters from --with- matches	for _, match := range paramMatches {		if len(match) > 1 {			module := match[1]			var params string			// Check if there's a parameter value			if len(match) > 2 && match[2] != "" {				params = match[2]				// Remove surrounding quotes if present				params = strings.TrimPrefix(params, "'")				params = strings.TrimPrefix(params, "\"")				params = strings.TrimSuffix(params, "'")				params = strings.TrimSuffix(params, "\"")			}			// Special handling for configuration options like cc-opt, not actual modules			if module == "cc-opt" || module == "ld-opt" || module == "prefix" {				modulesCache.Set(module, &Module{					Name:    module,					Params:  params,					Dynamic: false,					Loaded:  true,				})				continue			}			// Normalize the module name for consistent internal representation			normalizedModuleName := normalizeModuleNameFromConfigure(module)			// Determine if the module is dynamic			isDynamic := false			if strings.Contains(out, "--with-"+module+"=dynamic") ||				strings.Contains(out, "--with-"+module+"_module=dynamic") {				isDynamic = true			}			if params == "dynamic" {				params = ""			}			modulesCache.Set(normalizedModuleName, &Module{				Name:    normalizedModuleName,				Params:  params,				Dynamic: isDynamic,				Loaded:  !isDynamic, // Static modules are always loaded			})		}	}	// Regular expression to find --add-module parameters	// Matches patterns like: --add-module=../ngx_devel_kit-0.3.3 or --add-module=../echo-nginx-module-0.63	addModuleRe := regexp.MustCompile(`--add-module=(?:[^/\s]+/)?([^/\s-]+(?:-[^/\s-]+)*)-[0-9.]+`)	addModuleMatches := addModuleRe.FindAllStringSubmatch(out, -1)	// Extract module names from --add-module matches	for _, match := range addModuleMatches {		if len(match) > 1 {			moduleName := match[1]			// Convert dashes to underscores for consistency			normalizedName := strings.ReplaceAll(moduleName, "-", "_")			// Further normalize the name			finalNormalizedName := normalizeAddModuleName(normalizedName)			// Add-modules are statically compiled, so they're always loaded but not dynamic			modulesCache.Set(finalNormalizedName, &Module{				Name:    finalNormalizedName,				Params:  "",				Dynamic: false, // --add-module creates static modules				Loaded:  true,  // Static modules are always loaded			})		}	}	modulesCacheLock.Unlock()	// Also check for modules loaded via load_module statements that might not be in configure args	addLoadedDynamicModules()	// Update dynamic modules status by checking if they're actually loaded	updateDynamicModulesStatus()	// Update PID file info	updatePIDFileInfo()	return modulesCache}// IsModuleLoaded checks if a module is loaded in Nginxfunc IsModuleLoaded(module string) bool {	// Get fresh modules to ensure we have the latest state	GetModules()	modulesCacheLock.RLock()	defer modulesCacheLock.RUnlock()	status, exists := modulesCache.Get(module)	if !exists {		return false	}	return status.Loaded}
 |