fcors

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 2, 2024 License: MIT Imports: 2 Imported by: 9

README

jub0bs/fcors

Go Reference license build codecov goreport

An experimental CORS middleware library for Go.

Unless you're a big fan of functional options, you should use github.com/jub0bs/cors instead.

About CORS

The Same-Origin Policy (SOP) is a security mechanism that Web browsers implement to protect their users. In particular, the SOP restricts cross-origin network access in terms of both sending and reading. Cross-Origin Resource Sharing (CORS) is a protocol that lets servers instruct browsers to relax those restrictions for select clients.

jub0bs/fcors allows you to configure and build net/http middleware that implement CORS.

Design philosophy

jub0bs/fcors is designed to be both easier to use and harder to misuse than other CORS middleware libraries; see Fearless CORS: a design philosophy for CORS middleware libraries (and a Go implementation) and Useful Functional-Options Tricks for Better Libraries (GopherCon Europe 2023).

Praise for jub0bs/fcors

I really like the declarative API. It lets you say what behavior you want rather than setting specific headers. It means that, as a user, you don’t have to relearn the nuances of CORS every time you want to make a change.

Paul Carleton (Staff Software Engineer at Stripe)

Installation

go get github.com/jub0bs/fcors

jub0bs/fcors requires Go 1.21 or above.

Example

The following program demonstrates how to create a CORS middleware that

  • allows anonymous access from Web origin https://example.com,
  • with requests whose method is either GET or POST,
  • and (optionally) with request header Authorization,

and how to apply the middleware in question to all the resources accessible under some /api/ path:

package main

import (
  "io"
  "log"
  "net/http"

  "github.com/jub0bs/fcors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // note: not configured for CORS

  // create CORS middleware
  cors, err := fcors.AllowAccess(
    fcors.FromOrigins("https://example.com"),
    fcors.WithMethods(http.MethodGet, http.MethodPost),
    fcors.WithRequestHeaders("Authorization"),
  )
  if err != nil {
    log.Fatal(err)
  }

  api := http.NewServeMux()
  api.HandleFunc("GET /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", cors(api))) // note: method-less pattern here

  log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, World!")
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

Try it out yourself by saving this program to a file named server.go. You may need to adjust the port number if port 8080 happens to be unavailable on your machine. Then build and run your server:

go build server.go
./server

If no error occurred, the server is now running on localhost:8080 and the various resources accessible under the /api/ path are now configured for CORS as desired.

Documentation

The documentation is available on pkg.go.dev.

Code coverage

coverage

License

All source code is covered by the MIT License.

Documentation

Overview

Package fcors provides net/http middleware for Cross-Origin Resource Sharing (CORS).

To create a CORS middleware that only allows anonymous access, use the AllowAccess function. To create a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies), use the AllowAccessWithCredentials function.

To avoid negative interference from reverse proxies, other middleware in the chain, or from the handler at the end of the chain, follow the rules listed below. The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" used below are to be interpreted as described in RFC 2119.

  • Because CORS-preflight requests use OPTIONS as their method, you SHOULD NOT prevent OPTIONS requests from reaching your CORS middleware. Otherwise, preflight requests will not get properly handled and browser-based clients will likely experience CORS-related errors. The examples provided by this package contain further guidance for avoiding such pitfalls.
  • Because CORS-preflight requests are not authenticated, authentication SHOULD NOT take place "ahead of" a CORS middleware (e.g. in a reverse proxy or an earlier middleware). However, a CORS middleware MAY wrap an authentication middleware.
  • Multiple CORS middleware MUST NOT be stacked.
  • Other middleware (if any) in the chain MUST NOT alter any CORS response headers that are set by this library's middleware and MUST NOT add more CORS response headers.
  • Other middleware (if any) in the chain SHOULD NOT alter any Vary header that is set by this library's middleware, but they MAY add more Vary headers.

This package provides basic options for configuring a CORS middleware, but more advanced (and potentially dangerous) options can be found in the github.com/jub0bs/fcors/risky package.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware is a convenience alias for the type of a function that takes a http.Handler and returns a http.Handler.

The middleware provided by this package are, of course, safe for concurrent use by multiple goroutines.

func AllowAccess

func AllowAccess(one OptionAnon, others ...OptionAnon) (Middleware, error)

