main.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net"
  7. "net/http"
  8. "os"
  9. "os/exec"
  10. "runtime"
  11. "strconv"
  12. "strings"
  13. "syscall"
  14. "time"
  15. config "codeberg.org/vnpower/pixivfe/v2/core/config"
  16. "codeberg.org/vnpower/pixivfe/v2/pages"
  17. "codeberg.org/vnpower/pixivfe/v2/serve"
  18. "github.com/goccy/go-json"
  19. "github.com/gofiber/fiber/v2"
  20. "github.com/gofiber/fiber/v2/middleware/cache"
  21. "github.com/gofiber/fiber/v2/middleware/compress"
  22. "github.com/gofiber/fiber/v2/middleware/limiter"
  23. "github.com/gofiber/fiber/v2/middleware/logger"
  24. "github.com/gofiber/fiber/v2/middleware/recover"
  25. "github.com/gofiber/fiber/v2/utils"
  26. "github.com/gofiber/template/jet/v2"
  27. "codeberg.org/vnpower/pixivfe/v2/core/kmutex"
  28. )
  29. func CanRequestSkipLimiter(c *fiber.Ctx) bool {
  30. path := c.Path()
  31. return strings.HasPrefix(path, "/assets/") ||
  32. strings.HasPrefix(path, "/css/") ||
  33. strings.HasPrefix(path, "/js/") ||
  34. strings.HasPrefix(path, "/proxy/s.pximg.net/")
  35. }
  36. func CanRequestSkipLogger(c *fiber.Ctx) bool {
  37. path := c.Path()
  38. return CanRequestSkipLimiter(c) ||
  39. strings.HasPrefix(path, "/proxy/i.pximg.net/")
  40. }
  41. func main() {
  42. config.SetupStorage()
  43. config.GlobalServerConfig.InitializeConfig()
  44. engine := jet.New("./views", ".jet.html")
  45. engine.AddFuncMap(serve.GetTemplateFunctions())
  46. if config.GlobalServerConfig.InDevelopment {
  47. engine.Reload(true)
  48. }
  49. // gofiber bug: no error even if the templates are invalid???
  50. err := engine.Load()
  51. if err != nil {
  52. panic(err)
  53. }
  54. server := fiber.New(fiber.Config{
  55. AppName: "PixivFE",
  56. DisableStartupMessage: true,
  57. Views: engine,
  58. Prefork: false,
  59. JSONEncoder: json.Marshal,
  60. JSONDecoder: json.Unmarshal,
  61. ViewsLayout: "layout",
  62. EnableTrustedProxyCheck: true,
  63. TrustedProxies: []string{"0.0.0.0/0"},
  64. ProxyHeader: fiber.HeaderXForwardedFor,
  65. ErrorHandler: func(c *fiber.Ctx, err error) error {
  66. log.Println(err)
  67. // Status code defaults to 500
  68. code := fiber.StatusInternalServerError
  69. // // Retrieve the custom status code if it's a *fiber.Error
  70. // var e *fiber.Error
  71. // if errors.As(err, &e) {
  72. // code = e.Code
  73. // }
  74. // Send custom error page
  75. err = c.Status(code).Render("pages/error", fiber.Map{"Title": "Error", "Error": err})
  76. if err != nil {
  77. return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("Internal Server Error: %s", err))
  78. }
  79. return nil
  80. },
  81. })
  82. keyedSleepingSpot := kmutex.New()
  83. server.Use(limiter.New(limiter.Config{
  84. Next: CanRequestSkipLimiter,
  85. Expiration: 30 * time.Second,
  86. Max: config.GlobalServerConfig.RequestLimit,
  87. LimiterMiddleware: limiter.SlidingWindow{},
  88. LimitReached: func(c *fiber.Ctx) error {
  89. // limit response throughput by pacing, since not every bot reads X-RateLimit-*
  90. // on limit reached, they just have to wait
  91. // the design of this means that if they send multiple requests when reaching rate limit, they will wait even longer (since `retryAfter` is calculated before anything has slept)
  92. retryAfter_s := c.GetRespHeader(fiber.HeaderRetryAfter)
  93. retryAfter, err := strconv.ParseUint(retryAfter_s, 10, 64)
  94. if err != nil {
  95. log.Panicf("response header 'RetryAfter' should be a number: %v", err)
  96. }
  97. requestIP := c.IP()
  98. refcount := keyedSleepingSpot.Lock(requestIP)
  99. defer keyedSleepingSpot.Unlock(requestIP)
  100. if refcount >= 4 { // on too much concurrent requests
  101. // todo: maybe blackhole `requestIP` here
  102. log.Println("Limit Reached!")
  103. return fmt.Errorf("Woah! You are going too fast! I'll have to keep an eye on you.")
  104. }
  105. ctx, cancel := context.WithTimeout(c.Context(), time.Duration(retryAfter)*time.Second)
  106. defer cancel()
  107. <-ctx.Done()
  108. return c.Next()
  109. },
  110. }))
  111. server.Use(logger.New(
  112. logger.Config{
  113. Format: "${time} ~${latencySI} ${ip} ${method} ${path} ${status} ${error} \n",
  114. Next: CanRequestSkipLogger,
  115. EnableLatency: true,
  116. CustomTags: map[string]logger.LogFunc{
  117. "latencySI": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
  118. latency := data.Stop.Sub(data.Start).Seconds()
  119. return output.WriteString(fmt.Sprintf("%.13f", latency))
  120. },
  121. },
  122. },
  123. ))
  124. server.Use(compress.New(compress.Config{
  125. Level: compress.LevelBestSpeed, // 1
  126. }))
  127. if !config.GlobalServerConfig.InDevelopment {
  128. server.Use(cache.New(
  129. cache.Config{
  130. Next: func(c *fiber.Ctx) bool {
  131. resp_code := c.Response().StatusCode()
  132. if resp_code < 200 || resp_code >= 300 {
  133. return true
  134. }
  135. // Disable cache for settings page
  136. return strings.Contains(c.Path(), "/settings") || c.Path() == "/"
  137. },
  138. Expiration: 5 * time.Minute,
  139. CacheControl: true,
  140. KeyGenerator: func(c *fiber.Ctx) string {
  141. return utils.CopyString(c.OriginalURL())
  142. },
  143. },
  144. ))
  145. }
  146. // Global HTTP headers
  147. server.Use(func(c *fiber.Ctx) error {
  148. c.Set("X-Frame-Options", "DENY")
  149. // use this if need iframe: `X-Frame-Options: SAMEORIGIN`
  150. c.Set("X-Content-Type-Options", "nosniff")
  151. c.Set("Referrer-Policy", "no-referrer")
  152. c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
  153. c.Set("Content-Security-Policy", fmt.Sprintf("base-uri 'self'; default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' %s; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; ", config.GetImageProxyOrigin(c)))
  154. // use this if need iframe: `frame-ancestors 'self'`
  155. return c.Next()
  156. })
  157. server.Use(func(c *fiber.Ctx) error {
  158. baseURL := c.BaseURL() + c.OriginalURL()
  159. c.Bind(fiber.Map{"BaseURL": baseURL})
  160. return c.Next()
  161. })
  162. server.Static("/favicon.ico", "./views/assets/favicon.ico")
  163. server.Static("/robots.txt", "./views/assets/robots.txt")
  164. server.Static("/assets/", "./views/assets")
  165. server.Static("/css/", "./views/css")
  166. server.Static("/js/", "./views/js")
  167. server.Use(recover.New(recover.Config{EnableStackTrace: config.GlobalServerConfig.InDevelopment}))
  168. // Routes
  169. server.Get("/", pages.IndexPage)
  170. server.Get("/about", pages.AboutPage)
  171. server.Get("/newest", pages.NewestPage)
  172. server.Get("/discovery", pages.DiscoveryPage)
  173. server.Get("/discovery/novel", pages.NovelDiscoveryPage)
  174. server.Get("/ranking", pages.RankingPage)
  175. server.Get("/rankingCalendar", pages.RankingCalendarPage)
  176. server.Post("/rankingCalendar", pages.RankingCalendarPicker)
  177. server.Get("/users/:id/:category?", pages.UserPage)
  178. server.Get("/artworks/:id/", pages.ArtworkPage).Name("artworks")
  179. server.Get("/artworks/:id/embed", pages.ArtworkEmbedPage)
  180. server.Get("/artworks-multi/:ids/", pages.ArtworkMultiPage)
  181. server.Get("/novel/:id/", pages.NovelPage)
  182. // Settings group
  183. settings := server.Group("/settings")
  184. settings.Get("/", pages.SettingsPage)
  185. settings.Post("/:type", pages.SettingsPost)
  186. // Personal group
  187. self := server.Group("/self")
  188. self.Get("/", pages.LoginUserPage)
  189. self.Get("/followingWorks", pages.FollowingWorksPage)
  190. self.Get("/bookmarks", pages.LoginBookmarkPage)
  191. self.Post("/addBookmark/:id", pages.AddBookmarkRoute)
  192. self.Post("/deleteBookmark/:id", pages.DeleteBookmarkRoute)
  193. self.Post("/like/:id", pages.LikeRoute)
  194. server.Get("/tags/:name", pages.TagPage)
  195. server.Post("/tags/:name", pages.TagPage)
  196. server.Post("/tags",
  197. func(c *fiber.Ctx) error {
  198. name := c.FormValue("name")
  199. return c.Redirect("/tags/"+name, http.StatusFound)
  200. })
  201. // Legacy illust URL
  202. server.Get("/member_illust.php", func(c *fiber.Ctx) error {
  203. return c.Redirect("/artworks/" + c.Query("illust_id"))
  204. })
  205. // Proxy routes
  206. proxy := server.Group("/proxy")
  207. proxy.Get("/i.pximg.net/*", pages.IPximgProxy)
  208. proxy.Get("/s.pximg.net/*", pages.SPximgProxy)
  209. proxy.Get("/ugoira.com/*", pages.UgoiraProxy)
  210. // run sass when in development mode
  211. if config.GlobalServerConfig.InDevelopment {
  212. go func() {
  213. cmd := exec.Command("sass", "--watch", "views/css")
  214. cmd.Stdout = os.Stderr // Sass quirk
  215. cmd.Stderr = os.Stderr
  216. cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGHUP}
  217. runtime.LockOSThread() // Go quirk https://github.com/golang/go/issues/27505
  218. err := cmd.Run()
  219. if err != nil {
  220. log.Println(fmt.Errorf("when running sass: %w", err))
  221. }
  222. }()
  223. }
  224. // Listen
  225. if config.GlobalServerConfig.UnixSocket != "" {
  226. ln, err := net.Listen("unix", config.GlobalServerConfig.UnixSocket)
  227. if err != nil {
  228. panic(err)
  229. }
  230. log.Printf("PixivFE is running on %v\n", config.GlobalServerConfig.UnixSocket)
  231. err = server.Listener(ln)
  232. if err != nil {
  233. panic(err)
  234. }
  235. } else {
  236. addr := config.GlobalServerConfig.Host + ":" + config.GlobalServerConfig.Port
  237. log.Printf("PixivFE is running on http://%v/\n", addr)
  238. // note: string concatenation is very flaky
  239. err := server.Listen(addr)
  240. if err != nil {
  241. panic(err)
  242. }
  243. }
  244. }