modules.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. // normalizeAddModuleName converts a module name from --add-module arguments
  210. // to a consistent format for internal use.
  211. // Examples:
  212. // - "ngx_devel_kit" -> "devel_kit"
  213. // - "echo-nginx-module" -> "echo_nginx"
  214. // - "headers-more-nginx-module" -> "headers_more_nginx"
  215. // - "ngx_lua" -> "lua"
  216. // - "set-misc-nginx-module" -> "set_misc_nginx"
  217. // - "ngx_stream_lua" -> "stream_lua"
  218. func normalizeAddModuleName(addModuleName string) string {
  219. // Convert dashes to underscores
  220. normalized := strings.ReplaceAll(addModuleName, "-", "_")
  221. // Remove common prefixes
  222. normalized = strings.TrimPrefix(normalized, "ngx_")
  223. // Remove common suffixes - prioritize longer suffixes first
  224. if strings.HasSuffix(normalized, "_nginx_module") {
  225. // For modules ending with "_nginx_module", remove only "_module" to keep "_nginx"
  226. normalized = strings.TrimSuffix(normalized, "_module")
  227. } else if strings.HasSuffix(normalized, "_module") {
  228. normalized = strings.TrimSuffix(normalized, "_module")
  229. }
  230. return normalized
  231. }
  232. func GetModules() *orderedmap.OrderedMap[string, *Module] {
  233. modulesCacheLock.RLock()
  234. cachedModules := modulesCache
  235. modulesCacheLock.RUnlock()
  236. // If we have cached modules and PID file hasn't changed, return cached modules
  237. if cachedModules.Len() > 0 && !isPIDFileChanged() {
  238. return cachedModules
  239. }
  240. // If PID has changed or we don't have cached modules, get fresh modules
  241. out := getNginxV()
  242. // Update cache
  243. modulesCacheLock.Lock()
  244. modulesCache = orderedmap.NewOrderedMap[string, *Module]()
  245. // Regular expression to find --with- module parameters with values
  246. paramRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(?:_module)?(?:=([^"'\s]+|"[^"]*"|'[^']*'))?`)
  247. paramMatches := paramRe.FindAllStringSubmatch(out, -1)
  248. // Extract module names and parameters from --with- matches
  249. for _, match := range paramMatches {
  250. if len(match) > 1 {
  251. module := match[1]
  252. var params string
  253. // Check if there's a parameter value
  254. if len(match) > 2 && match[2] != "" {
  255. params = match[2]
  256. // Remove surrounding quotes if present
  257. params = strings.TrimPrefix(params, "'")
  258. params = strings.TrimPrefix(params, "\"")
  259. params = strings.TrimSuffix(params, "'")
  260. params = strings.TrimSuffix(params, "\"")
  261. }
  262. // Special handling for configuration options like cc-opt, not actual modules
  263. if module == "cc-opt" || module == "ld-opt" || module == "prefix" {
  264. modulesCache.Set(module, &Module{
  265. Name: module,
  266. Params: params,
  267. Dynamic: false,
  268. Loaded: true,
  269. })
  270. continue
  271. }
  272. // Normalize the module name for consistent internal representation
  273. normalizedModuleName := normalizeModuleNameFromConfigure(module)
  274. // Determine if the module is dynamic
  275. isDynamic := false
  276. if strings.Contains(out, "--with-"+module+"=dynamic") ||
  277. strings.Contains(out, "--with-"+module+"_module=dynamic") {
  278. isDynamic = true
  279. }
  280. if params == "dynamic" {
  281. params = ""
  282. }
  283. modulesCache.Set(normalizedModuleName, &Module{
  284. Name: normalizedModuleName,
  285. Params: params,
  286. Dynamic: isDynamic,
  287. Loaded: !isDynamic, // Static modules are always loaded
  288. })
  289. }
  290. }
  291. // Regular expression to find --add-module parameters
  292. // Matches patterns like: --add-module=../ngx_devel_kit-0.3.3 or --add-module=../echo-nginx-module-0.63
  293. addModuleRe := regexp.MustCompile(`--add-module=(?:[^/\s]+/)?([^/\s-]+(?:-[^/\s-]+)*)-[0-9.]+`)
  294. addModuleMatches := addModuleRe.FindAllStringSubmatch(out, -1)
  295. // Extract module names from --add-module matches
  296. for _, match := range addModuleMatches {
  297. if len(match) > 1 {
  298. moduleName := match[1]
  299. // Convert dashes to underscores for consistency
  300. normalizedName := strings.ReplaceAll(moduleName, "-", "_")
  301. // Further normalize the name
  302. finalNormalizedName := normalizeAddModuleName(normalizedName)
  303. // Add-modules are statically compiled, so they're always loaded but not dynamic
  304. modulesCache.Set(finalNormalizedName, &Module{
  305. Name: finalNormalizedName,
  306. Params: "",
  307. Dynamic: false, // --add-module creates static modules
  308. Loaded: true, // Static modules are always loaded
  309. })
  310. }
  311. }
  312. modulesCacheLock.Unlock()
  313. // Also check for modules loaded via load_module statements that might not be in configure args
  314. addLoadedDynamicModules()
  315. // Update dynamic modules status by checking if they're actually loaded
  316. updateDynamicModulesStatus()
  317. // Update PID file info
  318. updatePIDFileInfo()
  319. return modulesCache
  320. }
  321. // IsModuleLoaded checks if a module is loaded in Nginx
  322. func IsModuleLoaded(module string) bool {
  323. // Get fresh modules to ensure we have the latest state
  324. GetModules()
  325. modulesCacheLock.RLock()
  326. defer modulesCacheLock.RUnlock()
  327. status, exists := modulesCache.Get(module)
  328. if !exists {
  329. return false
  330. }
  331. return status.Loaded
  332. }