{"id":21275676,"url":"https://github.com/halimath/httputils","last_synced_at":"2025-03-15T13:13:48.599Z","repository":{"id":57617949,"uuid":"386931362","full_name":"halimath/httputils","owner":"halimath","description":"Utilities for implementing http services in Go","archived":false,"fork":false,"pushed_at":"2024-05-03T20:11:10.000Z","size":54,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-22T03:27:28.792Z","etag":null,"topics":["auth","go","golang","http","middleware"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/halimath.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-17T12:25:51.000Z","updated_at":"2024-05-03T20:11:13.000Z","dependencies_parsed_at":"2024-05-03T21:27:16.420Z","dependency_job_id":"3951c80c-922e-4e0a-8bb4-2bfe3a8aa9b8","html_url":"https://github.com/halimath/httputils","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halimath%2Fhttputils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halimath%2Fhttputils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halimath%2Fhttputils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halimath%2Fhttputils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/halimath","download_url":"https://codeload.github.com/halimath/httputils/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243732303,"owners_count":20338839,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","go","golang","http","middleware"],"created_at":"2024-11-21T09:36:08.345Z","updated_at":"2025-03-15T13:13:48.570Z","avatar_url":"https://github.com/halimath.png","language":"Go","readme":"# httputils\n\nUtilities for implementing HTTP services in [Go](https://golang.org)\n\n![CI Status][ci-img-url] [![Go Report Card][go-report-card-img-url]][go-report-card-url] [![Package Doc][package-doc-img-url]][package-doc-url] [![Releases][release-img-url]][release-url]\n\nThis repo contains a library for the Golang programming language that provides utilities for implementing\nHTTP services. The library focusses on using the standard library only and thus be agnostic about any \nother web framework.\n\n# Installation\n\nUse `go get` to install the libary with your project. You need Go \u003e= 1.18 to use the lib. \n\n```\n$ go get github.com/halimath/httputils\n```\n\n# Usage\n\n`httputils` contains a set of different features that can be used independently or together. The following\nsections each describe a single feature.\n\n## Authorization\n\n`httputils` contains a HTTP middleware that handles HTTP Authorization. The middleware extracts the\nauthorization credentials and stores them in the request's context before forwarding the request to\nthe next handler.\n\nCurrently, _Basic Auth_ and _Bearer Token_ are supported but the middleware allows for an easy extension.\n\nThe following example demonstrates how to use the `auth` package.\n\n```go\n// h is a http.Handler, that actualy handles the request.\nh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n    w.Header().Set(\"Content-Type\", \"text/plain\")\n\n    // We can assume here that auth is always set. See below\n    a := auth.GetAuthorization(r.Context())\n\n    switch a.(type) {\n    case *auth.UsernamePassword:\n        // Use username/password to authorize the usert\n    case *auth.BearerToken:\n        // Decode token and authorizes\n    }\n})\n\nhttp.ListenAndServe(\":1234\",\n    auth.Bearer(\n        auth.Basic(\n            auth.Authorized(h,\n                auth.AuthenticationChallenge{\n                    Scheme: auth.AuthorizationSchemeBasic,\n                    Realm:  \"test\",\n                },\n                auth.AuthenticationChallenge{\n                    Scheme: auth.AuthorizationSchemeBearer,\n                    Realm:  \"test\",\n                },\n            ),\n        ),\n    ),\n)\n```\n\nIn the example above `h` is a simple `http.Handler`; replace it with your \"real\" handler implementation. \nYou can also use any kind of framework here as long as the framework's router implements the \n`http.Handler` interface.\n\nThe call to `ListenAndServe` uses three middleware that wrap each other with the inner most wrapping `h`. \nLet's go through them from outer to inner:\n\n* `auth.Bearer` creates a middleware that tries to extract a _Token Bearer Authorization_ credentials\n  from the request and - if found - stores the credentials in the requests's context. It always invokes\n  the wrapped handler.\n* `auth.Basic` creates a middleware that extracts any _Basic Auth_ credentials and stores them in the \n  requests context. It always invokes the wrapped handler.\n* `auth.Authorized` creates a middleware that checks if the request's context contains a non-`nil`\n  Authorization (extracted from either of the above middlewares). If such an authorization is found\n  the wrapped handler is invoked. If no authoriation has been found, the request is rejected with\n  a HTTP status code `401 Unauthorized` and a `WWW-Authentication` header is added with the given\n  HTTP authentication challenges.\n\nIt's important to keep the order of the middlewares correct: \n\n* If you put `auth.Authorized` first then _every request_ will be rejected as there are no handlers \nstoring an `Authorization` value in the context.\n* The order of `Bearer` and `Basic` is important only for requests that contain _both_ authorizations\n  (i.e. by sending two `Authorization` header). The last (successful) middleware overwrites any \n  Authorization value stored by a middleware that ran previously.\n\n### How to implement your own Authorization scheme\n\nHTTP Authorization is pretty flexible so chances are that you need a custom implementation to grab the\nuser's credentials from a request. If you want to use the `Authorized` middleware you need to do the \nfollowing:\n\n* Create a type holding the user's credentials. This type implements `auth.Authorization` which is an\n  empty interface.\n* Create a middleware that extracts the credentials from a request and calls `auth.WithAuthorization` to\n  create a new context holding the credentials. If your implementation also uses the HTTP `Authorization`\n  header with a custom scheme, you may use `auth.AuthHandler` to simplify the implementation by providing\n  a function that creates an `Authorization` value from a credentials string.\n\nHere is a sketched example that demonstrates how to build some kind of HMAC authorization. The idea is, \nthat requests carry an `Authorization`-header with a scheme `Hmac` that contains a _keyed hashed method\nauthentication code_ for the request's URL signed with a user's secret. Username and hmac are separated\nwith a single colon; the HMAC is base64-encoded, such as\n\n```\nGET /foo/bar HTTP/1.1\nAuthorization: Hmac john.doe:eLKW1g44EJ52qiF7kFbzma7zf61yE0x8gUO2daRwqss=\n```\n\nThe example uses a SHA256 HMAC with the key `secret`. You can calculate it with \n\n```\necho -n \"/foo/bar\" | openssl dgst -sha256 -hmac \"secret\" -binary | openssl enc -base64 -A\n```\n\nThe following code demonstrates how to set up an authorization handler implementing the above. Note that\nthe middleware does not verify the HMAC - it only performs the Base64 decoding.\n\n```go\ntype HMAC struct {\n    Username string\n    MAC      []byte\n}\n\nh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n    // ...\n})\n\nhttp.ListenAndServe(\":1234\",\n    auth.AuthHandler(\n        auth.Authorized(h,\n            auth.AuthenticationChallenge{\n                Scheme: auth.AuthorizationSchemeBasic,\n                Realm:  \"test\",\n            },\n            auth.AuthenticationChallenge{\n                Scheme: auth.AuthorizationSchemeBearer,\n                Realm:  \"test\",\n            },\n        ),\n        \"Hmac\",\n        func(credentials string) auth.Authorization {\n            parts := strings.Split(credentials, \":\")\n            if len(parts) != 2 {\n                return nil\n            }\n\n            mac, err := base64.StdEncoding.DecodeString(parts[1])\n            if err != nil {\n                return nil\n            }\n\n            return \u0026HMAC{\n                Username: parts[0],\n                MAC:      mac,\n            }\n        },\n    ),\n)\n```\n\n### A note on how to verify the credentials\n\nYou may have noted that none of the above middlewares that extract user credentials actually performs a\nverificates besides some syntax checking. This task is intentionally left off the framework. The reason\nfor that is that the decision where to do authorization is a highly opinionated question with\ndifferent people argumenting for different directions. While some which to perform this step as part of\nthe response handling, others seek to implement this as part of the business layer (a _service_, domain \nfunction or whatever else is used to implement business logic). The `auth` package favors none of those\nopinions and allows both to be implemented with ease. \n\nIf you want to do the verification as part of the request handling, simply create another middleware\npositioned after the `Authorized` middleware that does the verification. If you want to implement the\nverification in a different software layer, simply pass the request's context to the business function\n(which in modern Go is a generally good advice) and use `auth.GetAuthorization` to read the credentials.\n\n## Request URI\n\nThe `requesturi` package contains a HTTP middleware that augments some of the request's `URL` fields that\nare left blank by default. The resulting `URL` can be used to reconstruct the requested URI _as specified\nby the client_. This is very usefull when creating dynanic links, redirect URLs or OAuth return URLs from\nwhat the user \"sees\". \n\nThe package also provides functions that extend the behavior when running behind a reverse proxy that\nsets HTTP header to forward the original request information.\n\nThe following example configures the middleware for use behind a reverse proxy and reads the HTTP standard\n`Forwarded`-header as well as the _defacto standard_ `X-Forwarded-*`-headers:\n\n```go\nh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n    http.Redirect(w, r, fmt.Sprintf(\"%s://%s/some/path\", r.URL.Scheme, r.URL.Host), http.StatusTemporaryRedirect)\n})\n\nhttp.ListenAndServe(\":1234\", requesturi.Middleware(h, requesturi.Forwarded, requesturi.XForwarded))\n```\n\n## CORS\n\nPackage `cors` provides a configurable middleware to handle _Cross Origin Resource Sharing_ (CORS). The\nmiddleware injects response headers and handles pre-flight requests completely.\n\nTo generally allow access to all resources (i.e. endpoints) from all origins use something like this:\n\n```go\n// restAPI is a http.Handler that defines some kind of resource.\nrestAPI := http.NewServeMux()\n\nhttp.ListenAndServe(\":1234\", cors.Middleware(restAPI))\n```\n\nTo enable CORS for specific endpoints and/or origins, you can pass additional configuration arguments to the\nmiddleware:\n\n```go\n// restAPI is a http.Handler that defines some kind of resource.\nrestAPI := http.NewServeMux()\n\nhttp.ListenAndServe(\":1234\",\n    cors.Middleware(\n        restAPI,\n        cors.Endpoint{\n            Path: \"/api/v1/resource1\",\n        },\n        cors.Endpoint{\n            Path:             \"/api/v1/resource2\",\n            AllowMethods:     []string{http.MethodPost},\n            AllowCredentials: true,\n        },\n    ),\n)\n```\n\n## Request Builder (for tests)\n\nPackage `requestbuilder` contains a builder that can be used to build `http.Request` values during tests.\nWhile package `httptest` provides a `NewRequest` function to create a request for tests, setting headers\nrequires you to use a local variable. The request builder allows you to set all kinds of request \nproperties using methods that return the builder.\n\n```go\naccessToken := \"...\"\ndata, _ := os.Open(\"/some/file\")\n\n_ = requestbuilder.Post(\"https://example.com/path/to/resource\").\n    Body(data).\n    AddHeader(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken)).\n    Request()\n```\n\nThis works extremely well when using \n[table driven tests](https://github.com/golang/go/wiki/TableDrivenTests). The following code is\nfrom the [`auth` package's tests](./auth/auth_test.gol):\n\n```go\nfunc TestBasicAuth(t *testing.T) {\n\ttab := map[*http.Request]Authorization{\n\t\trequestbuilder.Get(\"/\").Request(): nil,\n\n\t\trequestbuilder.Get(\"/\").AddHeader(HeaderAuthorization, \"foo bar\").Request(): nil,\n\n\t\trequestbuilder.Get(\"/\").AddHeader(HeaderAuthorization, \"Basic bar\").Request(): nil,\n\n\t\trequestbuilder.Get(\"/\").AddHeader(HeaderAuthorization, \"Basic \"+base64.StdEncoding.EncodeToString([]byte(\"foo\"))).Request(): nil,\n\n\t\trequestbuilder.Get(\"/\").AddHeader(HeaderAuthorization, \"Basic dGVzdDoxMjPCow==\").Request(): \u0026UsernamePassword{\n\t\t\tUsername: \"test\",\n\t\t\tPassword: \"123\\u00A3\",\n\t\t},\n\t}\n\n\tfor in, exp := range tab {\n\t\tvar w httptest.ResponseRecorder\n\t\th := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tact := GetAuthorization(r.Context())\n\t\t\tif diff := deep.Equal(exp, act); diff != nil {\n\t\t\t\tt.Error(diff)\n\t\t\t}\n\t\t})\n\t\tBasic(h).ServeHTTP(\u0026w, in)\n\t}\n}\n\n```\n\n## Buffered Response\n\nPackage `github.com/halimath/httputils/bufferedresponse` provides a type `ResponseWriter` that satisfies the interface\n[`http.ResponseWriter`](https://pkg.go.dev/net/http#ResponseWriter) as an in-memory buffered implementation.\nThe type \"collects\" all headers, status code and body bytes written and can then \"replay\" the response on\nany (even multiple) `http.ResponseWriter`s.\n\nUse this buffer implementation when implementing middlewares or request handlers that need a way to \"rewind\"\nthe response and start over (i.e. for handling errors).\n\n## Response\n\nPackage `github.com/halimath/httputils/reponse` provides several functions to easily create responses from\nhttp handler methods. These functions are built on the `bufferedresponse` package and provide easy to use,\neasy to extend builting of http responses.\n\nSee the package doc and the corresponding tests for examples.\n\n### Problem JSON\n\nOne special response helper is capable of sending problem details as described in [RFC9457]. The Problem\nDetails RFC defines a JSON (and XML) structure as well as some rules on the field's semantics to report\nuseful details from problem results. This module only implements the JSON representation of the RFC.\n\n[RFC9457]: https://www.rfc-editor.org/rfc/rfc9457\n\n## `errmux`\n\nPackage `errmux` provides an augmented version of `http.ServeMux` which accept handler methods that return\n`error` values. The multiplexer uses a `http.ServeMux` under the hood and supports all the patterns supported\nby the Go version in use (i.e. all advanced patterns introduced with Go 1.22 if a version \u003e= 1.22 is used).\n\nAny error returned from a handler will be caught and the response written so far will be discarded. The error\nis then handled by an error handler which may be customized producing a final result to send to the client.\n\nSee the following example for a short demonstration:\n\n```go\nmux := errmux.NewServeMux()\n\nerrMissingQueryParameter := errors.New(\"missing query parameter\")\n\nmux.HandleFunc(\"/echo\", func(w http.ResponseWriter, r *http.Request) error {\n    if msg := r.URL.Query().Get(\"msg\"); len(msg) \u003e 0 {\n        return response.PlainText(w, r, msg)\n    }\n\n    return fmt.Errorf(\"%w: %s\", errMissingQueryParameter, \"msg\")\n})\n\nmux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {\n    if errors.Is(err, errMissingQueryParameter) {\n        http.Error(w, err.Error(), http.StatusBadRequest)\n        return\n    }\n\n    http.Error(w, err.Error(), http.StatusInternalServerError)\n}\n\nhttp.ListenAndServe(\":8080\", mux)\n```\n\n# License\n\nCopyright 2021 - 2024 Alexander Metzner\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n[ci-img-url]: https://github.com/halimath/httputils/workflows/CI/badge.svg\n[go-report-card-img-url]: https://goreportcard.com/badge/github.com/halimath/httputils\n[go-report-card-url]: https://goreportcard.com/report/github.com/halimath/httputils\n[package-doc-img-url]: https://img.shields.io/badge/GoDoc-Reference-blue.svg\n[package-doc-url]: https://pkg.go.dev/github.com/halimath/httputils\n[release-img-url]: https://img.shields.io/github/v/release/halimath/httputils.svg\n[release-url]: https://github.com/halimath/httputils/releases\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalimath%2Fhttputils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhalimath%2Fhttputils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalimath%2Fhttputils/lists"}