modules.go 11 KB

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