mirror of
https://github.com/distribution/distribution.git
synced 2026-10-28 10:15:27 +08:00
Merge commit from fork
proxy: bind bearer realms to upstream trust boundary
This commit is contained in:
11 files changed
+560
-2
No files matched your search
@@ -122,3 +122,44 @@ func testAuthChallengeConcurrent(t *testing.T, host string) {
|
||||
}()
|
||||
s.Wait()
|
||||
}
|
||||
|
||||
func TestFilteringManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := NewSimpleManager()
|
||||
manager := NewFilteringManager(base, func(c Challenge) bool {
|
||||
return c.Parameters["service"] == "keep.example.com"
|
||||
})
|
||||
|
||||
endpoint, err := url.Parse("https://registry.example.com/v2/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := &http.Response{
|
||||
Request: &http.Request{
|
||||
URL: endpoint,
|
||||
},
|
||||
Header: make(http.Header),
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
}
|
||||
resp.Header.Add("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="keep.example.com"`)
|
||||
resp.Header.Add("WWW-Authenticate", `Bearer realm="https://evil.example.net/token",service="drop.example.net"`)
|
||||
|
||||
if err := manager.AddResponse(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
challenges, err := manager.GetChallenges(*endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(challenges) != 1 {
|
||||
t.Fatalf("unexpected challenge count: got %d want 1", len(challenges))
|
||||
}
|
||||
|
||||
if got := challenges[0].Parameters["service"]; got != "keep.example.com" {
|
||||
t.Fatalf("unexpected surviving challenge service: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// FilteringManager decorates another Manager and drops challenges that do not
|
||||
// satisfy the configured predicate.
|
||||
type FilteringManager struct {
|
||||
base Manager
|
||||
keep func(Challenge) bool
|
||||
}
|
||||
|
||||
// NewFilteringManager returns a Manager that delegates storage to base and
|
||||
// filters challenges on reads. If keep is nil, the base manager is returned.
|
||||
func NewFilteringManager(base Manager, keep func(Challenge) bool) Manager {
|
||||
if keep == nil {
|
||||
return base
|
||||
}
|
||||
|
||||
return FilteringManager{
|
||||
base: base,
|
||||
keep: keep,
|
||||
}
|
||||
}
|
||||
|
||||
func (m FilteringManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
|
||||
challenges, err := m.base.GetChallenges(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]Challenge, 0, len(challenges))
|
||||
for _, c := range challenges {
|
||||
if m.keep(c) {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (m FilteringManager) AddResponse(resp *http.Response) error {
|
||||
return m.base.AddResponse(resp)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"github.com/distribution/distribution/v3/internal/client/auth"
|
||||
"github.com/distribution/distribution/v3/internal/client/auth/challenge"
|
||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
const challengeHeader = "Docker-Distribution-Api-Version"
|
||||
@@ -66,6 +68,11 @@ func configureAuth(username, password, remoteURL string) (auth.CredentialStore,
|
||||
func getAuthURLs(remoteURL string) ([]string, error) {
|
||||
authURLs := []string{}
|
||||
|
||||
remote, err := url.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(remoteURL + "/v2/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -73,7 +80,7 @@ func getAuthURLs(remoteURL string) ([]string, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
for _, c := range challenge.ResponseChallenges(resp) {
|
||||
if strings.EqualFold(c.Scheme, "bearer") {
|
||||
if strings.EqualFold(c.Scheme, "bearer") && realmAllowed(remote, c.Parameters["realm"]) {
|
||||
authURLs = append(authURLs, c.Parameters["realm"])
|
||||
}
|
||||
}
|
||||
@@ -81,6 +88,49 @@ func getAuthURLs(remoteURL string) ([]string, error) {
|
||||
return authURLs, nil
|
||||
}
|
||||
|
||||
func realmAllowed(remote *url.URL, realm string) bool {
|
||||
realmURL, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if realmURL.Host == "" || remote == nil || remote.Host == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.EqualFold(remote.Host, realmURL.Host) {
|
||||
return true
|
||||
}
|
||||
|
||||
remoteHost := strings.ToLower(remote.Hostname())
|
||||
realmHost := strings.ToLower(realmURL.Hostname())
|
||||
if remoteHost == "" || realmHost == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if isLiteralOrLocal(remoteHost) || isLiteralOrLocal(realmHost) {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.EqualFold(registrableDomain(remoteHost), registrableDomain(realmHost))
|
||||
}
|
||||
|
||||
func isLiteralOrLocal(host string) bool {
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
|
||||
return net.ParseIP(host) != nil
|
||||
}
|
||||
|
||||
func registrableDomain(host string) string {
|
||||
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
func ping(manager challenge.Manager, endpoint, versionHeader string) error {
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/distribution/v3/internal/client/auth/challenge"
|
||||
)
|
||||
|
||||
func TestConfigureAuthAllowsSameAuthorityRealm(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var serverURL string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s/token",service="test-service"`, serverURL))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
serverURL = upstream.URL
|
||||
|
||||
tokenCreds, _, err := configureAuth("user", "pass", upstream.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("configureAuth: %v", err)
|
||||
}
|
||||
|
||||
realmURL, err := url.Parse(serverURL + "/token")
|
||||
if err != nil {
|
||||
t.Fatalf("parse realm: %v", err)
|
||||
}
|
||||
|
||||
username, password := tokenCreds.Basic(realmURL)
|
||||
if username != "user" || password != "pass" {
|
||||
t.Fatalf("unexpected credentials for trusted realm: got (%q, %q)", username, password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureAuthRejectsLoopbackRealmOnDifferentAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
evil := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
t.Cleanup(evil.Close)
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s/token",service="test-service"`, evil.URL))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
tokenCreds, _, err := configureAuth("user", "pass", upstream.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("configureAuth: %v", err)
|
||||
}
|
||||
|
||||
realmURL, err := url.Parse(evil.URL + "/token")
|
||||
if err != nil {
|
||||
t.Fatalf("parse realm: %v", err)
|
||||
}
|
||||
|
||||
username, password := tokenCreds.Basic(realmURL)
|
||||
if username != "" || password != "" {
|
||||
t.Fatalf("unexpected credentials for off-origin realm: got (%q, %q)", username, password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealmAllowedForDockerHubStyleAuthService(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
remoteURL, err := url.Parse("https://registry-1.docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("parse remote url: %v", err)
|
||||
}
|
||||
|
||||
if !realmAllowed(remoteURL, "https://auth.docker.io/token") {
|
||||
t.Fatal("expected docker hub auth realm to remain allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealmFilteringChallengeManagerDropsOffOriginBearer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
remoteURL, err := url.Parse("https://registry.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("parse remote url: %v", err)
|
||||
}
|
||||
|
||||
endpoint, err := url.Parse("https://registry.example.com/v2/")
|
||||
if err != nil {
|
||||
t.Fatalf("parse endpoint: %v", err)
|
||||
}
|
||||
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{URL: endpoint},
|
||||
}
|
||||
resp.Header.Add("Www-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry.example.com"`)
|
||||
resp.Header.Add("Www-Authenticate", `Bearer realm="https://evil.example.net/token",service="registry.example.com"`)
|
||||
|
||||
manager := challenge.NewFilteringManager(challenge.NewSimpleManager(), func(c challenge.Challenge) bool {
|
||||
return !strings.EqualFold(c.Scheme, "bearer") || realmAllowed(remoteURL, c.Parameters["realm"])
|
||||
})
|
||||
if err := manager.AddResponse(resp); err != nil {
|
||||
t.Fatalf("add response: %v", err)
|
||||
}
|
||||
|
||||
challenges, err := manager.GetChallenges(*endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("get challenges: %v", err)
|
||||
}
|
||||
|
||||
if len(challenges) != 1 {
|
||||
t.Fatalf("unexpected challenge count: got %d want 1", len(challenges))
|
||||
}
|
||||
if challenges[0].Scheme != "bearer" {
|
||||
t.Fatalf("unexpected surviving challenge: %+v", challenges[0])
|
||||
}
|
||||
if challenges[0].Parameters["realm"] != "https://auth.example.com/token" {
|
||||
t.Fatalf("unexpected surviving realm: %q", challenges[0].Parameters["realm"])
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -263,7 +264,9 @@ func (r *remoteAuthChallenger) credentialStore() auth.CredentialStore {
|
||||
}
|
||||
|
||||
func (r *remoteAuthChallenger) challengeManager() challenge.Manager {
|
||||
return r.cm
|
||||
return challenge.NewFilteringManager(r.cm, func(c challenge.Challenge) bool {
|
||||
return !strings.EqualFold(c.Scheme, "bearer") || realmAllowed(&r.remoteURL, c.Parameters["realm"])
|
||||
})
|
||||
}
|
||||
|
||||
// tryEstablishChallenges will attempt to get a challenge type for the upstream if none currently exist
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
bolzano-altoadigevje-og-hornnes3-website-us-west-2bomlocustomer-ocienciabonavstackarasjoketokuyamashikokuchuobondigitaloceanspacesakurastoragextraspace-to-rentalstomakomaibarabonesakuratanishikatakazakindustriesteinkjerepbodynaliasnesoddeno-staginglobodoes-itcouldbeworfarsundiskussionsbereichateblobanazawarszawashtenawsapprunnerdpoliticaarparliamenthickarasuyamasoybookonlineboomladeskierniewiceboschristmasakilovecollegefantasyleaguedagestangebostik-serveronagasukeyword-oncillahppictetcieszynishikatsuragit-repostre-totendofinternet-dnsakurawebredirectmeiwamizawabostonakijinsekikogentlentapisa-geekaratsuginamikatagamimozaporizhzhegurinfinitigooglecode-builder-stg-buildereporthruhereclaimsakyotanabellunord-odalvdalcest-le-patron-k3salangenishikawazukamishihorobotdashgabadaddjabbotthuathienhuebouncemerckmsdscloudisrechtrafficplexus-4boutiquebecologialaichaugianglogowegroweibolognagasakikugawaltervistaikillondonetskarelianceboutireserve-onlineboyfriendoftheinternetflixn--11b4c3ditchyouriparmabozen-sudtirolondrinaplesknsalatrobeneventoeidsvollorenskogloomy-gatewaybozen-suedtirolovableprojectjeldsundivtasvuodnakamai-stagingloppennebplaceditorxn--12c1fe0bradescotaruinternationalovepoparochernihivgubamblebtimnetzjaworznotebook-fips3-fips-us-gov-east-1brandivttasvuotnakamuratajirintlon-2brasiliadboxoslodingenishimerabravendbarcelonagawakuyabukikiraragusabaerobatickets3-fips-us-gov-west-1bresciaogashimadachicappabianiceobridgestonebrindisiciliabroadwaybroke-itvedestrandixn--12cfi8ixb8lovesickarlsoybrokerevistathellebrothermesserlidlplfinancialpusercontentjmaxxxn--12co0c3b4evalleaostargets-itjomeldalucaniabrumunddaluccampobassociatesalon-1brusselsaloonishinomiyashironobryanskiervadsoccerhcloudyclusterbrynebweirbzhitomirumaintenanceclothingdustdatadetectoyouracngovtoystre-slidrettozawacnpyatigorskjakamaiedge-stagingreatercnsapporocntozsdeliverycodebergrayjayleaguesardegnarutoshimatta-varjjatranatalcodespotenzakopanecoffeedbackanagawatsonrendercommunity-prochowicecomockashiharacompanyantaishinomakimobetsulifestylefrakkestadurumisakindlegnicahcesuolohmusashimurayamaizuruhr-uni-bochuminamiechizenisshingucciminamifuranocomparemarkerryhotelsardiniacomputercomsecretrosnubarclays3-me-south-1condoshiibabymilk3conferenceconstructioniyodogawaconsuladobeio-static-accesscamdvrcampaniaconsultantranbyconsultingretakamoriokakudamatsuecontactivetrail-central-1contagematsubaracontractorstabacgiangiangryconvexecute-apictureshinordkappaviacookingrimstadynathomebuiltwithdarklangevagrarchitectestingripeeweeklylotterycooperativano-frankivskjervoyagecoprofesionalchikugodaddyn-o-saureadymadethis-a-anarchistjordalshalsenl-ams-1corsicafederationfabricable-modemoneycosenzamamidorivnecosidnsdojoburgriwataraindroppdalcouchpotatofriesarlcouncilcouponstackitagawacozoracpservernamegataitogodoesntexisteingeekashiwaracqcxn--1lqs71dyndns-at-homedepotrani-andria-barletta-trani-andriacrankyotobetsulubin-dsldyndns-at-workisboringsakershusrcfdyndns-blogsiteleaf-south-1crdyndns-freeboxosarpsborgroks-theatrentin-sud-tirolcreditcardyndns-homednsarufutsunomiyawakasaikaitakokonoecreditunioncremonasharis-a-bulls-fancrewp2cricketnedalcrimeast-kazakhstanangercrispawnextdirectraniandriabarlettatraniandriacrminamiiseharacrotonecrownipfizercrsasayamacruisesaseboknowsitallcryptonomichiharacuisinellamdongnairflowersassaris-a-candidatecuneocuritibackdropalermobarag-cloud-charitydalp1cutegirlfriendyndns-ipgwangjulvikashiwazakizunokuniminamiashigarafedoraprojectransiphdfcbankasserverrankoshigayakagefeirafembetsukubankasukabeautypedreamhosterscrapper-sitefermodalenferraraferraris-a-celticsfanferreroticallynxn--2scrj9cargoboavistanbulsan-sudtiroluhanskarmoyfetsundyndns-remotewdhlx3fgroundhandlingroznyfhvalerfilegear-sg-1filminamiminowafinalfinancefinnoyfirebaseapphilipscrappingrphonefosscryptedyndns-serverdalfirenetgamerscrysecuritytacticscwestus2firenzeaburfirestonefirmdaleilaocairportranslatedyndns-webhareidsbergroks-thisayamanobearalvahkikonaikawachinaganoharamcoachampionshiphoplixn--1qqw23afishingokasellfyresdalfitjarfitnessettsurugashimamurogawafjalerfkasumigaurayasudaflesbergrueflickragerotikagoshimandalflierneflirflogintohmangoldpoint2thisamitsukefloppymntransportefloraclegovcloudappservehttpbincheonflorencefloripadualstackasuyakumoduminamioguni5floristanohatakaharunservehumourfloromskoguidefinimalopolskanittedalfltransurlflutterflowhitesnowflakeflyfncarrdiyfndyndns-wikinkobayashimofusadojin-the-bandairlinemurorangecloudplatformshakotanpachihayaakasakawaharacingrondarfoolfor-ourfor-somedusajserveircasacampinagrandebulsan-suedtirolukowesleyfor-theaterfordebianforexrotheworkpccwhminamisanrikubetsupersaleksvikaszubytemarketingvollforgotdnserveminecraftrapanikkoelnforli-cesena-forlicesenaforlikescandypopensocialforsalesforceforsandasuoloisirservemp3fortalfosneservep2photographysiofotravelersinsurancefoxn--30rr7yfozfr-1fr-par-1fr-par-2franalytics-gatewayfredrikstadyndns-worksauheradyndns-mailfreedesktopazimuthaibinhphuocprapidyndns1freemyiphostyhostinguitarservepicservequaLine truncated
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// Package publicsuffix provides a public suffix list based on data from
|
||||
// https://publicsuffix.org/
|
||||
//
|
||||
// A public suffix is one under which Internet users can directly register
|
||||
// names. It is related to, but different from, a TLD (top level domain).
|
||||
//
|
||||
// "com" is a TLD (top level domain). Top level means it has no dots.
|
||||
//
|
||||
// "com" is also a public suffix. Amazon and Google have registered different
|
||||
// siblings under that domain: "amazon.com" and "google.com".
|
||||
//
|
||||
// "au" is another TLD, again because it has no dots. But it's not "amazon.au".
|
||||
// Instead, it's "amazon.com.au".
|
||||
//
|
||||
// "com.au" isn't an actual TLD, because it's not at the top level (it has
|
||||
// dots). But it is an eTLD (effective TLD), because that's the branching point
|
||||
// for domain name registrars.
|
||||
//
|
||||
// Another name for "an eTLD" is "a public suffix". Often, what's more of
|
||||
// interest is the eTLD+1, or one more label than the public suffix. For
|
||||
// example, browsers partition read/write access to HTTP cookies according to
|
||||
// the eTLD+1. Web pages served from "amazon.com.au" can't read cookies from
|
||||
// "google.com.au", but web pages served from "maps.google.com" can share
|
||||
// cookies from "www.google.com", so you don't have to sign into Google Maps
|
||||
// separately from signing into Google Web Search. Note that all four of those
|
||||
// domains have 3 labels and 2 dots. The first two domains are each an eTLD+1,
|
||||
// the last two are not (but share the same eTLD+1: "google.com").
|
||||
//
|
||||
// All of these domains have the same eTLD+1:
|
||||
// - "www.books.amazon.co.uk"
|
||||
// - "books.amazon.co.uk"
|
||||
// - "amazon.co.uk"
|
||||
//
|
||||
// Specifically, the eTLD+1 is "amazon.co.uk", because the eTLD is "co.uk".
|
||||
//
|
||||
// There is no closed form algorithm to calculate the eTLD of a domain.
|
||||
// Instead, the calculation is data driven. This package provides a
|
||||
// pre-compiled snapshot of Mozilla's PSL (Public Suffix List) data at
|
||||
// https://publicsuffix.org/
|
||||
package publicsuffix // import "golang.org/x/net/publicsuffix"
|
||||
|
||||
// TODO: specify case sensitivity and leading/trailing dot behavior for
|
||||
// func PublicSuffix and func EffectiveTLDPlusOne.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/cookiejar"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// List implements the cookiejar.PublicSuffixList interface by calling the
|
||||
// PublicSuffix function.
|
||||
var List cookiejar.PublicSuffixList = list{}
|
||||
|
||||
type list struct{}
|
||||
|
||||
func (list) PublicSuffix(domain string) string {
|
||||
ps, _ := PublicSuffix(domain)
|
||||
return ps
|
||||
}
|
||||
|
||||
func (list) String() string {
|
||||
return version
|
||||
}
|
||||
|
||||
// PublicSuffix returns the public suffix of the domain using a copy of the
|
||||
// publicsuffix.org database compiled into the library.
|
||||
//
|
||||
// icann is whether the public suffix is managed by the Internet Corporation
|
||||
// for Assigned Names and Numbers. If not, the public suffix is either a
|
||||
// privately managed domain (and in practice, not a top level domain) or an
|
||||
// unmanaged top level domain (and not explicitly mentioned in the
|
||||
// publicsuffix.org list). For example, "foo.org" and "foo.co.uk" are ICANN
|
||||
// domains, "foo.dyndns.org" is a private domain and
|
||||
// "cromulent" is an unmanaged top level domain.
|
||||
//
|
||||
// Use cases for distinguishing ICANN domains like "foo.com" from private
|
||||
// domains like "foo.appspot.com" can be found at
|
||||
// https://wiki.mozilla.org/Public_Suffix_List/Use_Cases
|
||||
func PublicSuffix(domain string) (publicSuffix string, icann bool) {
|
||||
if _, err := netip.ParseAddr(domain); err == nil {
|
||||
return domain, false
|
||||
}
|
||||
|
||||
lo, hi := uint32(0), uint32(numTLD)
|
||||
s, suffix, icannNode, wildcard := domain, len(domain), false, false
|
||||
loop:
|
||||
for {
|
||||
dot := strings.LastIndexByte(s, '.')
|
||||
if wildcard {
|
||||
icann = icannNode
|
||||
suffix = 1 + dot
|
||||
}
|
||||
if lo == hi {
|
||||
break
|
||||
}
|
||||
f := find(s[1+dot:], lo, hi)
|
||||
if f == notFound {
|
||||
break
|
||||
}
|
||||
|
||||
u := uint32(nodes.get(f) >> (nodesBitsTextOffset + nodesBitsTextLength))
|
||||
icannNode = u&(1<<nodesBitsICANN-1) != 0
|
||||
u >>= nodesBitsICANN
|
||||
u = children.get(u & (1<<nodesBitsChildren - 1))
|
||||
lo = u & (1<<childrenBitsLo - 1)
|
||||
u >>= childrenBitsLo
|
||||
hi = u & (1<<childrenBitsHi - 1)
|
||||
u >>= childrenBitsHi
|
||||
switch u & (1<<childrenBitsNodeType - 1) {
|
||||
case nodeTypeNormal:
|
||||
suffix = 1 + dot
|
||||
case nodeTypeException:
|
||||
suffix = 1 + len(s)
|
||||
break loop
|
||||
}
|
||||
u >>= childrenBitsNodeType
|
||||
wildcard = u&(1<<childrenBitsWildcard-1) != 0
|
||||
if !wildcard {
|
||||
icann = icannNode
|
||||
}
|
||||
|
||||
if dot == -1 {
|
||||
break
|
||||
}
|
||||
s = s[:dot]
|
||||
}
|
||||
if suffix == len(domain) {
|
||||
// If no rules match, the prevailing rule is "*".
|
||||
return domain[1+strings.LastIndexByte(domain, '.'):], icann
|
||||
}
|
||||
return domain[suffix:], icann
|
||||
}
|
||||
|
||||
const notFound uint32 = 1<<32 - 1
|
||||
|
||||
// find returns the index of the node in the range [lo, hi) whose label equals
|
||||
// label, or notFound if there is no such node. The range is assumed to be in
|
||||
// strictly increasing node label order.
|
||||
func find(label string, lo, hi uint32) uint32 {
|
||||
for lo < hi {
|
||||
mid := lo + (hi-lo)/2
|
||||
s := nodeLabel(mid)
|
||||
if s < label {
|
||||
lo = mid + 1
|
||||
} else if s == label {
|
||||
return mid
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return notFound
|
||||
}
|
||||
|
||||
// nodeLabel returns the label for the i'th node.
|
||||
func nodeLabel(i uint32) string {
|
||||
x := nodes.get(i)
|
||||
length := x & (1<<nodesBitsTextLength - 1)
|
||||
x >>= nodesBitsTextLength
|
||||
offset := x & (1<<nodesBitsTextOffset - 1)
|
||||
return text[offset : offset+length]
|
||||
}
|
||||
|
||||
// EffectiveTLDPlusOne returns the effective top level domain plus one more
|
||||
// label. For example, the eTLD+1 for "foo.bar.golang.org" is "golang.org".
|
||||
func EffectiveTLDPlusOne(domain string) (string, error) {
|
||||
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") || strings.Contains(domain, "..") {
|
||||
return "", fmt.Errorf("publicsuffix: empty label in domain %q", domain)
|
||||
}
|
||||
|
||||
suffix, _ := PublicSuffix(domain)
|
||||
if len(domain) <= len(suffix) {
|
||||
return "", fmt.Errorf("publicsuffix: cannot derive eTLD+1 for domain %q", domain)
|
||||
}
|
||||
i := len(domain) - len(suffix) - 1
|
||||
if domain[i] != '.' {
|
||||
return "", fmt.Errorf("publicsuffix: invalid public suffix %q for domain %q", suffix, domain)
|
||||
}
|
||||
return domain[1+strings.LastIndexByte(domain[:i], '.'):], nil
|
||||
}
|
||||
|
||||
type uint32String string
|
||||
|
||||
func (u uint32String) get(i uint32) uint32 {
|
||||
off := i * 4
|
||||
u = u[off:] // help the compiler reduce bounds checks
|
||||
return uint32(u[3]) |
|
||||
uint32(u[2])<<8 |
|
||||
uint32(u[1])<<16 |
|
||||
uint32(u[0])<<24
|
||||
}
|
||||
|
||||
type uint40String string
|
||||
|
||||
func (u uint40String) get(i uint32) uint64 {
|
||||
off := uint64(i * (nodesBits / 8))
|
||||
u = u[off:] // help the compiler reduce bounds checks
|
||||
return uint64(u[4]) |
|
||||
uint64(u[3])<<8 |
|
||||
uint64(u[2])<<16 |
|
||||
uint64(u[1])<<24 |
|
||||
uint64(u[0])<<32
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// generated by go run gen.go; DO NOT EDIT
|
||||
|
||||
package publicsuffix
|
||||
|
||||
import _ "embed"
|
||||
|
||||
const version = "publicsuffix.org's public_suffix_list.dat, git revision 2c960dac3d39ba521eb5db9da192968f5be0aded (2025-03-18T07:22:13Z)"
|
||||
|
||||
const (
|
||||
nodesBits = 40
|
||||
nodesBitsChildren = 10
|
||||
nodesBitsICANN = 1
|
||||
nodesBitsTextOffset = 16
|
||||
nodesBitsTextLength = 6
|
||||
|
||||
childrenBitsWildcard = 1
|
||||
childrenBitsNodeType = 2
|
||||
childrenBitsHi = 14
|
||||
childrenBitsLo = 14
|
||||
)
|
||||
|
||||
const (
|
||||
nodeTypeNormal = 0
|
||||
nodeTypeException = 1
|
||||
nodeTypeParentOnly = 2
|
||||
)
|
||||
|
||||
// numTLD is the number of top level domains.
|
||||
const numTLD = 1454
|
||||
|
||||
// text is the combined text of all labels.
|
||||
//
|
||||
//go:embed data/text
|
||||
var text string
|
||||
|
||||
// nodes is the list of nodes. Each node is represented as a 40-bit integer,
|
||||
// which encodes the node's children, wildcard bit and node type (as an index
|
||||
// into the children array), ICANN bit and text.
|
||||
//
|
||||
// The layout within the node, from MSB to LSB, is:
|
||||
//
|
||||
// [ 7 bits] unused
|
||||
// [10 bits] children index
|
||||
// [ 1 bits] ICANN bit
|
||||
// [16 bits] text index
|
||||
// [ 6 bits] text length
|
||||
//
|
||||
//go:embed data/nodes
|
||||
var nodes uint40String
|
||||
|
||||
// children is the list of nodes' children, the parent's wildcard bit and the
|
||||
// parent's node type. If a node has no children then their children index
|
||||
// will be in the range [0, 6), depending on the wildcard bit and node type.
|
||||
//
|
||||
// The layout within the uint32, from MSB to LSB, is:
|
||||
//
|
||||
// [ 1 bits] unused
|
||||
// [ 1 bits] wildcard bit
|
||||
// [ 2 bits] node type
|
||||
// [14 bits] high nodes index (exclusive) of children
|
||||
// [14 bits] low nodes index (inclusive) of children
|
||||
//
|
||||
//go:embed data/children
|
||||
var children uint32String
|
||||
|
||||
// max children 870 (capacity 1023)
|
||||
// max text offset 31785 (capacity 65535)
|
||||
// max text length 31 (capacity 63)
|
||||
// max hi 10100 (capacity 16383)
|
||||
// max lo 10095 (capacity 16383)
|
||||
Vendored
+1
@@ -680,6 +680,7 @@ golang.org/x/net/idna
|
||||
golang.org/x/net/internal/httpcommon
|
||||
golang.org/x/net/internal/httpsfv
|
||||
golang.org/x/net/internal/timeseries
|
||||
golang.org/x/net/publicsuffix
|
||||
golang.org/x/net/trace
|
||||
# golang.org/x/oauth2 v0.35.0
|
||||
## explicit; go 1.24.0
|
||||
|
||||
Reference in new issue
Block a user