AllowAccess creates a CORS middleware that only allows anonymous access, according to the specified options. The behavior of the resulting middleware is insensitive to the order in which the options that configure it are specified.

AllowAccess requires a single call to option FromOrigins or option FromAnyOrigin as one of its arguments.

Using a given option more than once in a call to AllowAccess results in a failure to build the corresponding middleware.

If the specified options are invalid or mutually incompatible, AllowAccess returns a nil Middleware and some non-nil error. Otherwise, it returns a functioning Middleware and a nil error.

Any occurrence of a nil option results in a panic.

Example
package main

import (
	"io"
	"log"
	"net/http"

	"github.com/jub0bs/fcors"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello", handleHello) // note: not configured for CORS

	// create CORS middleware
	cors, err := fcors.AllowAccess(
		fcors.FromOrigins("https://example.com"),
		fcors.WithMethods(http.MethodGet, http.MethodPost),
		fcors.WithRequestHeaders("Authorization"),
	)
	if err != nil {
		log.Fatal(err)
	}

	api := http.NewServeMux()
	api.HandleFunc("GET /users", handleUsersGet)
	api.HandleFunc("POST /users", handleUsersPost)
	mux.Handle("/api/", http.StripPrefix("/api", cors(api))) // note: method-less pattern here

	log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
	io.WriteString(w, "Hello, World!")
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
	// omitted
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
	// omitted
}
Output:

Example (Incorrect)

The example below illustrates a common pitfall.

A good rule of thumb for avoiding this pitfall consists in registering the result of a Middleware, not for a method-full pattern (e.g. "GET /api/dogs"), but for a "method-less" pattern; see the other example.

package main

import (
	"log"
	"net/http"

	"github.com/jub0bs/fcors"
)

// The example below illustrates a common pitfall.
//
// A good rule of thumb for avoiding this pitfall consists in
// registering the result of a Middleware,
// not for a method-full pattern (e.g. "GET /api/dogs"),
// but for a "method-less" pattern; see the other example.
func main() {
	cors, err := fcors.AllowAccess(
		fcors.FromOrigins("https://example"),
	)
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	// Because the pattern for which the result of Middleware is registered
	// unduly specifies a method (other than OPTIONS),
	// CORS-preflight requests to /api/dogs cannot reach the CORS middleware.
	// Therefore, CORS preflight will systematically fail
	// and you'll have a bad day...
	mux.Handle("GET /api/dogs", cors(http.HandlerFunc(handleDogsGet))) // incorrect!
	log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleDogsGet(w http.ResponseWriter, _ *http.Request) {
	// omitted
}
Output:

func AllowAccessWithCredentials

func AllowAccessWithCredentials(one Option, others ...Option) (Middleware, error)

AllowAccessWithCredentials creates a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies), according to the specified options. The behavior of the resulting middleware is insensitive to the order in which the options that configure it are specified.

AllowAccessWithCredentials requires a single call to option FromOrigins as one of its arguments.

Using a given option more than once in a call to AllowAccessWithCredentials results in a failure to build the corresponding middleware.

If the specified options are invalid or mutually incompatible, AllowAccessWithCredentials returns a nil Middleware and some non-nil error. Otherwise, it returns a functioning Middleware and a nil error.

Any occurrence of a nil option results in a panic.

type Option

type Option = internal.Option

An Option configures a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies).

You're not meant to implement this interface.

func ExposeResponseHeaders

func ExposeResponseHeaders(one string, others ...string) Option

ExposeResponseHeaders configures a CORS middleware to expose the specified response headers to clients.

Using this option in conjunction with option ExposeAllResponseHeaders in a call to AllowAccess results in a failure to build the corresponding middleware.

Any occurrence of an invalid header name results in a failure to build the corresponding middleware.

Header names are case-insensitive.

The CORS protocol defines a number of so-called "CORS-safelisted response-header names", which need not be explicitly specified as exposed. The CORS protocol also defines a number of so-called "forbidden response-header names", which cannot be exposed to clients. Accordingly, specifying one or more safelisted or forbidden response-header name(s) results in a failure to build the desired middleware.

Finally, some header names that have no place in a response are prohibited:

  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Access-Control-Request-Private-Network
  • Origin

Although a valid response-header name, a literal * is also prohibited; to expose all response headers, use option ExposeAllResponseHeaders instead of this one.

