handler.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. package setup
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/mail"
  6. "regexp"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/sysutil"
  12. "github.com/gin-gonic/gin"
  13. )
  14. // installMutex prevents concurrent installation attempts (TOCTOU protection)
  15. var installMutex sync.Mutex
  16. // RegisterRoutes registers setup wizard routes
  17. func RegisterRoutes(r *gin.Engine) {
  18. setup := r.Group("/setup")
  19. {
  20. // Status endpoint is always accessible (read-only)
  21. setup.GET("/status", getStatus)
  22. // All modification endpoints are protected by setupGuard
  23. protected := setup.Group("")
  24. protected.Use(setupGuard())
  25. {
  26. protected.POST("/test-db", testDatabase)
  27. protected.POST("/test-redis", testRedis)
  28. protected.POST("/install", install)
  29. }
  30. }
  31. }
  32. // SetupStatus represents the current setup state
  33. type SetupStatus struct {
  34. NeedsSetup bool `json:"needs_setup"`
  35. Step string `json:"step"`
  36. }
  37. // getStatus returns the current setup status
  38. func getStatus(c *gin.Context) {
  39. response.Success(c, SetupStatus{
  40. NeedsSetup: NeedsSetup(),
  41. Step: "welcome",
  42. })
  43. }
  44. // setupGuard middleware ensures setup endpoints are only accessible during setup mode
  45. func setupGuard() gin.HandlerFunc {
  46. return func(c *gin.Context) {
  47. if !NeedsSetup() {
  48. response.Error(c, http.StatusForbidden, "Setup is not allowed: system is already installed")
  49. c.Abort()
  50. return
  51. }
  52. c.Next()
  53. }
  54. }
  55. // validateHostname checks if a hostname/IP is safe (no injection characters)
  56. func validateHostname(host string) bool {
  57. // Allow only alphanumeric, dots, hyphens, and colons (for IPv6)
  58. validHost := regexp.MustCompile(`^[a-zA-Z0-9.\-:]+$`)
  59. return validHost.MatchString(host) && len(host) <= 253
  60. }
  61. // validateDBName checks if database name is safe
  62. func validateDBName(name string) bool {
  63. // Allow only alphanumeric and underscores, starting with letter
  64. validName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
  65. return validName.MatchString(name) && len(name) <= 63
  66. }
  67. // validateUsername checks if username is safe
  68. func validateUsername(name string) bool {
  69. // Allow only alphanumeric and underscores
  70. validName := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
  71. return validName.MatchString(name) && len(name) <= 63
  72. }
  73. // validateEmail checks if email format is valid
  74. func validateEmail(email string) bool {
  75. _, err := mail.ParseAddress(email)
  76. return err == nil && len(email) <= 254
  77. }
  78. // validatePassword checks password strength
  79. func validatePassword(password string) error {
  80. if len(password) < 8 {
  81. return fmt.Errorf("password must be at least 8 characters")
  82. }
  83. if len(password) > 128 {
  84. return fmt.Errorf("password must be at most 128 characters")
  85. }
  86. return nil
  87. }
  88. // validatePort checks if port is in valid range
  89. func validatePort(port int) bool {
  90. return port > 0 && port <= 65535
  91. }
  92. // validateSSLMode checks if SSL mode is valid
  93. func validateSSLMode(mode string) bool {
  94. validModes := map[string]bool{
  95. "disable": true, "require": true, "verify-ca": true, "verify-full": true,
  96. }
  97. return validModes[mode]
  98. }
  99. // TestDatabaseRequest represents database test request
  100. type TestDatabaseRequest struct {
  101. Host string `json:"host" binding:"required"`
  102. Port int `json:"port" binding:"required"`
  103. User string `json:"user" binding:"required"`
  104. Password string `json:"password"`
  105. DBName string `json:"dbname" binding:"required"`
  106. SSLMode string `json:"sslmode"`
  107. }
  108. // testDatabase tests database connection
  109. func testDatabase(c *gin.Context) {
  110. var req TestDatabaseRequest
  111. if err := c.ShouldBindJSON(&req); err != nil {
  112. response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error())
  113. return
  114. }
  115. // Security: Validate all inputs to prevent injection attacks
  116. if !validateHostname(req.Host) {
  117. response.Error(c, http.StatusBadRequest, "Invalid hostname format")
  118. return
  119. }
  120. if !validatePort(req.Port) {
  121. response.Error(c, http.StatusBadRequest, "Invalid port number")
  122. return
  123. }
  124. if !validateUsername(req.User) {
  125. response.Error(c, http.StatusBadRequest, "Invalid username format")
  126. return
  127. }
  128. if !validateDBName(req.DBName) {
  129. response.Error(c, http.StatusBadRequest, "Invalid database name format")
  130. return
  131. }
  132. if req.SSLMode == "" {
  133. req.SSLMode = "disable"
  134. }
  135. if !validateSSLMode(req.SSLMode) {
  136. response.Error(c, http.StatusBadRequest, "Invalid SSL mode")
  137. return
  138. }
  139. cfg := &DatabaseConfig{
  140. Host: req.Host,
  141. Port: req.Port,
  142. User: req.User,
  143. Password: req.Password,
  144. DBName: req.DBName,
  145. SSLMode: req.SSLMode,
  146. }
  147. if err := TestDatabaseConnection(cfg); err != nil {
  148. response.Error(c, http.StatusBadRequest, "Connection failed: "+err.Error())
  149. return
  150. }
  151. response.Success(c, gin.H{"message": "Connection successful"})
  152. }
  153. // TestRedisRequest represents Redis test request
  154. type TestRedisRequest struct {
  155. Host string `json:"host" binding:"required"`
  156. Port int `json:"port" binding:"required"`
  157. Username string `json:"username"`
  158. Password string `json:"password"`
  159. DB int `json:"db"`
  160. EnableTLS bool `json:"enable_tls"`
  161. }
  162. // testRedis tests Redis connection
  163. func testRedis(c *gin.Context) {
  164. var req TestRedisRequest
  165. if err := c.ShouldBindJSON(&req); err != nil {
  166. response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error())
  167. return
  168. }
  169. // Security: Validate inputs
  170. if !validateHostname(req.Host) {
  171. response.Error(c, http.StatusBadRequest, "Invalid hostname format")
  172. return
  173. }
  174. if !validatePort(req.Port) {
  175. response.Error(c, http.StatusBadRequest, "Invalid port number")
  176. return
  177. }
  178. if req.DB < 0 || req.DB > 15 {
  179. response.Error(c, http.StatusBadRequest, "Invalid Redis database number (0-15)")
  180. return
  181. }
  182. req.Username = strings.TrimSpace(req.Username)
  183. if len(req.Username) > 128 {
  184. response.Error(c, http.StatusBadRequest, "Invalid Redis username")
  185. return
  186. }
  187. cfg := &RedisConfig{
  188. Host: req.Host,
  189. Port: req.Port,
  190. Username: req.Username,
  191. Password: req.Password,
  192. DB: req.DB,
  193. EnableTLS: req.EnableTLS,
  194. }
  195. if err := TestRedisConnection(cfg); err != nil {
  196. response.Error(c, http.StatusBadRequest, "Connection failed: "+err.Error())
  197. return
  198. }
  199. response.Success(c, gin.H{"message": "Connection successful"})
  200. }
  201. // InstallRequest represents installation request
  202. type InstallRequest struct {
  203. Database DatabaseConfig `json:"database" binding:"required"`
  204. Redis RedisConfig `json:"redis" binding:"required"`
  205. Admin AdminConfig `json:"admin" binding:"required"`
  206. Server ServerConfig `json:"server"`
  207. }
  208. // install performs the installation
  209. func install(c *gin.Context) {
  210. // TOCTOU Protection: Acquire mutex to prevent concurrent installation
  211. installMutex.Lock()
  212. defer installMutex.Unlock()
  213. // Double-check after acquiring lock
  214. if !NeedsSetup() {
  215. response.Error(c, http.StatusForbidden, "Setup is not allowed: system is already installed")
  216. return
  217. }
  218. var req InstallRequest
  219. if err := c.ShouldBindJSON(&req); err != nil {
  220. response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error())
  221. return
  222. }
  223. req.Admin.Email = strings.TrimSpace(req.Admin.Email)
  224. req.Database.Host = strings.TrimSpace(req.Database.Host)
  225. req.Database.User = strings.TrimSpace(req.Database.User)
  226. req.Database.DBName = strings.TrimSpace(req.Database.DBName)
  227. req.Redis.Host = strings.TrimSpace(req.Redis.Host)
  228. req.Redis.Username = strings.TrimSpace(req.Redis.Username)
  229. // ========== COMPREHENSIVE INPUT VALIDATION ==========
  230. // Database validation
  231. if !validateHostname(req.Database.Host) {
  232. response.Error(c, http.StatusBadRequest, "Invalid database hostname")
  233. return
  234. }
  235. if !validatePort(req.Database.Port) {
  236. response.Error(c, http.StatusBadRequest, "Invalid database port")
  237. return
  238. }
  239. if !validateUsername(req.Database.User) {
  240. response.Error(c, http.StatusBadRequest, "Invalid database username")
  241. return
  242. }
  243. if !validateDBName(req.Database.DBName) {
  244. response.Error(c, http.StatusBadRequest, "Invalid database name")
  245. return
  246. }
  247. // Redis validation
  248. if !validateHostname(req.Redis.Host) {
  249. response.Error(c, http.StatusBadRequest, "Invalid Redis hostname")
  250. return
  251. }
  252. if !validatePort(req.Redis.Port) {
  253. response.Error(c, http.StatusBadRequest, "Invalid Redis port")
  254. return
  255. }
  256. if req.Redis.DB < 0 || req.Redis.DB > 15 {
  257. response.Error(c, http.StatusBadRequest, "Invalid Redis database number")
  258. return
  259. }
  260. if len(req.Redis.Username) > 128 {
  261. response.Error(c, http.StatusBadRequest, "Invalid Redis username")
  262. return
  263. }
  264. // Admin validation
  265. if !validateEmail(req.Admin.Email) {
  266. response.Error(c, http.StatusBadRequest, "Invalid admin email format")
  267. return
  268. }
  269. if err := validatePassword(req.Admin.Password); err != nil {
  270. response.Error(c, http.StatusBadRequest, err.Error())
  271. return
  272. }
  273. // Server validation
  274. if req.Server.Port != 0 && !validatePort(req.Server.Port) {
  275. response.Error(c, http.StatusBadRequest, "Invalid server port")
  276. return
  277. }
  278. // ========== SET DEFAULTS ==========
  279. if req.Database.SSLMode == "" {
  280. req.Database.SSLMode = "disable"
  281. }
  282. if !validateSSLMode(req.Database.SSLMode) {
  283. response.Error(c, http.StatusBadRequest, "Invalid SSL mode")
  284. return
  285. }
  286. if req.Server.Host == "" {
  287. req.Server.Host = "0.0.0.0"
  288. }
  289. if req.Server.Port == 0 {
  290. req.Server.Port = 8080
  291. }
  292. if req.Server.Mode == "" {
  293. req.Server.Mode = "release"
  294. }
  295. // Validate server mode
  296. if req.Server.Mode != "release" && req.Server.Mode != "debug" {
  297. response.Error(c, http.StatusBadRequest, "Invalid server mode (must be 'release' or 'debug')")
  298. return
  299. }
  300. cfg := &SetupConfig{
  301. Database: req.Database,
  302. Redis: req.Redis,
  303. Admin: req.Admin,
  304. Server: req.Server,
  305. JWT: JWTConfig{
  306. ExpireHour: 24,
  307. },
  308. }
  309. if err := Install(cfg); err != nil {
  310. response.Error(c, http.StatusInternalServerError, "Installation failed: "+err.Error())
  311. return
  312. }
  313. // Schedule service restart in background after sending response
  314. // This ensures the client receives the success response before the service restarts
  315. go func() {
  316. // Wait a moment to ensure the response is sent
  317. time.Sleep(500 * time.Millisecond)
  318. sysutil.RestartServiceAsync()
  319. }()
  320. response.Success(c, gin.H{
  321. "message": "Installation completed successfully. Service will restart automatically.",
  322. "restart": true,
  323. })
  324. }