changes required to convert to Echo v5 #7

Merged
amy merged 5 commits from echo5-update into main 2026-05-03 14:59:50 -06:00
20 changed files with 349 additions and 258 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
)
// ENOJOIN is an error for not being permitted to join a community.
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"git.erbosoft.com/amy/amsterdam/exports"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -513,11 +513,11 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
resetLastRead := false
if ctxt.HasParameter("r") {
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
} else if ctxt.HasParameter("rgo") {
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
} else {
postRange[0] = lastRead + 1
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
+39 -20
View File
@@ -100,7 +100,10 @@ type AmConfig struct {
SessionExpire string `yaml:"sessionExpire"`
UserAgreementResource string `yaml:"userAgreementResource"`
PolicyResource string `yaml:"policyResource"`
FrameTemplate string `yaml:"frameTemplate"`
FooterTemplate string `yaml:"footerTemplate"`
TopMenuId string `yaml:"topMenuId"`
FixedMenuId string `yaml:"fixedMenuId"`
DefaultCommunityLogo string `yaml:"defaultCommunityLogo"`
DefaultUserPhoto string `yaml:"defaultUserPhoto"`
WelcomeTitle string `yaml:"welcomeTitle"`
@@ -148,6 +151,9 @@ type AmConfig struct {
Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
PanicRecovery struct {
StackDataSize string `yaml:"stackDataSize"`
} `yaml:"panicRecovery"`
} `yaml:"rendering"`
Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"`
@@ -167,7 +173,14 @@ type AmConfig struct {
} `yaml:"posting"`
Tuning struct {
WorkerTasks int `yaml:"workerTasks"`
Queues struct {
Timeouts struct {
HttpRead int `yaml:"httpRead"`
HttpWrite int `yaml:"httpWrite"`
HttpIdle int `yaml:"httpIdle"`
PageExecute int `yaml:"pageExecute"`
PageRender int `yaml:"pageRender"`
} `yaml:"timeouts"`
Queues struct {
AuditWrites int `yaml:"auditWrites"`
ContextRecycle int `yaml:"contextRecycle"`
EmailRecycle int `yaml:"emailRecycle"`
@@ -200,24 +213,25 @@ func (c *AmConfig) ExPath(path string) string {
return filepath.Join(c.baseDir, path)
}
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig.
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig and CommandLine.
type AmConfigComputed struct {
DebugMode bool // are we in debug mode?
LogLevel string // the logging level
Listen string // listen address
DatabaseDriver string // name of database driver
DatabaseHost string // hostname for database
DatabaseUser string // user name for database
DatabasePassword string // password for database
DatabaseName string // database name
MailHost string // SMTP host
MailPort int // SMTP port
MailTLS string // SMTP TLS setting
MailAuthType string // SMTP auth type
MailUser string // SMTP user name
MailPassword string // SMTP password
UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed?
DebugMode bool // are we in debug mode?
LogLevel string // the logging level
Listen string // listen address
DatabaseDriver string // name of database driver
DatabaseHost string // hostname for database
DatabaseUser string // user name for database
DatabasePassword string // password for database
DatabaseName string // database name
MailHost string // SMTP host
MailPort int // SMTP port
MailTLS string // SMTP TLS setting
MailAuthType string // SMTP auth type
MailUser string // SMTP user name
MailPassword string // SMTP password
PanicRecoveryStack int32 // stack size for panic recovery
UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed?
}
//go:embed default.yaml
@@ -321,7 +335,7 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
}
} else {
// if we see this message, this function needs more work
log.Errorf("*** unable to deal with field %s of type %s", structField.Name, typ.Name())
log.Fatalf("*** unable to deal with field %s of type %s", structField.Name, typ.Name())
}
}
}
@@ -398,7 +412,12 @@ func SetupConfig() {
GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType)
GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User)
GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password)
tmp, err := humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
tmp, err := humanize.ParseBytes(GlobalConfig.Rendering.PanicRecovery.StackDataSize)
if err != nil {
panic(err.Error())
}
GlobalComputedConfig.PanicRecoveryStack = int32(tmp)
tmp, err = humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
if err != nil {
panic(err.Error())
}
+11
View File
@@ -25,7 +25,10 @@ site:
sessionExpire: "3h"
userAgreementResource: "useragreement.html"
policyResource: "policy.html"
frameTemplate: "frame.jet"
footerTemplate: "footer.jet"
topMenuId: "top"
fixedMenuId: "fixed"
defaultCommunityLogo: "/img/builtin/default-community.jpg"
defaultUserPhoto: "/img/builtin/no-user.png"
welcomeTitle: "Welcome to Amsterdam"
@@ -72,6 +75,8 @@ rendering:
countryList:
prioritize: US
veniceCompatibleImageURLs: false
panicRecovery:
stackDataSize: "4 KiB"
resources:
viewTemplateDir: ""
dialogTemplateDir: ""
@@ -91,6 +96,12 @@ posting:
- "image/png"
tuning:
workerTasks: 4
timeouts:
httpRead: 30
httpWrite: 30
httpIdle: 120
pageExecute: 15
pageRender: 15
queues:
auditWrites: 16
contextRecycle: 16
+7 -8
View File
@@ -19,10 +19,9 @@ import (
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
// EBUTTON is the standard error for an unknown button.
@@ -74,9 +73,9 @@ func AmNotFoundHandler(ctxt ui.AmContext) (string, any) {
* err - The error to be handled.
* c - The Echo context error is being handled on.
*/
func AmErrorHandler(err error, c echo.Context) {
func AmErrorHandler(c *echo.Context, err error) {
log.Infof("-> AmErrorHandler on path %s", c.Request().URL.Path)
if c.Response().Committed {
if c.Response().(*echo.Response).Committed {
return
}
cerr := ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
@@ -88,14 +87,14 @@ func AmErrorHandler(err error, c echo.Context) {
}
// rateLimitErrorHandler is called if there's an error getting the identifier for a connection (unlikely).
func rateLimitErrorHandler(c echo.Context, err error) error {
func rateLimitErrorHandler(c *echo.Context, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
return "error", err
})
}
// rateLimitDenyHandler is called if the rate limit is exceeded by a connection.
func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
func rateLimitDenyHandler(c *echo.Context, identifier string, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("identifier", identifier)
return "ratelimit", err
@@ -105,7 +104,7 @@ func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
// AmSetupRateLimiter sets up the rate-limiting middleware.
func AmSetupRateLimiter() echo.MiddlewareFunc {
rcfg := middleware.RateLimiterMemoryStoreConfig{
Rate: rate.Limit(config.GlobalConfig.Site.RateLimit.Rate),
Rate: config.GlobalConfig.Site.RateLimit.Rate,
Burst: config.GlobalConfig.Site.RateLimit.Burst,
ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute,
}
+7 -8
View File
@@ -14,13 +14,12 @@ require (
github.com/hashicorp/golang-lru v1.0.2
github.com/jmoiron/sqlx v1.4.0
github.com/klauspost/lctime v0.1.0
github.com/labstack/echo/v4 v4.15.1
github.com/labstack/gommon v0.4.2
github.com/labstack/echo/v5 v5.1.1
github.com/labstack/gommon v0.5.0
github.com/sirupsen/logrus v1.9.4
github.com/tkuchiki/go-timezone v0.2.3
golang.org/x/net v0.52.0
golang.org/x/text v0.35.0
golang.org/x/time v0.15.0
golang.org/x/net v0.53.0
golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -29,10 +28,10 @@ require (
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/image v0.37.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
+12 -15
View File
@@ -31,16 +31,16 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -56,19 +56,16 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+62 -87
View File
@@ -13,104 +13,90 @@
package main
import (
"bufio"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"log/slog"
"maps"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"github.com/dustin/go-humanize"
"github.com/labstack/echo/v4"
glog "github.com/labstack/gommon/log"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
/*----------------------------------------------------------------------------
* Gommon-log to logrus adapter
* slog handler that outputs to Logrus
*----------------------------------------------------------------------------
*/
/* toglog converts a Logrus logging level to a glog one.
* Parameters:
* l - The Logrus log level to be converted.
* Returns:
* The equivalent glog log level.
*/
func toglog(l log.Level) glog.Lvl {
switch l {
case log.DebugLevel:
return glog.DEBUG
case log.InfoLevel:
return glog.INFO
case log.WarnLevel:
return glog.WARN
case log.ErrorLevel:
return glog.ERROR
default:
return glog.OFF
}
// slog2logrus converts slog levels to Logrus levels.
var slog2logrus = map[slog.Level]log.Level{
slog.LevelDebug: log.DebugLevel,
slog.LevelInfo: log.InfoLevel,
slog.LevelWarn: log.WarnLevel,
slog.LevelError: log.ErrorLevel,
}
/* fromglog converts a glog logging level to a Logrus one.
* Parameters:
* l - The glog log level to be converted.
* Returns:
* The equivalent Logrus log level.
*/
func fromglog(l glog.Lvl) log.Level {
switch l {
case glog.DEBUG:
return log.DebugLevel
case glog.INFO:
return log.InfoLevel
case glog.WARN:
return log.WarnLevel
case glog.ERROR:
return log.ErrorLevel
default:
return log.PanicLevel
}
// SlogLogrusHandler implements slog.Handler and routes to Logrus.
type SlogLogrusHandler struct {
fields log.Fields // fields defined in this handler
groupPrefix string // group prefix
}
// EchoLogrusAdapter implements echo.Logger using logrus.
type EchoLogrusAdapter struct{}
// NewSlogLogrusHandler creates a SlogLogrusHandler with base information.
func NewSlogLogrusHandler() *SlogLogrusHandler {
rc := new(SlogLogrusHandler{
fields: make(log.Fields),
groupPrefix: "",
})
return rc
}
func (l *EchoLogrusAdapter) Output() io.Writer { return log.StandardLogger().Out }
func (l *EchoLogrusAdapter) SetOutput(w io.Writer) { log.SetOutput(w) }
func (l *EchoLogrusAdapter) Prefix() string { return "" }
func (l *EchoLogrusAdapter) SetPrefix(p string) {}
func (l *EchoLogrusAdapter) Level() glog.Lvl { return toglog(log.GetLevel()) }
func (l *EchoLogrusAdapter) SetLevel(lvl glog.Lvl) { log.SetLevel(fromglog(lvl)) }
func (l *EchoLogrusAdapter) Print(i ...any) { log.Print(i...) }
func (l *EchoLogrusAdapter) Printf(format string, args ...any) { log.Printf(format, args...) }
func (l *EchoLogrusAdapter) Printj(j glog.JSON) { log.WithFields(log.Fields(j)).Print() }
func (l *EchoLogrusAdapter) Debug(i ...any) { log.Debug(i...) }
func (l *EchoLogrusAdapter) Debugf(format string, args ...any) { log.Debugf(format, args...) }
func (l *EchoLogrusAdapter) Debugj(j glog.JSON) { log.WithFields(log.Fields(j)).Debug() }
func (l *EchoLogrusAdapter) Info(i ...any) { log.Info(i...) }
func (l *EchoLogrusAdapter) Infof(format string, args ...any) { log.Infof(format, args...) }
func (l *EchoLogrusAdapter) Infoj(j glog.JSON) { log.WithFields(log.Fields(j)).Info() }
func (l *EchoLogrusAdapter) Warn(i ...any) { log.Warn(i...) }
func (l *EchoLogrusAdapter) Warnf(format string, args ...any) { log.Warnf(format, args...) }
func (l *EchoLogrusAdapter) Warnj(j glog.JSON) { log.WithFields(log.Fields(j)).Warn() }
func (l *EchoLogrusAdapter) Error(i ...any) { log.Error(i...) }
func (l *EchoLogrusAdapter) Errorf(format string, args ...any) { log.Errorf(format, args...) }
func (l *EchoLogrusAdapter) Errorj(j glog.JSON) { log.WithFields(log.Fields(j)).Error() }
func (l *EchoLogrusAdapter) Fatal(i ...any) { log.Fatal(i...) }
func (l *EchoLogrusAdapter) Fatalf(format string, args ...any) { log.Fatalf(format, args...) }
func (l *EchoLogrusAdapter) Fatalj(j glog.JSON) { log.WithFields(log.Fields(j)).Fatal() }
func (l *EchoLogrusAdapter) Panic(i ...any) { log.Panic(i...) }
func (l *EchoLogrusAdapter) Panicf(format string, args ...any) { log.Panicf(format, args...) }
func (l *EchoLogrusAdapter) Panicj(j glog.JSON) { log.WithFields(log.Fields(j)).Panic() }
func (l *EchoLogrusAdapter) SetHeader(h string) {}
// Enabled returns true if the specified log level is handled.
func (h *SlogLogrusHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
return log.IsLevelEnabled(slog2logrus[lvl])
}
// Handle sends a slog.Record to the log output.
func (h *SlogLogrusHandler) Handle(ctx context.Context, r slog.Record) error {
flds := make(log.Fields)
for k, v := range h.fields {
flds[h.groupPrefix+k] = v
}
r.Attrs(func(a slog.Attr) bool {
flds[h.groupPrefix+a.Key] = a.Value.Any()
return true
})
ntry := log.NewEntry(log.StandardLogger()).WithTime(r.Time).WithFields(flds)
ntry.Log(slog2logrus[r.Level], r.Message)
return nil
}
// WithAttrs creates a new Handler from this one, with extra attributes.
func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields)})
maps.Copy(newh.fields, h.fields)
for _, a := range attrs {
newh.fields[a.Key] = a.Value.Any()
}
newh.groupPrefix = h.groupPrefix
return newh
}
// WithGroup creates a new Handler from this one, with an extra group prefix.
func (h *SlogLogrusHandler) WithGroup(name string) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields)})
maps.Copy(newh.fields, h.fields)
newh.groupPrefix = h.groupPrefix + name + "."
return newh
}
/*----------------------------------------------------------------------------
* Echo middleware adapters
@@ -119,13 +105,13 @@ func (l *EchoLogrusAdapter) SetHeader(h string) {}
// LogrusMiddleware installs Logrus logging into the Echo middleware chain.
func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
start := time.Now()
err := next(c)
stop := time.Now()
req := c.Request()
res := c.Response()
res := c.Response().(*echo.Response)
log.WithFields(log.Fields{
"remote_ip": c.RealIP(),
@@ -139,17 +125,6 @@ func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
}
}
// LogrusPanicLogging is a log function hooked into the recovery middleware.
func LogrusPanicLogging(c echo.Context, err error, stack []byte) error {
log.Errorf("[PANIC RECOVERY] %v", err)
scanner := bufio.NewScanner(bytes.NewReader(stack))
for scanner.Scan() {
line := strings.ReplaceAll(scanner.Text(), "\t", " ")
log.Error(line)
}
return scanner.Err()
}
/*----------------------------------------------------------------------------
* Log output file implementation
*----------------------------------------------------------------------------
+32 -14
View File
@@ -17,7 +17,9 @@ package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
@@ -30,24 +32,26 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus"
)
// READ_HEADER_TIMEOUT is the timeout value for reading headers in seconds. (Deliberately NOT configurable because this is a security issue)
const READ_HEADER_TIMEOUT = 2
// GetAndPost is used to have functions that respond to both GET and POST on a URI.
var GetAndPost = []string{http.MethodGet, http.MethodPost}
// setupEcho creates, configures, and returns a new Echo instance.
func setupEcho() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.Logger = &EchoLogrusAdapter{}
e.Logger = slog.New(NewSlogLogrusHandler())
e.Renderer = &ui.TemplateRenderer{}
e.HTTPErrorHandler = AmErrorHandler
if !config.CommandLine.DebugPanic {
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: LogrusPanicLogging,
StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack),
}))
} else {
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!")
@@ -215,7 +219,7 @@ func setupEcho() *echo.Echo {
// ampool is the worker pool for one-shot background tasks.
var ampool *util.WorkerPool
// SystemStartTime records the time since the system was started.
// SystemStartTime records the time the system was started.
var SystemStartTime time.Time
// main is Ye Olde Main Function.
@@ -264,21 +268,35 @@ func main() {
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
}()
// Set up the start configuration.
sconf := echo.StartConfig{
Address: config.GlobalComputedConfig.Listen,
HideBanner: true,
HidePort: true,
GracefulTimeout: 10 * time.Second,
OnShutdownError: func(err error) {
log.Fatalf("error in shutting down the server: %v", err)
},
BeforeServeFunc: func(s *http.Server) error {
s.ReadTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpRead) * time.Second
s.WriteTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpWrite) * time.Second
s.IdleTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpIdle) * time.Second
s.ReadHeaderTimeout = READ_HEADER_TIMEOUT * time.Second
return nil
},
}
stime := time.Since(SystemStartTime)
log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime)
// Start server
go func() {
if err := e.Start(config.GlobalComputedConfig.Listen); err != nil && err != http.ErrServerClosed {
e.Logger.Fatalf("shutting down the server: %v", err)
if err := sconf.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("shutting down the server: %v", err)
}
}()
// Wait for the interrupt signal and then gracefully shut the server down.
// Wait for the context to be done, when the server is shut down.
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
log.Infof("Amsterdam shut down")
}
+3 -3
View File
@@ -24,7 +24,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -285,14 +285,14 @@ func PolicyPage(ctxt ui.AmContext) (string, any) {
func JumpToShortcut(ctxt ui.AmContext) (string, any) {
link, err := database.AmDecodePostLink(ctxt.URLParam("postlink"))
if err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err)
}
scope, target := link.Classify()
if scope != "global" {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink")))
}
if err = link.VerifyNames(ctxt.Ctx()); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err)
}
targetURL := ""
switch target {
+6 -6
View File
@@ -27,7 +27,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -98,7 +98,7 @@ type AmContext interface {
// amContext is the internal structure that implements AmContext.
type amContext struct {
echoContext echo.Context
echoContext *echo.Context
rendervars jet.VarMap
frameTitle string
frameMeta map[int]map[string]string
@@ -243,7 +243,7 @@ func (c *amContext) FormFieldIsSet(name string) bool {
// FormFieldValues returns all values for a specified parameter name.
func (c *amContext) FormFieldValues(name string) ([]string, error) {
vals, err := c.echoContext.FormParams()
vals, err := c.echoContext.FormValues()
if err != nil {
return make([]string, 0), err
}
@@ -525,7 +525,7 @@ var amContextRecycleBin chan *amContext
* Internal Amsterdam context structure pointer, or nil.
* Standard Go error status.
*/
func newContext(ctxt echo.Context) (*amContext, error) {
func newContext(ctxt *echo.Context) (*amContext, error) {
var rc *amContext
tmp := freeContext.Get()
if tmp == nil {
@@ -593,7 +593,7 @@ func newContext(ctxt echo.Context) (*amContext, error) {
* Returns:
* The associated AmContext.
*/
func AmContextFromEchoContext(ctxt echo.Context) AmContext {
func AmContextFromEchoContext(ctxt *echo.Context) AmContext {
myctxt := ctxt.Get("__amsterdam_context")
if myctxt != nil {
rc, ok := myctxt.(*amContext)
@@ -641,7 +641,7 @@ func setupContext() func() {
// ContextCreator is middleware that creates and recycles the AmContext.
func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
myctxt, err := newContext(c)
if err == nil {
err = next(c)
+2 -2
View File
@@ -23,7 +23,7 @@ import (
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -410,7 +410,7 @@ func setupSessionManager() func() {
// SessionStoreInjector is middleware that injects the session store into the context variables.
func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
c.Set("AmSessionStore", sessionStore)
return next(c)
}
+3 -3
View File
@@ -29,7 +29,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
)
//go:embed static_images/*
@@ -64,7 +64,7 @@ func mimeTypeFromFilename(filename string) string {
* Returns:
* Standard Go error return.
*/
func AmServeImage(c echo.Context) error {
func AmServeImage(c *echo.Context) error {
components := strings.SplitAfter(c.Request().URL.Path, "/")
var err error = nil
if len(components) == 4 {
@@ -105,7 +105,7 @@ func AmServeImage(c echo.Context) error {
* Returns:
* Standard Go error return.
*/
func AmServeVeniceCompatibleImage(c echo.Context) error {
func AmServeVeniceCompatibleImage(c *echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err == nil {
var img *database.ImageStore
+9 -9
View File
@@ -21,17 +21,17 @@ import (
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
// IPBanTest is middleware that handles the IP banning.
func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
// Check IP banning.
banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP())
if banerr != nil {
c.Logger().Warnf("address %s could not be tested: %v", c.RealIP(), banerr)
log.Warnf("address %s could not be tested: %v", c.RealIP(), banerr)
// but let the request pass anyway
} else if banmsg != "" {
amctxt := AmContextFromEchoContext(c)
@@ -43,7 +43,7 @@ func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
// CookieLoginTest is middleware that handles cookie logins.
func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
amctxt := AmContextFromEchoContext(c)
// Check for cookie login.
if amctxt.CurrentUser().IsAnon {
@@ -77,11 +77,11 @@ func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
// SetCommunity is middleware that sets the community context based on the URL.
func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil {
return AmSendPageData(c, ctxt, "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err))
return AmSendPageData(c, ctxt, "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err))
}
var b strings.Builder
b.WriteString("/comm/")
@@ -94,7 +94,7 @@ func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
// ValidateConference is middleware that validates the user has access to the community's conference facility.
func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
comm := ctxt.CurrentCommunity() // set by middleware
b, err := database.AmTestService(c.Request().Context(), comm, "Conference")
@@ -116,7 +116,7 @@ func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
// SetConference is middleware that sets the conference context based on the URL.
func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
conf, err := database.AmGetConferenceByAlias(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid"))
if err != nil {
@@ -144,7 +144,7 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
// SetTopic is middleware that sets the topic context based on the URL.
func SetTopic(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
conf := ctxt.GetScratch("currentConference").(*database.Conference)
+145 -72
View File
@@ -1,6 +1,6 @@
/*
* Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,18 +13,82 @@
package ui
import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/klauspost/lctime"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
// panicRecoveryErr is the error created for panic recovery.
type panicRecoveryErr struct {
Phase string // phase of operation
Err error // error value
Stack []byte // stack trace
}
// Error returns the actual error string.
func (e *panicRecoveryErr) Error() string {
return fmt.Sprintf("[Panic Recovery in %s Phase] %s %s", e.Phase, e.Err.Error(), e.Stack)
}
// Unwrap returns the error "nested" inside this error.
func (e *panicRecoveryErr) Unwrap() error {
return e.Err
}
// doFrameRender renders the outer frame template with an inner template.
func doFrameRender(ctxt *echo.Context, amctxt AmContext, statusCode int, innerPage string) error {
if amctxt.FrameTitle() == "" {
log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", innerPage)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu(config.GlobalConfig.Site.FixedMenuId)
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
return ctxt.Render(statusCode, config.GlobalConfig.Site.FrameTemplate, amctxt)
}
/* AmSendPageData sends page data to the output based on the command string.
* Parameters:
* ctxt - The Echo context from the request.
@@ -44,40 +108,56 @@ import (
* Returns:
* Standard Go error status.
*/
func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data any) error {
func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data any) error {
// Enable panic recovery.
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length])
}
}()
}
// Preprocess certain commands into different ones.
httprc := http.StatusOK
switch command {
case "error":
message := ""
if data == nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
} else if he, ok := data.(*echo.HTTPError); ok {
httprc = he.Code
m1 := he.Message
e1 := he.Unwrap()
if m1 == nil || m1 == "" {
if e1 == nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
message := fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
if data != nil {
if he, ok := data.(*echo.HTTPError); ok {
httprc = he.Code
m1 := he.Message
e1 := he.Unwrap()
if m1 == "" {
if e1 != nil {
message = e1.Error()
}
} else {
message = e1.Error()
if e1 == nil {
message = fmt.Sprintf("%v", m1)
} else {
message = fmt.Sprintf("%v (%v)", m1, e1)
}
}
} else if er, ok := data.(error); ok {
message = er.Error()
} else {
if e1 == nil {
message = fmt.Sprintf("%v", m1)
} else {
message = fmt.Sprintf("%v (%v)", m1, e1)
}
message = fmt.Sprintf("%v", data)
}
} else if er, ok := data.(error); ok {
message = er.Error()
} else {
message = fmt.Sprintf("%v", data)
}
if httprc < 400 {
httprc = http.StatusInternalServerError
}
amctxt.SetFrameTitle("Internal Server Error")
amctxt.SetFrameTitle(http.StatusText(httprc))
amctxt.VarMap().Set("error", message)
if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil {
amctxt.VarMap().Set("recovery", tmp)
@@ -98,6 +178,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
}
// Process commands.
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageRender)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)
var err error
switch command {
case "bytes":
@@ -113,47 +198,7 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
case "template":
err = ctxt.Render(httprc, data.(string), amctxt)
case "framed":
if amctxt.FrameTitle() == "" {
ctxt.Logger().Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", data)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu("top")
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu("top")
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu("fixed")
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
err = ctxt.Render(httprc, "frame.jet", amctxt)
err = doFrameRender(ctxt, amctxt, httprc, data.(string))
default:
err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command)
}
@@ -169,6 +214,34 @@ var expireTime string = lctime.Strftime("%c", time.Unix(1, 0))
// AmPageFunc is the definition for an Amsterdam "page function" that handles most of the work and defers to the wrapper for rendering.
type AmPageFunc func(AmContext) (string, any)
// callWrappedPageFunc calls the specified page functon inside a wrapper that handles timeouts and panic recovery.
func callWrappedPageFunc(f AmPageFunc, ctxt *echo.Context, amctxt AmContext) (command string, arg any) {
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]}
command = "error"
}
}()
}
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageExecute)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)
command, arg = f(amctxt)
return
}
/* AmWrap wraps the Amsterdam handler function in a wrapper that implements the spec for
* Echo handler functions.
* Parameters:
@@ -177,7 +250,7 @@ type AmPageFunc func(AmContext) (string, any)
* The wrapped function.
*/
func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
// Add the dynamic headers.
@@ -186,16 +259,16 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
c.Response().Header().Set("Expires", expireTime)
// Exec the wrapped function.
command, arg := myfunc(ctxt)
command, arg := callWrappedPageFunc(myfunc, c, ctxt)
if command != "error" && command != "ipban" {
ctxt.SetSession("lastKnownGood", ctxt.Locator())
}
if err := ctxt.SaveSession(); err != nil {
c.Logger().Errorf("Session save error: %v", err)
log.Errorf("Session save error: %v", err)
return err
}
if err := AmSendPageData(c, ctxt, command, arg); err != nil {
c.Logger().Errorf("Rendering error: %v", err)
log.Errorf("Rendering error: %v", err)
return err
}
return nil
@@ -203,7 +276,7 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
}
// AmWithTempContext runs a page function with a temporary context. Used in error handling.
func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
func AmWithTempContext(c *echo.Context, fn AmPageFunc) error {
var ctxt AmContext = nil
myctxt := c.Get("__amsterdam_context")
if myctxt != nil {
@@ -225,7 +298,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
}
// Call the function
command, arg := fn(ctxt)
command, arg := callWrappedPageFunc(fn, c, ctxt)
// Add the dynamic headers.
c.Response().Header().Set("Pragma", "No-cache")
@@ -233,7 +306,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
c.Response().Header().Set("Expires", expireTime)
if err := AmSendPageData(c, ctxt, command, arg); err != nil {
c.Logger().Errorf("Rendering error: %v", err)
log.Errorf("Rendering error: %v", err)
return err
}
return nil
+1 -1
View File
@@ -23,7 +23,7 @@ import (
"strings"
"git.erbosoft.com/amy/amsterdam/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
"golang.org/x/net/html"
)
+2 -2
View File
@@ -31,7 +31,7 @@ import (
"github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs"
"github.com/CloudyKit/jet/v6/loaders/multi"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -363,7 +363,7 @@ type TemplateRenderer struct{}
* Returns:
* Standard Go error status.
*/
func (r *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
func (r *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error {
defer util.MeasureTime(fmt.Sprintf("ui.Render(%s)", name))()
view, err := views.GetTemplate(name)
+2 -2
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -382,7 +382,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any) {
// Gather the info on the current user.
user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil)
if err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
ci, err := user.ContactInfo(ctxt.Ctx())
if err != nil {