modules.go 8.6 KB

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