7 Commits

21 changed files with 358 additions and 301 deletions
+9 -43
View File
@@ -20,48 +20,14 @@ jobs:
GOOS=windows GOARCH=amd64 go build -o amsterdam-windows-amd64.exe GOOS=windows GOARCH=amd64 go build -o amsterdam-windows-amd64.exe
GOOS=darwin GOARCH=arm64 go build -o amsterdam-macos-arm64 GOOS=darwin GOARCH=arm64 go build -o amsterdam-macos-arm64
GOOS=darwin GOARCH=amd64 go build -o amsterdam-macos-amd64 GOOS=darwin GOARCH=amd64 go build -o amsterdam-macos-amd64
- name: Upload Linux AMD64 Binary - name: Upload Assets to Release
uses: actions/upload-release-asset@v1 uses: softprops/action-gh-release@v2
with:
files: |
amsterdam-linux-amd64
amsterdam-linux-arm64
amsterdam-windows-amd64.exe
amsterdam-macos-arm64
amsterdam-macos-amd64
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./amsterdam-linux-amd64
asset_name: amsterdam-linux-amd64
asset_content_type: application/octet-stream
- name: Upload Linux ARM64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./amsterdam-linux-arm64
asset_name: amsterdam-linux-arm64
asset_content_type: application/octet-stream
- name: Upload Windows Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./amsterdam-windows-amd64.exe
asset_name: amsterdam-windows-amd64.exe
asset_content_type: application/vnd.microsoft.portable-executable
- name: Upload Mac ARM64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./amsterdam-macos-arm64
asset_name: amsterdam-macos-arm64
asset_content_type: application/octet-stream
- name: Upload Mac AMD64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./amsterdam-macos-amd64
asset_name: amsterdam-macos-amd64
asset_content_type: application/octet-stream
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries" "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. // 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/exports"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" 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/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -513,11 +513,11 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
resetLastRead := false resetLastRead := false
if ctxt.HasParameter("r") { if ctxt.HasParameter("r") {
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil { 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") { } else if ctxt.HasParameter("rgo") {
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil { 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 { } else {
postRange[0] = lastRead + 1 postRange[0] = lastRead + 1
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+22 -3
View File
@@ -100,7 +100,10 @@ type AmConfig struct {
SessionExpire string `yaml:"sessionExpire"` SessionExpire string `yaml:"sessionExpire"`
UserAgreementResource string `yaml:"userAgreementResource"` UserAgreementResource string `yaml:"userAgreementResource"`
PolicyResource string `yaml:"policyResource"` PolicyResource string `yaml:"policyResource"`
FrameTemplate string `yaml:"frameTemplate"`
FooterTemplate string `yaml:"footerTemplate"` FooterTemplate string `yaml:"footerTemplate"`
TopMenuId string `yaml:"topMenuId"`
FixedMenuId string `yaml:"fixedMenuId"`
DefaultCommunityLogo string `yaml:"defaultCommunityLogo"` DefaultCommunityLogo string `yaml:"defaultCommunityLogo"`
DefaultUserPhoto string `yaml:"defaultUserPhoto"` DefaultUserPhoto string `yaml:"defaultUserPhoto"`
WelcomeTitle string `yaml:"welcomeTitle"` WelcomeTitle string `yaml:"welcomeTitle"`
@@ -148,6 +151,9 @@ type AmConfig struct {
Prioritize string `yaml:"prioritize"` Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"` } `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"` VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
PanicRecovery struct {
StackDataSize string `yaml:"stackDataSize"`
} `yaml:"panicRecovery"`
} `yaml:"rendering"` } `yaml:"rendering"`
Resources struct { Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"` ViewTemplateDir string `yaml:"viewTemplateDir"`
@@ -167,6 +173,13 @@ type AmConfig struct {
} `yaml:"posting"` } `yaml:"posting"`
Tuning struct { Tuning struct {
WorkerTasks int `yaml:"workerTasks"` WorkerTasks int `yaml:"workerTasks"`
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 { Queues struct {
AuditWrites int `yaml:"auditWrites"` AuditWrites int `yaml:"auditWrites"`
ContextRecycle int `yaml:"contextRecycle"` ContextRecycle int `yaml:"contextRecycle"`
@@ -200,7 +213,7 @@ func (c *AmConfig) ExPath(path string) string {
return filepath.Join(c.baseDir, path) 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 { type AmConfigComputed struct {
DebugMode bool // are we in debug mode? DebugMode bool // are we in debug mode?
LogLevel string // the logging level LogLevel string // the logging level
@@ -216,6 +229,7 @@ type AmConfigComputed struct {
MailAuthType string // SMTP auth type MailAuthType string // SMTP auth type
MailUser string // SMTP user name MailUser string // SMTP user name
MailPassword string // SMTP password MailPassword string // SMTP password
PanicRecoveryStack int32 // stack size for panic recovery
UploadMaxSize int32 // maximum upload size in bytes UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed? UploadNoCompress map[string]bool // which upload types are not compressed?
} }
@@ -321,7 +335,7 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
} }
} else { } else {
// if we see this message, this function needs more work // 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.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType)
GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User) GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User)
GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password) 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 { if err != nil {
panic(err.Error()) panic(err.Error())
} }
+11
View File
@@ -25,7 +25,10 @@ site:
sessionExpire: "3h" sessionExpire: "3h"
userAgreementResource: "useragreement.html" userAgreementResource: "useragreement.html"
policyResource: "policy.html" policyResource: "policy.html"
frameTemplate: "frame.jet"
footerTemplate: "footer.jet" footerTemplate: "footer.jet"
topMenuId: "top"
fixedMenuId: "fixed"
defaultCommunityLogo: "/img/builtin/default-community.jpg" defaultCommunityLogo: "/img/builtin/default-community.jpg"
defaultUserPhoto: "/img/builtin/no-user.png" defaultUserPhoto: "/img/builtin/no-user.png"
welcomeTitle: "Welcome to Amsterdam" welcomeTitle: "Welcome to Amsterdam"
@@ -72,6 +75,8 @@ rendering:
countryList: countryList:
prioritize: US prioritize: US
veniceCompatibleImageURLs: false veniceCompatibleImageURLs: false
panicRecovery:
stackDataSize: "4 KiB"
resources: resources:
viewTemplateDir: "" viewTemplateDir: ""
dialogTemplateDir: "" dialogTemplateDir: ""
@@ -91,6 +96,12 @@ posting:
- "image/png" - "image/png"
tuning: tuning:
workerTasks: 4 workerTasks: 4
timeouts:
httpRead: 30
httpWrite: 30
httpIdle: 120
pageExecute: 15
pageRender: 15
queues: queues:
auditWrites: 16 auditWrites: 16
contextRecycle: 16 contextRecycle: 16
+7 -8
View File
@@ -19,10 +19,9 @@ import (
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
) )
// EBUTTON is the standard error for an unknown button. // 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. * err - The error to be handled.
* c - The Echo context error is being handled on. * 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) log.Infof("-> AmErrorHandler on path %s", c.Request().URL.Path)
if c.Response().Committed { if c.Response().(*echo.Response).Committed {
return return
} }
cerr := ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) { 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). // 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 ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
return "error", err return "error", err
}) })
} }
// rateLimitDenyHandler is called if the rate limit is exceeded by a connection. // 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) { return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("identifier", identifier) ctxt.VarMap().Set("identifier", identifier)
return "ratelimit", err return "ratelimit", err
@@ -105,7 +104,7 @@ func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
// AmSetupRateLimiter sets up the rate-limiting middleware. // AmSetupRateLimiter sets up the rate-limiting middleware.
func AmSetupRateLimiter() echo.MiddlewareFunc { func AmSetupRateLimiter() echo.MiddlewareFunc {
rcfg := middleware.RateLimiterMemoryStoreConfig{ rcfg := middleware.RateLimiterMemoryStoreConfig{
Rate: rate.Limit(config.GlobalConfig.Site.RateLimit.Rate), Rate: config.GlobalConfig.Site.RateLimit.Rate,
Burst: config.GlobalConfig.Site.RateLimit.Burst, Burst: config.GlobalConfig.Site.RateLimit.Burst,
ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute, 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/hashicorp/golang-lru v1.0.2
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/klauspost/lctime v0.1.0 github.com/klauspost/lctime v0.1.0
github.com/labstack/echo/v4 v4.15.1 github.com/labstack/echo/v5 v5.1.1
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.5.0
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/tkuchiki/go-timezone v0.2.3 github.com/tkuchiki/go-timezone v0.2.3
golang.org/x/net v0.52.0 golang.org/x/net v0.53.0
golang.org/x/text v0.35.0 golang.org/x/text v0.36.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -29,10 +28,10 @@ require (
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // 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/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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/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/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 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= 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/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= 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/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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 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.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 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= 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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= 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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+62 -87
View File
@@ -13,104 +13,90 @@
package main package main
import ( import (
"bufio"
"bytes"
"compress/gzip" "compress/gzip"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
glog "github.com/labstack/gommon/log"
log "github.com/sirupsen/logrus" 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. // slog2logrus converts slog levels to Logrus levels.
* Parameters: var slog2logrus = map[slog.Level]log.Level{
* l - The Logrus log level to be converted. slog.LevelDebug: log.DebugLevel,
* Returns: slog.LevelInfo: log.InfoLevel,
* The equivalent glog log level. slog.LevelWarn: log.WarnLevel,
*/ slog.LevelError: log.ErrorLevel,
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
}
} }
/* fromglog converts a glog logging level to a Logrus one. // SlogLogrusHandler implements slog.Handler and routes to Logrus.
* Parameters: type SlogLogrusHandler struct {
* l - The glog log level to be converted. fields log.Fields // fields defined in this handler
* Returns: groupPrefix string // group prefix
* 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
}
} }
// EchoLogrusAdapter implements echo.Logger using logrus. // NewSlogLogrusHandler creates a SlogLogrusHandler with base information.
type EchoLogrusAdapter struct{} func NewSlogLogrusHandler() *SlogLogrusHandler {
rc := new(SlogLogrusHandler{
fields: make(log.Fields),
groupPrefix: "",
})
return rc
}
func (l *EchoLogrusAdapter) Output() io.Writer { return log.StandardLogger().Out } // Enabled returns true if the specified log level is handled.
func (l *EchoLogrusAdapter) SetOutput(w io.Writer) { log.SetOutput(w) } func (h *SlogLogrusHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
func (l *EchoLogrusAdapter) Prefix() string { return "" } return log.IsLevelEnabled(slog2logrus[lvl])
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)) } // Handle sends a slog.Record to the log output.
func (l *EchoLogrusAdapter) Print(i ...any) { log.Print(i...) } func (h *SlogLogrusHandler) Handle(ctx context.Context, r slog.Record) error {
func (l *EchoLogrusAdapter) Printf(format string, args ...any) { log.Printf(format, args...) } flds := make(log.Fields)
func (l *EchoLogrusAdapter) Printj(j glog.JSON) { log.WithFields(log.Fields(j)).Print() } for k, v := range h.fields {
func (l *EchoLogrusAdapter) Debug(i ...any) { log.Debug(i...) } flds[h.groupPrefix+k] = v
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() } r.Attrs(func(a slog.Attr) bool {
func (l *EchoLogrusAdapter) Info(i ...any) { log.Info(i...) } flds[h.groupPrefix+a.Key] = a.Value.Any()
func (l *EchoLogrusAdapter) Infof(format string, args ...any) { log.Infof(format, args...) } return true
func (l *EchoLogrusAdapter) Infoj(j glog.JSON) { log.WithFields(log.Fields(j)).Info() } })
func (l *EchoLogrusAdapter) Warn(i ...any) { log.Warn(i...) } ntry := log.NewEntry(log.StandardLogger()).WithTime(r.Time).WithFields(flds)
func (l *EchoLogrusAdapter) Warnf(format string, args ...any) { log.Warnf(format, args...) } ntry.Log(slog2logrus[r.Level], r.Message)
func (l *EchoLogrusAdapter) Warnj(j glog.JSON) { log.WithFields(log.Fields(j)).Warn() } return nil
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() } // WithAttrs creates a new Handler from this one, with extra attributes.
func (l *EchoLogrusAdapter) Fatal(i ...any) { log.Fatal(i...) } func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
func (l *EchoLogrusAdapter) Fatalf(format string, args ...any) { log.Fatalf(format, args...) } newh := new(SlogLogrusHandler{fields: make(log.Fields)})
func (l *EchoLogrusAdapter) Fatalj(j glog.JSON) { log.WithFields(log.Fields(j)).Fatal() } maps.Copy(newh.fields, h.fields)
func (l *EchoLogrusAdapter) Panic(i ...any) { log.Panic(i...) } for _, a := range attrs {
func (l *EchoLogrusAdapter) Panicf(format string, args ...any) { log.Panicf(format, args...) } newh.fields[a.Key] = a.Value.Any()
func (l *EchoLogrusAdapter) Panicj(j glog.JSON) { log.WithFields(log.Fields(j)).Panic() } }
func (l *EchoLogrusAdapter) SetHeader(h string) {} 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 * Echo middleware adapters
@@ -119,13 +105,13 @@ func (l *EchoLogrusAdapter) SetHeader(h string) {}
// LogrusMiddleware installs Logrus logging into the Echo middleware chain. // LogrusMiddleware installs Logrus logging into the Echo middleware chain.
func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc { func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
start := time.Now() start := time.Now()
err := next(c) err := next(c)
stop := time.Now() stop := time.Now()
req := c.Request() req := c.Request()
res := c.Response() res := c.Response().(*echo.Response)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"remote_ip": c.RealIP(), "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 * Log output file implementation
*---------------------------------------------------------------------------- *----------------------------------------------------------------------------
+32 -14
View File
@@ -17,7 +17,9 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@@ -30,24 +32,26 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck" "git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus" 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. // GetAndPost is used to have functions that respond to both GET and POST on a URI.
var GetAndPost = []string{http.MethodGet, http.MethodPost} var GetAndPost = []string{http.MethodGet, http.MethodPost}
// setupEcho creates, configures, and returns a new Echo instance. // setupEcho creates, configures, and returns a new Echo instance.
func setupEcho() *echo.Echo { func setupEcho() *echo.Echo {
e := echo.New() e := echo.New()
e.HideBanner = true e.Logger = slog.New(NewSlogLogrusHandler())
e.Logger = &EchoLogrusAdapter{}
e.Renderer = &ui.TemplateRenderer{} e.Renderer = &ui.TemplateRenderer{}
e.HTTPErrorHandler = AmErrorHandler e.HTTPErrorHandler = AmErrorHandler
if !config.CommandLine.DebugPanic { if !config.CommandLine.DebugPanic {
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: LogrusPanicLogging, StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack),
})) }))
} else { } else {
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!") 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. // ampool is the worker pool for one-shot background tasks.
var ampool *util.WorkerPool 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 var SystemStartTime time.Time
// main is Ye Olde Main Function. // main is Ye Olde Main Function.
@@ -264,21 +268,35 @@ func main() {
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String())) 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) stime := time.Since(SystemStartTime)
log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime) log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime)
// Start server // Start server
go func() { go func() {
if err := e.Start(config.GlobalComputedConfig.Listen); err != nil && err != http.ErrServerClosed { if err := sconf.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
e.Logger.Fatalf("shutting down the server: %v", err) 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.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) log.Infof("Amsterdam shut down")
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
} }
+3 -3
View File
@@ -24,7 +24,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -285,14 +285,14 @@ func PolicyPage(ctxt ui.AmContext) (string, any) {
func JumpToShortcut(ctxt ui.AmContext) (string, any) { func JumpToShortcut(ctxt ui.AmContext) (string, any) {
link, err := database.AmDecodePostLink(ctxt.URLParam("postlink")) link, err := database.AmDecodePostLink(ctxt.URLParam("postlink"))
if err != nil { 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() scope, target := link.Classify()
if scope != "global" { if scope != "global" {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))) return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink")))
} }
if err = link.VerifyNames(ctxt.Ctx()); err != nil { 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 := "" targetURL := ""
switch target { switch target {
+6 -6
View File
@@ -27,7 +27,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -98,7 +98,7 @@ type AmContext interface {
// amContext is the internal structure that implements AmContext. // amContext is the internal structure that implements AmContext.
type amContext struct { type amContext struct {
echoContext echo.Context echoContext *echo.Context
rendervars jet.VarMap rendervars jet.VarMap
frameTitle string frameTitle string
frameMeta map[int]map[string]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. // FormFieldValues returns all values for a specified parameter name.
func (c *amContext) FormFieldValues(name string) ([]string, error) { func (c *amContext) FormFieldValues(name string) ([]string, error) {
vals, err := c.echoContext.FormParams() vals, err := c.echoContext.FormValues()
if err != nil { if err != nil {
return make([]string, 0), err return make([]string, 0), err
} }
@@ -525,7 +525,7 @@ var amContextRecycleBin chan *amContext
* Internal Amsterdam context structure pointer, or nil. * Internal Amsterdam context structure pointer, or nil.
* Standard Go error status. * Standard Go error status.
*/ */
func newContext(ctxt echo.Context) (*amContext, error) { func newContext(ctxt *echo.Context) (*amContext, error) {
var rc *amContext var rc *amContext
tmp := freeContext.Get() tmp := freeContext.Get()
if tmp == nil { if tmp == nil {
@@ -593,7 +593,7 @@ func newContext(ctxt echo.Context) (*amContext, error) {
* Returns: * Returns:
* The associated AmContext. * The associated AmContext.
*/ */
func AmContextFromEchoContext(ctxt echo.Context) AmContext { func AmContextFromEchoContext(ctxt *echo.Context) AmContext {
myctxt := ctxt.Get("__amsterdam_context") myctxt := ctxt.Get("__amsterdam_context")
if myctxt != nil { if myctxt != nil {
rc, ok := myctxt.(*amContext) rc, ok := myctxt.(*amContext)
@@ -641,7 +641,7 @@ func setupContext() func() {
// ContextCreator is middleware that creates and recycles the AmContext. // ContextCreator is middleware that creates and recycles the AmContext.
func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc { func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
myctxt, err := newContext(c) myctxt, err := newContext(c)
if err == nil { if err == nil {
err = next(c) err = next(c)
+2 -2
View File
@@ -23,7 +23,7 @@ import (
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -410,7 +410,7 @@ func setupSessionManager() func() {
// SessionStoreInjector is middleware that injects the session store into the context variables. // SessionStoreInjector is middleware that injects the session store into the context variables.
func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc { func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
c.Set("AmSessionStore", sessionStore) c.Set("AmSessionStore", sessionStore)
return next(c) return next(c)
} }
+3 -3
View File
@@ -29,7 +29,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
//go:embed static_images/* //go:embed static_images/*
@@ -64,7 +64,7 @@ func mimeTypeFromFilename(filename string) string {
* Returns: * Returns:
* Standard Go error return. * Standard Go error return.
*/ */
func AmServeImage(c echo.Context) error { func AmServeImage(c *echo.Context) error {
components := strings.SplitAfter(c.Request().URL.Path, "/") components := strings.SplitAfter(c.Request().URL.Path, "/")
var err error = nil var err error = nil
if len(components) == 4 { if len(components) == 4 {
@@ -105,7 +105,7 @@ func AmServeImage(c echo.Context) error {
* Returns: * Returns:
* Standard Go error return. * Standard Go error return.
*/ */
func AmServeVeniceCompatibleImage(c echo.Context) error { func AmServeVeniceCompatibleImage(c *echo.Context) error {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err == nil { if err == nil {
var img *database.ImageStore var img *database.ImageStore
+9 -9
View File
@@ -21,17 +21,17 @@ import (
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// IPBanTest is middleware that handles the IP banning. // IPBanTest is middleware that handles the IP banning.
func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc { func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
// Check IP banning. // Check IP banning.
banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP()) banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP())
if banerr != nil { 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 // but let the request pass anyway
} else if banmsg != "" { } else if banmsg != "" {
amctxt := AmContextFromEchoContext(c) amctxt := AmContextFromEchoContext(c)
@@ -43,7 +43,7 @@ func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
// CookieLoginTest is middleware that handles cookie logins. // CookieLoginTest is middleware that handles cookie logins.
func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc { func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
amctxt := AmContextFromEchoContext(c) amctxt := AmContextFromEchoContext(c)
// Check for cookie login. // Check for cookie login.
if amctxt.CurrentUser().IsAnon { 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. // SetCommunity is middleware that sets the community context based on the URL.
func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc { func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
err := ctxt.SetCommunityContext(ctxt.URLParam("cid")) err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil { 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 var b strings.Builder
b.WriteString("/comm/") 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. // ValidateConference is middleware that validates the user has access to the community's conference facility.
func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc { func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
comm := ctxt.CurrentCommunity() // set by middleware comm := ctxt.CurrentCommunity() // set by middleware
b, err := database.AmTestService(c.Request().Context(), comm, "Conference") 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. // SetConference is middleware that sets the conference context based on the URL.
func SetConference(next echo.HandlerFunc) echo.HandlerFunc { func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
conf, err := database.AmGetConferenceByAlias(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid")) conf, err := database.AmGetConferenceByAlias(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid"))
if err != nil { 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. // SetTopic is middleware that sets the topic context based on the URL.
func SetTopic(next echo.HandlerFunc) echo.HandlerFunc { func SetTopic(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
+133 -60
View File
@@ -1,6 +1,6 @@
/* /*
* Amsterdam Web Communities System * 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 * 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,18 +13,82 @@
package ui package ui
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"runtime"
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/klauspost/lctime" "github.com/klauspost/lctime"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" 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. /* AmSendPageData sends page data to the output based on the command string.
* Parameters: * Parameters:
* ctxt - The Echo context from the request. * ctxt - The Echo context from the request.
@@ -44,22 +108,37 @@ import (
* Returns: * Returns:
* Standard Go error status. * 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. // Preprocess certain commands into different ones.
httprc := http.StatusOK httprc := http.StatusOK
switch command { switch command {
case "error": case "error":
message := "" message := fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
if data == nil { if data != nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String()) if he, ok := data.(*echo.HTTPError); ok {
} else if he, ok := data.(*echo.HTTPError); ok {
httprc = he.Code httprc = he.Code
m1 := he.Message m1 := he.Message
e1 := he.Unwrap() e1 := he.Unwrap()
if m1 == nil || m1 == "" { if m1 == "" {
if e1 == nil { if e1 != nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
} else {
message = e1.Error() message = e1.Error()
} }
} else { } else {
@@ -74,10 +153,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
} else { } else {
message = fmt.Sprintf("%v", data) message = fmt.Sprintf("%v", data)
} }
}
if httprc < 400 { if httprc < 400 {
httprc = http.StatusInternalServerError httprc = http.StatusInternalServerError
} }
amctxt.SetFrameTitle("Internal Server Error") amctxt.SetFrameTitle(http.StatusText(httprc))
amctxt.VarMap().Set("error", message) amctxt.VarMap().Set("error", message)
if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil { if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil {
amctxt.VarMap().Set("recovery", tmp) amctxt.VarMap().Set("recovery", tmp)
@@ -98,6 +178,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
} }
// Process commands. // 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 var err error
switch command { switch command {
case "bytes": case "bytes":
@@ -113,47 +198,7 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
case "template": case "template":
err = ctxt.Render(httprc, data.(string), amctxt) err = ctxt.Render(httprc, data.(string), amctxt)
case "framed": case "framed":
if amctxt.FrameTitle() == "" { err = doFrameRender(ctxt, amctxt, httprc, data.(string))
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)
default: default:
err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command) 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. // 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) 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 /* AmWrap wraps the Amsterdam handler function in a wrapper that implements the spec for
* Echo handler functions. * Echo handler functions.
* Parameters: * Parameters:
@@ -177,7 +250,7 @@ type AmPageFunc func(AmContext) (string, any)
* The wrapped function. * The wrapped function.
*/ */
func AmWrap(myfunc AmPageFunc) echo.HandlerFunc { func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
// Add the dynamic headers. // Add the dynamic headers.
@@ -186,16 +259,16 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
c.Response().Header().Set("Expires", expireTime) c.Response().Header().Set("Expires", expireTime)
// Exec the wrapped function. // Exec the wrapped function.
command, arg := myfunc(ctxt) command, arg := callWrappedPageFunc(myfunc, c, ctxt)
if command != "error" && command != "ipban" { if command != "error" && command != "ipban" {
ctxt.SetSession("lastKnownGood", ctxt.Locator()) ctxt.SetSession("lastKnownGood", ctxt.Locator())
} }
if err := ctxt.SaveSession(); err != nil { if err := ctxt.SaveSession(); err != nil {
c.Logger().Errorf("Session save error: %v", err) log.Errorf("Session save error: %v", err)
return err return err
} }
if err := AmSendPageData(c, ctxt, command, arg); err != nil { 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 err
} }
return nil 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. // 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 var ctxt AmContext = nil
myctxt := c.Get("__amsterdam_context") myctxt := c.Get("__amsterdam_context")
if myctxt != nil { if myctxt != nil {
@@ -225,7 +298,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
} }
// Call the function // Call the function
command, arg := fn(ctxt) command, arg := callWrappedPageFunc(fn, c, ctxt)
// Add the dynamic headers. // Add the dynamic headers.
c.Response().Header().Set("Pragma", "No-cache") 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) c.Response().Header().Set("Expires", expireTime)
if err := AmSendPageData(c, ctxt, command, arg); err != nil { 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 err
} }
return nil return nil
+1 -1
View File
@@ -23,7 +23,7 @@ import (
"strings" "strings"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
+2 -2
View File
@@ -31,7 +31,7 @@ import (
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs" "github.com/CloudyKit/jet/v6/loaders/embedfs"
"github.com/CloudyKit/jet/v6/loaders/multi" "github.com/CloudyKit/jet/v6/loaders/multi"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -363,7 +363,7 @@ type TemplateRenderer struct{}
* Returns: * Returns:
* Standard Go error status. * 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))() defer util.MeasureTime(fmt.Sprintf("ui.Render(%s)", name))()
view, err := views.GetTemplate(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/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries" "github.com/biter777/countries"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -382,7 +382,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any) {
// Gather the info on the current user. // Gather the info on the current user.
user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil) user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil)
if err != 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()) ci, err := user.ContactInfo(ctxt.Ctx())
if err != nil { if err != nil {