func FromOrigins

func FromOrigins(one string, others ...string) Option

FromOrigins configures a CORS middleware to allow access from any of the Web origins encompassed by the specified origin patterns.

Using this option in conjunction with option FromAnyOrigin in a call to AllowAccess results in a failure to build the corresponding middleware. Any occurrence of a prohibited pattern results in a failure to build the corresponding middleware.

Permitted schemes are limited to http (with a caveat explained further down) and https:

http://example.com             // permitted
https://example.com            // permitted
chrome-extension://example.com // prohibited

Origins must be specified in ASCII serialized form; Unicode is prohibited:

https://example.com            // permitted
https://www.xn--xample-9ua.com // permitted (Punycode)
https://www.éxample.com        // prohibited (Unicode)

For security reasons, the null origin is prohibited.

Hosts that are IPv4 addresses must be specified in dotted-quad notation:

http://255.0.0.0  // permitted
http://0xFF000000 // prohibited

Hosts that are IPv6 addresses must be specified in their compressed form:

http://[::1]:9090                                     // permitted
http://[0:0:0:0:0:0:0:0001]:9090                      // prohibited
http://[0000:0000:0000:0000:0000:0000:0000:0001]:9090 // prohibited

Valid port values range from 1 to 65,535 (inclusive):

https://example.com       // permitted (no port)
https://example.com:1     // permitted
https://example.com:65535 // permitted
https://example.com:0     // prohibited
https://example.com:65536 // prohibited

Default ports (80 for http, 443 for https) must be elided:

http://example.com      // permitted
https://example.com     // permitted
http://example.com:80   // prohibited
https://example.com:443 // prohibited

In addition to support for exact origins, this option provides limited support for origin patterns that encompass multiple origins. A leading asterisk (followed by a full stop) in a host pattern denotes exactly one arbitrary DNS label or several period-separated arbitrary DNS labels. For instance, the pattern

https://*.example.com

encompasses the following origins (among others):

https://foo.example.com
https://bar.example.com
https://bar.foo.example.com
https://baz.bar.foo.example.com

An asterisk in place of a port denotes an arbitrary (possibly implicit) port. For instance,

http://localhost:*

encompasses the following origins (among others),

http://localhost
http://localhost:80
http://localhost:9090

Specifying both arbitrary subdomains and arbitrary ports in a given origin pattern is prohibited:

https://*.example.com       // permitted
https://*.example.com:9090  // permitted
https://example.com:*       // permitted
https://*.example.com:*     // prohibited

No other types of origin patterns are supported. In particular, an origin pattern consisting of a single asterisk is prohibited. If you want to allow (anonymous) access from all origins, use option FromAnyOrigin instead of this one.

Origin patterns whose scheme is http and whose host is neither localhost nor a loopback IP address are deemed insecure; as such, for security reasons, they are by default prohibited when credentialed access and/or some form of Private-Network Access is enabled. If, even in such cases, you wish to deliberately tolerate insecure origins anyway, you must also activate option github.com/jub0bs/fcors/risky.DangerouslyTolerateInsecureOrigins. Otherwise, any occurence of an insecure origin without activating option github.com/jub0bs/fcors/risky.DangerouslyTolerateInsecureOrigins results in a failure to build the corresponding middleware.

Also for security reasons, allowing arbitrary subdomains of a base domain that happens to be a public suffix is by default prohibited:

https://*.example.com  // permitted: example.com is not a public suffix
https://*.com          // prohibited (by default): com is a public suffix
https://*.github.io    // prohibited (by default): github.io is a public suffix

If you need to deliberately allow arbitrary subdomains of a public suffix (danger!), you must also activate option github.com/jub0bs/fcors/risky.DangerouslyTolerateSubdomainsOfPublicSuffixes. Any occurrence of such a prohibited origin pattern without activating option github.com/jub0bs/fcors/risky.DangerouslyTolerateSubdomainsOfPublicSuffixes results in a failure to build the corresponding middleware.

func MaxAgeInSeconds

func MaxAgeInSeconds(delta uint) Option

MaxAgeInSeconds configures a CORS middleware to instruct browsers to cache preflight responses for a maximum duration of delta seconds.

Specifying a max-age value of 0 instructs browsers to eschew caching of preflight responses altogether, whereas omitting to specify a max age causes browsers to cache preflight responses with a default max-age value of 5 seconds.

