Files
Dmitry VerkhoturovandGitHub 1f34984dab Bump go-pkgz/rest to v1.24.0 and opt in to wildcard origins with credentials (#2157)
rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

The example module is tidied in the same commit, as it reaches go-pkgz/rest
through the replace directive and its indirect graph would otherwise keep the
old pin and fail the readonly module check in CI.

The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
2026-08-19 00:33:13 -05:00

67 lines
1.8 KiB
Go

package rest
import (
"fmt"
"net"
"net/http"
"strings"
"github.com/go-pkgz/rest/realip"
)
// OnlyFrom middleware allows access for limited list of source IPs.
// Rules can be complete IPs (like 192.168.1.12), textual prefixes (129.168.), or CIDRs (192.168.0.0/16).
// Complete IPs use semantic address equality, CIDRs use network containment, and all other rules use prefix matching.
func OnlyFrom(onlyIps ...string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if len(onlyIps) == 0 {
// no restrictions if no ips defined
h.ServeHTTP(w, r)
return
}
matched, ip, err := matchSourceIP(r, onlyIps)
if err != nil {
_ = EncodeJSON(w, http.StatusInternalServerError, JSON{"error": fmt.Sprintf("can't get realip: %s", err)})
return
}
if matched {
// matched ip - allow
h.ServeHTTP(w, r)
return
}
_ = EncodeJSON(w, http.StatusForbidden, JSON{"error": fmt.Sprintf("ip %q rejected", ip)})
}
return http.HandlerFunc(fn)
}
}
// matchSourceIP returns true if request's ip matches any of ips
func matchSourceIP(r *http.Request, ips []string) (result bool, match string, err error) {
ip, err := realip.Get(r)
if err != nil {
return false, "", fmt.Errorf("can't get realip: %w", err) // we can't get ip, so no match
}
parsedIP := net.ParseIP(ip)
// check for cidr, complete ip, or ip prefix
for _, exclIP := range ips {
if _, cidrnet, err := net.ParseCIDR(exclIP); err == nil {
if cidrnet.Contains(parsedIP) {
return true, ip, nil
}
continue
}
if allowedIP := net.ParseIP(exclIP); allowedIP != nil {
if allowedIP.Equal(parsedIP) {
return true, ip, nil
}
continue
}
if strings.HasPrefix(ip, exclIP) {
return true, ip, nil
}
}
return false, ip, nil
}