| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 | package nginximport (	"fmt"	"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"}// GetModuleMapping returns a map showing the relationship between different module name formats.// This is useful for debugging and understanding how module names are processed.// Returns a map with normalized names as keys and mapping info as values.func GetModuleMapping() map[string]map[string]string {	modules := GetModules()	mapping := make(map[string]map[string]string)	modulesCacheLock.RLock()	defer modulesCacheLock.RUnlock()	// Use AllFromFront() to iterate through the ordered map	for normalizedName, module := range modules.AllFromFront() {		if module == nil {			continue		}		expectedLoadName := getExpectedLoadModuleName(normalizedName)		mapping[normalizedName] = map[string]string{			"normalized":           normalizedName,			"expected_load_module": expectedLoadName,			"dynamic":              fmt.Sprintf("%t", module.Dynamic),			"loaded":               fmt.Sprintf("%t", module.Loaded),			"params":               module.Params,		}	}	return mapping}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()	// Regular expression to find module parameters with values	paramRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(?:_module)?(?:=([^"'\s]+|"[^"]*"|'[^']*'))?`)	paramMatches := paramRe.FindAllStringSubmatch(out, -1)	// Update cache	modulesCacheLock.Lock()	modulesCache = orderedmap.NewOrderedMap[string, *Module]()	// Extract module names and parameters from 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			})		}	}	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}
 |