Moved server packages to a separate folder
This commit is contained in:
23
server/middleware/HTTPSRedirect.go
Normal file
23
server/middleware/HTTPSRedirect.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
)
|
||||
|
||||
// HTTPSRedirect middleware redirects to HTTPS if needed.
|
||||
func HTTPSRedirect(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
request := ctx.Request()
|
||||
userAgent := request.Header("User-Agent")
|
||||
isBrowser := userAgent != ""
|
||||
|
||||
if isBrowser && request.Scheme() != "https" {
|
||||
return ctx.Redirect(http.StatusPermanentRedirect, "https://"+request.Host()+request.Path())
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
43
server/middleware/IPToHost.go
Normal file
43
server/middleware/IPToHost.go
Normal file
@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/akyoto/cache"
|
||||
)
|
||||
|
||||
var ipToHosts = cache.New(60 * time.Minute)
|
||||
|
||||
// GetHostsForIP returns all host names for the given IP (if cached).
|
||||
func GetHostsForIP(ip string) ([]string, bool) {
|
||||
hosts, found := ipToHosts.Get(ip)
|
||||
|
||||
if !found {
|
||||
hosts = findHostsForIP(ip)
|
||||
}
|
||||
|
||||
if hosts == nil {
|
||||
return nil, found
|
||||
}
|
||||
|
||||
return hosts.([]string), found
|
||||
}
|
||||
|
||||
// Finds all host names for the given IP
|
||||
func findHostsForIP(ip string) []string {
|
||||
hosts, err := net.LookupAddr(ip)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cache host names
|
||||
ipToHosts.Set(ip, hosts, 60*time.Minute)
|
||||
|
||||
return hosts
|
||||
}
|
49
server/middleware/Layout.go
Normal file
49
server/middleware/Layout.go
Normal file
@ -0,0 +1,49 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/akyoto/stringutils/unsafe"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/components"
|
||||
)
|
||||
|
||||
// Layout middleware modifies the response body
|
||||
// to be wrapped around the general layout.
|
||||
func Layout(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
ctx.AddModifier(func(content []byte) []byte {
|
||||
user := arn.GetUserFromContext(ctx)
|
||||
customCtx := ctx.(*OpenGraphContext)
|
||||
openGraph := customCtx.OpenGraph
|
||||
|
||||
// Make output order deterministic to profit from Aero caching.
|
||||
// To do this, we need to create slices and sort the tags.
|
||||
var meta []string
|
||||
var tags []string
|
||||
|
||||
if openGraph != nil {
|
||||
for name := range openGraph.Meta {
|
||||
meta = append(meta, name)
|
||||
}
|
||||
|
||||
sort.Strings(meta)
|
||||
|
||||
for name := range openGraph.Tags {
|
||||
tags = append(tags, name)
|
||||
}
|
||||
|
||||
sort.Strings(tags)
|
||||
}
|
||||
|
||||
// Assure that errors are formatted as HTML
|
||||
ctx.Response().SetHeader("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
html := components.Layout(ctx, user, openGraph, meta, tags, unsafe.BytesToString(content))
|
||||
return unsafe.StringToBytes(html)
|
||||
})
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
90
server/middleware/Log.go
Normal file
90
server/middleware/Log.go
Normal file
@ -0,0 +1,90 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/log"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
)
|
||||
|
||||
var (
|
||||
requestLog = log.New()
|
||||
errorLog = log.New()
|
||||
ipLog = log.New()
|
||||
)
|
||||
|
||||
// Initialize log files
|
||||
func init() {
|
||||
// The request log contains every single request to the server
|
||||
requestLog.AddWriter(log.File("logs/request.log"))
|
||||
|
||||
// The IP log contains the IPs accessing the server
|
||||
ipLog.AddWriter(log.File("logs/ip.log"))
|
||||
|
||||
// The error log contains all failed requests
|
||||
errorLog.AddWriter(log.File("logs/error.log"))
|
||||
errorLog.AddWriter(os.Stderr)
|
||||
}
|
||||
|
||||
// Log middleware logs every request into logs/request.log and errors into logs/error.log.
|
||||
func Log(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
start := time.Now()
|
||||
err := next(ctx)
|
||||
responseTime := time.Since(start)
|
||||
|
||||
logRequest(ctx, responseTime)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Logs a single request
|
||||
func logRequest(ctx aero.Context, responseTime time.Duration) {
|
||||
responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms"
|
||||
repeatSpaceCount := 8 - len(responseTimeString)
|
||||
|
||||
if repeatSpaceCount < 0 {
|
||||
repeatSpaceCount = 0
|
||||
}
|
||||
|
||||
responseTimeString = strings.Repeat(" ", repeatSpaceCount) + responseTimeString
|
||||
|
||||
user := arn.GetUserFromContext(ctx)
|
||||
ip := ctx.IP()
|
||||
hostNames, cached := GetHostsForIP(ip)
|
||||
|
||||
if !cached && len(hostNames) > 0 {
|
||||
ipLog.Info("%s = %s", ip, strings.Join(hostNames, ", "))
|
||||
}
|
||||
|
||||
// Log every request
|
||||
id := "id"
|
||||
nick := "guest"
|
||||
|
||||
if user != nil {
|
||||
id = user.ID
|
||||
nick = user.Nick
|
||||
}
|
||||
|
||||
requestLog.Info("%s | %s | %s | %s | %d | %s", nick, id, ip, responseTimeString, ctx.Status(), ctx.Path())
|
||||
|
||||
// Log all requests that failed
|
||||
switch ctx.Status() {
|
||||
case http.StatusOK, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
|
||||
// Ok.
|
||||
|
||||
default:
|
||||
errorLog.Error("%s | %s | %s | %s | %d | %s", nick, id, ip, responseTimeString, ctx.Status(), ctx.Path())
|
||||
}
|
||||
|
||||
// Notify us about long requests.
|
||||
// However ignore requests under /auth/ because those depend on 3rd party servers.
|
||||
if responseTime >= 500*time.Millisecond && !strings.HasPrefix(ctx.Path(), "/auth/") && !strings.HasPrefix(ctx.Path(), "/sitemap/") && !strings.HasPrefix(ctx.Path(), "/api/sse/") {
|
||||
errorLog.Error("%s | %s | %s | %s | %d | %s (long response time)", nick, id, ip, responseTimeString, ctx.Status(), ctx.Path())
|
||||
}
|
||||
}
|
24
server/middleware/OpenGraph.go
Normal file
24
server/middleware/OpenGraph.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
)
|
||||
|
||||
// OpenGraphContext is a context with open graph data.
|
||||
type OpenGraphContext struct {
|
||||
aero.Context
|
||||
*arn.OpenGraph
|
||||
}
|
||||
|
||||
// OpenGraph middleware modifies the context to be an OpenGraphContext.
|
||||
func OpenGraph(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
openGraphCtx := &OpenGraphContext{
|
||||
Context: ctx,
|
||||
OpenGraph: nil,
|
||||
}
|
||||
|
||||
return next(openGraphCtx)
|
||||
}
|
||||
}
|
59
server/middleware/Recover.go
Normal file
59
server/middleware/Recover.go
Normal file
@ -0,0 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
)
|
||||
|
||||
// Recover recovers from panics and shows them as the response body.
|
||||
func Recover(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, ok := r.(error)
|
||||
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
|
||||
stack := make([]byte, 4096)
|
||||
length := runtime.Stack(stack, false)
|
||||
stackString := string(stack[:length])
|
||||
fmt.Fprint(os.Stderr, stackString)
|
||||
|
||||
// Save crash in database
|
||||
crash := &arn.Crash{
|
||||
Error: err.Error(),
|
||||
Stack: stackString,
|
||||
Path: ctx.Path(),
|
||||
}
|
||||
|
||||
crash.ID = arn.GenerateID("Crash")
|
||||
crash.Created = arn.DateTimeUTC()
|
||||
user := arn.GetUserFromContext(ctx)
|
||||
|
||||
if user != nil {
|
||||
crash.CreatedBy = user.ID
|
||||
}
|
||||
|
||||
crash.Save()
|
||||
|
||||
// Send HTML
|
||||
message := "<div class='crash'>" + err.Error() + "<br><br>" + strings.ReplaceAll(stackString, "\n", "<br>") + "</div>"
|
||||
_ = ctx.Error(http.StatusInternalServerError, message)
|
||||
}()
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
18
server/middleware/Session.go
Normal file
18
server/middleware/Session.go
Normal file
@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/aerogo/aero"
|
||||
|
||||
// Session middleware saves an existing session if it has been modified.
|
||||
func Session(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
// Handle the request first
|
||||
err := next(ctx)
|
||||
|
||||
// Update session if it has been modified
|
||||
if ctx.HasSession() && ctx.Session().Modified() {
|
||||
_ = ctx.App().Sessions.Store.Set(ctx.Session().ID(), ctx.Session())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
122
server/middleware/UserInfo.go
Normal file
122
server/middleware/UserInfo.go
Normal file
@ -0,0 +1,122 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/http/client"
|
||||
"github.com/akyoto/color"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/mssola/user_agent"
|
||||
)
|
||||
|
||||
// UserInfo updates user related information after each request.
|
||||
func UserInfo(next aero.Handler) aero.Handler {
|
||||
return func(ctx aero.Context) error {
|
||||
err := next(ctx)
|
||||
|
||||
// Ignore non-HTML requests
|
||||
contentType := ctx.Response().Header("Content-Type")
|
||||
|
||||
if !strings.HasPrefix(contentType, "text/html") {
|
||||
return nil
|
||||
}
|
||||
|
||||
user := arn.GetUserFromContext(ctx)
|
||||
|
||||
// When there's no user logged in, nothing to update
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bind local variables and start a coroutine
|
||||
ip := ctx.IP()
|
||||
userAgent := ctx.Request().Header("User-Agent")
|
||||
go updateUserInfo(ip, userAgent, user)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update browser and OS data
|
||||
func updateUserInfo(ip string, userAgent string, user *arn.User) {
|
||||
if user.UserAgent != userAgent {
|
||||
user.UserAgent = userAgent
|
||||
|
||||
// Parse user agent
|
||||
parsed := user_agent.New(user.UserAgent)
|
||||
|
||||
// Browser
|
||||
user.Browser.Name, user.Browser.Version = parsed.Browser()
|
||||
|
||||
// OS
|
||||
os := parsed.OSInfo()
|
||||
user.OS.Name = os.Name
|
||||
user.OS.Version = os.Version
|
||||
}
|
||||
|
||||
user.LastSeen = arn.DateTimeUTC()
|
||||
user.Save()
|
||||
|
||||
if user.IP == ip {
|
||||
return
|
||||
}
|
||||
|
||||
updateUserLocation(user, ip)
|
||||
}
|
||||
|
||||
// Updates the location of the user.
|
||||
func updateUserLocation(user *arn.User, newIP string) {
|
||||
user.IP = newIP
|
||||
|
||||
if arn.APIKeys.IPInfoDB.ID == "" {
|
||||
if arn.IsProduction() {
|
||||
color.Red("IPInfoDB key not defined")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + arn.APIKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json"
|
||||
response, err := client.Get(locationAPI).End()
|
||||
|
||||
if err != nil {
|
||||
color.Red("Couldn't fetch location data | Error: %s | IP: %s", err.Error(), user.IP)
|
||||
return
|
||||
}
|
||||
|
||||
if response.StatusCode() != http.StatusOK {
|
||||
color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP)
|
||||
return
|
||||
}
|
||||
|
||||
newLocation := arn.IPInfoDBLocation{}
|
||||
err = response.Unmarshal(&newLocation)
|
||||
|
||||
if err != nil {
|
||||
color.Red("Couldn't deserialize location data | Status: %d | IP: %s", response.StatusCode, user.IP)
|
||||
return
|
||||
}
|
||||
|
||||
if newLocation.CountryName == "" || newLocation.CountryName == "-" {
|
||||
return
|
||||
}
|
||||
|
||||
user.Location.CountryName = newLocation.CountryName
|
||||
user.Location.CountryCode = newLocation.CountryCode
|
||||
user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64)
|
||||
user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Longitude, 64)
|
||||
user.Location.CityName = newLocation.CityName
|
||||
user.Location.RegionName = newLocation.RegionName
|
||||
user.Location.TimeZone = newLocation.TimeZone
|
||||
user.Location.ZipCode = newLocation.ZipCode
|
||||
|
||||
// Make South Korea easier to read
|
||||
if user.Location.CountryName == "Korea, Republic of" {
|
||||
user.Location.CountryName = "South Korea"
|
||||
}
|
||||
|
||||
user.Save()
|
||||
}
|
Reference in New Issue
Block a user