main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "runtime"
  10. "strconv"
  11. "strings"
  12. "syscall"
  13. "time"
  14. "codeberg.org/vnpower/pixivfe/v2/config"
  15. "codeberg.org/vnpower/pixivfe/v2/core"
  16. "codeberg.org/vnpower/pixivfe/v2/routes"
  17. "codeberg.org/vnpower/pixivfe/v2/session"
  18. "codeberg.org/vnpower/pixivfe/v2/utils/kmutex"
  19. "github.com/goccy/go-json"
  20. "github.com/gofiber/fiber/v2"
  21. "github.com/gofiber/fiber/v2/middleware/cache"
  22. "github.com/gofiber/fiber/v2/middleware/compress"
  23. "github.com/gofiber/fiber/v2/middleware/limiter"
  24. "github.com/gofiber/fiber/v2/middleware/logger"
  25. "github.com/gofiber/fiber/v2/middleware/recover"
  26. fiber_utils "github.com/gofiber/fiber/v2/utils"
  27. )
  28. func CanRequestSkipLimiter(c *fiber.Ctx) bool {
  29. path := c.Path()
  30. return strings.HasPrefix(path, "/img/") ||
  31. strings.HasPrefix(path, "/css/") ||
  32. strings.HasPrefix(path, "/js/") ||
  33. strings.HasPrefix(path, "/proxy/s.pximg.net/")
  34. }
  35. func CanRequestSkipLogger(c *fiber.Ctx) bool {
  36. // return false
  37. path := c.Path()
  38. return CanRequestSkipLimiter(c) ||
  39. strings.HasPrefix(path, "/proxy/i.pximg.net/")
  40. }
  41. func main() {
  42. config.GlobalServerConfig.InitializeConfig()
  43. core.CreateResponseAuditFolder()
  44. routes.InitTemplatingEngine(config.GlobalServerConfig.InDevelopment)
  45. server := fiber.New(fiber.Config{
  46. AppName: "PixivFE",
  47. DisableStartupMessage: true,
  48. Prefork: false,
  49. JSONEncoder: json.Marshal,
  50. JSONDecoder: json.Unmarshal,
  51. ViewsLayout: "_layout",
  52. EnableTrustedProxyCheck: true,
  53. TrustedProxies: []string{"0.0.0.0/0"},
  54. ProxyHeader: fiber.HeaderXForwardedFor,
  55. ErrorHandler: func(c *fiber.Ctx, err error) error {
  56. log.Println(err)
  57. // Status code defaults to 500
  58. code := fiber.StatusInternalServerError
  59. // // Retrieve the custom status code if it's a *fiber.Error
  60. // var e *fiber.Error
  61. // if errors.As(err, &e) {
  62. // code = e.Code
  63. // }
  64. // Send custom error page
  65. c.Status(code)
  66. err = routes.Render(c, routes.Data_error{Title: "Error", Error: err})
  67. if err != nil {
  68. return c.Status(code).SendString(fmt.Sprintf("Internal Server Error: %s", err))
  69. }
  70. return nil
  71. },
  72. })
  73. server.Use(func(c *fiber.Ctx) error {
  74. // Pass in values that we want to be available to all pages here
  75. token := session.GetPixivToken(c)
  76. pageURL := c.BaseURL() + c.OriginalURL()
  77. cookies := map[string]string{}
  78. for _, name := range session.AllCookieNames {
  79. value := session.GetCookie(c, name)
  80. cookies[string(name)] = value
  81. }
  82. c.Bind(fiber.Map{
  83. "BaseURL": c.BaseURL(),
  84. "OriginalURL": c.OriginalURL(),
  85. "PageURL": pageURL,
  86. "LoggedIn": token != "",
  87. "Queries": c.Queries(),
  88. "CookieList": cookies,
  89. })
  90. return c.Next()
  91. })
  92. if config.GlobalServerConfig.RequestLimit > 0 {
  93. keyedSleepingSpot := kmutex.New()
  94. server.Use(limiter.New(limiter.Config{
  95. Next: CanRequestSkipLimiter,
  96. Expiration: 30 * time.Second,
  97. Max: config.GlobalServerConfig.RequestLimit,
  98. LimiterMiddleware: limiter.SlidingWindow{},
  99. LimitReached: func(c *fiber.Ctx) error {
  100. // limit response throughput by pacing, since not every bot reads X-RateLimit-*
  101. // on limit reached, they just have to wait
  102. // 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)
  103. retryAfter_s := c.GetRespHeader(fiber.HeaderRetryAfter)
  104. retryAfter, err := strconv.ParseUint(retryAfter_s, 10, 64)
  105. if err != nil {
  106. log.Panicf("response header 'RetryAfter' should be a number: %v", err)
  107. }
  108. requestIP := c.IP()
  109. refcount := keyedSleepingSpot.Lock(requestIP)
  110. defer keyedSleepingSpot.Unlock(requestIP)
  111. if refcount >= 4 { // on too much concurrent requests
  112. // todo: maybe blackhole `requestIP` here
  113. log.Println("Limit Reached (Hard)!", requestIP)
  114. // close the connection immediately
  115. _ = c.Context().Conn().Close()
  116. return nil
  117. }
  118. // sleeping
  119. // here, sleeping is not the best solution.
  120. // todo: close this connection when this IP reaches hard limit
  121. dur := time.Duration(retryAfter) * time.Second
  122. log.Println("Limit Reached (Soft)! Sleeping for ", dur)
  123. ctx, cancel := context.WithTimeout(c.Context(), dur)
  124. defer cancel()
  125. <-ctx.Done()
  126. return c.Next()
  127. },
  128. }))
  129. }
  130. server.Use(logger.New(
  131. logger.Config{
  132. Format: "${time} +${latency} ${ip} ${method} ${path} ${status} ${error} \n",
  133. Next: CanRequestSkipLogger,
  134. CustomTags: map[string]logger.LogFunc{
  135. // make latency always print in seconds
  136. logger.TagLatency: func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
  137. latency := data.Stop.Sub(data.Start).Seconds()
  138. return output.WriteString(fmt.Sprintf("%.6f", latency))
  139. },
  140. },
  141. },
  142. ))
  143. server.Use(compress.New(compress.Config{
  144. Level: compress.LevelBestSpeed, // 1
  145. }))
  146. if !config.GlobalServerConfig.InDevelopment {
  147. server.Use(cache.New(
  148. cache.Config{
  149. Next: func(c *fiber.Ctx) bool {
  150. resp_code := c.Response().StatusCode()
  151. if resp_code < 200 || resp_code >= 300 {
  152. return true
  153. }
  154. // Disable cache for settings page
  155. return strings.Contains(c.Path(), "/settings") || c.Path() == "/"
  156. },
  157. Expiration: 5 * time.Minute,
  158. CacheControl: true,
  159. StoreResponseHeaders: true,
  160. KeyGenerator: func(c *fiber.Ctx) string {
  161. key := fiber_utils.CopyString(c.OriginalURL())
  162. for _, cookieName := range session.AllCookieNames {
  163. cookieValue := session.GetCookie(c, cookieName)
  164. if cookieValue != "" {
  165. key += "\x00\x00"
  166. key += string(cookieName)
  167. key += "\x00"
  168. key += cookieValue
  169. }
  170. }
  171. return key
  172. },
  173. },
  174. ))
  175. }
  176. // redirect any round with ?r=url
  177. // could this be unsafe with cross-site scripting?
  178. server.Use(func(c *fiber.Ctx) error {
  179. ret := c.Query("r")
  180. if ret != "" {
  181. c.Redirect(ret)
  182. }
  183. return c.Next()
  184. })
  185. // Global HTTP headers
  186. server.Use(func(c *fiber.Ctx) error {
  187. err := c.Next()
  188. if err != nil {
  189. return err
  190. }
  191. if strings.HasPrefix(string(c.Response().Header.ContentType()), "text/html") {
  192. c.Set("X-Frame-Options", "DENY")
  193. // use this if need iframe: `X-Frame-Options: SAMEORIGIN`
  194. c.Set("X-Content-Type-Options", "nosniff")
  195. c.Set("Referrer-Policy", "no-referrer")
  196. c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
  197. c.Set("Content-Security-Policy", fmt.Sprintf("base-uri 'self'; default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' %s; media-src 'self' %s; connect-src 'self'; form-action 'self'; frame-ancestors 'none';", session.GetImageProxyOrigin(c), session.GetImageProxyOrigin(c)))
  198. // use this if need iframe: `frame-ancestors 'self'`
  199. c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
  200. }
  201. return nil
  202. })
  203. server.Static("/favicon.ico", "./assets/img/favicon.ico")
  204. server.Static("/robots.txt", "./assets/robots.txt")
  205. server.Static("/img/", "./assets/img")
  206. server.Static("/css/", "./assets/css")
  207. server.Static("/js/", "./assets/js")
  208. server.Use(recover.New(recover.Config{EnableStackTrace: config.GlobalServerConfig.InDevelopment}))
  209. // Routes
  210. server.Get("/", routes.IndexPage)
  211. server.Get("/about", routes.AboutPage)
  212. server.Get("/newest", routes.NewestPage)
  213. server.Get("/discovery", routes.DiscoveryPage)
  214. server.Get("/discovery/novel", routes.NovelDiscoveryPage)
  215. server.Get("/ranking", routes.RankingPage)
  216. server.Get("/rankingCalendar", routes.RankingCalendarPage)
  217. server.Post("/rankingCalendar", routes.RankingCalendarPicker)
  218. server.Get("/users/:id.atom.xml", routes.UserAtomFeed)
  219. server.Get("/users/:id/:category?.atom.xml", routes.UserAtomFeed)
  220. server.Get("/users/:id/:category?", routes.UserPage)
  221. server.Get("/artworks/:id/", routes.ArtworkPage).Name("artworks")
  222. server.Get("/artworks-multi/:ids/", routes.ArtworkMultiPage)
  223. server.Get("/novel/:id/", routes.NovelPage)
  224. server.Get("/pixivision", routes.PixivisionHomePage)
  225. server.Get("/pixivision/a/:id", routes.PixivisionArticlePage)
  226. // Settings group
  227. settings := server.Group("/settings")
  228. settings.Get("/", routes.SettingsPage)
  229. settings.Post("/:type/:noredirect?", routes.SettingsPost)
  230. // Personal group
  231. self := server.Group("/self")
  232. self.Get("/", routes.LoginUserPage)
  233. self.Get("/followingWorks", routes.FollowingWorksPage)
  234. self.Get("/bookmarks", routes.LoginBookmarkPage)
  235. self.Get("/addBookmark/:id", routes.AddBookmarkRoute)
  236. self.Get("/deleteBookmark/:id", routes.DeleteBookmarkRoute)
  237. self.Get("/like/:id", routes.LikeRoute)
  238. // Oembed group
  239. server.Get("/oembed", routes.Oembed)
  240. server.Get("/tags/:name", routes.TagPage)
  241. server.Post("/tags/:name", routes.TagPage)
  242. server.Get("/tags", routes.TagPage)
  243. server.Post("/tags", routes.AdvancedTagPost)
  244. // Legacy illust URL
  245. server.Get("/member_illust.php", func(c *fiber.Ctx) error {
  246. return c.Redirect("/artworks/" + c.Query("illust_id"))
  247. })
  248. // Proxy routes
  249. proxy := server.Group("/proxy")
  250. proxy.Get("/i.pximg.net/*", routes.IPximgProxy)
  251. proxy.Get("/s.pximg.net/*", routes.SPximgProxy)
  252. proxy.Get("/ugoira.com/*", routes.UgoiraProxy)
  253. // Initialize and start the proxy checker
  254. ctx_timeout, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
  255. defer cancel()
  256. config.InitializeProxyChecker(ctx_timeout)
  257. // run sass when in development mode
  258. if config.GlobalServerConfig.InDevelopment {
  259. go func() {
  260. cmd := exec.Command("sass", "--watch", "assets/css")
  261. cmd.Stdout = os.Stderr // Sass quirk
  262. cmd.Stderr = os.Stderr
  263. cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGHUP}
  264. runtime.LockOSThread() // Go quirk https://github.com/golang/go/issues/27505
  265. err := cmd.Run()
  266. if err != nil {
  267. log.Println(fmt.Errorf("when running sass: %w", err))
  268. }
  269. }()
  270. }
  271. // Listen
  272. if config.GlobalServerConfig.UnixSocket != "" {
  273. ln, err := net.Listen("unix", config.GlobalServerConfig.UnixSocket)
  274. if err != nil {
  275. panic(err)
  276. }
  277. log.Printf("Listening on domain socket %v\n", config.GlobalServerConfig.UnixSocket)
  278. err = server.Listener(ln)
  279. if err != nil {
  280. panic(err)
  281. }
  282. } else {
  283. addr := config.GlobalServerConfig.Host + ":" + config.GlobalServerConfig.Port
  284. ln, err := net.Listen(server.Config().Network, addr)
  285. if err != nil {
  286. log.Panicf("failed to listen: %v", err)
  287. }
  288. addr = ln.Addr().String()
  289. log.Printf("Listening on http://%v/\n", addr)
  290. err = server.Listener(ln)
  291. if err != nil {
  292. panic(err)
  293. }
  294. }
  295. }