pkg/httpx => github.com/tkw1536/pkglib/httpx
This commit is contained in:
parent
5e89fadeeb
commit
010fd536ea
35 changed files with 37 additions and 1081 deletions
4
go.mod
4
go.mod
|
|
@ -9,12 +9,10 @@ require (
|
||||||
github.com/go-sql-driver/mysql v1.7.0
|
github.com/go-sql-driver/mysql v1.7.0
|
||||||
github.com/gorilla/csrf v1.7.1
|
github.com/gorilla/csrf v1.7.1
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/rs/zerolog v1.29.0
|
||||||
github.com/tdewolff/minify v2.3.6+incompatible
|
|
||||||
github.com/tkw1536/goprogram v0.3.0
|
github.com/tkw1536/goprogram v0.3.0
|
||||||
github.com/tkw1536/pkglib v0.0.0-20230225192547-93a1aa42a292
|
github.com/tkw1536/pkglib v0.0.0-20230225192547-93a1aa42a292
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.5.4
|
||||||
|
|
@ -34,12 +32,14 @@ require (
|
||||||
github.com/boombuler/barcode v1.0.1 // indirect
|
github.com/boombuler/barcode v1.0.1 // indirect
|
||||||
github.com/feiin/sqlstring v0.3.0 // indirect
|
github.com/feiin/sqlstring v0.3.0 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/gosuri/uilive v0.0.4 // indirect
|
github.com/gosuri/uilive v0.0.4 // indirect
|
||||||
github.com/jessevdk/go-flags v1.5.0 // indirect
|
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/tdewolff/minify v2.3.6+incompatible // indirect
|
||||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
|
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Next struct {
|
type Next struct {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserPanel struct {
|
type UserPanel struct {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import (
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed "templates/password.html"
|
//go:embed "templates/password.html"
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
|
||||||
"github.com/gliderlabs/ssh"
|
"github.com/gliderlabs/ssh"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
|
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/tkw1536/pkglib/lazy"
|
"github.com/tkw1536/pkglib/lazy"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/tkw1536/pkglib/lifetime"
|
"github.com/tkw1536/pkglib/lifetime"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Admin struct {
|
type Admin struct {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/tkw1536/pkglib/lifetime"
|
"github.com/tkw1536/pkglib/lifetime"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed "html/anal.html"
|
//go:embed "html/anal.html"
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
|
@ -12,8 +11,10 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed "html/instance.html"
|
//go:embed "html/instance.html"
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/tkw1536/goprogram/status"
|
"github.com/tkw1536/goprogram/status"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Sockets struct {
|
type Sockets struct {
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
|
"github.com/tkw1536/pkglib/httpx/field"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed "html/users.html"
|
//go:embed "html/users.html"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/tkw1536/pkglib/pools"
|
"github.com/tkw1536/pkglib/pools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
@ -41,8 +41,8 @@ var faviconRoute = httpx.Response{
|
||||||
|
|
||||||
var logoSVGRoute = httpx.Response{
|
var logoSVGRoute = httpx.Response{
|
||||||
ContentType: "image/svg+xml",
|
ContentType: "image/svg+xml",
|
||||||
Body: httpx.MinifySVG(logoSVG),
|
Body: logoSVG,
|
||||||
}
|
}.Minify()
|
||||||
|
|
||||||
func (*Logo) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
|
func (*Logo) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||||
"github.com/tkw1536/pkglib/contextx"
|
"github.com/tkw1536/pkglib/contextx"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/tkw1536/pkglib/mux"
|
"github.com/tkw1536/pkglib/mux"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
"github.com/tkw1536/pkglib/pools"
|
"github.com/tkw1536/pkglib/pools"
|
||||||
"github.com/tkw1536/pkglib/timex"
|
"github.com/tkw1536/pkglib/timex"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrInterceptor intercepts errors and directly returns specific responses for them
|
|
||||||
type ErrInterceptor struct {
|
|
||||||
Errors map[error]Response
|
|
||||||
Fallback Response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intercept attempts to intercept the given error.
|
|
||||||
// When err is nil, does nothing.
|
|
||||||
//
|
|
||||||
// When err is not nil, first attempts to find a static response in errors and respond with that.
|
|
||||||
// Otherwise it returns the Fallback response.
|
|
||||||
// intercepted indicates if some response was sent.
|
|
||||||
func (ei ErrInterceptor) Intercept(w http.ResponseWriter, r *http.Request, err error) (intercepted bool) {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
res, ok := ei.Errors[err]
|
|
||||||
if !ok {
|
|
||||||
res = ei.Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusInterceptor creates a new ErrInterceptor handling default responses.
|
|
||||||
// If body returns err != nil, StatusInterceptor calls panic().
|
|
||||||
func StatusInterceptor(contentType string, body func(code int, text string) ([]byte, error)) ErrInterceptor {
|
|
||||||
makeResponse := func(code int) (res Response) {
|
|
||||||
var err error
|
|
||||||
res.Body, err = body(code, http.StatusText(code))
|
|
||||||
if err != nil {
|
|
||||||
panic("StatusInterceptor: err != nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
res.ContentType = contentType
|
|
||||||
res.StatusCode = code
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrInterceptor{
|
|
||||||
Errors: map[error]Response{
|
|
||||||
ErrInternalServerError: makeResponse(http.StatusInternalServerError),
|
|
||||||
ErrBadRequest: makeResponse(http.StatusBadRequest),
|
|
||||||
ErrNotFound: makeResponse(http.StatusNotFound),
|
|
||||||
ErrForbidden: makeResponse(http.StatusForbidden),
|
|
||||||
ErrMethodNotAllowed: makeResponse(http.StatusMethodNotAllowed),
|
|
||||||
},
|
|
||||||
Fallback: makeResponse(http.StatusInternalServerError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common errors accepted by all httpx handlers
|
|
||||||
var (
|
|
||||||
ErrInternalServerError = errors.New("httpx: Internal Server Error")
|
|
||||||
ErrBadRequest = errors.New("httpx: Bad Request")
|
|
||||||
ErrNotFound = errors.New("httpx: Not Found")
|
|
||||||
ErrForbidden = errors.New("httpx: Forbidden")
|
|
||||||
ErrMethodNotAllowed = errors.New("httpx: Method Not Allowed")
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
TextInterceptor = StatusInterceptor("text/plain", func(code int, text string) ([]byte, error) {
|
|
||||||
return []byte(text), nil
|
|
||||||
})
|
|
||||||
JSONInterceptor = StatusInterceptor("application/json", func(code int, text string) ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]any{"status": text, "code": code})
|
|
||||||
})
|
|
||||||
HTMLInterceptor = StatusInterceptor("text/html", func(code int, text string) ([]byte, error) {
|
|
||||||
return MinifyHTML([]byte(`<!DOCTYPE HTML><title>` + text + `</title>` + text)), nil
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package field
|
|
||||||
|
|
||||||
// Autocomplete represents different autocomplete options
|
|
||||||
type Autocomplete string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Off Autocomplete = "off"
|
|
||||||
On Autocomplete = "on"
|
|
||||||
Name Autocomplete = "name"
|
|
||||||
HonorificPrefix Autocomplete = "honorific-prefix"
|
|
||||||
GivenName Autocomplete = "given-name"
|
|
||||||
AdditionalName Autocomplete = "additional-name"
|
|
||||||
FamilyName Autocomplete = "family-name"
|
|
||||||
HonorificSuffix Autocomplete = "honorific-suffix"
|
|
||||||
Nickname Autocomplete = "nickname"
|
|
||||||
Email_ Autocomplete = "email"
|
|
||||||
Username Autocomplete = "username"
|
|
||||||
NewPassword Autocomplete = "new-password"
|
|
||||||
CurrentPassword Autocomplete = "current-password"
|
|
||||||
OneTimeCode Autocomplete = "one-time-code"
|
|
||||||
OrganizationTitle Autocomplete = "organization-title"
|
|
||||||
Organization Autocomplete = "organization"
|
|
||||||
AddressLine1 Autocomplete = "address-line1"
|
|
||||||
AddressLine2 Autocomplete = "address-line2"
|
|
||||||
AddressLine3 Autocomplete = "address-line3"
|
|
||||||
StreetAddress Autocomplete = "street-address"
|
|
||||||
AddressLevel4 Autocomplete = "address-level4"
|
|
||||||
AddressLevel3 Autocomplete = "address-level3"
|
|
||||||
AddressLevel2 Autocomplete = "address-level2"
|
|
||||||
AddressLevel1 Autocomplete = "address-level1"
|
|
||||||
Country Autocomplete = "country"
|
|
||||||
CountryName Autocomplete = "country-name"
|
|
||||||
PostalCode Autocomplete = "postal-code"
|
|
||||||
CcName Autocomplete = "cc-name"
|
|
||||||
CcGivenName Autocomplete = "cc-given-name"
|
|
||||||
CcAdditionalName Autocomplete = "cc-additional-name"
|
|
||||||
CcFamilyName Autocomplete = "cc-family-name"
|
|
||||||
CcNumber Autocomplete = "cc-number"
|
|
||||||
CcExp Autocomplete = "cc-exp"
|
|
||||||
CcExpMonth Autocomplete = "cc-exp-month"
|
|
||||||
CcExpYear Autocomplete = "cc-exp-year"
|
|
||||||
CcCsc Autocomplete = "cc-csc"
|
|
||||||
CcType Autocomplete = "cc-type"
|
|
||||||
TransactionCurrency Autocomplete = "transaction-currency"
|
|
||||||
TransactionAmount Autocomplete = "transaction-amount"
|
|
||||||
Language Autocomplete = "language"
|
|
||||||
Bday Autocomplete = "bday"
|
|
||||||
BdayDay Autocomplete = "bday-day"
|
|
||||||
BdayMonth Autocomplete = "bday-month"
|
|
||||||
BdayYear Autocomplete = "bday-year"
|
|
||||||
Sex Autocomplete = "sex"
|
|
||||||
Tel_ Autocomplete = "tel"
|
|
||||||
TelCountryCode Autocomplete = "tel-country-code"
|
|
||||||
TelNational Autocomplete = "tel-national"
|
|
||||||
TelAreaCode Autocomplete = "tel-area-code"
|
|
||||||
TelLocal Autocomplete = "tel-local"
|
|
||||||
TelLocalPrefix Autocomplete = "tel-local-prefix"
|
|
||||||
TelLocalSuffix Autocomplete = "tel-local-suffix"
|
|
||||||
TelExtension Autocomplete = "tel-extension"
|
|
||||||
Impp Autocomplete = "impp"
|
|
||||||
Url_ Autocomplete = "url"
|
|
||||||
Photo Autocomplete = "photo"
|
|
||||||
)
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package field
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultFieldTemplate is the default template to render fields.
|
|
||||||
var DefaultFieldTemplate = template.Must(template.New("").Parse(`
|
|
||||||
{{ if (eq .Type "textarea" )}}
|
|
||||||
<textarea name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>{{.Value}}</textarea>
|
|
||||||
{{ else }}
|
|
||||||
<input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" placeholder={{.Placeholder}}{{if .Autocomplete }} autocomplete="{{.Autocomplete}}{{end}}>
|
|
||||||
{{ end }}`))
|
|
||||||
|
|
||||||
var PureCSSFieldTemplate = template.Must(template.New("").Parse(`
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="{{.Name}}">{{.Label}}</label>
|
|
||||||
{{ if (eq .Type "textarea" )}}
|
|
||||||
<textarea name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>{{.Value}}</textarea>
|
|
||||||
{{ else }}
|
|
||||||
<input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>
|
|
||||||
{{ end }}
|
|
||||||
</div>`))
|
|
||||||
|
|
||||||
// Field represents a field inside a form.
|
|
||||||
type Field struct {
|
|
||||||
Name string // Name is the name of the field
|
|
||||||
Type InputType // Type is the type of the field. It corresponds to the "name" attribute in html.
|
|
||||||
|
|
||||||
Placeholder string // Value for the "placeholder" attribute
|
|
||||||
Label string // (External) Label for the field. Not used by the default template.
|
|
||||||
|
|
||||||
Autocomplete Autocomplete
|
|
||||||
|
|
||||||
EmptyOnError bool // indicates if the field should be reset on error
|
|
||||||
}
|
|
||||||
|
|
||||||
// fieldContext is passed to the template context
|
|
||||||
type fieldContext struct {
|
|
||||||
Field
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (field Field) WriteTo(w io.Writer, template *template.Template, value string) {
|
|
||||||
if template == nil {
|
|
||||||
template = DefaultFieldTemplate
|
|
||||||
}
|
|
||||||
template.Execute(w, fieldContext{Field: field, Value: value})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckboxChecked is the default value of a checked checkbox
|
|
||||||
const CheckboxChecked = "on"
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package field
|
|
||||||
|
|
||||||
// InputType represents the type of input
|
|
||||||
type InputType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Button InputType = "button"
|
|
||||||
Checkbox InputType = "checkbox"
|
|
||||||
Color InputType = "color"
|
|
||||||
Date InputType = "date"
|
|
||||||
DatetimeLocal InputType = "datetime-local"
|
|
||||||
Email InputType = "email"
|
|
||||||
File InputType = "file"
|
|
||||||
Hidden InputType = "hidden"
|
|
||||||
Image InputType = "image"
|
|
||||||
Month InputType = "month"
|
|
||||||
Number InputType = "number"
|
|
||||||
Password InputType = "password"
|
|
||||||
Radio InputType = "radio"
|
|
||||||
Range InputType = "range"
|
|
||||||
Reset InputType = "reset"
|
|
||||||
Search InputType = "search"
|
|
||||||
Submit InputType = "submit"
|
|
||||||
Tel InputType = "tel"
|
|
||||||
Text InputType = "text"
|
|
||||||
Time InputType = "time"
|
|
||||||
Url InputType = "url"
|
|
||||||
Week InputType = "week"
|
|
||||||
Datetime InputType = "datetime"
|
|
||||||
|
|
||||||
Textarea InputType = "textarea" // special
|
|
||||||
)
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
"github.com/tkw1536/pkglib/pools"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Form provides a form that a user can submit via a http POST method call.
|
|
||||||
// It implements [http.Handler].
|
|
||||||
type Form[D any] struct {
|
|
||||||
// Fields are the fields this form consists of.
|
|
||||||
Fields []field.Field
|
|
||||||
|
|
||||||
// FieldTemplate is an optional template to be executed for each field.
|
|
||||||
// FieldTemplate may be nil; in which case [DefaultFieldTemplate] is used.
|
|
||||||
FieldTemplate *template.Template
|
|
||||||
|
|
||||||
// SkipCSRF if CSRF should be explicitly omitted
|
|
||||||
SkipCSRF bool
|
|
||||||
|
|
||||||
// SkipForm, if non-nil, is called on every get request to determine if form parsing should be skipped entirely.
|
|
||||||
// If skip is true, RenderSuccess is directly called with the given values map.
|
|
||||||
SkipForm func(r *http.Request) (data D, skip bool)
|
|
||||||
|
|
||||||
// RenderTemplate represents the template to render for GET requests.
|
|
||||||
// It is passed the return value of [RenderTemplateContext], or a [FormContext] instance if this does not exist.
|
|
||||||
RenderTemplate *template.Template
|
|
||||||
|
|
||||||
// RenderTemplateContext is the context to be used for RenderTemplate.
|
|
||||||
// When nil, assumed to be the identify function
|
|
||||||
RenderTemplateContext func(ctx FormContext, r *http.Request) any
|
|
||||||
|
|
||||||
// Validate, if non-nil, validates the given submitted values.
|
|
||||||
// There is no guarantee that the values are set.
|
|
||||||
Validate func(r *http.Request, values map[string]string) (D, error)
|
|
||||||
|
|
||||||
// RenderSuccess handles rendering a success result into a response.
|
|
||||||
RenderSuccess func(data D, values map[string]string, w http.ResponseWriter, r *http.Request) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form renders the gives values into a template html string to be inserted into a template.
|
|
||||||
func (form *Form[D]) Form(values map[string]string, isError bool) template.HTML {
|
|
||||||
builder := pools.GetBuilder()
|
|
||||||
defer pools.ReleaseBuilder(builder)
|
|
||||||
|
|
||||||
for _, field := range form.Fields {
|
|
||||||
value := values[field.Name]
|
|
||||||
if isError && field.EmptyOnError {
|
|
||||||
value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
field.WriteTo(builder, form.FieldTemplate, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return template.HTML(builder.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns (validated) form values contained in the given request
|
|
||||||
func (form *Form[D]) Values(r *http.Request) (v map[string]string, d D, err error) {
|
|
||||||
// parse the form
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
return nil, d, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// pick each of the values
|
|
||||||
values := make(map[string]string, len(form.Fields))
|
|
||||||
for _, field := range form.Fields {
|
|
||||||
values[field.Name] = r.PostForm.Get(field.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the form
|
|
||||||
if form.Validate != nil {
|
|
||||||
d, err = form.Validate(r, values)
|
|
||||||
if err != nil {
|
|
||||||
return nil, d, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// and return them
|
|
||||||
return values, d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP implements [http.Handler] and serves the form
|
|
||||||
func (form *Form[D]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch {
|
|
||||||
default:
|
|
||||||
TextInterceptor.Intercept(w, r, ErrMethodNotAllowed)
|
|
||||||
return
|
|
||||||
case r.Method == http.MethodPost:
|
|
||||||
values, data, err := form.Values(r)
|
|
||||||
if err != nil {
|
|
||||||
form.renderForm(err, values, w, r)
|
|
||||||
} else {
|
|
||||||
form.renderSuccess(data, values, w, r)
|
|
||||||
}
|
|
||||||
case r.Method == http.MethodGet && form.SkipForm != nil:
|
|
||||||
if data, skip := form.SkipForm(r); skip {
|
|
||||||
form.renderSuccess(data, nil, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case r.Method == http.MethodGet:
|
|
||||||
form.renderForm(nil, nil, w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderForm renders the form onto the request
|
|
||||||
func (form *Form[D]) renderForm(err error, values map[string]string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
template := form.Form(values, err != nil)
|
|
||||||
if !form.SkipCSRF {
|
|
||||||
template += csrf.TemplateField(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := FormContext{Err: err, Form: template}
|
|
||||||
|
|
||||||
// must have a form or a RenderForm
|
|
||||||
if form.RenderTemplate == nil {
|
|
||||||
panic("form.RenderTemplate is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the template context
|
|
||||||
var tplctx any
|
|
||||||
if form.RenderTemplateContext == nil {
|
|
||||||
tplctx = ctx
|
|
||||||
} else {
|
|
||||||
tplctx = form.RenderTemplateContext(ctx, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render the form
|
|
||||||
WriteHTML(tplctx, nil, form.RenderTemplate, "", w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormContext is passed to Form.Form when used
|
|
||||||
type FormContext struct {
|
|
||||||
// Error is the underlying error (if any)
|
|
||||||
Err error
|
|
||||||
|
|
||||||
// Template is the underlying template rendered as html
|
|
||||||
Form template.HTML
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the underlying error string
|
|
||||||
func (fc FormContext) Error() string {
|
|
||||||
if fc.Err == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fc.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderSuccess renders a successfull pass of the form
|
|
||||||
// if an error occurs during rendering, renderForm is called instead
|
|
||||||
func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := form.RenderSuccess(data, values, w, r)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.renderForm(err, values, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed "form.html"
|
|
||||||
var formBytes []byte
|
|
||||||
|
|
||||||
// FormTeplate is a template to embed a form
|
|
||||||
var FormTemplate = template.Must(template.New("form.html").Parse(string(formBytes)))
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<div class="pure-u-1">
|
|
||||||
{{ block "form/extra" . }}<!-- no extra -->{{ end }}
|
|
||||||
|
|
||||||
<form class="pure-form pure-form-aligned" method="POST">
|
|
||||||
<fieldset>
|
|
||||||
{{ block "form/message" . }}
|
|
||||||
{{ $E := .Error }}
|
|
||||||
{{ if not (eq $E "") }}
|
|
||||||
<div class="pure-form-group">
|
|
||||||
<p class="error-message">
|
|
||||||
{{ $E }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ block "form/inside" . }}<!-- no inside -->{{ end }}
|
|
||||||
{{ .Form }}
|
|
||||||
<input type="submit" value="{{ block "form/button" .}}Submit{{ end }}" class="pure-button">
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/tkw1536/pkglib/timex"
|
|
||||||
)
|
|
||||||
|
|
||||||
const HTMLFlushInterval = time.Second / 10
|
|
||||||
|
|
||||||
// WriteHTML writes a html response of type T to w.
|
|
||||||
// If an error occured, writes an error response instead.
|
|
||||||
func WriteHTML[T any](result T, err error, template *template.Template, templateName string, w http.ResponseWriter, r *http.Request) (e error) {
|
|
||||||
// log any error that occurs;
|
|
||||||
defer func() {
|
|
||||||
if e != nil {
|
|
||||||
zerolog.Ctx(r.Context()).Err(e).Str("path", r.URL.String()).Msg("error rendering template")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// create a synced respone writer
|
|
||||||
sw := &SyncedResponseWriter{ResponseWriter: w}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
// and regularly flush it until the end of the function
|
|
||||||
go func() {
|
|
||||||
timer := timex.NewTimer()
|
|
||||||
defer timex.ReleaseTimer(timer)
|
|
||||||
|
|
||||||
for {
|
|
||||||
timer.Reset(HTMLFlushInterval)
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
sw.Flush()
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// intercept any errors
|
|
||||||
if HTMLInterceptor.Intercept(sw, r, err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out the response as html
|
|
||||||
sw.Header().Set("Content-Type", "text/html")
|
|
||||||
sw.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
// minify html!
|
|
||||||
minifier := MinifyHTMLWriter(sw)
|
|
||||||
defer minifier.Close()
|
|
||||||
|
|
||||||
// and return the template
|
|
||||||
if templateName != "" {
|
|
||||||
return template.ExecuteTemplate(minifier, templateName, result)
|
|
||||||
} else {
|
|
||||||
return template.Execute(minifier, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTMLHandler[T any] struct {
|
|
||||||
Handler func(r *http.Request) (T, error)
|
|
||||||
|
|
||||||
Template *template.Template // called with T
|
|
||||||
TemplateName string // name of template to render, defaults to root
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP calls j(r) and returns json
|
|
||||||
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// call the function
|
|
||||||
result, err := h.Handler(r)
|
|
||||||
WriteHTML(result, err, h.Template, h.TemplateName, w, r)
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
//go:build !nominify
|
|
||||||
|
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/tdewolff/minify"
|
|
||||||
"github.com/tdewolff/minify/css"
|
|
||||||
"github.com/tdewolff/minify/html"
|
|
||||||
"github.com/tdewolff/minify/js"
|
|
||||||
"github.com/tdewolff/minify/svg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// minifier holds the minfier used for all html minification
|
|
||||||
//
|
|
||||||
// NOTE(twiesing): We can't use an init function for this, because otherwise initialization order is incorrect.
|
|
||||||
var minifier = (func() *minify.M {
|
|
||||||
m := minify.New()
|
|
||||||
m.AddFunc("text/html", html.Minify)
|
|
||||||
m.AddFunc("text/css", css.Minify)
|
|
||||||
m.AddFunc("image/svg+xml", svg.Minify)
|
|
||||||
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
|
|
||||||
return m
|
|
||||||
})()
|
|
||||||
|
|
||||||
// MinifyHTMLWriter wraps the given io.Writer to minify the given html instead.
|
|
||||||
// The writer must be closed explicitly.
|
|
||||||
//
|
|
||||||
// Specific environments may chose to disable http minification, in which case MinifyHTMLWriter becomes the identity function.
|
|
||||||
func MinifyHTMLWriter(dest io.Writer) io.WriteCloser {
|
|
||||||
return minifier.Writer("text/html", dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MinifyHTML minifies the html source.
|
|
||||||
// If an error occurs, returns the unmodified source instead.
|
|
||||||
func MinifyHTML(source []byte) []byte {
|
|
||||||
result, err := minifier.Bytes("text/html", source)
|
|
||||||
if err != nil {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// MinifySVG minifies the svg source.
|
|
||||||
// If an error occurs, returns the minified source instead.
|
|
||||||
func MinifySVG(source []byte) []byte {
|
|
||||||
result, err := minifier.Bytes("image/svg+xml", source)
|
|
||||||
if err != nil {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
//go:build nominify
|
|
||||||
|
|
||||||
package httpx
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
// MinifyHTMLWriter wraps the given io.Writer to minify the given html instead.
|
|
||||||
// The writer must be closed explicitly.
|
|
||||||
//
|
|
||||||
// Specific environments may chose to disable http minification, in which case MinifyHTMLWriter becomes the identity function.
|
|
||||||
func MinifyHTMLWriter(dest io.Writer) io.WriteCloser {
|
|
||||||
return noop{Writer: dest}
|
|
||||||
}
|
|
||||||
|
|
||||||
type noop struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (noop) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MinifyHTML minifies the html source.
|
|
||||||
// If an error occurs, returns the unmodified source instead.
|
|
||||||
func MinifyHTML(source []byte) []byte {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
||||||
func MinifySVG(source []byte) []byte {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response represents a response to an http request.
|
|
||||||
type Response struct {
|
|
||||||
ContentType string // defaults to text/plain
|
|
||||||
Body []byte
|
|
||||||
StatusCode int // defaults to [http.StatusOK]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response Response) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if response.ContentType == "" {
|
|
||||||
response.ContentType = "text/plain"
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", response.ContentType)
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(response.Body)))
|
|
||||||
|
|
||||||
if response.StatusCode <= 0 {
|
|
||||||
response.StatusCode = http.StatusOK
|
|
||||||
}
|
|
||||||
w.WriteHeader(response.StatusCode)
|
|
||||||
w.Write(response.Body)
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSON creates a new JSONHandler
|
|
||||||
func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
|
|
||||||
return JSONHandler[T](f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteJSON writes a JSON response of type T to w.
|
|
||||||
// If an error occured, writes an error response instead.
|
|
||||||
func WriteJSON[T any](result T, err error, w http.ResponseWriter, r *http.Request) {
|
|
||||||
// handle any errors
|
|
||||||
if JSONInterceptor.Intercept(w, r, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out the response as json
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONHandler implements [http.Handler] by returning values as json to the caller.
|
|
||||||
// In case of an error, a generic "internal server error" message is returned.
|
|
||||||
type JSONHandler[T any] func(r *http.Request) (T, error)
|
|
||||||
|
|
||||||
// ServeHTTP calls j(r) and returns json
|
|
||||||
func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
result, err := j(r)
|
|
||||||
WriteJSON(result, err, w, r)
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RedirectHandler represents a handler that redirects the user to the address returned
|
|
||||||
type RedirectHandler func(r *http.Request) (string, int, error)
|
|
||||||
|
|
||||||
// ServeHTTP calls r(r) and returns json
|
|
||||||
func (rh RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// call the function
|
|
||||||
url, code, err := rh(r)
|
|
||||||
|
|
||||||
// intercept the errors
|
|
||||||
if TextInterceptor.Intercept(w, r, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the redirect
|
|
||||||
http.Redirect(w, r, url, code)
|
|
||||||
}
|
|
||||||
|
|
@ -1,316 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/tkw1536/pkglib/lazy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WebSocket implements serving a WebSocket
|
|
||||||
type WebSocket struct {
|
|
||||||
Context context.Context // context which closes all connections
|
|
||||||
Limits WebSocketLimits // limits for websocket operations
|
|
||||||
|
|
||||||
Handler func(ws WebSocketConnection)
|
|
||||||
Fallback http.Handler
|
|
||||||
|
|
||||||
pool lazy.Lazy[*sync.Pool] // pool holds *WebSocketConn objects
|
|
||||||
upgrader websocket.Upgrader // upgrades upgrades connections
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebSocketLimits struct {
|
|
||||||
WriteWait time.Duration // maximum time to wait for writing
|
|
||||||
PongWait time.Duration // time to wait for pong responses
|
|
||||||
PingInterval time.Duration // interval to send pings to the client
|
|
||||||
MaxMessageSize int64 // maximal message size in bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (limits *WebSocketLimits) SetDefaults() {
|
|
||||||
if limits.WriteWait == 0 {
|
|
||||||
limits.WriteWait = 10 * time.Second
|
|
||||||
}
|
|
||||||
if limits.PongWait == 0 {
|
|
||||||
limits.PongWait = time.Minute
|
|
||||||
}
|
|
||||||
if limits.PingInterval <= 0 {
|
|
||||||
limits.PingInterval = (limits.PongWait * 9) / 10
|
|
||||||
}
|
|
||||||
if limits.MaxMessageSize <= 0 {
|
|
||||||
limits.MaxMessageSize = 2048
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// makePoolSocket creates a new socket and makes sure that the pool is initialized
|
|
||||||
func (h *WebSocket) makePoolSocket() *webSocketConn {
|
|
||||||
return h.pool.Get(func() *sync.Pool {
|
|
||||||
return &sync.Pool{
|
|
||||||
New: func() any { return new(webSocketConn) },
|
|
||||||
}
|
|
||||||
}).Get().(*webSocketConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// if the user did not request a websocket, go to the fallbacjk handler
|
|
||||||
if !websocket.IsWebSocketUpgrade(r) {
|
|
||||||
h.serveFallback(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// else deal with the websocket!
|
|
||||||
h.serveWebsocket(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WebSocket) serveFallback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if h.Fallback == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Fallback.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WebSocket) serveWebsocket(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// upgrade the connection or bail out!
|
|
||||||
conn, err := h.upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get a new socket from the pool
|
|
||||||
socket := h.makePoolSocket()
|
|
||||||
socket.Serve(h.Context, h.Limits, conn, h.Handler)
|
|
||||||
|
|
||||||
// return a reset socket to the pool
|
|
||||||
socket.reset()
|
|
||||||
h.pool.Get(nil).Put(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocketConnection represents a connected WebSocket.
|
|
||||||
type WebSocketConnection interface {
|
|
||||||
// Context returns a context that is closed once the connection is terminated.
|
|
||||||
Context() context.Context
|
|
||||||
|
|
||||||
// Read returns a channel that receives message.
|
|
||||||
// The channel is closed if no more messags are available (for instance because the server closed).
|
|
||||||
Read() <-chan WebSocketMessage
|
|
||||||
|
|
||||||
// Write queues the provided message for sending.
|
|
||||||
// The returned channel is closed once the message has been sent.
|
|
||||||
Write(WebSocketMessage) <-chan struct{}
|
|
||||||
|
|
||||||
// WriteText is a convenience method to send a TextMessage.
|
|
||||||
// The returned channel is closed once the message has been sent.
|
|
||||||
WriteText(text string) <-chan struct{}
|
|
||||||
|
|
||||||
// Close closes the underlying connection
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocketMessage represents a connected Websocket
|
|
||||||
type WebSocketMessage struct {
|
|
||||||
Type int
|
|
||||||
Bytes []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type outWebSocketMessage struct {
|
|
||||||
WebSocketMessage
|
|
||||||
done chan<- struct{} // done should be closed when finished
|
|
||||||
}
|
|
||||||
|
|
||||||
// webSocketConn implements [WebSocketConnection]
|
|
||||||
type webSocketConn struct {
|
|
||||||
conn *websocket.Conn // underlying connection
|
|
||||||
limits WebSocketLimits
|
|
||||||
|
|
||||||
context context.Context // context to cancel the connection
|
|
||||||
cancel context.CancelFunc
|
|
||||||
|
|
||||||
wg sync.WaitGroup // blocks all the ongoing tasks
|
|
||||||
|
|
||||||
// incoming and outgoing tasks
|
|
||||||
incoming chan WebSocketMessage
|
|
||||||
outgoing chan outWebSocketMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve serves the provided connection
|
|
||||||
func (h *webSocketConn) Serve(ctx context.Context, limits WebSocketLimits, conn *websocket.Conn, handler func(ws WebSocketConnection)) {
|
|
||||||
// use the connection!
|
|
||||||
h.conn = conn
|
|
||||||
|
|
||||||
// setup limits
|
|
||||||
h.limits = limits
|
|
||||||
h.limits.SetDefaults()
|
|
||||||
|
|
||||||
// create a context for the connection
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
h.context, h.cancel = context.WithCancel(ctx)
|
|
||||||
|
|
||||||
// start receiving and sending messages
|
|
||||||
h.wg.Add(2)
|
|
||||||
h.sendMessages()
|
|
||||||
h.recvMessages()
|
|
||||||
|
|
||||||
// wait for the context to be cancelled, then close the connection
|
|
||||||
h.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer h.wg.Done()
|
|
||||||
<-h.context.Done()
|
|
||||||
h.conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// start the application logic
|
|
||||||
h.wg.Add(1)
|
|
||||||
go h.handle(handler)
|
|
||||||
|
|
||||||
// wait for closing operations
|
|
||||||
h.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *webSocketConn) handle(handler func(ws WebSocketConnection)) {
|
|
||||||
defer func() {
|
|
||||||
h.wg.Done()
|
|
||||||
h.cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
handler(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *webSocketConn) sendMessages() {
|
|
||||||
h.outgoing = make(chan outWebSocketMessage)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// close connection when done!
|
|
||||||
defer func() {
|
|
||||||
h.wg.Done()
|
|
||||||
h.cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// setup a timer for pings!
|
|
||||||
ticker := time.NewTicker(h.limits.PingInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
// everything is done!
|
|
||||||
case <-h.context.Done():
|
|
||||||
return
|
|
||||||
|
|
||||||
// send outgoing messages
|
|
||||||
case message := <-h.outgoing:
|
|
||||||
(func() {
|
|
||||||
defer close(message.done)
|
|
||||||
|
|
||||||
err := h.writeRaw(message.Type, message.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message.done <- struct{}{}
|
|
||||||
})()
|
|
||||||
// send a ping message
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := h.writeRaw(websocket.PingMessage, []byte{}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeRaw writes to the underlying socket
|
|
||||||
func (h *webSocketConn) writeRaw(messageType int, data []byte) error {
|
|
||||||
h.conn.SetWriteDeadline(time.Now().Add(h.limits.WriteWait))
|
|
||||||
return h.conn.WriteMessage(messageType, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes a message to the websocket connection.
|
|
||||||
func (sh *webSocketConn) Write(message WebSocketMessage) <-chan struct{} {
|
|
||||||
callback := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
// write an outgoing message
|
|
||||||
case sh.outgoing <- outWebSocketMessage{
|
|
||||||
WebSocketMessage: message,
|
|
||||||
done: callback,
|
|
||||||
}:
|
|
||||||
// context
|
|
||||||
case <-sh.context.Done():
|
|
||||||
close(callback)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return callback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sh *webSocketConn) WriteText(text string) <-chan struct{} {
|
|
||||||
return sh.Write(WebSocketMessage{
|
|
||||||
Type: websocket.TextMessage,
|
|
||||||
Bytes: []byte(text),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *webSocketConn) recvMessages() {
|
|
||||||
h.incoming = make(chan WebSocketMessage)
|
|
||||||
|
|
||||||
// set a read handler
|
|
||||||
h.conn.SetReadLimit(h.limits.MaxMessageSize)
|
|
||||||
|
|
||||||
// configure a pong handler
|
|
||||||
h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait))
|
|
||||||
h.conn.SetPongHandler(func(string) error { h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait)); return nil })
|
|
||||||
|
|
||||||
// handle incoming messages
|
|
||||||
go func() {
|
|
||||||
// close connection when done!
|
|
||||||
defer func() {
|
|
||||||
h.wg.Done()
|
|
||||||
h.cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
messageType, messageBytes, err := h.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to send a message to the incoming message channel
|
|
||||||
select {
|
|
||||||
case h.incoming <- WebSocketMessage{
|
|
||||||
Type: messageType,
|
|
||||||
Bytes: messageBytes,
|
|
||||||
}:
|
|
||||||
case <-h.context.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read returns a channel that receives incoming messages.
|
|
||||||
// The channel is close once no more messages are available, or the context is canceled.
|
|
||||||
func (h *webSocketConn) Read() <-chan WebSocketMessage {
|
|
||||||
return h.incoming
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context returns a context that is closed once this connection is closed.
|
|
||||||
func (h *webSocketConn) Context() context.Context {
|
|
||||||
return h.context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *webSocketConn) Close() {
|
|
||||||
h.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset resets this websocket
|
|
||||||
func (h *webSocketConn) reset() {
|
|
||||||
h.limits = WebSocketLimits{}
|
|
||||||
h.conn = nil
|
|
||||||
h.incoming = nil
|
|
||||||
h.outgoing = nil
|
|
||||||
h.context, h.cancel = nil, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SyncedResponseWriter wraps a http ResponseWriter to syncronize all actions
|
|
||||||
type SyncedResponseWriter struct {
|
|
||||||
m sync.Mutex
|
|
||||||
http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *SyncedResponseWriter) Header() http.Header {
|
|
||||||
rw.m.Lock()
|
|
||||||
defer rw.m.Unlock()
|
|
||||||
|
|
||||||
return rw.ResponseWriter.Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *SyncedResponseWriter) Write(data []byte) (int, error) {
|
|
||||||
rw.m.Lock()
|
|
||||||
defer rw.m.Unlock()
|
|
||||||
|
|
||||||
return rw.ResponseWriter.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *SyncedResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
rw.m.Lock()
|
|
||||||
defer rw.m.Unlock()
|
|
||||||
|
|
||||||
rw.ResponseWriter.WriteHeader(statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush flushes any partial output to the underlying ResponseWriter.
|
|
||||||
// If the wrapped ResponseWriter does not implement flush, the function performs no operation.
|
|
||||||
func (rw *SyncedResponseWriter) Flush() {
|
|
||||||
f, ok := rw.ResponseWriter.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rw.m.Lock()
|
|
||||||
defer rw.m.Unlock()
|
|
||||||
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithContextWrapper generates a new handler that wraps the context of each request with the wrapper function.
|
|
||||||
func WithContextWrapper(handler http.Handler, wrapper func(context.Context) context.Context) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = r.WithContext(wrapper(r.Context()))
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue