package main import ( "context" "gifuu/routes" "gifuu/tools" "net" "net/http" "os" "os/signal" "strings" "sync" "syscall" "time" ) func main() { time.Local = time.UTC // Startup Services var stopCtx, stop = context.WithCancel(context.Background()) var stopWg sync.WaitGroup var syncWg sync.WaitGroup tools.LoggerInit.Log(tools.INFO, "Starting Services") for _, fn := range []func(stop context.Context, await *sync.WaitGroup){ tools.SetupDatabase, tools.SetupModel, } { syncWg.Add(1) go func() { defer syncWg.Done() fn(stopCtx, &stopWg) }() } syncWg.Wait() go StartupHTTP(stopCtx, &stopWg) // Await Shutdown Signal cancel := make(chan os.Signal, 1) signal.Notify(cancel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) <-cancel stop() // Begin Shutdown Process tools.LoggerInit.Log(tools.WARN, "Shutting Down!") timeout, finish := context.WithTimeout(context.Background(), tools.TIMEOUT_CONTEXT) defer finish() go func() { <-timeout.Done() if timeout.Err() == context.DeadlineExceeded { tools.LoggerInit.Log(tools.FATAL, "Shutdown Deadline Exceeded") return } }() stopWg.Wait() os.Exit(0) } func SetupMux() http.HandlerFunc { var ( mux = http.NewServeMux() limitPublic = tools.NewRatelimiter("PUBLIC", 900, 5*time.Minute) limitDelete = tools.NewRatelimiter("DELETE", 100, 5*time.Minute) limitStart = tools.NewRatelimiter("START", 100, 5*time.Minute) limitCreate = tools.NewRatelimiter("CREATE", 50, 5*time.Minute) limitModerators = tools.NewRatelimiter("MOD", 300, 5*time.Minute) gatekeepModerator = tools.NewGatekeeper(true) powExpensive = tools.NewChallenger(20) powNormal = tools.NewChallenger(18) ) // General mux.Handle("/uploads", tools.MethodHandler{ http.MethodPost: tools.Chain(routes.POST_Uploads, limitCreate, powExpensive), }) mux.Handle("/tags/popular", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Tags_Popular, limitPublic), }) mux.Handle("/tags/autocomplete", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Tags_Autocomplete, limitPublic), }) mux.Handle("/art/latest", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Art_Latest, limitPublic), }) mux.Handle("/art/search", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Art_Search, limitPublic), }) mux.Handle("/art/{id}", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Art_ID, limitPublic), http.MethodDelete: tools.Chain(routes.DELETE_Art_ID, limitDelete), }) mux.Handle("/art/{id}/report", tools.MethodHandler{ http.MethodPost: tools.Chain(routes.POST_Art_ID_Reports, limitCreate, powNormal), }) mux.Handle("/metadata/{id}", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Metadata_ID, limitPublic), }) // Moderation mux.Handle("/moderation/art/{id}", tools.MethodHandler{ http.MethodDelete: tools.Chain(routes.DELETE_Moderation_Art_ID, limitModerators, gatekeepModerator), }) // mux.Handle("/moderation/art/{id}/rating", tools.MethodHandler{ // // TODO: Sets the items moderation to 100% hiding it from the public // http.MethodPatch: tools.Chain(routes.PATCH_Moderation_Art_ID_Rating, limitModerators, gatekeepModerator), // }) // mux.Handle("/moderation/art/{id}/bypass", tools.MethodHandler{ // // TODO: Disables Reporting for the item by setting the 'flag_bypass' flag // http.MethodPatch: tools.Chain(routes.PATCH_Moderation_Art_ID_Bypass, limitModerators, gatekeepModerator), // }) // mux.Handle("/moderation/art/{id}/reports", tools.MethodHandler{ // // TODO: Get Latest Reports for an Item // http.MethodGet: tools.Chain(routes.GET_Moderation_Art_ID_Reports, limitModerators, gatekeepModerator), // // TODO: Delete all Reports for an Item // http.MethodDelete: tools.Chain(routes.DELETE_Moderation_Art_ID_Reports, limitModerators, gatekeepModerator), // }) // // TODO: Get the Latest Reports across Site // mux.Handle("/moderation/latest", tools.MethodHandler{ // http.MethodGet: tools.Chain(routes.GET_Moderation_Latest, limitModerators, gatekeepModerator), // }) // Other mux.Handle("/challenge", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Challenge, limitStart), }) mux.Handle("/limits", tools.MethodHandler{ http.MethodGet: tools.Chain(routes.GET_Limits, limitPublic), }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { tools.SendClientError(w, r, tools.ERROR_UNKNOWN_ENDPOINT) }) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Inject CORS w.Header().Set("Access-Control-Allow-Origin", "*") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Challenge-Nonce, X-Challenge-Counter") w.WriteHeader(http.StatusNoContent) return } mux.ServeHTTP(w, r) }) } func StartupHTTP(stop context.Context, await *sync.WaitGroup) { var listener net.Listener var err error if strings.HasPrefix(tools.HTTP_ADDRESS, "unix/") { path := strings.TrimPrefix(tools.HTTP_ADDRESS, "unix/") os.Remove(path) listener, err = net.Listen("unix", path) if err == nil { os.Chmod(path, 0660) } } else { listener, err = net.Listen("tcp", tools.HTTP_ADDRESS) } if err != nil { tools.LoggerHTTP.Log(tools.FATAL, "Listen failed: %s", err) return } svr := http.Server{ Handler: SetupMux(), MaxHeaderBytes: 4096, IdleTimeout: 10 * time.Second, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } // Shutdown Logic await.Add(1) go func() { defer await.Done() <-stop.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), tools.TIMEOUT_SHUTDOWN) defer cancel() if err := svr.Shutdown(shutdownCtx); err != nil { tools.LoggerHTTP.Log(tools.ERROR, "Shutdown error: %s", err) } tools.LoggerHTTP.Log(tools.INFO, "Closed") }() tools.LoggerHTTP.Log(tools.INFO, "Listening @ %s", tools.HTTP_ADDRESS) if err := svr.Serve(listener); err != http.ErrServerClosed { tools.LoggerHTTP.Log(tools.FATAL, "Startup Failed: %s", err) return } }