diff --git a/go.mod b/go.mod index e48bae3..1ad4c34 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,10 @@ require ( github.com/go-sql-driver/mysql v1.7.0 github.com/gorilla/csrf v1.7.1 github.com/gorilla/sessions v1.2.1 - github.com/gorilla/websocket v1.5.0 github.com/julienschmidt/httprouter v1.3.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.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/pkglib v0.0.0-20230225192547-93a1aa42a292 github.com/yuin/goldmark v1.5.4 @@ -34,12 +32,14 @@ require ( github.com/boombuler/barcode v1.0.1 // indirect github.com/feiin/sqlstring v0.3.0 // 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/jessevdk/go-flags v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-colorable v0.1.13 // 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 golang.org/x/sys v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/internal/dis/component/auth/next/next.go b/internal/dis/component/auth/next/next.go index 7415c26..6e985eb 100644 --- a/internal/dis/component/auth/next/next.go +++ b/internal/dis/component/auth/next/next.go @@ -11,7 +11,7 @@ import ( "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/ingredient/php/users" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/tkw1536/pkglib/httpx" ) type Next struct { diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go index 2e2e9c2..d90995d 100644 --- a/internal/dis/component/auth/panel/panel.go +++ b/internal/dis/component/auth/panel/panel.go @@ -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/ssh2/sshkeys" "github.com/FAU-CDI/wisski-distillery/internal/models" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/julienschmidt/httprouter" + "github.com/tkw1536/pkglib/httpx" ) type UserPanel struct { diff --git a/internal/dis/component/auth/panel/password.go b/internal/dis/component/auth/panel/password.go index 1ae39f2..f199575 100644 --- a/internal/dis/component/auth/panel/password.go +++ b/internal/dis/component/auth/panel/password.go @@ -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/templating" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" + "github.com/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" ) //go:embed "templates/password.html" diff --git a/internal/dis/component/auth/panel/ssh.go b/internal/dis/component/auth/panel/ssh.go index 846daa7..d7c27fc 100644 --- a/internal/dis/component/auth/panel/ssh.go +++ b/internal/dis/component/auth/panel/ssh.go @@ -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/templating" "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/rs/zerolog" + "github.com/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" gossh "golang.org/x/crypto/ssh" diff --git a/internal/dis/component/auth/panel/totp.go b/internal/dis/component/auth/panel/totp.go index 7a74588..c8b14fd 100644 --- a/internal/dis/component/auth/panel/totp.go +++ b/internal/dis/component/auth/panel/totp.go @@ -8,8 +8,8 @@ import ( "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/templating" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" + "github.com/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" _ "embed" ) diff --git a/internal/dis/component/auth/protect.go b/internal/dis/component/auth/protect.go index 24ca1fa..8f64c3b 100644 --- a/internal/dis/component/auth/protect.go +++ b/internal/dis/component/auth/protect.go @@ -5,7 +5,7 @@ import ( "net/http" "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. diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index e8eada1..3c56308 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -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/assets" "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/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" + "github.com/gorilla/sessions" _ "embed" diff --git a/internal/dis/component/resolver/resolver.go b/internal/dis/component/resolver/resolver.go index 15a74e6..4b8d2b7 100644 --- a/internal/dis/component/resolver/resolver.go +++ b/internal/dis/component/resolver/resolver.go @@ -14,8 +14,8 @@ import ( "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/templating" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/rs/zerolog" + "github.com/tkw1536/pkglib/httpx" "github.com/tkw1536/pkglib/lazy" _ "embed" diff --git a/internal/dis/component/server/admin/admin.go b/internal/dis/component/server/admin/admin.go index 79f34c3..a9e8aac 100644 --- a/internal/dis/component/server/admin/admin.go +++ b/internal/dis/component/server/admin/admin.go @@ -14,7 +14,7 @@ import ( "github.com/tkw1536/pkglib/lifetime" "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 { diff --git a/internal/dis/component/server/admin/components.go b/internal/dis/component/server/admin/components.go index 985ad2e..36b0d3c 100644 --- a/internal/dis/component/server/admin/components.go +++ b/internal/dis/component/server/admin/components.go @@ -5,15 +5,15 @@ import ( "html/template" "net/http" - _ "embed" - "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/server/assets" "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/tkw1536/pkglib/httpx" "github.com/tkw1536/pkglib/lifetime" + + _ "embed" ) //go:embed "html/anal.html" diff --git a/internal/dis/component/server/admin/grants.go b/internal/dis/component/server/admin/grants.go index df104a0..0e29fb3 100644 --- a/internal/dis/component/server/admin/grants.go +++ b/internal/dis/component/server/admin/grants.go @@ -13,8 +13,9 @@ import ( "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/wisski" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" + "github.com/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" + "github.com/julienschmidt/httprouter" "golang.org/x/exp/maps" "golang.org/x/exp/slices" diff --git a/internal/dis/component/server/admin/instance.go b/internal/dis/component/server/admin/instance.go index 298b1eb..7cd8f4a 100644 --- a/internal/dis/component/server/admin/instance.go +++ b/internal/dis/component/server/admin/instance.go @@ -2,7 +2,6 @@ package admin import ( "context" - _ "embed" "html/template" "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/models" "github.com/FAU-CDI/wisski-distillery/internal/status" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/julienschmidt/httprouter" + "github.com/tkw1536/pkglib/httpx" + + _ "embed" ) //go:embed "html/instance.html" diff --git a/internal/dis/component/server/admin/socket/socket.go b/internal/dis/component/server/admin/socket/socket.go index 4ef0fb2..7dda845 100644 --- a/internal/dis/component/server/admin/socket/socket.go +++ b/internal/dis/component/server/admin/socket/socket.go @@ -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/purger" "github.com/FAU-CDI/wisski-distillery/internal/wisski" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/tkw1536/goprogram/status" + "github.com/tkw1536/pkglib/httpx" ) type Sockets struct { diff --git a/internal/dis/component/server/admin/users.go b/internal/dis/component/server/admin/users.go index 3bf960b..016662f 100644 --- a/internal/dis/component/server/admin/users.go +++ b/internal/dis/component/server/admin/users.go @@ -6,14 +6,14 @@ import ( "net/http" "net/url" - _ "embed" - "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/templating" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/rs/zerolog" + "github.com/tkw1536/pkglib/httpx" + "github.com/tkw1536/pkglib/httpx/field" + + _ "embed" ) //go:embed "html/users.html" diff --git a/internal/dis/component/server/home/public.go b/internal/dis/component/server/home/public.go index f61a1fb..8fef9fc 100644 --- a/internal/dis/component/server/home/public.go +++ b/internal/dis/component/server/home/public.go @@ -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/templating" "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" ) diff --git a/internal/dis/component/server/logo/logo.go b/internal/dis/component/server/logo/logo.go index dd1e173..ab92802 100644 --- a/internal/dis/component/server/logo/logo.go +++ b/internal/dis/component/server/logo/logo.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/tkw1536/pkglib/httpx" _ "embed" ) @@ -41,8 +41,8 @@ var faviconRoute = httpx.Response{ var logoSVGRoute = httpx.Response{ ContentType: "image/svg+xml", - Body: httpx.MinifySVG(logoSVG), -} + Body: logoSVG, +}.Minify() func (*Logo) HandleRoute(ctx context.Context, path string) (http.Handler, error) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/dis/component/server/server.go b/internal/dis/component/server/server.go index db2577e..e27d8d8 100644 --- a/internal/dis/component/server/server.go +++ b/internal/dis/component/server/server.go @@ -9,9 +9,9 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" "github.com/tkw1536/pkglib/contextx" + "github.com/tkw1536/pkglib/httpx" "github.com/tkw1536/pkglib/mux" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/gorilla/csrf" "github.com/rs/zerolog" ) diff --git a/internal/dis/component/server/templating/base.go b/internal/dis/component/server/templating/base.go index 382931a..1d21fcf 100644 --- a/internal/dis/component/server/templating/base.go +++ b/internal/dis/component/server/templating/base.go @@ -10,9 +10,9 @@ import ( "reflect" "time" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/gorilla/csrf" "github.com/rs/zerolog" + "github.com/tkw1536/pkglib/httpx" "github.com/tkw1536/pkglib/pools" "github.com/tkw1536/pkglib/timex" ) diff --git a/internal/dis/component/ssh2/api.go b/internal/dis/component/ssh2/api.go index 783a7e9..6d65699 100644 --- a/internal/dis/component/ssh2/api.go +++ b/internal/dis/component/ssh2/api.go @@ -5,7 +5,7 @@ import ( "net/http" "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" ) diff --git a/pkg/httpx/errors.go b/pkg/httpx/errors.go deleted file mode 100644 index 6300142..0000000 --- a/pkg/httpx/errors.go +++ /dev/null @@ -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(`` + text + `` + text)), nil - }) -) diff --git a/pkg/httpx/field/autocomplete.go b/pkg/httpx/field/autocomplete.go deleted file mode 100644 index fcb05d7..0000000 --- a/pkg/httpx/field/autocomplete.go +++ /dev/null @@ -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" -) diff --git a/pkg/httpx/field/field.go b/pkg/httpx/field/field.go deleted file mode 100644 index 50615bf..0000000 --- a/pkg/httpx/field/field.go +++ /dev/null @@ -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" )}} - -{{ else }} - - -{{ if (eq .Type "textarea" )}} - -{{ else }} - -{{ end }} -`)) - -// 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" diff --git a/pkg/httpx/field/type.go b/pkg/httpx/field/type.go deleted file mode 100644 index c056854..0000000 --- a/pkg/httpx/field/type.go +++ /dev/null @@ -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 -) diff --git a/pkg/httpx/form.go b/pkg/httpx/form.go deleted file mode 100644 index bbe982a..0000000 --- a/pkg/httpx/form.go +++ /dev/null @@ -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))) diff --git a/pkg/httpx/form.html b/pkg/httpx/form.html deleted file mode 100644 index 455776c..0000000 --- a/pkg/httpx/form.html +++ /dev/null @@ -1,22 +0,0 @@ -
- {{ block "form/extra" . }}{{ end }} - -
-
- {{ block "form/message" . }} - {{ $E := .Error }} - {{ if not (eq $E "") }} -
-

- {{ $E }} -

-
- {{ end }} - {{ end }} - - {{ block "form/inside" . }}{{ end }} - {{ .Form }} - -
-
-
diff --git a/pkg/httpx/html.go b/pkg/httpx/html.go deleted file mode 100644 index b87fba5..0000000 --- a/pkg/httpx/html.go +++ /dev/null @@ -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) -} diff --git a/pkg/httpx/html_minify.go b/pkg/httpx/html_minify.go deleted file mode 100644 index 6bf7617..0000000 --- a/pkg/httpx/html_minify.go +++ /dev/null @@ -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 -} diff --git a/pkg/httpx/html_minify_off.go b/pkg/httpx/html_minify_off.go deleted file mode 100644 index b42b14b..0000000 --- a/pkg/httpx/html_minify_off.go +++ /dev/null @@ -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 -} diff --git a/pkg/httpx/httpx.go b/pkg/httpx/httpx.go deleted file mode 100644 index b9b23b9..0000000 --- a/pkg/httpx/httpx.go +++ /dev/null @@ -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) -} diff --git a/pkg/httpx/json.go b/pkg/httpx/json.go deleted file mode 100644 index 468aa26..0000000 --- a/pkg/httpx/json.go +++ /dev/null @@ -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) -} diff --git a/pkg/httpx/redirect.go b/pkg/httpx/redirect.go deleted file mode 100644 index a82b414..0000000 --- a/pkg/httpx/redirect.go +++ /dev/null @@ -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) -} diff --git a/pkg/httpx/socket.go b/pkg/httpx/socket.go deleted file mode 100644 index 3ebea2b..0000000 --- a/pkg/httpx/socket.go +++ /dev/null @@ -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 -} diff --git a/pkg/httpx/sync.go b/pkg/httpx/sync.go deleted file mode 100644 index 2d7aa72..0000000 --- a/pkg/httpx/sync.go +++ /dev/null @@ -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() -} diff --git a/pkg/httpx/wrap.go b/pkg/httpx/wrap.go deleted file mode 100644 index 0c082d4..0000000 --- a/pkg/httpx/wrap.go +++ /dev/null @@ -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) - }) -}