modules.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. package nginx
  2. import (
  3. "fmt"
  4. "os"
  5. "regexp"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/elliotchance/orderedmap/v3"
  10. )
  11. const (
  12. ModuleStream = "stream"
  13. )
  14. type Module struct {
  15. Name string `json:"name"`
  16. Params string `json:"params,omitempty"`
  17. Dynamic bool `json:"dynamic"`
  18. Loaded bool `json:"loaded"`
  19. }
  20. // modulesCache stores the cached modules list and related metadata
  21. var (
  22. modulesCache = orderedmap.NewOrderedMap[string, *Module]()
  23. modulesCacheLock sync.RWMutex
  24. lastPIDPath string
  25. lastPIDModTime time.Time
  26. lastPIDSize int64
  27. )
  28. // clearModulesCache clears the modules cache
  29. func clearModulesCache() {
  30. modulesCacheLock.Lock()
  31. defer modulesCacheLock.Unlock()
  32. modulesCache = orderedmap.NewOrderedMap[string, *Module]()
  33. lastPIDPath = ""
  34. lastPIDModTime = time.Time{}
  35. lastPIDSize = 0
  36. }
  37. // ClearModulesCache clears the modules cache (public version for external use)
  38. func ClearModulesCache() {
  39. clearModulesCache()
  40. }
  41. // isPIDFileChanged checks if the PID file has changed since the last check
  42. func isPIDFileChanged() bool {
  43. pidPath := GetPIDPath()
  44. // If PID path has changed, consider it changed
  45. if pidPath != lastPIDPath {
  46. return true
  47. }
  48. // If Nginx is not running, consider PID changed
  49. if !IsRunning() {
  50. return true
  51. }
  52. // Check if PID file has changed (modification time or size)
  53. fileInfo, err := os.Stat(pidPath)
  54. if err != nil {
  55. return true
  56. }
  57. modTime := fileInfo.ModTime()
  58. size := fileInfo.Size()
  59. return modTime != lastPIDModTime || size != lastPIDSize
  60. }
  61. // updatePIDFileInfo updates the stored PID file information
  62. func updatePIDFileInfo() {
  63. pidPath := GetPIDPath()
  64. if fileInfo, err := os.Stat(pidPath); err == nil {
  65. modulesCacheLock.Lock()
  66. defer modulesCacheLock.Unlock()
  67. lastPIDPath = pidPath
  68. lastPIDModTime = fileInfo.ModTime()
  69. lastPIDSize = fileInfo.Size()
  70. }
  71. }
  72. // addLoadedDynamicModules discovers modules loaded via load_module statements
  73. // that might not be present in the configure arguments (e.g., externally installed modules)
  74. func addLoadedDynamicModules() {
  75. // Get nginx -T output to find load_module statements
  76. out := getNginxT()
  77. if out == "" {
  78. return
  79. }
  80. // Use the shared regex function to find loaded dynamic modules
  81. loadModuleRe := GetLoadModuleRegex()
  82. matches := loadModuleRe.FindAllStringSubmatch(out, -1)
  83. modulesCacheLock.Lock()
  84. defer modulesCacheLock.Unlock()
  85. for _, match := range matches {
  86. if len(match) > 1 {
  87. // Extract the module name from load_module statement and normalize it
  88. loadModuleName := match[1]
  89. normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)
  90. // Check if this module is already in our cache
  91. if _, exists := modulesCache.Get(normalizedName); !exists {
  92. // This is a module that's loaded but not in configure args
  93. // Add it as a dynamic module that's loaded
  94. modulesCache.Set(normalizedName, &Module{
  95. Name: normalizedName,
  96. Params: "",
  97. Dynamic: true, // Loaded via load_module, so it's dynamic
  98. Loaded: true, // We found it in load_module statements, so it's loaded
  99. })
  100. }
  101. }
  102. }
  103. }
  104. // updateDynamicModulesStatus checks which dynamic modules are actually loaded in the running Nginx
  105. func updateDynamicModulesStatus() {
  106. modulesCacheLock.Lock()
  107. defer modulesCacheLock.Unlock()
  108. // If cache is empty, there's nothing to update
  109. if modulesCache.Len() == 0 {
  110. return
  111. }
  112. // Get nginx -T output to check for loaded modules
  113. out := getNginxT()
  114. if out == "" {
  115. return
  116. }
  117. // Use the shared regex function to find loaded dynamic modules
  118. loadModuleRe := GetLoadModuleRegex()
  119. matches := loadModuleRe.FindAllStringSubmatch(out, -1)
  120. for _, match := range matches {
  121. if len(match) > 1 {
  122. // Extract the module name from load_module statement and normalize it
  123. loadModuleName := match[1]
  124. normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)
  125. // Try to find the module in our cache using the normalized name
  126. module, ok := modulesCache.Get(normalizedName)
  127. if ok {
  128. module.Loaded = true
  129. }
  130. }
  131. }
  132. }
  133. // GetLoadModuleRegex returns a compiled regular expression to match nginx load_module statements.
  134. // It matches both quoted and unquoted module paths:
  135. // - load_module "/usr/local/nginx/modules/ngx_stream_module.so";
  136. // - load_module modules/ngx_http_upstream_fair_module.so;
  137. //
  138. // The regex captures the module name (without path and extension).
  139. func GetLoadModuleRegex() *regexp.Regexp {
  140. // Pattern explanation:
  141. // load_module\s+ - matches "load_module" followed by whitespace
  142. // "? - optional opening quote
  143. // (?:[^"\s]+/)? - non-capturing group for optional path (any non-quote, non-space chars ending with /)
  144. // ([a-zA-Z0-9_-]+) - capturing group for module name
  145. // \.so - matches ".so" extension
  146. // "? - optional closing quote
  147. // \s*; - optional whitespace followed by semicolon
  148. return regexp.MustCompile(`load_module\s+"?(?:[^"\s]+/)?([a-zA-Z0-9_-]+)\.so"?\s*;`)
  149. }
  150. // normalizeModuleNameFromLoadModule converts a module name from load_module statement
  151. // to match the format used in configure arguments.
  152. // Examples:
  153. // - "ngx_stream_module" -> "stream"
  154. // - "ngx_http_geoip_module" -> "http_geoip"
  155. // - "ngx_stream_geoip_module" -> "stream_geoip"
  156. // - "ngx_http_image_filter_module" -> "http_image_filter"
  157. func normalizeModuleNameFromLoadModule(moduleName string) string {
  158. // Remove "ngx_" prefix if present
  159. normalized := strings.TrimPrefix(moduleName, "ngx_")
  160. // Remove "_module" suffix if present
  161. normalized = strings.TrimSuffix(normalized, "_module")
  162. return normalized
  163. }
  164. // normalizeModuleNameFromConfigure converts a module name from configure arguments
  165. // to a consistent format for internal use.
  166. // Examples:
  167. // - "stream" -> "stream"
  168. // - "http_geoip_module" -> "http_geoip"
  169. // - "http_image_filter_module" -> "http_image_filter"
  170. func normalizeModuleNameFromConfigure(moduleName string) string {
  171. // Remove "_module" suffix if present to keep consistent format
  172. normalized := strings.TrimSuffix(moduleName, "_module")
  173. return normalized
  174. }
  175. // getExpectedLoadModuleName converts a configure argument module name
  176. // to the expected load_module statement module name.
  177. // Examples:
  178. // - "stream" -> "ngx_stream_module"
  179. // - "http_geoip" -> "ngx_http_geoip_module"
  180. // - "stream_geoip" -> "ngx_stream_geoip_module"
  181. func getExpectedLoadModuleName(configureModuleName string) string {
  182. normalized := normalizeModuleNameFromConfigure(configureModuleName)
  183. return "ngx_" + normalized + "_module"
  184. }
  185. // GetModuleMapping returns a map showing the relationship between different module name formats.
  186. // This is useful for debugging and understanding how module names are processed.
  187. // Returns a map with normalized names as keys and mapping info as values.
  188. func GetModuleMapping() map[string]map[string]string {
  189. modules := GetModules()
  190. mapping := make(map[string]map[string]string)
  191. modulesCacheLock.RLock()
  192. defer modulesCacheLock.RUnlock()
  193. // Use AllFromFront() to iterate through the ordered map
  194. for normalizedName, module := range modules.AllFromFront() {
  195. if module == nil {
  196. continue
  197. }
  198. expectedLoadName := getExpectedLoadModuleName(normalizedName)
  199. mapping[normalizedName] = map[string]string{
  200. "normalized": normalizedName,
  201. "expected_load_module": expectedLoadName,
  202. "dynamic": fmt.Sprintf("%t", module.Dynamic),
  203. "loaded": fmt.Sprintf("%t", module.Loaded),
  204. "params": module.Params,
  205. }
  206. }
  207. return mapping
  208. }
  209. func GetModules() *orderedmap.OrderedMap[string, *Module] {
  210. modulesCacheLock.RLock()
  211. cachedModules := modulesCache
  212. modulesCacheLock.RUnlock()
  213. // If we have cached modules and PID file hasn't changed, return cached modules
  214. if cachedModules.Len() > 0 && !isPIDFileChanged() {
  215. return cachedModules
  216. }
  217. // If PID has changed or we don't have cached modules, get fresh modules
  218. out := getNginxV()
  219. // Regular expression to find module parameters with values
  220. paramRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(?:_module)?(?:=([^"'\s]+|"[^"]*"|'[^']*'))?`)
  221. paramMatches := paramRe.FindAllStringSubmatch(out, -1)
  222. // Update cache
  223. modulesCacheLock.Lock()
  224. modulesCache = orderedmap.NewOrderedMap[string, *Module]()
  225. // Extract module names and parameters from matches
  226. for _, match := range paramMatches {
  227. if len(match) > 1 {
  228. module := match[1]
  229. var params string
  230. // Check if there's a parameter value
  231. if len(match) > 2 && match[2] != "" {
  232. params = match[2]
  233. // Remove surrounding quotes if present
  234. params = strings.TrimPrefix(params, "'")
  235. params = strings.TrimPrefix(params, "\"")
  236. params = strings.TrimSuffix(params, "'")
  237. params = strings.TrimSuffix(params, "\"")
  238. }
  239. // Special handling for configuration options like cc-opt, not actual modules
  240. if module == "cc-opt" || module == "ld-opt" || module == "prefix" {
  241. modulesCache.Set(module, &Module{
  242. Name: module,
  243. Params: params,
  244. Dynamic: false,
  245. Loaded: true,
  246. })
  247. continue
  248. }
  249. // Normalize the module name for consistent internal representation
  250. normalizedModuleName := normalizeModuleNameFromConfigure(module)
  251. // Determine if the module is dynamic
  252. isDynamic := false
  253. if strings.Contains(out, "--with-"+module+"=dynamic") ||
  254. strings.Contains(out, "--with-"+module+"_module=dynamic") {
  255. isDynamic = true
  256. }
  257. if params == "dynamic" {
  258. params = ""
  259. }
  260. modulesCache.Set(normalizedModuleName, &Module{
  261. Name: normalizedModuleName,
  262. Params: params,
  263. Dynamic: isDynamic,
  264. Loaded: !isDynamic, // Static modules are always loaded
  265. })
  266. }
  267. }
  268. modulesCacheLock.Unlock()
  269. // Also check for modules loaded via load_module statements that might not be in configure args
  270. addLoadedDynamicModules()
  271. // Update dynamic modules status by checking if they're actually loaded
  272. updateDynamicModulesStatus()
  273. // Update PID file info
  274. updatePIDFileInfo()
  275. return modulesCache
  276. }
  277. // IsModuleLoaded checks if a module is loaded in Nginx
  278. func IsModuleLoaded(module string) bool {
  279. // Get fresh modules to ensure we have the latest state
  280. GetModules()
  281. modulesCacheLock.RLock()
  282. defer modulesCacheLock.RUnlock()
  283. status, exists := modulesCache.Get(module)
  284. if !exists {
  285. return false
  286. }
  287. return status.Loaded
  288. }