Because all modern browsers cap the max-age value (the larger upper bound currently is Firefox's: 86,400 seconds), this option accordingly imposes an upper bound on its argument: attempts to specify a max-age value larger than 86400 result in a failure to build the corresponding middleware.

func PreflightSuccessStatus

func PreflightSuccessStatus(code uint) Option

PreflightSuccessStatus configures a CORS middleware to use the specified status code in successful preflight responses.

When this option is not used, the status used in successful preflight responses is 204 No Content.

Specifying a custom status code outside the 2xx range results in a failure to build the corresponding middleware.

func WithAnyMethod

func WithAnyMethod() Option

WithAnyMethod configures a CORS middleware to allow any HTTP method.

Using this option in conjunction with option WithMethods in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

func WithAnyRequestHeaders

func WithAnyRequestHeaders() Option

WithAnyRequestHeaders configures a CORS middleware to allow any request headers.

Using this option in conjunction with option WithRequestHeaders in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

func WithMethods

func WithMethods(one string, others ...string) Option

WithMethods configures a CORS middleware to allow any of the specified HTTP methods.

Using this option in conjunction with option WithAnyMethod in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

Method names are case-sensitive.

The three so-called "CORS-safelisted methods" (GET, HEAD, and POST) are by default allowed by the CORS protocol. As such, allowing them explicitly in your CORS configuration is harmless but never actually necessary.

Moreover, the CORS protocol forbids the use of some method names. Accordingly, any occurrence of an invalid or forbidden method name results in a failure to build the corresponding middleware.

Although a valid method name, a literal * is also prohibited; to allow all methods, use option WithAnyMethod instead of this one.

Note that, contrary to popular belief, listing OPTIONS as an allowed method in your CORS configuration is only required if you wish to allow clients to make explicit use of that method, e.g. via the following client code:

fetch('https://example.com', {method: 'OPTIONS'})

In the great majority of cases, listing OPTIONS as an allowed method in your CORS configuration is unnecessary.

func WithRequestHeaders

func WithRequestHeaders(one string, others ...string) Option

WithRequestHeaders configures a CORS middleware to allow any of the specified request headers.

Using this option in conjunction with option WithAnyRequestHeaders in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

Any occurrence of an invalid header name results in a failure to build the corresponding middleware.

Header names are case-insensitive.

The CORS protocol defines a number of so-called "forbidden request-header names", which are never allowed and that browsers silently drop from client requests. Specifying one or more forbidden request-header name(s) results in a failure to build the corresponding middleware.

Finally, some header names that have no place in a request are prohibited:

  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Private-Network
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age

Although a valid request-header name, a literal * is also prohibited; to allow all request headers, use option WithAnyRequestHeaders instead of this one.

type OptionAnon

type OptionAnon = internal.OptionAnon

An OptionAnon configures a CORS middleware that only allows anonymous access.

You're not meant to implement this interface.

func ExposeAllResponseHeaders

func ExposeAllResponseHeaders() OptionAnon

ExposeAllResponseHeaders configures a CORS middleware to expose all response headers to clients.

Using this option in conjunction with option ExposeResponseHeaders in a call to AllowAccess results in a failure to build the corresponding middleware.

func FromAnyOrigin

func FromAnyOrigin() OptionAnon

FromAnyOrigin configures a CORS middleware to allow any Web origin.

Using this option in conjunction with option FromOrigins in a call to AllowAccess results in a failure to build the corresponding middleware.

Directories

Path Synopsis
Package internal hides all implementation details of packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package internal hides all implementation details of packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
origin
Package origin implements parsing of origins and origin patterns and provides data structures useful for representing a set of origins.
Package origin implements parsing of origins and origin patterns and provides data structures useful for representing a set of origins.
radix
Package radix provides an implementation of a specialized radix tree.
Package radix provides an implementation of a specialized radix tree.
util
Package util provides a generic type representing a mathematical set, and convenience functions for generating error values for packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package util provides a generic type representing a mathematical set, and convenience functions for generating error values for packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package risky provides additional options that complement those provided by package github.com/jub0bs/fcors but that are potentially dangerous.
Package risky provides additional options that complement those provided by package github.com/jub0bs/fcors but that are potentially dangerous.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL