Merge branch 'develop' into sdk

This commit is contained in:
hahwu 2025-01-24 12:30:11 +08:00
commit 5a3e42628a
676 changed files with 97 additions and 239047 deletions

BIN
src/server/Order.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -90,7 +90,7 @@ func (f *FriendMgr) sendToPlayer(m *msg.Msg) (interface{}, error) {
func (f *FriendMgr) sync(m *msg.Msg) (interface{}, error) {
data := f.getData().List[m.From]
f.getData().List[m.From] = make([]*msg.Msg, 0)
log.Debug("sync friendMgr msg to player success")
log.Debug("sync friendMgr msg to player %d success mailbox %v", m.From, data)
return data, nil
}

View File

@ -561,6 +561,7 @@ func (ad *GameLogic) ReplaceExistPlayerAndAgent(a gate.Agent, player *Player) er
Timer.Stop()
}
player.LoginBackData()
SyncFriendMsg(player)
log.Debug("player %d 重连", player.M_DwUin)
return nil
}

View File

@ -2,9 +2,12 @@ package game
import (
"fmt"
"math"
mergeDataCfg "server/conf/mergeData"
"server/game/mod/order"
"server/game/mod/sevenLogin"
"server/msg"
"server/pkg/github.com/name5566/leaf/log"
)
func UnitEndlessReward(p *Player) error {
@ -105,3 +108,75 @@ func UnitCard(p *Player) error {
}
return nil
}
func UnitOrder1(p *Player, Lv, EnergyMul int) ([]string, [][]float64) {
// ChessMod := p.PlayMod.getChessMod()
// Emit := ChessMod.GetEmitList()
// OrderMod := p.PlayMod.getOrderMod()
// OrderMod.LastDiff = 1
// OrderMod.OrderList = make(map[int]order.Order)
// // ChessMod := p.PlayMod.getChessMod()
// // EmitList := ChessMod.GetEmitList()
// // fmt.Println(EmitList)
// OrderMod.Debug = make(map[int]int)
// for i := 0; i < 5000; i++ {
// err := OrderMod.CreateNormalOrder(Lv, Emit, EnergyMul)
// log.Debug("OrderMod.CreateNormalOrder %d", i)
// if err != nil {
// return nil, nil
// }
// }
// I := make(map[int]int)
// for k, v := range OrderMod.Debug {
// // fmt.Printf("chessId %d, num %d\n", k, v)
// ChessLv := mergeDataCfg.GetLvById(k)
// I[ChessLv] += v
// }
// keys := make([]int, 0, len(I))
// Sum := 0
// for k, v := range I {
// Sum += v
// keys = append(keys, k)
// }
// sort.Ints(keys)
// xValue := make([]string, 0)
// yValue := make([][]float64, 0)
// line := make([]float64, 0)
// for _, k := range keys {
// xValue = append(xValue, fmt.Sprintf("Lv %d", k))
// line = append(line, float64(I[k])/float64(Sum)*100)
// fmt.Printf("Lv %d, num %.2f %%\n", k, float64(I[k])/float64(Sum)*100)
// }
// yValue = append(yValue, line)
// return xValue, yValue
return nil, nil
}
func UnitOrder2(p *Player, Lv, EnergyMul int) float64 {
ChessMod := p.PlayMod.getChessMod()
Emit := ChessMod.GetEmitList()
OrderMod := p.PlayMod.getOrderMod()
OrderMod.LastDiff = 1
OrderMod.OrderList = make(map[int]order.Order)
// ChessMod := p.PlayMod.getChessMod()
// EmitList := ChessMod.GetEmitList()
// fmt.Println(EmitList)
// OrderMod.Debug = make(map[int]int)
for i := 0; i < 5000; i++ {
err := OrderMod.CreateNormalOrder(Lv, Emit, EnergyMul)
log.Debug("OrderMod.CreateNormalOrder %d", i)
if err != nil {
return 0
}
}
Energy := 0.0
for _, v := range OrderMod.OrderList {
for _, v1 := range v.MergeId {
Color := mergeDataCfg.GetColorById(v1)
Lv := mergeDataCfg.GetLvById(v1)
EmitId := order.GetEmitByColor(Emit, Color)
AdjustLv := mergeDataCfg.GetAdjust(EmitId, Color)
Energy += math.Pow(2, float64(Lv-1+AdjustLv))
}
}
return Energy / float64(5000)
}

View File

@ -335,11 +335,13 @@ func getEmitSeries(o *OrderMod, Emit []int) string {
if len(EmitProduct) == 0 || EmitProduct[0] == "" {
continue
}
EmitId := mergeDataCfg.GetEmitId(v)
if GoUtil.InStringArray(EmitId, o.EmitShuffle) {
EmitSerie := mergeDataCfg.GetEmitId(v)
if GoUtil.InStringArray(EmitSerie, o.EmitShuffle) {
continue
}
o.EmitShuffle = append(o.EmitShuffle, EmitId)
for i := 0; i < len(EmitProduct); i++ {
o.EmitShuffle = append(o.EmitShuffle, EmitSerie)
}
}
GoUtil.ShuffleStringArray(o.EmitShuffle)
}
@ -348,11 +350,17 @@ func getEmitSeries(o *OrderMod, Emit []int) string {
if len(o.EmitShuffle) == 0 {
o.EmitShuffle = make([]string, 0)
for _, v := range Emit {
EmitId := mergeDataCfg.GetEmitId(v)
if GoUtil.InStringArray(EmitId, o.EmitShuffle) {
EmitProduct := mergeDataCfg.GetEmitProduceType(v)
if len(EmitProduct) == 0 || EmitProduct[0] == "" {
continue
}
o.EmitShuffle = append(o.EmitShuffle, EmitId)
EmitSerie := mergeDataCfg.GetEmitId(v)
if GoUtil.InStringArray(EmitSerie, o.EmitShuffle) {
continue
}
for i := 0; i < len(EmitProduct); i++ {
o.EmitShuffle = append(o.EmitShuffle, EmitSerie)
}
}
Num := 1
for {

View File

@ -11,37 +11,20 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/segmentio/kafka-go v0.4.47
github.com/vicanso/go-charts/v2 v2.6.10
google.golang.org/api v0.217.0
google.golang.org/protobuf v1.36.2
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
)
require (
cloud.google.com/go/auth v0.14.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.31.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.69.4 // indirect
)
require (

View File

@ -1,9 +1,3 @@
cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/ThinkingDataAnalytics/go-sdk/v2 v2.0.3 h1:+2h2cOKzZgP8DmtuvkmUhOs5WfyseFTc0KLNR3EU2eA=
@ -21,32 +15,17 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@ -91,23 +70,9 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -120,13 +85,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -156,14 +117,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU=
google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,382 +0,0 @@
# Changelog
## [0.14.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.13.0...auth/v0.14.0) (2025-01-08)
### Features
* **auth:** Add universe domain support to idtoken ([#11059](https://github.com/googleapis/google-cloud-go/issues/11059)) ([72add7e](https://github.com/googleapis/google-cloud-go/commit/72add7e9f8f455af695e8ef79212a4bd3122fb3a))
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.33.0 ([e9b0b69](https://github.com/googleapis/google-cloud-go/commit/e9b0b69644ea5b276cacff0a707e8a5e87efafc9))
* **auth:** Fix copy of delegates in impersonate.NewIDTokenCredentials ([#11386](https://github.com/googleapis/google-cloud-go/issues/11386)) ([ff7ef8e](https://github.com/googleapis/google-cloud-go/commit/ff7ef8e7ade7171bce3e4f30ff10a2e9f6c27ca0)), refs [#11379](https://github.com/googleapis/google-cloud-go/issues/11379)
* **auth:** Update golang.org/x/net to v0.33.0 ([e9b0b69](https://github.com/googleapis/google-cloud-go/commit/e9b0b69644ea5b276cacff0a707e8a5e87efafc9))
## [0.13.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.1...auth/v0.13.0) (2024-12-13)
### Features
* **auth:** Add logging support ([#11079](https://github.com/googleapis/google-cloud-go/issues/11079)) ([c80e31d](https://github.com/googleapis/google-cloud-go/commit/c80e31df5ecb33a810be3dfb9d9e27ac531aa91d))
* **auth:** Pass logger from auth layer to metadata package ([#11288](https://github.com/googleapis/google-cloud-go/issues/11288)) ([b552efd](https://github.com/googleapis/google-cloud-go/commit/b552efd6ab34e5dfded18438e0fbfd925805614f))
### Bug Fixes
* **auth:** Check compute cred type before non-default flag for DP ([#11255](https://github.com/googleapis/google-cloud-go/issues/11255)) ([4347ca1](https://github.com/googleapis/google-cloud-go/commit/4347ca141892be8ae813399b4b437662a103bc90))
## [0.12.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.0...auth/v0.12.1) (2024-12-10)
### Bug Fixes
* **auth:** Correct typo in link ([#11160](https://github.com/googleapis/google-cloud-go/issues/11160)) ([af6fb46](https://github.com/googleapis/google-cloud-go/commit/af6fb46d7cd694ddbe8c9d63bc4cdcd62b9fb2c1))
## [0.12.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.11.0...auth/v0.12.0) (2024-12-04)
### Features
* **auth:** Add support for providing custom certificate URL ([#11006](https://github.com/googleapis/google-cloud-go/issues/11006)) ([ebf3657](https://github.com/googleapis/google-cloud-go/commit/ebf36579724afb375d3974cf1da38f703e3b7dbc)), refs [#11005](https://github.com/googleapis/google-cloud-go/issues/11005)
### Bug Fixes
* **auth:** Ensure endpoints are present in Validator ([#11209](https://github.com/googleapis/google-cloud-go/issues/11209)) ([106cd53](https://github.com/googleapis/google-cloud-go/commit/106cd53309facaef1b8ea78376179f523f6912b9)), refs [#11006](https://github.com/googleapis/google-cloud-go/issues/11006) [#11190](https://github.com/googleapis/google-cloud-go/issues/11190) [#11189](https://github.com/googleapis/google-cloud-go/issues/11189) [#11188](https://github.com/googleapis/google-cloud-go/issues/11188)
## [0.11.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.2...auth/v0.11.0) (2024-11-21)
### Features
* **auth:** Add universe domain support to mTLS ([#11159](https://github.com/googleapis/google-cloud-go/issues/11159)) ([117748b](https://github.com/googleapis/google-cloud-go/commit/117748ba1cfd4ae62a6a4feb7e30951cb2bc9344))
## [0.10.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.1...auth/v0.10.2) (2024-11-12)
### Bug Fixes
* **auth:** Restore use of grpc.Dial ([#11118](https://github.com/googleapis/google-cloud-go/issues/11118)) ([2456b94](https://github.com/googleapis/google-cloud-go/commit/2456b943b7b8aaabd4d8bfb7572c0f477ae0db45)), refs [#7556](https://github.com/googleapis/google-cloud-go/issues/7556)
## [0.10.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.0...auth/v0.10.1) (2024-11-06)
### Bug Fixes
* **auth:** Restore Application Default Credentials support to idtoken ([#11083](https://github.com/googleapis/google-cloud-go/issues/11083)) ([8771f2e](https://github.com/googleapis/google-cloud-go/commit/8771f2ea9807ab822083808e0678392edff3b4f2))
* **auth:** Skip impersonate universe domain check if empty ([#11086](https://github.com/googleapis/google-cloud-go/issues/11086)) ([87159c1](https://github.com/googleapis/google-cloud-go/commit/87159c1059d4a18d1367ce62746a838a94964ab6))
## [0.10.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.9...auth/v0.10.0) (2024-10-30)
### Features
* **auth:** Add universe domain support to credentials/impersonate ([#10953](https://github.com/googleapis/google-cloud-go/issues/10953)) ([e06cb64](https://github.com/googleapis/google-cloud-go/commit/e06cb6499f7eda3aef08ab18ff197016f667684b))
## [0.9.9](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.8...auth/v0.9.9) (2024-10-22)
### Bug Fixes
* **auth:** Fallback cert lookups for missing files ([#11013](https://github.com/googleapis/google-cloud-go/issues/11013)) ([bd76695](https://github.com/googleapis/google-cloud-go/commit/bd766957ec238b7c40ddbabb369e612dc9b07313)), refs [#10844](https://github.com/googleapis/google-cloud-go/issues/10844)
* **auth:** Replace MDS endpoint universe_domain with universe-domain ([#11000](https://github.com/googleapis/google-cloud-go/issues/11000)) ([6a1586f](https://github.com/googleapis/google-cloud-go/commit/6a1586f2ce9974684affaea84e7b629313b4d114))
## [0.9.8](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.7...auth/v0.9.8) (2024-10-09)
### Bug Fixes
* **auth:** Restore OpenTelemetry handling in transports ([#10968](https://github.com/googleapis/google-cloud-go/issues/10968)) ([08c6d04](https://github.com/googleapis/google-cloud-go/commit/08c6d04901c1a20e219b2d86df41dbaa6d7d7b55)), refs [#10962](https://github.com/googleapis/google-cloud-go/issues/10962)
* **auth:** Try talk to plaintext S2A if credentials can not be found for mTLS-S2A ([#10941](https://github.com/googleapis/google-cloud-go/issues/10941)) ([0f0bf2d](https://github.com/googleapis/google-cloud-go/commit/0f0bf2d18c97dd8b65bcf0099f0802b5631c6287))
## [0.9.7](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.6...auth/v0.9.7) (2024-10-01)
### Bug Fixes
* **auth:** Restore support for non-default service accounts for DirectPath ([#10937](https://github.com/googleapis/google-cloud-go/issues/10937)) ([a38650e](https://github.com/googleapis/google-cloud-go/commit/a38650edbf420223077498cafa537aec74b37aad)), refs [#10907](https://github.com/googleapis/google-cloud-go/issues/10907)
## [0.9.6](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.5...auth/v0.9.6) (2024-09-30)
### Bug Fixes
* **auth:** Make aws credentials provider retrieve fresh credentials ([#10920](https://github.com/googleapis/google-cloud-go/issues/10920)) ([250fbf8](https://github.com/googleapis/google-cloud-go/commit/250fbf87d858d865e399a241b7e537c4ff0c3dd8))
## [0.9.5](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.4...auth/v0.9.5) (2024-09-25)
### Bug Fixes
* **auth:** Restore support for GOOGLE_CLOUD_UNIVERSE_DOMAIN env ([#10915](https://github.com/googleapis/google-cloud-go/issues/10915)) ([94caaaa](https://github.com/googleapis/google-cloud-go/commit/94caaaa061362d0e00ef6214afcc8a0a3e7ebfb2))
* **auth:** Skip directpath credentials overwrite when it's not on GCE ([#10833](https://github.com/googleapis/google-cloud-go/issues/10833)) ([7e5e8d1](https://github.com/googleapis/google-cloud-go/commit/7e5e8d10b761b0a6e43e19a028528db361bc07b1))
* **auth:** Use new context for non-blocking token refresh ([#10919](https://github.com/googleapis/google-cloud-go/issues/10919)) ([cf7102d](https://github.com/googleapis/google-cloud-go/commit/cf7102d33a21be1e5a9d47a49456b3a57c43b350))
## [0.9.4](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.3...auth/v0.9.4) (2024-09-11)
### Bug Fixes
* **auth:** Enable self-signed JWT for non-GDU universe domain ([#10831](https://github.com/googleapis/google-cloud-go/issues/10831)) ([f9869f7](https://github.com/googleapis/google-cloud-go/commit/f9869f7903cfd34d1b97c25d0dc5669d2c5138e6))
## [0.9.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.2...auth/v0.9.3) (2024-09-03)
### Bug Fixes
* **auth:** Choose quota project envvar over file when both present ([#10807](https://github.com/googleapis/google-cloud-go/issues/10807)) ([2d8dd77](https://github.com/googleapis/google-cloud-go/commit/2d8dd7700eff92d4b95027be55e26e1e7aa79181)), refs [#10804](https://github.com/googleapis/google-cloud-go/issues/10804)
## [0.9.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.1...auth/v0.9.2) (2024-08-30)
### Bug Fixes
* **auth:** Handle non-Transport DefaultTransport ([#10733](https://github.com/googleapis/google-cloud-go/issues/10733)) ([98d91dc](https://github.com/googleapis/google-cloud-go/commit/98d91dc8316b247498fab41ab35e57a0446fe556)), refs [#10742](https://github.com/googleapis/google-cloud-go/issues/10742)
* **auth:** Make sure quota option takes precedence over env/file ([#10797](https://github.com/googleapis/google-cloud-go/issues/10797)) ([f1b050d](https://github.com/googleapis/google-cloud-go/commit/f1b050d56d804b245cab048c2980d32b0eaceb4e)), refs [#10795](https://github.com/googleapis/google-cloud-go/issues/10795)
### Documentation
* **auth:** Fix Go doc comment link ([#10751](https://github.com/googleapis/google-cloud-go/issues/10751)) ([015acfa](https://github.com/googleapis/google-cloud-go/commit/015acfab4d172650928bb1119bc2cd6307b9a437))
## [0.9.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.0...auth/v0.9.1) (2024-08-22)
### Bug Fixes
* **auth:** Setting expireEarly to default when the value is 0 ([#10732](https://github.com/googleapis/google-cloud-go/issues/10732)) ([5e67869](https://github.com/googleapis/google-cloud-go/commit/5e67869a31e9e8ecb4eeebd2cfa11a761c3b1948))
## [0.9.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.1...auth/v0.9.0) (2024-08-16)
### Features
* **auth:** Auth library can talk to S2A over mTLS ([#10634](https://github.com/googleapis/google-cloud-go/issues/10634)) ([5250a13](https://github.com/googleapis/google-cloud-go/commit/5250a13ec95b8d4eefbe0158f82857ff2189cb45))
## [0.8.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.0...auth/v0.8.1) (2024-08-13)
### Bug Fixes
* **auth:** Make default client creation more lenient ([#10669](https://github.com/googleapis/google-cloud-go/issues/10669)) ([1afb9ee](https://github.com/googleapis/google-cloud-go/commit/1afb9ee1ee9de9810722800018133304a0ca34d1)), refs [#10638](https://github.com/googleapis/google-cloud-go/issues/10638)
## [0.8.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.3...auth/v0.8.0) (2024-08-07)
### Features
* **auth:** Adds support for X509 workload identity federation ([#10373](https://github.com/googleapis/google-cloud-go/issues/10373)) ([5d07505](https://github.com/googleapis/google-cloud-go/commit/5d075056cbe27bb1da4072a26070c41f8999eb9b))
## [0.7.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.2...auth/v0.7.3) (2024-08-01)
### Bug Fixes
* **auth/oauth2adapt:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
* **auth:** Disable automatic universe domain check for MDS ([#10620](https://github.com/googleapis/google-cloud-go/issues/10620)) ([7cea5ed](https://github.com/googleapis/google-cloud-go/commit/7cea5edd5a0c1e6bca558696f5607879141910e8))
* **auth:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
## [0.7.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.1...auth/v0.7.2) (2024-07-22)
### Bug Fixes
* **auth:** Use default client for universe metadata lookup ([#10551](https://github.com/googleapis/google-cloud-go/issues/10551)) ([d9046fd](https://github.com/googleapis/google-cloud-go/commit/d9046fdd1435d1ce48f374806c1def4cb5ac6cd3)), refs [#10544](https://github.com/googleapis/google-cloud-go/issues/10544)
## [0.7.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.0...auth/v0.7.1) (2024-07-10)
### Bug Fixes
* **auth:** Bump google.golang.org/grpc@v1.64.1 ([8ecc4e9](https://github.com/googleapis/google-cloud-go/commit/8ecc4e9622e5bbe9b90384d5848ab816027226c5))
## [0.7.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.1...auth/v0.7.0) (2024-07-09)
### Features
* **auth:** Add workload X509 cert provider as a default cert provider ([#10479](https://github.com/googleapis/google-cloud-go/issues/10479)) ([c51ee6c](https://github.com/googleapis/google-cloud-go/commit/c51ee6cf65ce05b4d501083e49d468c75ac1ea63))
### Bug Fixes
* **auth/oauth2adapt:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
* **auth:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
* **auth:** Check len of slices, not non-nil ([#10483](https://github.com/googleapis/google-cloud-go/issues/10483)) ([0a966a1](https://github.com/googleapis/google-cloud-go/commit/0a966a183e5f0e811977216d736d875b7233e942))
## [0.6.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.0...auth/v0.6.1) (2024-07-01)
### Bug Fixes
* **auth:** Support gRPC API keys ([#10460](https://github.com/googleapis/google-cloud-go/issues/10460)) ([daa6646](https://github.com/googleapis/google-cloud-go/commit/daa6646d2af5d7fb5b30489f4934c7db89868c7c))
* **auth:** Update http and grpc transports to support token exchange over mTLS ([#10397](https://github.com/googleapis/google-cloud-go/issues/10397)) ([c6dfdcf](https://github.com/googleapis/google-cloud-go/commit/c6dfdcf893c3f971eba15026c12db0a960ae81f2))
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.2...auth/v0.6.0) (2024-06-25)
### Features
* **auth:** Add non-blocking token refresh for compute MDS ([#10263](https://github.com/googleapis/google-cloud-go/issues/10263)) ([9ac350d](https://github.com/googleapis/google-cloud-go/commit/9ac350da11a49b8e2174d3fc5b1a5070fec78b4e))
### Bug Fixes
* **auth:** Return error if envvar detected file returns an error ([#10431](https://github.com/googleapis/google-cloud-go/issues/10431)) ([e52b9a7](https://github.com/googleapis/google-cloud-go/commit/e52b9a7c45468827f5d220ab00965191faeb9d05))
## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.1...auth/v0.5.2) (2024-06-24)
### Bug Fixes
* **auth:** Fetch initial token when CachedTokenProviderOptions.DisableAutoRefresh is true ([#10415](https://github.com/googleapis/google-cloud-go/issues/10415)) ([3266763](https://github.com/googleapis/google-cloud-go/commit/32667635ca2efad05cd8c087c004ca07d7406913)), refs [#10414](https://github.com/googleapis/google-cloud-go/issues/10414)
## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.0...auth/v0.5.1) (2024-05-31)
### Bug Fixes
* **auth:** Pass through client to 2LO and 3LO flows ([#10290](https://github.com/googleapis/google-cloud-go/issues/10290)) ([685784e](https://github.com/googleapis/google-cloud-go/commit/685784ea84358c15e9214bdecb307d37aa3b6d2f))
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.2...auth/v0.5.0) (2024-05-28)
### Features
* **auth:** Adds X509 workload certificate provider ([#10233](https://github.com/googleapis/google-cloud-go/issues/10233)) ([17a9db7](https://github.com/googleapis/google-cloud-go/commit/17a9db73af35e3d1a7a25ac4fd1377a103de6150))
## [0.4.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.1...auth/v0.4.2) (2024-05-16)
### Bug Fixes
* **auth:** Enable client certificates by default only for GDU ([#10151](https://github.com/googleapis/google-cloud-go/issues/10151)) ([7c52978](https://github.com/googleapis/google-cloud-go/commit/7c529786275a39b7e00525f7d5e7be0d963e9e15))
* **auth:** Handle non-Transport DefaultTransport ([#10162](https://github.com/googleapis/google-cloud-go/issues/10162)) ([fa3bfdb](https://github.com/googleapis/google-cloud-go/commit/fa3bfdb23aaa45b34394a8b61e753b3587506782)), refs [#10159](https://github.com/googleapis/google-cloud-go/issues/10159)
* **auth:** Have refresh time match docs ([#10147](https://github.com/googleapis/google-cloud-go/issues/10147)) ([bcb5568](https://github.com/googleapis/google-cloud-go/commit/bcb5568c07a54dd3d2e869d15f502b0741a609e8))
* **auth:** Update compute token fetching error with named prefix ([#10180](https://github.com/googleapis/google-cloud-go/issues/10180)) ([4573504](https://github.com/googleapis/google-cloud-go/commit/4573504828d2928bebedc875d87650ba227829ea))
## [0.4.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.0...auth/v0.4.1) (2024-05-09)
### Bug Fixes
* **auth:** Don't try to detect default creds it opt configured ([#10143](https://github.com/googleapis/google-cloud-go/issues/10143)) ([804632e](https://github.com/googleapis/google-cloud-go/commit/804632e7c5b0b85ff522f7951114485e256eb5bc))
## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.3.0...auth/v0.4.0) (2024-05-07)
### Features
* **auth:** Enable client certificates by default ([#10102](https://github.com/googleapis/google-cloud-go/issues/10102)) ([9013e52](https://github.com/googleapis/google-cloud-go/commit/9013e5200a6ec0f178ed91acb255481ffb073a2c))
### Bug Fixes
* **auth:** Get s2a logic up to date ([#10093](https://github.com/googleapis/google-cloud-go/issues/10093)) ([4fe9ae4](https://github.com/googleapis/google-cloud-go/commit/4fe9ae4b7101af2a5221d6d6b2e77b479305bb06))
## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.2...auth/v0.3.0) (2024-04-23)
### Features
* **auth/httptransport:** Add ability to customize transport ([#10023](https://github.com/googleapis/google-cloud-go/issues/10023)) ([72c7f6b](https://github.com/googleapis/google-cloud-go/commit/72c7f6bbec3136cc7a62788fc7186bc33ef6c3b3)), refs [#9812](https://github.com/googleapis/google-cloud-go/issues/9812) [#9814](https://github.com/googleapis/google-cloud-go/issues/9814)
### Bug Fixes
* **auth/credentials:** Error on bad file name if explicitly set ([#10018](https://github.com/googleapis/google-cloud-go/issues/10018)) ([55beaa9](https://github.com/googleapis/google-cloud-go/commit/55beaa993aaf052d8be39766afc6777c3c2a0bdd)), refs [#9809](https://github.com/googleapis/google-cloud-go/issues/9809)
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.1...auth/v0.2.2) (2024-04-19)
### Bug Fixes
* **auth:** Add internal opt to skip validation on transports ([#9999](https://github.com/googleapis/google-cloud-go/issues/9999)) ([9e20ef8](https://github.com/googleapis/google-cloud-go/commit/9e20ef89f6287d6bd03b8697d5898dc43b4a77cf)), refs [#9823](https://github.com/googleapis/google-cloud-go/issues/9823)
* **auth:** Set secure flag for gRPC conn pools ([#10002](https://github.com/googleapis/google-cloud-go/issues/10002)) ([14e3956](https://github.com/googleapis/google-cloud-go/commit/14e3956dfd736399731b5ee8d9b178ae085cf7ba)), refs [#9833](https://github.com/googleapis/google-cloud-go/issues/9833)
## [0.2.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.0...auth/v0.2.1) (2024-04-18)
### Bug Fixes
* **auth:** Default gRPC token type to Bearer if not set ([#9800](https://github.com/googleapis/google-cloud-go/issues/9800)) ([5284066](https://github.com/googleapis/google-cloud-go/commit/5284066670b6fe65d79089cfe0199c9660f87fc7))
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.1...auth/v0.2.0) (2024-04-15)
### Breaking Changes
In the below mentioned commits there were a few large breaking changes since the
last release of the module.
1. The `Credentials` type has been moved to the root of the module as it is
becoming the core abstraction for the whole module.
2. Because of the above mentioned change many functions that previously
returned a `TokenProvider` now return `Credentials`. Similarly, these
functions have been renamed to be more specific.
3. Most places that used to take an optional `TokenProvider` now accept
`Credentials`. You can make a `Credentials` from a `TokenProvider` using the
constructor found in the `auth` package.
4. The `detect` package has been renamed to `credentials`. With this change some
function signatures were also updated for better readability.
5. Derivative auth flows like `impersonate` and `downscope` have been moved to
be under the new `credentials` package.
Although these changes are disruptive we think that they are for the best of the
long-term health of the module. We do not expect any more large breaking changes
like these in future revisions, even before 1.0.0. This version will be the
first version of the auth library that our client libraries start to use and
depend on.
### Features
* **auth/credentials/externalaccount:** Add default TokenURL ([#9700](https://github.com/googleapis/google-cloud-go/issues/9700)) ([81830e6](https://github.com/googleapis/google-cloud-go/commit/81830e6848ceefd055aa4d08f933d1154455a0f6))
* **auth:** Add downscope.Options.UniverseDomain ([#9634](https://github.com/googleapis/google-cloud-go/issues/9634)) ([52cf7d7](https://github.com/googleapis/google-cloud-go/commit/52cf7d780853594291c4e34302d618299d1f5a1d))
* **auth:** Add universe domain to grpctransport and httptransport ([#9663](https://github.com/googleapis/google-cloud-go/issues/9663)) ([67d353b](https://github.com/googleapis/google-cloud-go/commit/67d353beefe3b607c08c891876fbd95ab89e5fe3)), refs [#9670](https://github.com/googleapis/google-cloud-go/issues/9670)
* **auth:** Add UniverseDomain to DetectOptions ([#9536](https://github.com/googleapis/google-cloud-go/issues/9536)) ([3618d3f](https://github.com/googleapis/google-cloud-go/commit/3618d3f7061615c0e189f376c75abc201203b501))
* **auth:** Make package externalaccount public ([#9633](https://github.com/googleapis/google-cloud-go/issues/9633)) ([a0978d8](https://github.com/googleapis/google-cloud-go/commit/a0978d8e96968399940ebd7d092539772bf9caac))
* **auth:** Move credentials to base auth package ([#9590](https://github.com/googleapis/google-cloud-go/issues/9590)) ([1a04baf](https://github.com/googleapis/google-cloud-go/commit/1a04bafa83c27342b9308d785645e1e5423ea10d))
* **auth:** Refactor public sigs to use Credentials ([#9603](https://github.com/googleapis/google-cloud-go/issues/9603)) ([69cb240](https://github.com/googleapis/google-cloud-go/commit/69cb240c530b1f7173a9af2555c19e9a1beb56c5))
### Bug Fixes
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
* **auth:** Fix uint32 conversion ([9221c7f](https://github.com/googleapis/google-cloud-go/commit/9221c7fa12cef9d5fb7ddc92f41f1d6204971c7b))
* **auth:** Port sts expires fix ([#9618](https://github.com/googleapis/google-cloud-go/issues/9618)) ([7bec97b](https://github.com/googleapis/google-cloud-go/commit/7bec97b2f51ed3ac4f9b88bf100d301da3f5d1bd))
* **auth:** Read universe_domain from all credentials files ([#9632](https://github.com/googleapis/google-cloud-go/issues/9632)) ([16efbb5](https://github.com/googleapis/google-cloud-go/commit/16efbb52e39ea4a319e5ee1e95c0e0305b6d9824))
* **auth:** Remove content-type header from idms get requests ([#9508](https://github.com/googleapis/google-cloud-go/issues/9508)) ([8589f41](https://github.com/googleapis/google-cloud-go/commit/8589f41599d265d7c3d46a3d86c9fab2329cbdd9))
* **auth:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
## [0.1.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.0...auth/v0.1.1) (2024-03-10)
### Bug Fixes
* **auth/impersonate:** Properly send default detect params ([#9529](https://github.com/googleapis/google-cloud-go/issues/9529)) ([5b6b8be](https://github.com/googleapis/google-cloud-go/commit/5b6b8bef577f82707e51f5cc5d258d5bdf90218f)), refs [#9136](https://github.com/googleapis/google-cloud-go/issues/9136)
* **auth:** Update grpc-go to v1.56.3 ([343cea8](https://github.com/googleapis/google-cloud-go/commit/343cea8c43b1e31ae21ad50ad31d3b0b60143f8c))
* **auth:** Update grpc-go to v1.59.0 ([81a97b0](https://github.com/googleapis/google-cloud-go/commit/81a97b06cb28b25432e4ece595c55a9857e960b7))
## 0.1.0 (2023-10-18)
### Features
* **auth:** Add base auth package ([#8465](https://github.com/googleapis/google-cloud-go/issues/8465)) ([6a45f26](https://github.com/googleapis/google-cloud-go/commit/6a45f26b809b64edae21f312c18d4205f96b180e))
* **auth:** Add cert support to httptransport ([#8569](https://github.com/googleapis/google-cloud-go/issues/8569)) ([37e3435](https://github.com/googleapis/google-cloud-go/commit/37e3435f8e98595eafab481bdfcb31a4c56fa993))
* **auth:** Add Credentials.UniverseDomain() ([#8654](https://github.com/googleapis/google-cloud-go/issues/8654)) ([af0aa1e](https://github.com/googleapis/google-cloud-go/commit/af0aa1ed8015bc8fe0dd87a7549ae029107cbdb8))
* **auth:** Add detect package ([#8491](https://github.com/googleapis/google-cloud-go/issues/8491)) ([d977419](https://github.com/googleapis/google-cloud-go/commit/d977419a3269f6acc193df77a2136a6eb4b4add7))
* **auth:** Add downscope package ([#8532](https://github.com/googleapis/google-cloud-go/issues/8532)) ([dda9bff](https://github.com/googleapis/google-cloud-go/commit/dda9bff8ec70e6d104901b4105d13dcaa4e2404c))
* **auth:** Add grpctransport package ([#8625](https://github.com/googleapis/google-cloud-go/issues/8625)) ([69a8347](https://github.com/googleapis/google-cloud-go/commit/69a83470bdcc7ed10c6c36d1abc3b7cfdb8a0ee5))
* **auth:** Add httptransport package ([#8567](https://github.com/googleapis/google-cloud-go/issues/8567)) ([6898597](https://github.com/googleapis/google-cloud-go/commit/6898597d2ea95d630fcd00fd15c58c75ea843bff))
* **auth:** Add idtoken package ([#8580](https://github.com/googleapis/google-cloud-go/issues/8580)) ([a79e693](https://github.com/googleapis/google-cloud-go/commit/a79e693e97e4e3e1c6742099af3dbc58866d88fe))
* **auth:** Add impersonate package ([#8578](https://github.com/googleapis/google-cloud-go/issues/8578)) ([e29ba0c](https://github.com/googleapis/google-cloud-go/commit/e29ba0cb7bd3888ab9e808087027dc5a32474c04))
* **auth:** Add support for external accounts in detect ([#8508](https://github.com/googleapis/google-cloud-go/issues/8508)) ([62210d5](https://github.com/googleapis/google-cloud-go/commit/62210d5d3e56e8e9f35db8e6ac0defec19582507))
* **auth:** Port external account changes ([#8697](https://github.com/googleapis/google-cloud-go/issues/8697)) ([5823db5](https://github.com/googleapis/google-cloud-go/commit/5823db5d633069999b58b9131a7f9cd77e82c899))
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
* **auth:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,40 +0,0 @@
# Google Auth Library for Go
[![Go Reference](https://pkg.go.dev/badge/cloud.google.com/go/auth.svg)](https://pkg.go.dev/cloud.google.com/go/auth)
## Install
``` bash
go get cloud.google.com/go/auth@latest
```
## Usage
The most common way this library is used is transitively, by default, from any
of our Go client libraries.
### Notable use-cases
- To create a credential directly please see examples in the
[credentials](https://pkg.go.dev/cloud.google.com/go/auth/credentials)
package.
- To create a authenticated HTTP client please see examples in the
[httptransport](https://pkg.go.dev/cloud.google.com/go/auth/httptransport)
package.
- To create a authenticated gRPC connection please see examples in the
[grpctransport](https://pkg.go.dev/cloud.google.com/go/auth/grpctransport)
package.
- To create an ID token please see examples in the
[idtoken](https://pkg.go.dev/cloud.google.com/go/auth/credentials/idtoken)
package.
## Contributing
Contributions are welcome. Please, see the
[CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
document for details.
Please note that this project is released with a Contributor Code of Conduct.
By participating in this project you agree to abide by its terms.
See [Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
for more information.

View File

@ -1,618 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package auth provides utilities for managing Google Cloud credentials,
// including functionality for creating, caching, and refreshing OAuth2 tokens.
// It offers customizable options for different OAuth2 flows, such as 2-legged
// (2LO) and 3-legged (3LO) OAuth, along with support for PKCE and automatic
// token management.
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"sync"
"time"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/jwt"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
// Parameter keys for AuthCodeURL method to support PKCE.
codeChallengeKey = "code_challenge"
codeChallengeMethodKey = "code_challenge_method"
// Parameter key for Exchange method to support PKCE.
codeVerifierKey = "code_verifier"
// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
defaultExpiryDelta = 225 * time.Second
universeDomainDefault = "googleapis.com"
)
// tokenState represents different states for a [Token].
type tokenState int
const (
// fresh indicates that the [Token] is valid. It is not expired or close to
// expired, or the token has no expiry.
fresh tokenState = iota
// stale indicates that the [Token] is close to expired, and should be
// refreshed. The token can be used normally.
stale
// invalid indicates that the [Token] is expired or invalid. The token
// cannot be used for a normal operation.
invalid
)
var (
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
// for testing
timeNow = time.Now
)
// TokenProvider specifies an interface for anything that can return a token.
type TokenProvider interface {
// Token returns a Token or an error.
// The Token returned must be safe to use
// concurrently.
// The returned Token must not be modified.
// The context provided must be sent along to any requests that are made in
// the implementing code.
Token(context.Context) (*Token, error)
}
// Token holds the credential token used to authorized requests. All fields are
// considered read-only.
type Token struct {
// Value is the token used to authorize requests. It is usually an access
// token but may be other types of tokens such as ID tokens in some flows.
Value string
// Type is the type of token Value is. If uninitialized, it should be
// assumed to be a "Bearer" token.
Type string
// Expiry is the time the token is set to expire.
Expiry time.Time
// Metadata may include, but is not limited to, the body of the token
// response returned by the server.
Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
}
// IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
// expired. A token is considered expired if [Token.Expiry] has passed or will
// pass in the next 225 seconds.
func (t *Token) IsValid() bool {
return t.isValidWithEarlyExpiry(defaultExpiryDelta)
}
// MetadataString is a convenience method for accessing string values in the
// token's metadata. Returns an empty string if the metadata is nil or the value
// for the given key cannot be cast to a string.
func (t *Token) MetadataString(k string) string {
if t.Metadata == nil {
return ""
}
s, ok := t.Metadata[k].(string)
if !ok {
return ""
}
return s
}
func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
if t.isEmpty() {
return false
}
if t.Expiry.IsZero() {
return true
}
return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
}
func (t *Token) isEmpty() bool {
return t == nil || t.Value == ""
}
// Credentials holds Google credentials, including
// [Application Default Credentials].
//
// [Application Default Credentials]: https://developers.google.com/accounts/docs/application-default-credentials
type Credentials struct {
json []byte
projectID CredentialsPropertyProvider
quotaProjectID CredentialsPropertyProvider
// universeDomain is the default service domain for a given Cloud universe.
universeDomain CredentialsPropertyProvider
TokenProvider
}
// JSON returns the bytes associated with the the file used to source
// credentials if one was used.
func (c *Credentials) JSON() []byte {
return c.json
}
// ProjectID returns the associated project ID from the underlying file or
// environment.
func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
if c.projectID == nil {
return internal.GetProjectID(c.json, ""), nil
}
v, err := c.projectID.GetProperty(ctx)
if err != nil {
return "", err
}
return internal.GetProjectID(c.json, v), nil
}
// QuotaProjectID returns the associated quota project ID from the underlying
// file or environment.
func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
if c.quotaProjectID == nil {
return internal.GetQuotaProject(c.json, ""), nil
}
v, err := c.quotaProjectID.GetProperty(ctx)
if err != nil {
return "", err
}
return internal.GetQuotaProject(c.json, v), nil
}
// UniverseDomain returns the default service domain for a given Cloud universe.
// The default value is "googleapis.com".
func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
if c.universeDomain == nil {
return universeDomainDefault, nil
}
v, err := c.universeDomain.GetProperty(ctx)
if err != nil {
return "", err
}
if v == "" {
return universeDomainDefault, nil
}
return v, err
}
// CredentialsPropertyProvider provides an implementation to fetch a property
// value for [Credentials].
type CredentialsPropertyProvider interface {
GetProperty(context.Context) (string, error)
}
// CredentialsPropertyFunc is a type adapter to allow the use of ordinary
// functions as a [CredentialsPropertyProvider].
type CredentialsPropertyFunc func(context.Context) (string, error)
// GetProperty loads the properly value provided the given context.
func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
return p(ctx)
}
// CredentialsOptions are used to configure [Credentials].
type CredentialsOptions struct {
// TokenProvider is a means of sourcing a token for the credentials. Required.
TokenProvider TokenProvider
// JSON is the raw contents of the credentials file if sourced from a file.
JSON []byte
// ProjectIDProvider resolves the project ID associated with the
// credentials.
ProjectIDProvider CredentialsPropertyProvider
// QuotaProjectIDProvider resolves the quota project ID associated with the
// credentials.
QuotaProjectIDProvider CredentialsPropertyProvider
// UniverseDomainProvider resolves the universe domain with the credentials.
UniverseDomainProvider CredentialsPropertyProvider
}
// NewCredentials returns new [Credentials] from the provided options.
func NewCredentials(opts *CredentialsOptions) *Credentials {
creds := &Credentials{
TokenProvider: opts.TokenProvider,
json: opts.JSON,
projectID: opts.ProjectIDProvider,
quotaProjectID: opts.QuotaProjectIDProvider,
universeDomain: opts.UniverseDomainProvider,
}
return creds
}
// CachedTokenProviderOptions provides options for configuring a cached
// [TokenProvider].
type CachedTokenProviderOptions struct {
// DisableAutoRefresh makes the TokenProvider always return the same token,
// even if it is expired. The default is false. Optional.
DisableAutoRefresh bool
// ExpireEarly configures the amount of time before a token expires, that it
// should be refreshed. If unset, the default value is 3 minutes and 45
// seconds. Optional.
ExpireEarly time.Duration
// DisableAsyncRefresh configures a synchronous workflow that refreshes
// tokens in a blocking manner. The default is false. Optional.
DisableAsyncRefresh bool
}
func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
if ctpo == nil {
return true
}
return !ctpo.DisableAutoRefresh
}
func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
if ctpo == nil || ctpo.ExpireEarly == 0 {
return defaultExpiryDelta
}
return ctpo.ExpireEarly
}
func (ctpo *CachedTokenProviderOptions) blockingRefresh() bool {
if ctpo == nil {
return false
}
return ctpo.DisableAsyncRefresh
}
// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
// by the underlying provider. By default it will refresh tokens asynchronously
// a few minutes before they expire.
func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
if ctp, ok := tp.(*cachedTokenProvider); ok {
return ctp
}
return &cachedTokenProvider{
tp: tp,
autoRefresh: opts.autoRefresh(),
expireEarly: opts.expireEarly(),
blockingRefresh: opts.blockingRefresh(),
}
}
type cachedTokenProvider struct {
tp TokenProvider
autoRefresh bool
expireEarly time.Duration
blockingRefresh bool
mu sync.Mutex
cachedToken *Token
// isRefreshRunning ensures that the non-blocking refresh will only be
// attempted once, even if multiple callers enter the Token method.
isRefreshRunning bool
// isRefreshErr ensures that the non-blocking refresh will only be attempted
// once per refresh window if an error is encountered.
isRefreshErr bool
}
func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
if c.blockingRefresh {
return c.tokenBlocking(ctx)
}
return c.tokenNonBlocking(ctx)
}
func (c *cachedTokenProvider) tokenNonBlocking(ctx context.Context) (*Token, error) {
switch c.tokenState() {
case fresh:
c.mu.Lock()
defer c.mu.Unlock()
return c.cachedToken, nil
case stale:
// Call tokenAsync with a new Context because the user-provided context
// may have a short timeout incompatible with async token refresh.
c.tokenAsync(context.Background())
// Return the stale token immediately to not block customer requests to Cloud services.
c.mu.Lock()
defer c.mu.Unlock()
return c.cachedToken, nil
default: // invalid
return c.tokenBlocking(ctx)
}
}
// tokenState reports the token's validity.
func (c *cachedTokenProvider) tokenState() tokenState {
c.mu.Lock()
defer c.mu.Unlock()
t := c.cachedToken
now := timeNow()
if t == nil || t.Value == "" {
return invalid
} else if t.Expiry.IsZero() {
return fresh
} else if now.After(t.Expiry.Round(0)) {
return invalid
} else if now.After(t.Expiry.Round(0).Add(-c.expireEarly)) {
return stale
}
return fresh
}
// tokenAsync uses a bool to ensure that only one non-blocking token refresh
// happens at a time, even if multiple callers have entered this function
// concurrently. This avoids creating an arbitrary number of concurrent
// goroutines. Retries should be attempted and managed within the Token method.
// If the refresh attempt fails, no further attempts are made until the refresh
// window expires and the token enters the invalid state, at which point the
// blocking call to Token should likely return the same error on the main goroutine.
func (c *cachedTokenProvider) tokenAsync(ctx context.Context) {
fn := func() {
c.mu.Lock()
c.isRefreshRunning = true
c.mu.Unlock()
t, err := c.tp.Token(ctx)
c.mu.Lock()
defer c.mu.Unlock()
c.isRefreshRunning = false
if err != nil {
// Discard errors from the non-blocking refresh, but prevent further
// attempts.
c.isRefreshErr = true
return
}
c.cachedToken = t
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.isRefreshRunning && !c.isRefreshErr {
go fn()
}
}
func (c *cachedTokenProvider) tokenBlocking(ctx context.Context) (*Token, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.isRefreshErr = false
if c.cachedToken.IsValid() || (!c.autoRefresh && !c.cachedToken.isEmpty()) {
return c.cachedToken, nil
}
t, err := c.tp.Token(ctx)
if err != nil {
return nil, err
}
c.cachedToken = t
return t, nil
}
// Error is a error associated with retrieving a [Token]. It can hold useful
// additional details for debugging.
type Error struct {
// Response is the HTTP response associated with error. The body will always
// be already closed and consumed.
Response *http.Response
// Body is the HTTP response body.
Body []byte
// Err is the underlying wrapped error.
Err error
// code returned in the token response
code string
// description returned in the token response
description string
// uri returned in the token response
uri string
}
func (e *Error) Error() string {
if e.code != "" {
s := fmt.Sprintf("auth: %q", e.code)
if e.description != "" {
s += fmt.Sprintf(" %q", e.description)
}
if e.uri != "" {
s += fmt.Sprintf(" %q", e.uri)
}
return s
}
return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
}
// Temporary returns true if the error is considered temporary and may be able
// to be retried.
func (e *Error) Temporary() bool {
if e.Response == nil {
return false
}
sc := e.Response.StatusCode
return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
}
func (e *Error) Unwrap() error {
return e.Err
}
// Style describes how the token endpoint wants to receive the ClientID and
// ClientSecret.
type Style int
const (
// StyleUnknown means the value has not been initiated. Sending this in
// a request will cause the token exchange to fail.
StyleUnknown Style = iota
// StyleInParams sends client info in the body of a POST request.
StyleInParams
// StyleInHeader sends client info using Basic Authorization header.
StyleInHeader
)
// Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
type Options2LO struct {
// Email is the OAuth2 client ID. This value is set as the "iss" in the
// JWT.
Email string
// PrivateKey contains the contents of an RSA private key or the
// contents of a PEM file that contains a private key. It is used to sign
// the JWT created.
PrivateKey []byte
// TokenURL is th URL the JWT is sent to. Required.
TokenURL string
// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
// "kid" in the JWT header. Optional.
PrivateKeyID string
// Subject is the used for to impersonate a user. It is used as the "sub" in
// the JWT.m Optional.
Subject string
// Scopes specifies requested permissions for the token. Optional.
Scopes []string
// Expires specifies the lifetime of the token. Optional.
Expires time.Duration
// Audience specifies the "aud" in the JWT. Optional.
Audience string
// PrivateClaims allows specifying any custom claims for the JWT. Optional.
PrivateClaims map[string]interface{}
// Client is the client to be used to make the underlying token requests.
// Optional.
Client *http.Client
// UseIDToken requests that the token returned be an ID token if one is
// returned from the server. Optional.
UseIDToken bool
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *Options2LO) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.DefaultClient()
}
func (o *Options2LO) validate() error {
if o == nil {
return errors.New("auth: options must be provided")
}
if o.Email == "" {
return errors.New("auth: email must be provided")
}
if len(o.PrivateKey) == 0 {
return errors.New("auth: private key must be provided")
}
if o.TokenURL == "" {
return errors.New("auth: token URL must be provided")
}
return nil
}
// New2LOTokenProvider returns a [TokenProvider] from the provided options.
func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
return tokenProvider2LO{opts: opts, Client: opts.client(), logger: internallog.New(opts.Logger)}, nil
}
type tokenProvider2LO struct {
opts *Options2LO
Client *http.Client
logger *slog.Logger
}
func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
pk, err := internal.ParseKey(tp.opts.PrivateKey)
if err != nil {
return nil, err
}
claimSet := &jwt.Claims{
Iss: tp.opts.Email,
Scope: strings.Join(tp.opts.Scopes, " "),
Aud: tp.opts.TokenURL,
AdditionalClaims: tp.opts.PrivateClaims,
Sub: tp.opts.Subject,
}
if t := tp.opts.Expires; t > 0 {
claimSet.Exp = time.Now().Add(t).Unix()
}
if aud := tp.opts.Audience; aud != "" {
claimSet.Aud = aud
}
h := *defaultHeader
h.KeyID = tp.opts.PrivateKeyID
payload, err := jwt.EncodeJWS(&h, claimSet, pk)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("grant_type", defaultGrantType)
v.Set("assertion", payload)
req, err := http.NewRequestWithContext(ctx, "POST", tp.opts.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
tp.logger.DebugContext(ctx, "2LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
resp, body, err := internal.DoRequest(tp.Client, req)
if err != nil {
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
}
tp.logger.DebugContext(ctx, "2LO token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return nil, &Error{
Response: resp,
Body: body,
}
}
// tokenRes is the JSON response body.
var tokenRes struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
IDToken string `json:"id_token"`
ExpiresIn int64 `json:"expires_in"`
}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
}
token := &Token{
Value: tokenRes.AccessToken,
Type: tokenRes.TokenType,
}
token.Metadata = make(map[string]interface{})
json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
}
if v := tokenRes.IDToken; v != "" {
// decode returned id token to get expiry
claimSet, err := jwt.DecodeJWS(v)
if err != nil {
return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
}
token.Expiry = time.Unix(claimSet.Exp, 0)
}
if tp.opts.UseIDToken {
if tokenRes.IDToken == "" {
return nil, fmt.Errorf("auth: response doesn't have JWT token")
}
token.Value = tokenRes.IDToken
}
return token, nil
}

View File

@ -1,90 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/compute/metadata"
)
var (
computeTokenMetadata = map[string]interface{}{
"auth.google.tokenSource": "compute-metadata",
"auth.google.serviceAccount": "default",
}
computeTokenURI = "instance/service-accounts/default/token"
)
// computeTokenProvider creates a [cloud.google.com/go/auth.TokenProvider] that
// uses the metadata service to retrieve tokens.
func computeTokenProvider(opts *DetectOptions, client *metadata.Client) auth.TokenProvider {
return auth.NewCachedTokenProvider(&computeProvider{
scopes: opts.Scopes,
client: client,
}, &auth.CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenRefresh,
DisableAsyncRefresh: opts.DisableAsyncRefresh,
})
}
// computeProvider fetches tokens from the google cloud metadata service.
type computeProvider struct {
scopes []string
client *metadata.Client
}
type metadataTokenResp struct {
AccessToken string `json:"access_token"`
ExpiresInSec int `json:"expires_in"`
TokenType string `json:"token_type"`
}
func (cs *computeProvider) Token(ctx context.Context) (*auth.Token, error) {
tokenURI, err := url.Parse(computeTokenURI)
if err != nil {
return nil, err
}
if len(cs.scopes) > 0 {
v := url.Values{}
v.Set("scopes", strings.Join(cs.scopes, ","))
tokenURI.RawQuery = v.Encode()
}
tokenJSON, err := cs.client.GetWithContext(ctx, tokenURI.String())
if err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
var res metadataTokenResp
if err := json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res); err != nil {
return nil, fmt.Errorf("credentials: invalid token JSON from metadata: %w", err)
}
if res.ExpiresInSec == 0 || res.AccessToken == "" {
return nil, errors.New("credentials: incomplete token received from metadata")
}
return &auth.Token{
Value: res.AccessToken,
Type: res.TokenType,
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
Metadata: computeTokenMetadata,
}, nil
}

View File

@ -1,279 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/compute/metadata"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
// jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
jwtTokenURL = "https://oauth2.googleapis.com/token"
// Google's OAuth 2.0 default endpoints.
googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
googleTokenURL = "https://oauth2.googleapis.com/token"
// GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
// Help on default credentials
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
)
var (
// for testing
allowOnGCECheck = true
)
// OnGCE reports whether this process is running in Google Cloud.
func OnGCE() bool {
// TODO(codyoss): once all libs use this auth lib move metadata check here
return allowOnGCECheck && metadata.OnGCE()
}
// DetectDefault searches for "Application Default Credentials" and returns
// a credential based on the [DetectOptions] provided.
//
// It looks for credentials in the following places, preferring the first
// location found:
//
// - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
// environment variable. For workload identity federation, refer to
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
// on how to generate the JSON configuration file for on-prem/non-Google
// cloud platforms.
// - A JSON file in a location known to the gcloud command-line tool. On
// Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
// other systems, $HOME/.config/gcloud/application_default_credentials.json.
// - On Google Compute Engine, Google App Engine standard second generation
// runtimes, and Google App Engine flexible environment, it fetches
// credentials from the metadata server.
func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if len(opts.CredentialsJSON) > 0 {
return readCredentialsFileJSON(opts.CredentialsJSON, opts)
}
if opts.CredentialsFile != "" {
return readCredentialsFile(opts.CredentialsFile, opts)
}
if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
creds, err := readCredentialsFile(filename, opts)
if err != nil {
return nil, err
}
return creds, nil
}
fileName := credsfile.GetWellKnownFileName()
if b, err := os.ReadFile(fileName); err == nil {
return readCredentialsFileJSON(b, opts)
}
if OnGCE() {
metadataClient := metadata.NewWithOptions(&metadata.Options{
Logger: opts.logger(),
})
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: computeTokenProvider(opts, metadataClient),
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return metadataClient.ProjectIDWithContext(ctx)
}),
UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{
MetadataClient: metadataClient,
},
}), nil
}
return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
}
// DetectOptions provides configuration for [DetectDefault].
type DetectOptions struct {
// Scopes that credentials tokens should have. Example:
// https://www.googleapis.com/auth/cloud-platform. Required if Audience is
// not provided.
Scopes []string
// Audience that credentials tokens should have. Only applicable for 2LO
// flows with service accounts. If specified, scopes should not be provided.
Audience string
// Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
// Optional.
Subject string
// EarlyTokenRefresh configures how early before a token expires that it
// should be refreshed. Once the tokens time until expiration has entered
// this refresh window the token is considered valid but stale. If unset,
// the default value is 3 minutes and 45 seconds. Optional.
EarlyTokenRefresh time.Duration
// DisableAsyncRefresh configures a synchronous workflow that refreshes
// stale tokens while blocking. The default is false. Optional.
DisableAsyncRefresh bool
// AuthHandlerOptions configures an authorization handler and other options
// for 3LO flows. It is required, and only used, for client credential
// flows.
AuthHandlerOptions *auth.AuthorizationHandlerOptions
// TokenURL allows to set the token endpoint for user credential flows. If
// unset the default value is: https://oauth2.googleapis.com/token.
// Optional.
TokenURL string
// STSAudience is the audience sent to when retrieving an STS token.
// Currently this only used for GDCH auth flow, for which it is required.
STSAudience string
// CredentialsFile overrides detection logic and sources a credential file
// from the provided filepath. If provided, CredentialsJSON must not be.
// Optional.
CredentialsFile string
// CredentialsJSON overrides detection logic and uses the JSON bytes as the
// source for the credential. If provided, CredentialsFile must not be.
// Optional.
CredentialsJSON []byte
// UseSelfSignedJWT directs service account based credentials to create a
// self-signed JWT with the private key found in the file, skipping any
// network requests that would normally be made. Optional.
UseSelfSignedJWT bool
// Client configures the underlying client used to make network requests
// when fetching tokens. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This option is ignored for
// authentication flows that do not support universe domain. Optional.
UniverseDomain string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *DetectOptions) validate() error {
if o == nil {
return errors.New("credentials: options must be provided")
}
if len(o.Scopes) > 0 && o.Audience != "" {
return errors.New("credentials: both scopes and audience were provided")
}
if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
return errors.New("credentials: both credentials file and JSON were provided")
}
return nil
}
func (o *DetectOptions) tokenURL() string {
if o.TokenURL != "" {
return o.TokenURL
}
return googleTokenURL
}
func (o *DetectOptions) scopes() []string {
scopes := make([]string, len(o.Scopes))
copy(scopes, o.Scopes)
return scopes
}
func (o *DetectOptions) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.DefaultClient()
}
func (o *DetectOptions) logger() *slog.Logger {
return internallog.New(o.Logger)
}
func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return readCredentialsFileJSON(b, opts)
}
func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
// attempt to parse jsonData as a Google Developers Console client_credentials.json.
config := clientCredConfigFromJSON(b, opts)
if config != nil {
if config.AuthHandlerOpts == nil {
return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
}
tp, err := auth.New3LOTokenProvider(config)
if err != nil {
return nil, err
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
JSON: b,
}), nil
}
return fileCredentials(b, opts)
}
func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
var creds credsfile.ClientCredentialsFile
var c *credsfile.Config3LO
if err := json.Unmarshal(b, &creds); err != nil {
return nil
}
switch {
case creds.Web != nil:
c = creds.Web
case creds.Installed != nil:
c = creds.Installed
default:
return nil
}
if len(c.RedirectURIs) < 1 {
return nil
}
var handleOpts *auth.AuthorizationHandlerOptions
if opts.AuthHandlerOptions != nil {
handleOpts = &auth.AuthorizationHandlerOptions{
Handler: opts.AuthHandlerOptions.Handler,
State: opts.AuthHandlerOptions.State,
PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
}
}
return &auth.Options3LO{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
RedirectURL: c.RedirectURIs[0],
Scopes: opts.scopes(),
AuthURL: c.AuthURI,
TokenURL: c.TokenURI,
Client: opts.client(),
Logger: opts.logger(),
EarlyTokenExpiry: opts.EarlyTokenRefresh,
AuthHandlerOpts: handleOpts,
// TODO(codyoss): refactor this out. We need to add in auto-detection
// for this use case.
AuthStyle: auth.StyleInParams,
}
}

View File

@ -1,45 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package credentials provides support for making OAuth2 authorized and
// authenticated HTTP requests to Google APIs. It supports the Web server flow,
// client-side credentials, service accounts, Google Compute Engine service
// accounts, Google App Engine service accounts and workload identity federation
// from non-Google cloud platforms.
//
// A brief overview of the package follows. For more information, please read
// https://developers.google.com/accounts/docs/OAuth2
// and
// https://developers.google.com/accounts/docs/application-default-credentials.
// For more information on using workload identity federation, refer to
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation.
//
// # Credentials
//
// The [cloud.google.com/go/auth.Credentials] type represents Google
// credentials, including Application Default Credentials.
//
// Use [DetectDefault] to obtain Application Default Credentials.
//
// Application Default Credentials support workload identity federation to
// access Google Cloud resources from non-Google Cloud platforms including Amazon
// Web Services (AWS), Microsoft Azure or any identity provider that supports
// OpenID Connect (OIDC). Workload identity federation is recommended for
// non-Google Cloud environments as it avoids the need to download, manage, and
// store service account private keys locally.
//
// # Workforce Identity Federation
//
// For more information on this feature see [cloud.google.com/go/auth/credentials/externalaccount].
package credentials

View File

@ -1,231 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"errors"
"fmt"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/externalaccount"
"cloud.google.com/go/auth/credentials/internal/externalaccountuser"
"cloud.google.com/go/auth/credentials/internal/gdch"
"cloud.google.com/go/auth/credentials/internal/impersonate"
internalauth "cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
func fileCredentials(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
fileType, err := credsfile.ParseFileType(b)
if err != nil {
return nil, err
}
var projectID, universeDomain string
var tp auth.TokenProvider
switch fileType {
case credsfile.ServiceAccountKey:
f, err := credsfile.ParseServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleServiceAccount(f, opts)
if err != nil {
return nil, err
}
projectID = f.ProjectID
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.UserCredentialsKey:
f, err := credsfile.ParseUserCredentials(b)
if err != nil {
return nil, err
}
tp, err = handleUserCredential(f, opts)
if err != nil {
return nil, err
}
universeDomain = f.UniverseDomain
case credsfile.ExternalAccountKey:
f, err := credsfile.ParseExternalAccount(b)
if err != nil {
return nil, err
}
tp, err = handleExternalAccount(f, opts)
if err != nil {
return nil, err
}
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.ExternalAccountAuthorizedUserKey:
f, err := credsfile.ParseExternalAccountAuthorizedUser(b)
if err != nil {
return nil, err
}
tp, err = handleExternalAccountAuthorizedUser(f, opts)
if err != nil {
return nil, err
}
universeDomain = f.UniverseDomain
case credsfile.ImpersonatedServiceAccountKey:
f, err := credsfile.ParseImpersonatedServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleImpersonatedServiceAccount(f, opts)
if err != nil {
return nil, err
}
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.GDCHServiceAccountKey:
f, err := credsfile.ParseGDCHServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleGDCHServiceAccount(f, opts)
if err != nil {
return nil, err
}
projectID = f.Project
universeDomain = f.UniverseDomain
default:
return nil, fmt.Errorf("credentials: unsupported filetype %q", fileType)
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenRefresh,
}),
JSON: b,
ProjectIDProvider: internalauth.StaticCredentialsProperty(projectID),
// TODO(codyoss): only set quota project here if there was a user override
UniverseDomainProvider: internalauth.StaticCredentialsProperty(universeDomain),
}), nil
}
// resolveUniverseDomain returns optsUniverseDomain if non-empty, in order to
// support configuring universe-specific credentials in code. Auth flows
// unsupported for universe domain should not use this func, but should instead
// simply set the file universe domain on the credentials.
func resolveUniverseDomain(optsUniverseDomain, fileUniverseDomain string) string {
if optsUniverseDomain != "" {
return optsUniverseDomain
}
return fileUniverseDomain
}
func handleServiceAccount(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
ud := resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
if opts.UseSelfSignedJWT {
return configureSelfSignedJWT(f, opts)
} else if ud != "" && ud != internalauth.DefaultUniverseDomain {
// For non-GDU universe domains, token exchange is impossible and services
// must support self-signed JWTs.
opts.UseSelfSignedJWT = true
return configureSelfSignedJWT(f, opts)
}
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
Scopes: opts.scopes(),
TokenURL: f.TokenURL,
Subject: opts.Subject,
Client: opts.client(),
Logger: opts.logger(),
}
if opts2LO.TokenURL == "" {
opts2LO.TokenURL = jwtTokenURL
}
return auth.New2LOTokenProvider(opts2LO)
}
func handleUserCredential(f *credsfile.UserCredentialsFile, opts *DetectOptions) (auth.TokenProvider, error) {
opts3LO := &auth.Options3LO{
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
Scopes: opts.scopes(),
AuthURL: googleAuthURL,
TokenURL: opts.tokenURL(),
AuthStyle: auth.StyleInParams,
EarlyTokenExpiry: opts.EarlyTokenRefresh,
RefreshToken: f.RefreshToken,
Client: opts.client(),
Logger: opts.logger(),
}
return auth.New3LOTokenProvider(opts3LO)
}
func handleExternalAccount(f *credsfile.ExternalAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
externalOpts := &externalaccount.Options{
Audience: f.Audience,
SubjectTokenType: f.SubjectTokenType,
TokenURL: f.TokenURL,
TokenInfoURL: f.TokenInfoURL,
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
ClientSecret: f.ClientSecret,
ClientID: f.ClientID,
CredentialSource: f.CredentialSource,
QuotaProjectID: f.QuotaProjectID,
Scopes: opts.scopes(),
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
Client: opts.client(),
Logger: opts.logger(),
IsDefaultClient: opts.Client == nil,
}
if f.ServiceAccountImpersonation != nil {
externalOpts.ServiceAccountImpersonationLifetimeSeconds = f.ServiceAccountImpersonation.TokenLifetimeSeconds
}
return externalaccount.NewTokenProvider(externalOpts)
}
func handleExternalAccountAuthorizedUser(f *credsfile.ExternalAccountAuthorizedUserFile, opts *DetectOptions) (auth.TokenProvider, error) {
externalOpts := &externalaccountuser.Options{
Audience: f.Audience,
RefreshToken: f.RefreshToken,
TokenURL: f.TokenURL,
TokenInfoURL: f.TokenInfoURL,
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
Scopes: opts.scopes(),
Client: opts.client(),
Logger: opts.logger(),
}
return externalaccountuser.NewTokenProvider(externalOpts)
}
func handleImpersonatedServiceAccount(f *credsfile.ImpersonatedServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
}
tp, err := fileCredentials(f.CredSource, opts)
if err != nil {
return nil, err
}
return impersonate.NewTokenProvider(&impersonate.Options{
URL: f.ServiceAccountImpersonationURL,
Scopes: opts.scopes(),
Tp: tp,
Delegates: f.Delegates,
Client: opts.client(),
Logger: opts.logger(),
})
}
func handleGDCHServiceAccount(f *credsfile.GDCHServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
return gdch.NewTokenProvider(f, &gdch.Options{
STSAudience: opts.STSAudience,
Client: opts.client(),
Logger: opts.logger(),
})
}

View File

@ -1,531 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
"time"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
var (
// getenv aliases os.Getenv for testing
getenv = os.Getenv
)
const (
// AWS Signature Version 4 signing algorithm identifier.
awsAlgorithm = "AWS4-HMAC-SHA256"
// The termination string for the AWS credential scope value as defined in
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
awsRequestType = "aws4_request"
// The AWS authorization header name for the security session token if available.
awsSecurityTokenHeader = "x-amz-security-token"
// The name of the header containing the session token for metadata endpoint calls
awsIMDSv2SessionTokenHeader = "X-aws-ec2-metadata-token"
awsIMDSv2SessionTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
awsIMDSv2SessionTTL = "300"
// The AWS authorization header name for the auto-generated date.
awsDateHeader = "x-amz-date"
defaultRegionalCredentialVerificationURL = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
// Supported AWS configuration environment variables.
awsAccessKeyIDEnvVar = "AWS_ACCESS_KEY_ID"
awsDefaultRegionEnvVar = "AWS_DEFAULT_REGION"
awsRegionEnvVar = "AWS_REGION"
awsSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
awsSessionTokenEnvVar = "AWS_SESSION_TOKEN"
awsTimeFormatLong = "20060102T150405Z"
awsTimeFormatShort = "20060102"
awsProviderType = "aws"
)
type awsSubjectProvider struct {
EnvironmentID string
RegionURL string
RegionalCredVerificationURL string
CredVerificationURL string
IMDSv2SessionTokenURL string
TargetResource string
requestSigner *awsRequestSigner
region string
securityCredentialsProvider AwsSecurityCredentialsProvider
reqOpts *RequestOptions
Client *http.Client
logger *slog.Logger
}
func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) {
// Set Defaults
if sp.RegionalCredVerificationURL == "" {
sp.RegionalCredVerificationURL = defaultRegionalCredentialVerificationURL
}
headers := make(map[string]string)
if sp.shouldUseMetadataServer() {
awsSessionToken, err := sp.getAWSSessionToken(ctx)
if err != nil {
return "", err
}
if awsSessionToken != "" {
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
}
}
awsSecurityCredentials, err := sp.getSecurityCredentials(ctx, headers)
if err != nil {
return "", err
}
if sp.region, err = sp.getRegion(ctx, headers); err != nil {
return "", err
}
sp.requestSigner = &awsRequestSigner{
RegionName: sp.region,
AwsSecurityCredentials: awsSecurityCredentials,
}
// Generate the signed request to AWS STS GetCallerIdentity API.
// Use the required regional endpoint. Otherwise, the request will fail.
req, err := http.NewRequestWithContext(ctx, "POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil)
if err != nil {
return "", err
}
// The full, canonical resource name of the workload identity pool
// provider, with or without the HTTPS prefix.
// Including this header as part of the signature is recommended to
// ensure data integrity.
if sp.TargetResource != "" {
req.Header.Set("x-goog-cloud-target-resource", sp.TargetResource)
}
sp.requestSigner.signRequest(req)
/*
The GCP STS endpoint expects the headers to be formatted as:
# [
# {key: 'x-amz-date', value: '...'},
# {key: 'Authorization', value: '...'},
# ...
# ]
# And then serialized as:
# quote(json.dumps({
# url: '...',
# method: 'POST',
# headers: [{key: 'x-amz-date', value: '...'}, ...]
# }))
*/
awsSignedReq := awsRequest{
URL: req.URL.String(),
Method: "POST",
}
for headerKey, headerList := range req.Header {
for _, headerValue := range headerList {
awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{
Key: headerKey,
Value: headerValue,
})
}
}
sort.Slice(awsSignedReq.Headers, func(i, j int) bool {
headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key)
if headerCompare == 0 {
return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0
}
return headerCompare < 0
})
result, err := json.Marshal(awsSignedReq)
if err != nil {
return "", err
}
return url.QueryEscape(string(result)), nil
}
func (sp *awsSubjectProvider) providerType() string {
if sp.securityCredentialsProvider != nil {
return programmaticProviderType
}
return awsProviderType
}
func (sp *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, error) {
if sp.IMDSv2SessionTokenURL == "" {
return "", nil
}
req, err := http.NewRequestWithContext(ctx, "PUT", sp.IMDSv2SessionTokenURL, nil)
if err != nil {
return "", err
}
req.Header.Set(awsIMDSv2SessionTTLHeader, awsIMDSv2SessionTTL)
sp.logger.DebugContext(ctx, "aws session token request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(sp.Client, req)
if err != nil {
return "", err
}
sp.logger.DebugContext(ctx, "aws session token response", "response", internallog.HTTPResponse(resp, body))
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", body)
}
return string(body), nil
}
func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string]string) (string, error) {
if sp.securityCredentialsProvider != nil {
return sp.securityCredentialsProvider.AwsRegion(ctx, sp.reqOpts)
}
if canRetrieveRegionFromEnvironment() {
if envAwsRegion := getenv(awsRegionEnvVar); envAwsRegion != "" {
return envAwsRegion, nil
}
return getenv(awsDefaultRegionEnvVar), nil
}
if sp.RegionURL == "" {
return "", errors.New("credentials: unable to determine AWS region")
}
req, err := http.NewRequestWithContext(ctx, "GET", sp.RegionURL, nil)
if err != nil {
return "", err
}
for name, value := range headers {
req.Header.Add(name, value)
}
sp.logger.DebugContext(ctx, "aws region request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(sp.Client, req)
if err != nil {
return "", err
}
sp.logger.DebugContext(ctx, "aws region response", "response", internallog.HTTPResponse(resp, body))
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", body)
}
// This endpoint will return the region in format: us-east-2b.
// Only the us-east-2 part should be used.
bodyLen := len(body)
if bodyLen == 0 {
return "", nil
}
return string(body[:bodyLen-1]), nil
}
func (sp *awsSubjectProvider) getSecurityCredentials(ctx context.Context, headers map[string]string) (result *AwsSecurityCredentials, err error) {
if sp.securityCredentialsProvider != nil {
return sp.securityCredentialsProvider.AwsSecurityCredentials(ctx, sp.reqOpts)
}
if canRetrieveSecurityCredentialFromEnvironment() {
return &AwsSecurityCredentials{
AccessKeyID: getenv(awsAccessKeyIDEnvVar),
SecretAccessKey: getenv(awsSecretAccessKeyEnvVar),
SessionToken: getenv(awsSessionTokenEnvVar),
}, nil
}
roleName, err := sp.getMetadataRoleName(ctx, headers)
if err != nil {
return
}
credentials, err := sp.getMetadataSecurityCredentials(ctx, roleName, headers)
if err != nil {
return
}
if credentials.AccessKeyID == "" {
return result, errors.New("credentials: missing AccessKeyId credential")
}
if credentials.SecretAccessKey == "" {
return result, errors.New("credentials: missing SecretAccessKey credential")
}
return credentials, nil
}
func (sp *awsSubjectProvider) getMetadataSecurityCredentials(ctx context.Context, roleName string, headers map[string]string) (*AwsSecurityCredentials, error) {
var result *AwsSecurityCredentials
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s", sp.CredVerificationURL, roleName), nil)
if err != nil {
return result, err
}
for name, value := range headers {
req.Header.Add(name, value)
}
sp.logger.DebugContext(ctx, "aws security credential request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(sp.Client, req)
if err != nil {
return result, err
}
sp.logger.DebugContext(ctx, "aws security credential response", "response", internallog.HTTPResponse(resp, body))
if resp.StatusCode != http.StatusOK {
return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", body)
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}
func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers map[string]string) (string, error) {
if sp.CredVerificationURL == "" {
return "", errors.New("credentials: unable to determine the AWS metadata server security credentials endpoint")
}
req, err := http.NewRequestWithContext(ctx, "GET", sp.CredVerificationURL, nil)
if err != nil {
return "", err
}
for name, value := range headers {
req.Header.Add(name, value)
}
sp.logger.DebugContext(ctx, "aws metadata role request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(sp.Client, req)
if err != nil {
return "", err
}
sp.logger.DebugContext(ctx, "aws metadata role response", "response", internallog.HTTPResponse(resp, body))
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", body)
}
return string(body), nil
}
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
type awsRequestSigner struct {
RegionName string
AwsSecurityCredentials *AwsSecurityCredentials
}
// signRequest adds the appropriate headers to an http.Request
// or returns an error if something prevented this.
func (rs *awsRequestSigner) signRequest(req *http.Request) error {
// req is assumed non-nil
signedRequest := cloneRequest(req)
timestamp := Now()
signedRequest.Header.Set("host", requestHost(req))
if rs.AwsSecurityCredentials.SessionToken != "" {
signedRequest.Header.Set(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
}
if signedRequest.Header.Get("date") == "" {
signedRequest.Header.Set(awsDateHeader, timestamp.Format(awsTimeFormatLong))
}
authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp)
if err != nil {
return err
}
signedRequest.Header.Set("Authorization", authorizationCode)
req.Header = signedRequest.Header
return nil
}
func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) {
canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req)
dateStamp := timestamp.Format(awsTimeFormatShort)
serviceName := ""
if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 {
serviceName = splitHost[0]
}
credentialScope := strings.Join([]string{dateStamp, rs.RegionName, serviceName, awsRequestType}, "/")
requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData)
if err != nil {
return "", err
}
requestHash, err := getSha256([]byte(requestString))
if err != nil {
return "", err
}
stringToSign := strings.Join([]string{awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash}, "\n")
signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey)
for _, signingInput := range []string{
dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign,
} {
signingKey, err = getHmacSha256(signingKey, []byte(signingInput))
if err != nil {
return "", err
}
}
return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil
}
func getSha256(input []byte) (string, error) {
hash := sha256.New()
if _, err := hash.Write(input); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func getHmacSha256(key, input []byte) ([]byte, error) {
hash := hmac.New(sha256.New, key)
if _, err := hash.Write(input); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}
func cloneRequest(r *http.Request) *http.Request {
r2 := new(http.Request)
*r2 = *r
if r.Header != nil {
r2.Header = make(http.Header, len(r.Header))
// Find total number of values.
headerCount := 0
for _, headerValues := range r.Header {
headerCount += len(headerValues)
}
copiedHeaders := make([]string, headerCount) // shared backing array for headers' values
for headerKey, headerValues := range r.Header {
headerCount = copy(copiedHeaders, headerValues)
r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount]
copiedHeaders = copiedHeaders[headerCount:]
}
}
return r2
}
func canonicalPath(req *http.Request) string {
result := req.URL.EscapedPath()
if result == "" {
return "/"
}
return path.Clean(result)
}
func canonicalQuery(req *http.Request) string {
queryValues := req.URL.Query()
for queryKey := range queryValues {
sort.Strings(queryValues[queryKey])
}
return queryValues.Encode()
}
func canonicalHeaders(req *http.Request) (string, string) {
// Header keys need to be sorted alphabetically.
var headers []string
lowerCaseHeaders := make(http.Header)
for k, v := range req.Header {
k := strings.ToLower(k)
if _, ok := lowerCaseHeaders[k]; ok {
// include additional values
lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...)
} else {
headers = append(headers, k)
lowerCaseHeaders[k] = v
}
}
sort.Strings(headers)
var fullHeaders bytes.Buffer
for _, header := range headers {
headerValue := strings.Join(lowerCaseHeaders[header], ",")
fullHeaders.WriteString(header)
fullHeaders.WriteRune(':')
fullHeaders.WriteString(headerValue)
fullHeaders.WriteRune('\n')
}
return strings.Join(headers, ";"), fullHeaders.String()
}
func requestDataHash(req *http.Request) (string, error) {
var requestData []byte
if req.Body != nil {
requestBody, err := req.GetBody()
if err != nil {
return "", err
}
defer requestBody.Close()
requestData, err = internal.ReadAll(requestBody)
if err != nil {
return "", err
}
}
return getSha256(requestData)
}
func requestHost(req *http.Request) string {
if req.Host != "" {
return req.Host
}
return req.URL.Host
}
func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) {
dataHash, err := requestDataHash(req)
if err != nil {
return "", err
}
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil
}
type awsRequestHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
type awsRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Headers []awsRequestHeader `json:"headers"`
}
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
// required.
func canRetrieveRegionFromEnvironment() bool {
return getenv(awsRegionEnvVar) != "" || getenv(awsDefaultRegionEnvVar) != ""
}
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
func canRetrieveSecurityCredentialFromEnvironment() bool {
return getenv(awsAccessKeyIDEnvVar) != "" && getenv(awsSecretAccessKeyEnvVar) != ""
}
func (sp *awsSubjectProvider) shouldUseMetadataServer() bool {
return sp.securityCredentialsProvider == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
}

View File

@ -1,284 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"time"
"cloud.google.com/go/auth/internal"
)
const (
executableSupportedMaxVersion = 1
executableDefaultTimeout = 30 * time.Second
executableSource = "response"
executableProviderType = "executable"
outputFileSource = "output file"
allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
idTokenType = "urn:ietf:params:oauth:token-type:id_token"
saml2TokenType = "urn:ietf:params:oauth:token-type:saml2"
)
var (
serviceAccountImpersonationRE = regexp.MustCompile(`https://iamcredentials..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken`)
)
type nonCacheableError struct {
message string
}
func (nce nonCacheableError) Error() string {
return nce.message
}
// environment is a contract for testing
type environment interface {
existingEnv() []string
getenv(string) string
run(ctx context.Context, command string, env []string) ([]byte, error)
now() time.Time
}
type runtimeEnvironment struct{}
func (r runtimeEnvironment) existingEnv() []string {
return os.Environ()
}
func (r runtimeEnvironment) getenv(key string) string {
return os.Getenv(key)
}
func (r runtimeEnvironment) now() time.Time {
return time.Now().UTC()
}
func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
splitCommand := strings.Fields(command)
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
cmd.Env = env
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, context.DeadlineExceeded
}
if exitError, ok := err.(*exec.ExitError); ok {
return nil, exitCodeError(exitError)
}
return nil, executableError(err)
}
bytesStdout := bytes.TrimSpace(stdout.Bytes())
if len(bytesStdout) > 0 {
return bytesStdout, nil
}
return bytes.TrimSpace(stderr.Bytes()), nil
}
type executableSubjectProvider struct {
Command string
Timeout time.Duration
OutputFile string
client *http.Client
opts *Options
env environment
}
type executableResponse struct {
Version int `json:"version,omitempty"`
Success *bool `json:"success,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpirationTime int64 `json:"expiration_time,omitempty"`
IDToken string `json:"id_token,omitempty"`
SamlResponse string `json:"saml_response,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (sp *executableSubjectProvider) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
var result executableResponse
if err := json.Unmarshal(response, &result); err != nil {
return "", jsonParsingError(source, string(response))
}
// Validate
if result.Version == 0 {
return "", missingFieldError(source, "version")
}
if result.Success == nil {
return "", missingFieldError(source, "success")
}
if !*result.Success {
if result.Code == "" || result.Message == "" {
return "", malformedFailureError()
}
return "", userDefinedError(result.Code, result.Message)
}
if result.Version > executableSupportedMaxVersion || result.Version < 0 {
return "", unsupportedVersionError(source, result.Version)
}
if result.ExpirationTime == 0 && sp.OutputFile != "" {
return "", missingFieldError(source, "expiration_time")
}
if result.TokenType == "" {
return "", missingFieldError(source, "token_type")
}
if result.ExpirationTime != 0 && result.ExpirationTime < now {
return "", tokenExpiredError()
}
switch result.TokenType {
case jwtTokenType, idTokenType:
if result.IDToken == "" {
return "", missingFieldError(source, "id_token")
}
return result.IDToken, nil
case saml2TokenType:
if result.SamlResponse == "" {
return "", missingFieldError(source, "saml_response")
}
return result.SamlResponse, nil
default:
return "", tokenTypeError(source)
}
}
func (sp *executableSubjectProvider) subjectToken(ctx context.Context) (string, error) {
if token, err := sp.getTokenFromOutputFile(); token != "" || err != nil {
return token, err
}
return sp.getTokenFromExecutableCommand(ctx)
}
func (sp *executableSubjectProvider) providerType() string {
return executableProviderType
}
func (sp *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
if sp.OutputFile == "" {
// This ExecutableCredentialSource doesn't use an OutputFile.
return "", nil
}
file, err := os.Open(sp.OutputFile)
if err != nil {
// No OutputFile found. Hasn't been created yet, so skip it.
return "", nil
}
defer file.Close()
data, err := internal.ReadAll(file)
if err != nil || len(data) == 0 {
// Cachefile exists, but no data found. Get new credential.
return "", nil
}
token, err = sp.parseSubjectTokenFromSource(data, outputFileSource, sp.env.now().Unix())
if err != nil {
if _, ok := err.(nonCacheableError); ok {
// If the cached token is expired we need a new token,
// and if the cache contains a failure, we need to try again.
return "", nil
}
// There was an error in the cached token, and the developer should be aware of it.
return "", err
}
// Token parsing succeeded. Use found token.
return token, nil
}
func (sp *executableSubjectProvider) executableEnvironment() []string {
result := sp.env.existingEnv()
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", sp.opts.Audience))
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", sp.opts.SubjectTokenType))
result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
if sp.opts.ServiceAccountImpersonationURL != "" {
matches := serviceAccountImpersonationRE.FindStringSubmatch(sp.opts.ServiceAccountImpersonationURL)
if matches != nil {
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
}
}
if sp.OutputFile != "" {
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", sp.OutputFile))
}
return result
}
func (sp *executableSubjectProvider) getTokenFromExecutableCommand(ctx context.Context) (string, error) {
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
if sp.env.getenv(allowExecutablesEnvVar) != "1" {
return "", errors.New("credentials: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
}
ctx, cancel := context.WithDeadline(ctx, sp.env.now().Add(sp.Timeout))
defer cancel()
output, err := sp.env.run(ctx, sp.Command, sp.executableEnvironment())
if err != nil {
return "", err
}
return sp.parseSubjectTokenFromSource(output, executableSource, sp.env.now().Unix())
}
func missingFieldError(source, field string) error {
return fmt.Errorf("credentials: %q missing %q field", source, field)
}
func jsonParsingError(source, data string) error {
return fmt.Errorf("credentials: unable to parse %q: %v", source, data)
}
func malformedFailureError() error {
return nonCacheableError{"credentials: response must include `error` and `message` fields when unsuccessful"}
}
func userDefinedError(code, message string) error {
return nonCacheableError{fmt.Sprintf("credentials: response contains unsuccessful response: (%v) %v", code, message)}
}
func unsupportedVersionError(source string, version int) error {
return fmt.Errorf("credentials: %v contains unsupported version: %v", source, version)
}
func tokenExpiredError() error {
return nonCacheableError{"credentials: the token returned by the executable is expired"}
}
func tokenTypeError(source string) error {
return fmt.Errorf("credentials: %v contains unsupported token type", source)
}
func exitCodeError(err *exec.ExitError) error {
return fmt.Errorf("credentials: executable command failed with exit code %v: %w", err.ExitCode(), err)
}
func executableError(err error) error {
return fmt.Errorf("credentials: executable command failed: %w", err)
}

View File

@ -1,428 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/credentials/internal/stsexchange"
"cloud.google.com/go/auth/internal/credsfile"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
timeoutMinimum = 5 * time.Second
timeoutMaximum = 120 * time.Second
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
defaultUniverseDomain = "googleapis.com"
)
var (
// Now aliases time.Now for testing
Now = func() time.Time {
return time.Now().UTC()
}
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
)
// Options stores the configuration for fetching tokens with external credentials.
type Options struct {
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
// identity pool or the workforce pool and the provider identifier in that pool.
Audience string
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
SubjectTokenType string
// TokenURL is the STS token exchange endpoint.
TokenURL string
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
// user attributes like account identifier, eg. email, username, uid, etc). This is
// needed for gCloud session account identification.
TokenInfoURL string
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
// token will be valid for.
ServiceAccountImpersonationLifetimeSeconds int
// ClientSecret is currently only required if token_info endpoint also
// needs to be called with the generated GCP access token. When provided, STS will be
// called with additional basic authentication using client_id as username and client_secret as password.
ClientSecret string
// ClientID is only required in conjunction with ClientSecret, as described above.
ClientID string
// CredentialSource contains the necessary information to retrieve the token itself, as well
// as some environmental information.
CredentialSource *credsfile.CredentialSource
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
// will set the x-goog-user-project which overrides the project associated with the credentials.
QuotaProjectID string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// WorkforcePoolUserProject should be set when it is a workforce pool and
// not a workload identity pool. The underlying principal must still have
// serviceusage.services.use IAM permission to use the project for
// billing/quota. Optional.
WorkforcePoolUserProject string
// UniverseDomain is the default service domain for a given Cloud universe.
// This value will be used in the default STS token URL. The default value
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
UniverseDomain string
// SubjectTokenProvider is an optional token provider for OIDC/SAML
// credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
// or CredentialSource must be provided. Optional.
SubjectTokenProvider SubjectTokenProvider
// AwsSecurityCredentialsProvider is an AWS Security Credential provider
// for AWS credentials. One of SubjectTokenProvider,
// AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
// Client for token request.
Client *http.Client
// IsDefaultClient marks whether the client passed in is a default client that can be overriden.
// This is important for X509 credentials which should create a new client if the default was used
// but should respect a client explicitly passed in by the user.
IsDefaultClient bool
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
// SubjectTokenProvider can be used to supply a subject token to exchange for a
// GCP access token.
type SubjectTokenProvider interface {
// SubjectToken should return a valid subject token or an error.
// The external account token provider does not cache the returned subject
// token, so caching logic should be implemented in the provider to prevent
// multiple requests for the same subject token.
SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
}
// RequestOptions contains information about the requested subject token or AWS
// security credentials from the Google external account credential.
type RequestOptions struct {
// Audience is the requested audience for the external account credential.
Audience string
// Subject token type is the requested subject token type for the external
// account credential. Expected values include:
// “urn:ietf:params:oauth:token-type:jwt”
// “urn:ietf:params:oauth:token-type:id-token”
// “urn:ietf:params:oauth:token-type:saml2”
// “urn:ietf:params:aws:token-type:aws4_request”
SubjectTokenType string
}
// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
// and an AWS Region to exchange for a GCP access token.
type AwsSecurityCredentialsProvider interface {
// AwsRegion should return the AWS region or an error.
AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
// GetAwsSecurityCredentials should return a valid set of
// AwsSecurityCredentials or an error. The external account token provider
// does not cache the returned security credentials, so caching logic should
// be implemented in the provider to prevent multiple requests for the
// same security credentials.
AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
}
// AwsSecurityCredentials models AWS security credentials.
type AwsSecurityCredentials struct {
// AccessKeyId is the AWS Access Key ID - Required.
AccessKeyID string `json:"AccessKeyID"`
// SecretAccessKey is the AWS Secret Access Key - Required.
SecretAccessKey string `json:"SecretAccessKey"`
// SessionToken is the AWS Session token. This should be provided for
// temporary AWS security credentials - Optional.
SessionToken string `json:"Token"`
}
func (o *Options) validate() error {
if o.Audience == "" {
return fmt.Errorf("externalaccount: Audience must be set")
}
if o.SubjectTokenType == "" {
return fmt.Errorf("externalaccount: Subject token type must be set")
}
if o.WorkforcePoolUserProject != "" {
if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
}
}
count := 0
if o.CredentialSource != nil {
count++
}
if o.SubjectTokenProvider != nil {
count++
}
if o.AwsSecurityCredentialsProvider != nil {
count++
}
if count == 0 {
return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
if count > 1 {
return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
return nil
}
// client returns the http client that should be used for the token exchange. If a non-default client
// is provided, then the client configured in the options will always be returned. If a default client
// is provided and the options are configured for X509 credentials, a new client will be created.
func (o *Options) client() (*http.Client, error) {
// If a client was provided and no override certificate config location was provided, use the provided client.
if o.CredentialSource == nil || o.CredentialSource.Certificate == nil || (!o.IsDefaultClient && o.CredentialSource.Certificate.CertificateConfigLocation == "") {
return o.Client, nil
}
// If a new client should be created, validate and use the certificate source to create a new mTLS client.
cert := o.CredentialSource.Certificate
if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
}
if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
}
return createX509Client(cert.CertificateConfigLocation)
}
// resolveTokenURL sets the default STS token endpoint with the configured
// universe domain.
func (o *Options) resolveTokenURL() {
if o.TokenURL != "" {
return
} else if o.UniverseDomain != "" {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
} else {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
}
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
// configured with the provided options.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
opts.resolveTokenURL()
logger := internallog.New(opts.Logger)
stp, err := newSubjectTokenProvider(opts)
if err != nil {
return nil, err
}
client, err := opts.client()
if err != nil {
return nil, err
}
tp := &tokenProvider{
client: client,
opts: opts,
stp: stp,
logger: logger,
}
if opts.ServiceAccountImpersonationURL == "" {
return auth.NewCachedTokenProvider(tp, nil), nil
}
scopes := make([]string, len(opts.Scopes))
copy(scopes, opts.Scopes)
// needed for impersonation
tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
imp, err := impersonate.NewTokenProvider(&impersonate.Options{
Client: client,
URL: opts.ServiceAccountImpersonationURL,
Scopes: scopes,
Tp: auth.NewCachedTokenProvider(tp, nil),
TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
Logger: logger,
})
if err != nil {
return nil, err
}
return auth.NewCachedTokenProvider(imp, nil), nil
}
type subjectTokenProvider interface {
subjectToken(ctx context.Context) (string, error)
providerType() string
}
// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
type tokenProvider struct {
client *http.Client
logger *slog.Logger
opts *Options
stp subjectTokenProvider
}
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
subjectToken, err := tp.stp.subjectToken(ctx)
if err != nil {
return nil, err
}
stsRequest := &stsexchange.TokenRequest{
GrantType: stsexchange.GrantType,
Audience: tp.opts.Audience,
Scope: tp.opts.Scopes,
RequestedTokenType: stsexchange.TokenType,
SubjectToken: subjectToken,
SubjectTokenType: tp.opts.SubjectTokenType,
}
header := make(http.Header)
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: auth.StyleInHeader,
ClientID: tp.opts.ClientID,
ClientSecret: tp.opts.ClientSecret,
}
var options map[string]interface{}
// Do not pass workforce_pool_user_project when client authentication is used.
// The client ID is sufficient for determining the user project.
if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
options = map[string]interface{}{
"userProject": tp.opts.WorkforcePoolUserProject,
}
}
stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
Client: tp.client,
Endpoint: tp.opts.TokenURL,
Request: stsRequest,
Authentication: clientAuth,
Headers: header,
ExtraOpts: options,
Logger: tp.logger,
})
if err != nil {
return nil, err
}
tok := &auth.Token{
Value: stsResp.AccessToken,
Type: stsResp.TokenType,
}
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
if stsResp.ExpiresIn <= 0 {
return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
}
tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
return tok, nil
}
// newSubjectTokenProvider determines the type of credsfile.CredentialSource needed to create a
// subjectTokenProvider
func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
logger := internallog.New(o.Logger)
reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
if o.AwsSecurityCredentialsProvider != nil {
return &awsSubjectProvider{
securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
TargetResource: o.Audience,
reqOpts: reqOpts,
logger: logger,
}, nil
} else if o.SubjectTokenProvider != nil {
return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
} else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
if awsVersion != 1 {
return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
}
awsProvider := &awsSubjectProvider{
EnvironmentID: o.CredentialSource.EnvironmentID,
RegionURL: o.CredentialSource.RegionURL,
RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
CredVerificationURL: o.CredentialSource.URL,
TargetResource: o.Audience,
Client: o.Client,
logger: logger,
}
if o.CredentialSource.IMDSv2SessionTokenURL != "" {
awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
}
return awsProvider, nil
}
} else if o.CredentialSource.File != "" {
return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
} else if o.CredentialSource.URL != "" {
return &urlSubjectProvider{
URL: o.CredentialSource.URL,
Headers: o.CredentialSource.Headers,
Format: o.CredentialSource.Format,
Client: o.Client,
Logger: logger,
}, nil
} else if o.CredentialSource.Executable != nil {
ec := o.CredentialSource.Executable
if ec.Command == "" {
return nil, errors.New("credentials: missing `command` field — executable command must be provided")
}
execProvider := &executableSubjectProvider{}
execProvider.Command = ec.Command
if ec.TimeoutMillis == 0 {
execProvider.Timeout = executableDefaultTimeout
} else {
execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
}
}
execProvider.OutputFile = ec.OutputFile
execProvider.client = o.Client
execProvider.opts = o
execProvider.env = runtimeEnvironment{}
return execProvider, nil
} else if o.CredentialSource.Certificate != nil {
cert := o.CredentialSource.Certificate
if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
}
if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
}
return &x509Provider{}, nil
}
return nil, errors.New("credentials: unable to parse credential source")
}
func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
goVersion(),
"unknown",
p.providerType(),
conf.ServiceAccountImpersonationURL != "",
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
}

View File

@ -1,78 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
const (
fileProviderType = "file"
)
type fileSubjectProvider struct {
File string
Format *credsfile.Format
}
func (sp *fileSubjectProvider) subjectToken(context.Context) (string, error) {
tokenFile, err := os.Open(sp.File)
if err != nil {
return "", fmt.Errorf("credentials: failed to open credential file %q: %w", sp.File, err)
}
defer tokenFile.Close()
tokenBytes, err := internal.ReadAll(tokenFile)
if err != nil {
return "", fmt.Errorf("credentials: failed to read credential file: %w", err)
}
tokenBytes = bytes.TrimSpace(tokenBytes)
if sp.Format == nil {
return string(tokenBytes), nil
}
switch sp.Format.Type {
case fileTypeJSON:
jsonData := make(map[string]interface{})
err = json.Unmarshal(tokenBytes, &jsonData)
if err != nil {
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
}
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
if !ok {
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
}
token, ok := val.(string)
if !ok {
return "", errors.New("credentials: improperly formatted subject token")
}
return token, nil
case fileTypeText:
return string(tokenBytes), nil
default:
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
}
}
func (sp *fileSubjectProvider) providerType() string {
return fileProviderType
}

View File

@ -1,74 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"runtime"
"strings"
"unicode"
)
var (
// version is a package internal global variable for testing purposes.
version = runtime.Version
)
// versionUnknown is only used when the runtime version cannot be determined.
const versionUnknown = "UNKNOWN"
// goVersion returns a Go runtime version derived from the runtime environment
// that is modified to be suitable for reporting in a header, meaning it has no
// whitespace. If it is unable to determine the Go runtime version, it returns
// versionUnknown.
func goVersion() string {
const develPrefix = "devel +"
s := version()
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
notSemverRune := func(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
// Some release candidates already have a dash in them.
if !strings.HasPrefix(prerelease, "-") {
prerelease = "-" + prerelease
}
s += prerelease
}
return s
}
return versionUnknown
}

View File

@ -1,30 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import "context"
type programmaticProvider struct {
opts *RequestOptions
stp SubjectTokenProvider
}
func (pp *programmaticProvider) providerType() string {
return programmaticProviderType
}
func (pp *programmaticProvider) subjectToken(ctx context.Context) (string, error) {
return pp.stp.SubjectToken(ctx, pp.opts)
}

View File

@ -1,93 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
fileTypeText = "text"
fileTypeJSON = "json"
urlProviderType = "url"
programmaticProviderType = "programmatic"
x509ProviderType = "x509"
)
type urlSubjectProvider struct {
URL string
Headers map[string]string
Format *credsfile.Format
Client *http.Client
Logger *slog.Logger
}
func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", sp.URL, nil)
if err != nil {
return "", fmt.Errorf("credentials: HTTP request for URL-sourced credential failed: %w", err)
}
for key, val := range sp.Headers {
req.Header.Add(key, val)
}
sp.Logger.DebugContext(ctx, "url subject token request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(sp.Client, req)
if err != nil {
return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
}
sp.Logger.DebugContext(ctx, "url subject token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return "", fmt.Errorf("credentials: status code %d: %s", c, body)
}
if sp.Format == nil {
return string(body), nil
}
switch sp.Format.Type {
case "json":
jsonData := make(map[string]interface{})
err = json.Unmarshal(body, &jsonData)
if err != nil {
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
}
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
if !ok {
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
}
token, ok := val.(string)
if !ok {
return "", errors.New("credentials: improperly formatted subject token")
}
return token, nil
case fileTypeText:
return string(body), nil
default:
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
}
}
func (sp *urlSubjectProvider) providerType() string {
return urlProviderType
}

View File

@ -1,63 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"crypto/tls"
"net/http"
"time"
"cloud.google.com/go/auth/internal/transport/cert"
)
// x509Provider implements the subjectTokenProvider type for
// x509 workload identity credentials. Because x509 credentials
// rely on an mTLS connection to represent the 3rd party identity
// rather than a subject token, this provider will always return
// an empty string when a subject token is requested by the external account
// token provider.
type x509Provider struct {
}
func (xp *x509Provider) providerType() string {
return x509ProviderType
}
func (xp *x509Provider) subjectToken(ctx context.Context) (string, error) {
return "", nil
}
// createX509Client creates a new client that is configured with mTLS, using the
// certificate configuration specified in the credential source.
func createX509Client(certificateConfigLocation string) (*http.Client, error) {
certProvider, err := cert.NewWorkloadX509CertProvider(certificateConfigLocation)
if err != nil {
return nil, err
}
trans := http.DefaultTransport.(*http.Transport).Clone()
trans.TLSClientConfig = &tls.Config{
GetClientCertificate: certProvider,
}
// Create a client with default settings plus the X509 workload cert and key.
client := &http.Client{
Transport: trans,
Timeout: 30 * time.Second,
}
return client, nil
}

View File

@ -1,115 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccountuser
import (
"context"
"errors"
"log/slog"
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/stsexchange"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
// Options stores the configuration for fetching tokens with external authorized
// user credentials.
type Options struct {
// Audience is the Secure Token Service (STS) audience which contains the
// resource name for the workforce pool and the provider identifier in that
// pool.
Audience string
// RefreshToken is the OAuth 2.0 refresh token.
RefreshToken string
// TokenURL is the STS token exchange endpoint for refresh.
TokenURL string
// TokenInfoURL is the STS endpoint URL for token introspection. Optional.
TokenInfoURL string
// ClientID is only required in conjunction with ClientSecret, as described
// below.
ClientID string
// ClientSecret is currently only required if token_info endpoint also needs
// to be called with the generated a cloud access token. When provided, STS
// will be called with additional basic authentication using client_id as
// username and client_secret as password.
ClientSecret string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// Client for token request.
Client *http.Client
// Logger for logging.
Logger *slog.Logger
}
func (c *Options) validate() bool {
return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
// configured with the provided options.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if !opts.validate() {
return nil, errors.New("credentials: invalid external_account_authorized_user configuration")
}
tp := &tokenProvider{
o: opts,
}
return auth.NewCachedTokenProvider(tp, nil), nil
}
type tokenProvider struct {
o *Options
}
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
opts := tp.o
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: auth.StyleInHeader,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
}
headers := make(http.Header)
headers.Set("Content-Type", "application/x-www-form-urlencoded")
stsResponse, err := stsexchange.RefreshAccessToken(ctx, &stsexchange.Options{
Client: opts.Client,
Endpoint: opts.TokenURL,
RefreshToken: opts.RefreshToken,
Authentication: clientAuth,
Headers: headers,
Logger: internallog.New(tp.o.Logger),
})
if err != nil {
return nil, err
}
if stsResponse.ExpiresIn < 0 {
return nil, errors.New("credentials: invalid expiry from security token service")
}
// guarded by the wrapping with CachedTokenProvider
if stsResponse.RefreshToken != "" {
opts.RefreshToken = stsResponse.RefreshToken
}
return &auth.Token{
Value: stsResponse.AccessToken,
Expiry: time.Now().UTC().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
Type: internal.TokenTypeBearer,
}, nil
}

View File

@ -1,191 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gdch
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/jwt"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
// GrantType is the grant type for the token request.
GrantType = "urn:ietf:params:oauth:token-type:token-exchange"
requestTokenType = "urn:ietf:params:oauth:token-type:access_token"
subjectTokenType = "urn:k8s:params:oauth:token-type:serviceaccount"
)
var (
gdchSupportFormatVersions map[string]bool = map[string]bool{
"1": true,
}
)
// Options for [NewTokenProvider].
type Options struct {
STSAudience string
Client *http.Client
Logger *slog.Logger
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider] from a
// GDCH cred file.
func NewTokenProvider(f *credsfile.GDCHServiceAccountFile, o *Options) (auth.TokenProvider, error) {
if !gdchSupportFormatVersions[f.FormatVersion] {
return nil, fmt.Errorf("credentials: unsupported gdch_service_account format %q", f.FormatVersion)
}
if o.STSAudience == "" {
return nil, errors.New("credentials: STSAudience must be set for the GDCH auth flows")
}
signer, err := internal.ParseKey([]byte(f.PrivateKey))
if err != nil {
return nil, err
}
certPool, err := loadCertPool(f.CertPath)
if err != nil {
return nil, err
}
tp := gdchProvider{
serviceIdentity: fmt.Sprintf("system:serviceaccount:%s:%s", f.Project, f.Name),
tokenURL: f.TokenURL,
aud: o.STSAudience,
signer: signer,
pkID: f.PrivateKeyID,
certPool: certPool,
client: o.Client,
logger: internallog.New(o.Logger),
}
return tp, nil
}
func loadCertPool(path string) (*x509.CertPool, error) {
pool := x509.NewCertPool()
pem, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("credentials: failed to read certificate: %w", err)
}
pool.AppendCertsFromPEM(pem)
return pool, nil
}
type gdchProvider struct {
serviceIdentity string
tokenURL string
aud string
signer crypto.Signer
pkID string
certPool *x509.CertPool
client *http.Client
logger *slog.Logger
}
func (g gdchProvider) Token(ctx context.Context) (*auth.Token, error) {
addCertToTransport(g.client, g.certPool)
iat := time.Now()
exp := iat.Add(time.Hour)
claims := jwt.Claims{
Iss: g.serviceIdentity,
Sub: g.serviceIdentity,
Aud: g.tokenURL,
Iat: iat.Unix(),
Exp: exp.Unix(),
}
h := jwt.Header{
Algorithm: jwt.HeaderAlgRSA256,
Type: jwt.HeaderType,
KeyID: string(g.pkID),
}
payload, err := jwt.EncodeJWS(&h, &claims, g.signer)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("grant_type", GrantType)
v.Set("audience", g.aud)
v.Set("requested_token_type", requestTokenType)
v.Set("subject_token", payload)
v.Set("subject_token_type", subjectTokenType)
req, err := http.NewRequestWithContext(ctx, "POST", g.tokenURL, strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
g.logger.DebugContext(ctx, "gdch token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
resp, body, err := internal.DoRequest(g.client, req)
if err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
g.logger.DebugContext(ctx, "gdch token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
return nil, &auth.Error{
Response: resp,
Body: body,
}
}
var tokenRes struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
token := &auth.Token{
Value: tokenRes.AccessToken,
Type: tokenRes.TokenType,
}
raw := make(map[string]interface{})
json.Unmarshal(body, &raw) // no error checks for optional fields
token.Metadata = raw
if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
}
return token, nil
}
// addCertToTransport makes a best effort attempt at adding in the cert info to
// the client. It tries to keep all configured transport settings if the
// underlying transport is an http.Transport. Or else it overwrites the
// transport with defaults adding in the certs.
func addCertToTransport(hc *http.Client, certPool *x509.CertPool) {
trans, ok := hc.Transport.(*http.Transport)
if !ok {
trans = http.DefaultTransport.(*http.Transport).Clone()
}
trans.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
}

View File

@ -1,105 +0,0 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package impersonate
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
var (
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN"
)
// IDTokenIAMOptions provides configuration for [IDTokenIAMOptions.Token].
type IDTokenIAMOptions struct {
// Client is required.
Client *http.Client
// Logger is required.
Logger *slog.Logger
UniverseDomain auth.CredentialsPropertyProvider
ServiceAccountEmail string
GenerateIDTokenRequest
}
// GenerateIDTokenRequest holds the request to the IAM generateIdToken RPC.
type GenerateIDTokenRequest struct {
Audience string `json:"audience"`
IncludeEmail bool `json:"includeEmail"`
// Delegates are the ordered, fully-qualified resource name for service
// accounts in a delegation chain. Each service account must be granted
// roles/iam.serviceAccountTokenCreator on the next service account in the
// chain. The delegates must have the following format:
// projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID}. The - wildcard
// character is required; replacing it with a project ID is invalid.
// Optional.
Delegates []string `json:"delegates,omitempty"`
}
// GenerateIDTokenResponse holds the response from the IAM generateIdToken RPC.
type GenerateIDTokenResponse struct {
Token string `json:"token"`
}
// Token call IAM generateIdToken with the configuration provided in [IDTokenIAMOptions].
func (o IDTokenIAMOptions) Token(ctx context.Context) (*auth.Token, error) {
universeDomain, err := o.UniverseDomain.GetProperty(ctx)
if err != nil {
return nil, err
}
endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1)
url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountResource(o.ServiceAccountEmail))
bodyBytes, err := json.Marshal(o.GenerateIDTokenRequest)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
o.Logger.DebugContext(ctx, "impersonated idtoken request", "request", internallog.HTTPRequest(req, bodyBytes))
resp, body, err := internal.DoRequest(o.Client, req)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err)
}
o.Logger.DebugContext(ctx, "impersonated idtoken response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
}
var tokenResp GenerateIDTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
}
return &auth.Token{
Value: tokenResp.Token,
// Generated ID tokens are good for one hour.
Expiry: time.Now().Add(1 * time.Hour),
}, nil
}

View File

@ -1,156 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package impersonate
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
defaultTokenLifetime = "3600s"
authHeaderKey = "Authorization"
)
// generateAccesstokenReq is used for service account impersonation
type generateAccessTokenReq struct {
Delegates []string `json:"delegates,omitempty"`
Lifetime string `json:"lifetime,omitempty"`
Scope []string `json:"scope,omitempty"`
}
type impersonateTokenResponse struct {
AccessToken string `json:"accessToken"`
ExpireTime string `json:"expireTime"`
}
// NewTokenProvider uses a source credential, stored in Ts, to request an access token to the provided URL.
// Scopes can be defined when the access token is requested.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
return opts, nil
}
// Options for [NewTokenProvider].
type Options struct {
// Tp is the source credential used to generate a token on the
// impersonated service account. Required.
Tp auth.TokenProvider
// URL is the endpoint to call to generate a token
// on behalf of the service account. Required.
URL string
// Scopes that the impersonated credential should have. Required.
Scopes []string
// Delegates are the service account email addresses in a delegation chain.
// Each service account must be granted roles/iam.serviceAccountTokenCreator
// on the next service account in the chain. Optional.
Delegates []string
// TokenLifetimeSeconds is the number of seconds the impersonation token will
// be valid for. Defaults to 1 hour if unset. Optional.
TokenLifetimeSeconds int
// Client configures the underlying client used to make network requests
// when fetching tokens. Required.
Client *http.Client
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *Options) validate() error {
if o.Tp == nil {
return errors.New("credentials: missing required 'source_credentials' field in impersonated credentials")
}
if o.URL == "" {
return errors.New("credentials: missing required 'service_account_impersonation_url' field in impersonated credentials")
}
return nil
}
// Token performs the exchange to get a temporary service account token to allow access to GCP.
func (o *Options) Token(ctx context.Context) (*auth.Token, error) {
logger := internallog.New(o.Logger)
lifetime := defaultTokenLifetime
if o.TokenLifetimeSeconds != 0 {
lifetime = fmt.Sprintf("%ds", o.TokenLifetimeSeconds)
}
reqBody := generateAccessTokenReq{
Lifetime: lifetime,
Scope: o.Scopes,
Delegates: o.Delegates,
}
b, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("credentials: unable to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", o.URL, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("credentials: unable to create impersonation request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if err := setAuthHeader(ctx, o.Tp, req); err != nil {
return nil, err
}
logger.DebugContext(ctx, "impersonated token request", "request", internallog.HTTPRequest(req, b))
resp, body, err := internal.DoRequest(o.Client, req)
if err != nil {
return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
}
logger.DebugContext(ctx, "impersonated token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
}
var accessTokenResp impersonateTokenResponse
if err := json.Unmarshal(body, &accessTokenResp); err != nil {
return nil, fmt.Errorf("credentials: unable to parse response: %w", err)
}
expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
if err != nil {
return nil, fmt.Errorf("credentials: unable to parse expiry: %w", err)
}
return &auth.Token{
Value: accessTokenResp.AccessToken,
Expiry: expiry,
Type: internal.TokenTypeBearer,
}, nil
}
func setAuthHeader(ctx context.Context, tp auth.TokenProvider, r *http.Request) error {
t, err := tp.Token(ctx)
if err != nil {
return err
}
typ := t.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
r.Header.Set(authHeaderKey, typ+" "+t.Value)
return nil
}

View File

@ -1,167 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stsexchange
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strconv"
"strings"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
// GrantType for a sts exchange.
GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
// TokenType for a sts exchange.
TokenType = "urn:ietf:params:oauth:token-type:access_token"
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
)
// Options stores the configuration for making an sts exchange request.
type Options struct {
Client *http.Client
Logger *slog.Logger
Endpoint string
Request *TokenRequest
Authentication ClientAuthentication
Headers http.Header
// ExtraOpts are optional fields marshalled into the `options` field of the
// request body.
ExtraOpts map[string]interface{}
RefreshToken string
}
// RefreshAccessToken performs the token exchange using a refresh token flow.
func RefreshAccessToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("refresh_token", opts.RefreshToken)
return doRequest(ctx, opts, data)
}
// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
func ExchangeToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
data := url.Values{}
data.Set("audience", opts.Request.Audience)
data.Set("grant_type", GrantType)
data.Set("requested_token_type", TokenType)
data.Set("subject_token_type", opts.Request.SubjectTokenType)
data.Set("subject_token", opts.Request.SubjectToken)
data.Set("scope", strings.Join(opts.Request.Scope, " "))
if opts.ExtraOpts != nil {
opts, err := json.Marshal(opts.ExtraOpts)
if err != nil {
return nil, fmt.Errorf("credentials: failed to marshal additional options: %w", err)
}
data.Set("options", string(opts))
}
return doRequest(ctx, opts, data)
}
func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenResponse, error) {
opts.Authentication.InjectAuthentication(data, opts.Headers)
encodedData := data.Encode()
logger := internallog.New(opts.Logger)
req, err := http.NewRequestWithContext(ctx, "POST", opts.Endpoint, strings.NewReader(encodedData))
if err != nil {
return nil, fmt.Errorf("credentials: failed to properly build http request: %w", err)
}
for key, list := range opts.Headers {
for _, val := range list {
req.Header.Add(key, val)
}
}
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
logger.DebugContext(ctx, "sts token request", "request", internallog.HTTPRequest(req, []byte(encodedData)))
resp, body, err := internal.DoRequest(opts.Client, req)
if err != nil {
return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
}
logger.DebugContext(ctx, "sts token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
}
var stsResp TokenResponse
if err := json.Unmarshal(body, &stsResp); err != nil {
return nil, fmt.Errorf("credentials: failed to unmarshal response body from Secure Token Server: %w", err)
}
return &stsResp, nil
}
// TokenRequest contains fields necessary to make an oauth2 token
// exchange.
type TokenRequest struct {
ActingParty struct {
ActorToken string
ActorTokenType string
}
GrantType string
Resource string
Audience string
Scope []string
RequestedTokenType string
SubjectToken string
SubjectTokenType string
}
// TokenResponse is used to decode the remote server response during
// an oauth2 token exchange.
type TokenResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
// ClientAuthentication represents an OAuth client ID and secret and the
// mechanism for passing these credentials as stated in rfc6749#2.3.1.
type ClientAuthentication struct {
AuthStyle auth.Style
ClientID string
ClientSecret string
}
// InjectAuthentication is used to add authentication to a Secure Token Service
// exchange request. It modifies either the passed url.Values or http.Header
// depending on the desired authentication format.
func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
return
}
switch c.AuthStyle {
case auth.StyleInHeader:
plainHeader := c.ClientID + ":" + c.ClientSecret
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader)))
default:
values.Set("client_id", c.ClientID)
values.Set("client_secret", c.ClientSecret)
}
}

View File

@ -1,89 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"crypto"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/jwt"
)
var (
// for testing
now func() time.Time = time.Now
)
// configureSelfSignedJWT uses the private key in the service account to create
// a JWT without making a network call.
func configureSelfSignedJWT(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
if len(opts.scopes()) == 0 && opts.Audience == "" {
return nil, errors.New("credentials: both scopes and audience are empty")
}
signer, err := internal.ParseKey([]byte(f.PrivateKey))
if err != nil {
return nil, fmt.Errorf("credentials: could not parse key: %w", err)
}
return &selfSignedTokenProvider{
email: f.ClientEmail,
audience: opts.Audience,
scopes: opts.scopes(),
signer: signer,
pkID: f.PrivateKeyID,
logger: opts.logger(),
}, nil
}
type selfSignedTokenProvider struct {
email string
audience string
scopes []string
signer crypto.Signer
pkID string
logger *slog.Logger
}
func (tp *selfSignedTokenProvider) Token(context.Context) (*auth.Token, error) {
iat := now()
exp := iat.Add(time.Hour)
scope := strings.Join(tp.scopes, " ")
c := &jwt.Claims{
Iss: tp.email,
Sub: tp.email,
Aud: tp.audience,
Scope: scope,
Iat: iat.Unix(),
Exp: exp.Unix(),
}
h := &jwt.Header{
Algorithm: jwt.HeaderAlgRSA256,
Type: jwt.HeaderType,
KeyID: string(tp.pkID),
}
tok, err := jwt.EncodeJWS(h, c, tp.signer)
if err != nil {
return nil, fmt.Errorf("credentials: could not encode JWT: %w", err)
}
tp.logger.Debug("created self-signed JWT", "token", tok)
return &auth.Token{Value: tok, Type: internal.TokenTypeBearer, Expiry: exp}, nil
}

View File

@ -1,247 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package httptransport provides functionality for managing HTTP client
// connections to Google Cloud services.
package httptransport
import (
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/http"
"cloud.google.com/go/auth"
detect "cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport"
"github.com/googleapis/gax-go/v2/internallog"
)
// ClientCertProvider is a function that returns a TLS client certificate to be
// used when opening TLS connections. It follows the same semantics as
// [crypto/tls.Config.GetClientCertificate].
type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
// Options used to configure a [net/http.Client] from [NewClient].
type Options struct {
// DisableTelemetry disables default telemetry (OpenTelemetry). An example
// reason to do so would be to bind custom telemetry that overrides the
// defaults.
DisableTelemetry bool
// DisableAuthentication specifies that no authentication should be used. It
// is suitable only for testing and for accessing public resources, like
// public Google Cloud Storage buckets.
DisableAuthentication bool
// Headers are extra HTTP headers that will be appended to every outgoing
// request.
Headers http.Header
// BaseRoundTripper overrides the base transport used for serving requests.
// If specified ClientCertProvider is ignored.
BaseRoundTripper http.RoundTripper
// Endpoint overrides the default endpoint to be used for a service.
Endpoint string
// APIKey specifies an API key to be used as the basis for authentication.
// If set DetectOpts are ignored.
APIKey string
// Credentials used to add Authorization header to all requests. If set
// DetectOpts are ignored.
Credentials *auth.Credentials
// ClientCertProvider is a function that returns a TLS client certificate to
// be used when opening TLS connections. It follows the same semantics as
// crypto/tls.Config.GetClientCertificate.
ClientCertProvider ClientCertProvider
// DetectOpts configures settings for detect Application Default
// Credentials.
DetectOpts *detect.DetectOptions
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials.
UniverseDomain string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
// InternalOptions are NOT meant to be set directly by consumers of this
// package, they should only be set by generated client code.
InternalOptions *InternalOptions
}
func (o *Options) validate() error {
if o == nil {
return errors.New("httptransport: opts required to be non-nil")
}
if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
return nil
}
hasCreds := o.APIKey != "" ||
o.Credentials != nil ||
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
if o.DisableAuthentication && hasCreds {
return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials")
}
return nil
}
// client returns the client a user set for the detect options or nil if one was
// not set.
func (o *Options) client() *http.Client {
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
return o.DetectOpts.Client
}
return nil
}
func (o *Options) logger() *slog.Logger {
return internallog.New(o.Logger)
}
func (o *Options) resolveDetectOptions() *detect.DetectOptions {
io := o.InternalOptions
// soft-clone these so we are not updating a ref the user holds and may reuse
do := transport.CloneDetectOptions(o.DetectOpts)
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
do.UseSelfSignedJWT = true
}
// Only default scopes if user did not also set an audience.
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
do.Scopes = make([]string, len(io.DefaultScopes))
copy(do.Scopes, io.DefaultScopes)
}
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
do.Audience = o.InternalOptions.DefaultAudience
}
if o.ClientCertProvider != nil {
tlsConfig := &tls.Config{
GetClientCertificate: o.ClientCertProvider,
}
do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
do.TokenURL = detect.GoogleMTLSTokenURL
}
if do.Logger == nil {
do.Logger = o.logger()
}
return do
}
// InternalOptions are only meant to be set by generated client code. These are
// not meant to be set directly by consumers of this package. Configuration in
// this type is considered EXPERIMENTAL and may be removed at any time in the
// future without warning.
type InternalOptions struct {
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
EnableJWTWithScope bool
// DefaultAudience specifies a default audience to be used as the audience
// field ("aud") for the JWT token authentication.
DefaultAudience string
// DefaultEndpointTemplate combined with UniverseDomain specifies the
// default endpoint.
DefaultEndpointTemplate string
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
DefaultMTLSEndpoint string
// DefaultScopes specifies the default OAuth2 scopes to be used for a
// service.
DefaultScopes []string
// SkipValidation bypasses validation on Options. It should only be used
// internally for clients that need more control over their transport.
SkipValidation bool
// SkipUniverseDomainValidation skips the verification that the universe
// domain configured for the client matches the universe domain configured
// for the credentials. It should only be used internally for clients that
// need more control over their transport. The default is false.
SkipUniverseDomainValidation bool
}
// AddAuthorizationMiddleware adds a middleware to the provided client's
// transport that sets the Authorization header with the value produced by the
// provided [cloud.google.com/go/auth.Credentials]. An error is returned only
// if client or creds is nil.
//
// This function does not support setting a universe domain value on the client.
func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error {
if client == nil || creds == nil {
return fmt.Errorf("httptransport: client and tp must not be nil")
}
base := client.Transport
if base == nil {
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
base = dt.Clone()
} else {
// Directly reuse the DefaultTransport if the application has
// replaced it with an implementation of RoundTripper other than
// http.Transport.
base = http.DefaultTransport
}
}
client.Transport = &authTransport{
creds: creds,
base: base,
}
return nil
}
// NewClient returns a [net/http.Client] that can be used to communicate with a
// Google cloud service, configured with the provided [Options]. It
// automatically appends Authorization headers to all outgoing requests.
func NewClient(opts *Options) (*http.Client, error) {
if err := opts.validate(); err != nil {
return nil, err
}
tOpts := &transport.Options{
Endpoint: opts.Endpoint,
ClientCertProvider: opts.ClientCertProvider,
Client: opts.client(),
UniverseDomain: opts.UniverseDomain,
Logger: opts.logger(),
}
if io := opts.InternalOptions; io != nil {
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
}
clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts)
if err != nil {
return nil, err
}
baseRoundTripper := opts.BaseRoundTripper
if baseRoundTripper == nil {
baseRoundTripper = defaultBaseTransport(clientCertProvider, dialTLSContext)
}
// Ensure the token exchange transport uses the same ClientCertProvider as the API transport.
opts.ClientCertProvider = clientCertProvider
trans, err := newTransport(baseRoundTripper, opts)
if err != nil {
return nil, err
}
return &http.Client{
Transport: trans,
}, nil
}
// SetAuthHeader uses the provided token to set the Authorization header on a
// request. If the token.Type is empty, the type is assumed to be Bearer.
func SetAuthHeader(token *auth.Token, req *http.Request) {
typ := token.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
req.Header.Set("Authorization", typ+" "+token.Value)
}

View File

@ -1,234 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptransport
import (
"context"
"crypto/tls"
"net"
"net/http"
"os"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport"
"cloud.google.com/go/auth/internal/transport/cert"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"golang.org/x/net/http2"
)
const (
quotaProjectHeaderKey = "X-goog-user-project"
)
func newTransport(base http.RoundTripper, opts *Options) (http.RoundTripper, error) {
var headers = opts.Headers
ht := &headerTransport{
base: base,
headers: headers,
}
var trans http.RoundTripper = ht
trans = addOpenTelemetryTransport(trans, opts)
switch {
case opts.DisableAuthentication:
// Do nothing.
case opts.APIKey != "":
qp := internal.GetQuotaProject(nil, opts.Headers.Get(quotaProjectHeaderKey))
if qp != "" {
if headers == nil {
headers = make(map[string][]string, 1)
}
headers.Set(quotaProjectHeaderKey, qp)
}
trans = &apiKeyTransport{
Transport: trans,
Key: opts.APIKey,
}
default:
var creds *auth.Credentials
if opts.Credentials != nil {
creds = opts.Credentials
} else {
var err error
creds, err = credentials.DetectDefault(opts.resolveDetectOptions())
if err != nil {
return nil, err
}
}
qp, err := creds.QuotaProjectID(context.Background())
if err != nil {
return nil, err
}
if qp != "" {
if headers == nil {
headers = make(map[string][]string, 1)
}
// Don't overwrite user specified quota
if v := headers.Get(quotaProjectHeaderKey); v == "" {
headers.Set(quotaProjectHeaderKey, qp)
}
}
var skipUD bool
if iOpts := opts.InternalOptions; iOpts != nil {
skipUD = iOpts.SkipUniverseDomainValidation
}
creds.TokenProvider = auth.NewCachedTokenProvider(creds.TokenProvider, nil)
trans = &authTransport{
base: trans,
creds: creds,
clientUniverseDomain: opts.UniverseDomain,
skipUniverseDomainValidation: skipUD,
}
}
return trans, nil
}
// defaultBaseTransport returns the base HTTP transport.
// On App Engine, this is urlfetch.Transport.
// Otherwise, use a default transport, taking most defaults from
// http.DefaultTransport.
// If TLSCertificate is available, set TLSClientConfig as well.
func defaultBaseTransport(clientCertSource cert.Provider, dialTLSContext func(context.Context, string, string) (net.Conn, error)) http.RoundTripper {
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
if !ok {
defaultTransport = transport.BaseTransport()
}
trans := defaultTransport.Clone()
trans.MaxIdleConnsPerHost = 100
if clientCertSource != nil {
trans.TLSClientConfig = &tls.Config{
GetClientCertificate: clientCertSource,
}
}
if dialTLSContext != nil {
// If DialTLSContext is set, TLSClientConfig wil be ignored
trans.DialTLSContext = dialTLSContext
}
// Configures the ReadIdleTimeout HTTP/2 option for the
// transport. This allows broken idle connections to be pruned more quickly,
// preventing the client from attempting to re-use connections that will no
// longer work.
http2Trans, err := http2.ConfigureTransports(trans)
if err == nil {
http2Trans.ReadIdleTimeout = time.Second * 31
}
return trans
}
type apiKeyTransport struct {
// Key is the API Key to set on requests.
Key string
// Transport is the underlying HTTP transport.
// If nil, http.DefaultTransport is used.
Transport http.RoundTripper
}
func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
newReq := *req
args := newReq.URL.Query()
args.Set("key", t.Key)
newReq.URL.RawQuery = args.Encode()
return t.Transport.RoundTrip(&newReq)
}
type headerTransport struct {
headers http.Header
base http.RoundTripper
}
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rt := t.base
newReq := *req
newReq.Header = make(http.Header)
for k, vv := range req.Header {
newReq.Header[k] = vv
}
for k, v := range t.headers {
newReq.Header[k] = v
}
return rt.RoundTrip(&newReq)
}
func addOpenTelemetryTransport(trans http.RoundTripper, opts *Options) http.RoundTripper {
if opts.DisableTelemetry {
return trans
}
return otelhttp.NewTransport(trans)
}
type authTransport struct {
creds *auth.Credentials
base http.RoundTripper
clientUniverseDomain string
skipUniverseDomainValidation bool
}
// getClientUniverseDomain returns the default service domain for a given Cloud
// universe, with the following precedence:
//
// 1. A non-empty option.WithUniverseDomain or similar client option.
// 2. A non-empty environment variable GOOGLE_CLOUD_UNIVERSE_DOMAIN.
// 3. The default value "googleapis.com".
//
// This is the universe domain configured for the client, which will be compared
// to the universe domain that is separately configured for the credentials.
func (t *authTransport) getClientUniverseDomain() string {
if t.clientUniverseDomain != "" {
return t.clientUniverseDomain
}
if envUD := os.Getenv(internal.UniverseDomainEnvVar); envUD != "" {
return envUD
}
return internal.DefaultUniverseDomain
}
// RoundTrip authorizes and authenticates the request with an
// access token from Transport's Source. Per the RoundTripper contract we must
// not modify the initial request, so we clone it, and we must close the body
// on any errors that happens during our token logic.
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
reqBodyClosed := false
if req.Body != nil {
defer func() {
if !reqBodyClosed {
req.Body.Close()
}
}()
}
token, err := t.creds.Token(req.Context())
if err != nil {
return nil, err
}
if !t.skipUniverseDomainValidation && token.MetadataString("auth.google.tokenSource") != "compute-metadata" {
credentialsUniverseDomain, err := t.creds.UniverseDomain(req.Context())
if err != nil {
return nil, err
}
if err := transport.ValidateUniverseDomain(t.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
return nil, err
}
}
req2 := req.Clone(req.Context())
SetAuthHeader(token, req2)
reqBodyClosed = true
return t.base.RoundTrip(req2)
}

View File

@ -1,107 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package credsfile is meant to hide implementation details from the pubic
// surface of the detect package. It should not import any other packages in
// this module. It is located under the main internal package so other
// sub-packages can use these parsed types as well.
package credsfile
import (
"os"
"os/user"
"path/filepath"
"runtime"
)
const (
// GoogleAppCredsEnvVar is the environment variable for setting the
// application default credentials.
GoogleAppCredsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
userCredsFilename = "application_default_credentials.json"
)
// CredentialType represents different credential filetypes Google credentials
// can be.
type CredentialType int
const (
// UnknownCredType is an unidentified file type.
UnknownCredType CredentialType = iota
// UserCredentialsKey represents a user creds file type.
UserCredentialsKey
// ServiceAccountKey represents a service account file type.
ServiceAccountKey
// ImpersonatedServiceAccountKey represents a impersonated service account
// file type.
ImpersonatedServiceAccountKey
// ExternalAccountKey represents a external account file type.
ExternalAccountKey
// GDCHServiceAccountKey represents a GDCH file type.
GDCHServiceAccountKey
// ExternalAccountAuthorizedUserKey represents a external account authorized
// user file type.
ExternalAccountAuthorizedUserKey
)
// parseCredentialType returns the associated filetype based on the parsed
// typeString provided.
func parseCredentialType(typeString string) CredentialType {
switch typeString {
case "service_account":
return ServiceAccountKey
case "authorized_user":
return UserCredentialsKey
case "impersonated_service_account":
return ImpersonatedServiceAccountKey
case "external_account":
return ExternalAccountKey
case "external_account_authorized_user":
return ExternalAccountAuthorizedUserKey
case "gdch_service_account":
return GDCHServiceAccountKey
default:
return UnknownCredType
}
}
// GetFileNameFromEnv returns the override if provided or detects a filename
// from the environment.
func GetFileNameFromEnv(override string) string {
if override != "" {
return override
}
return os.Getenv(GoogleAppCredsEnvVar)
}
// GetWellKnownFileName tries to locate the filepath for the user credential
// file based on the environment.
func GetWellKnownFileName() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "gcloud", userCredsFilename)
}
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", userCredsFilename)
}
// guessUnixHomeDir default to checking for HOME, but not all unix systems have
// this set, do have a fallback.
func guessUnixHomeDir() string {
if v := os.Getenv("HOME"); v != "" {
return v
}
if u, err := user.Current(); err == nil {
return u.HomeDir
}
return ""
}

View File

@ -1,157 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credsfile
import (
"encoding/json"
)
// Config3LO is the internals of a client creds file.
type Config3LO struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURIs []string `json:"redirect_uris"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
}
// ClientCredentialsFile representation.
type ClientCredentialsFile struct {
Web *Config3LO `json:"web"`
Installed *Config3LO `json:"installed"`
UniverseDomain string `json:"universe_domain"`
}
// ServiceAccountFile representation.
type ServiceAccountFile struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}
// UserCredentialsFile representation.
type UserCredentialsFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
QuotaProjectID string `json:"quota_project_id"`
RefreshToken string `json:"refresh_token"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountFile representation.
type ExternalAccountFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Audience string `json:"audience"`
SubjectTokenType string `json:"subject_token_type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
TokenURL string `json:"token_url"`
CredentialSource *CredentialSource `json:"credential_source,omitempty"`
TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonation *ServiceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountAuthorizedUserFile representation.
type ExternalAccountAuthorizedUserFile struct {
Type string `json:"type"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RefreshToken string `json:"refresh_token"`
TokenURL string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"`
RevokeURL string `json:"revoke_url"`
QuotaProjectID string `json:"quota_project_id"`
UniverseDomain string `json:"universe_domain"`
}
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
//
// One field amongst File, URL, Certificate, and Executable should be filled, depending on the kind of credential in question.
// The EnvironmentID should start with AWS if being used for an AWS credential.
type CredentialSource struct {
File string `json:"file"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Executable *ExecutableConfig `json:"executable,omitempty"`
Certificate *CertificateConfig `json:"certificate"`
EnvironmentID string `json:"environment_id"` // TODO: Make type for this
RegionURL string `json:"region_url"`
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
CredVerificationURL string `json:"cred_verification_url"`
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
Format *Format `json:"format,omitempty"`
}
// Format describes the format of a [CredentialSource].
type Format struct {
// Type is either "text" or "json". When not provided "text" type is assumed.
Type string `json:"type"`
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
SubjectTokenFieldName string `json:"subject_token_field_name"`
}
// ExecutableConfig represents the command to run for an executable
// [CredentialSource].
type ExecutableConfig struct {
Command string `json:"command"`
TimeoutMillis int `json:"timeout_millis"`
OutputFile string `json:"output_file"`
}
// CertificateConfig represents the options used to set up X509 based workload
// [CredentialSource]
type CertificateConfig struct {
UseDefaultCertificateConfig bool `json:"use_default_certificate_config"`
CertificateConfigLocation string `json:"certificate_config_location"`
}
// ServiceAccountImpersonationInfo has impersonation configuration.
type ServiceAccountImpersonationInfo struct {
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
}
// ImpersonatedServiceAccountFile representation.
type ImpersonatedServiceAccountFile struct {
Type string `json:"type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
CredSource json.RawMessage `json:"source_credentials"`
UniverseDomain string `json:"universe_domain"`
}
// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
type GDCHServiceAccountFile struct {
Type string `json:"type"`
FormatVersion string `json:"format_version"`
Project string `json:"project"`
Name string `json:"name"`
CertPath string `json:"ca_cert_path"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}

View File

@ -1,98 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credsfile
import (
"encoding/json"
)
// ParseServiceAccount parses bytes into a [ServiceAccountFile].
func ParseServiceAccount(b []byte) (*ServiceAccountFile, error) {
var f *ServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseClientCredentials parses bytes into a
// [credsfile.ClientCredentialsFile].
func ParseClientCredentials(b []byte) (*ClientCredentialsFile, error) {
var f *ClientCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseUserCredentials parses bytes into a [UserCredentialsFile].
func ParseUserCredentials(b []byte) (*UserCredentialsFile, error) {
var f *UserCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccount parses bytes into a [ExternalAccountFile].
func ParseExternalAccount(b []byte) (*ExternalAccountFile, error) {
var f *ExternalAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccountAuthorizedUser parses bytes into a
// [ExternalAccountAuthorizedUserFile].
func ParseExternalAccountAuthorizedUser(b []byte) (*ExternalAccountAuthorizedUserFile, error) {
var f *ExternalAccountAuthorizedUserFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseImpersonatedServiceAccount parses bytes into a
// [ImpersonatedServiceAccountFile].
func ParseImpersonatedServiceAccount(b []byte) (*ImpersonatedServiceAccountFile, error) {
var f *ImpersonatedServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseGDCHServiceAccount parses bytes into a [GDCHServiceAccountFile].
func ParseGDCHServiceAccount(b []byte) (*GDCHServiceAccountFile, error) {
var f *GDCHServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
type fileTypeChecker struct {
Type string `json:"type"`
}
// ParseFileType determines the [CredentialType] based on bytes provided.
func ParseFileType(b []byte) (CredentialType, error) {
var f fileTypeChecker
if err := json.Unmarshal(b, &f); err != nil {
return 0, err
}
return parseCredentialType(f.Type), nil
}

View File

@ -1,225 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
"cloud.google.com/go/compute/metadata"
)
const (
// TokenTypeBearer is the auth header prefix for bearer tokens.
TokenTypeBearer = "Bearer"
// QuotaProjectEnvVar is the environment variable for setting the quota
// project.
QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
// UniverseDomainEnvVar is the environment variable for setting the default
// service domain for a given Cloud universe.
UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
projectEnvVar = "GOOGLE_CLOUD_PROJECT"
maxBodySize = 1 << 20
// DefaultUniverseDomain is the default value for universe domain.
// Universe domain is the default service domain for a given Cloud universe.
DefaultUniverseDomain = "googleapis.com"
)
type clonableTransport interface {
Clone() *http.Transport
}
// DefaultClient returns an [http.Client] with some defaults set. If
// the current [http.DefaultTransport] is a [clonableTransport], as
// is the case for an [*http.Transport], the clone will be used.
// Otherwise the [http.DefaultTransport] is used directly.
func DefaultClient() *http.Client {
if transport, ok := http.DefaultTransport.(clonableTransport); ok {
return &http.Client{
Transport: transport.Clone(),
Timeout: 30 * time.Second,
}
}
return &http.Client{
Transport: http.DefaultTransport,
Timeout: 30 * time.Second,
}
}
// ParseKey converts the binary contents of a private key file
// to an crypto.Signer. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (crypto.Signer, error) {
block, _ := pem.Decode(key)
if block != nil {
key = block.Bytes
}
var parsedKey crypto.PrivateKey
var err error
parsedKey, err = x509.ParsePKCS8PrivateKey(key)
if err != nil {
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
if err != nil {
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
}
}
parsed, ok := parsedKey.(crypto.Signer)
if !ok {
return nil, errors.New("private key is not a signer")
}
return parsed, nil
}
// GetQuotaProject retrieves quota project with precedence being: override,
// environment variable, creds json file.
func GetQuotaProject(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(QuotaProjectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
QuotaProject string `json:"quota_project_id"`
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
return v.QuotaProject
}
// GetProjectID retrieves project with precedence being: override,
// environment variable, creds json file.
func GetProjectID(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(projectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
ProjectID string `json:"project_id"` // standard service account key
Project string `json:"project"` // gdch key
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
if v.ProjectID != "" {
return v.ProjectID
}
return v.Project
}
// DoRequest executes the provided req with the client. It reads the response
// body, closes it, and returns it.
func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
if err != nil {
return nil, nil, err
}
return resp, body, nil
}
// ReadAll consumes the whole reader and safely reads the content of its body
// with some overflow protection.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxBodySize))
}
// StaticCredentialsProperty is a helper for creating static credentials
// properties.
func StaticCredentialsProperty(s string) StaticProperty {
return StaticProperty(s)
}
// StaticProperty always returns that value of the underlying string.
type StaticProperty string
// GetProperty loads the properly value provided the given context.
func (p StaticProperty) GetProperty(context.Context) (string, error) {
return string(p), nil
}
// ComputeUniverseDomainProvider fetches the credentials universe domain from
// the google cloud metadata service.
type ComputeUniverseDomainProvider struct {
MetadataClient *metadata.Client
universeDomainOnce sync.Once
universeDomain string
universeDomainErr error
}
// GetProperty fetches the credentials universe domain from the google cloud
// metadata service.
func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
c.universeDomainOnce.Do(func() {
c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
})
if c.universeDomainErr != nil {
return "", c.universeDomainErr
}
return c.universeDomain, nil
}
// httpGetMetadataUniverseDomain is a package var for unit test substitution.
var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
return client.GetWithContext(ctx, "universe/universe-domain")
}
func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
if err == nil {
return universeDomain, nil
}
if _, ok := err.(metadata.NotDefinedError); ok {
// http.StatusNotFound (404)
return DefaultUniverseDomain, nil
}
return "", err
}
// FormatIAMServiceAccountResource sets a service account name in an IAM resource
// name.
func FormatIAMServiceAccountResource(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}

View File

@ -1,171 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jwt
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
const (
// HeaderAlgRSA256 is the RS256 [Header.Algorithm].
HeaderAlgRSA256 = "RS256"
// HeaderAlgES256 is the ES256 [Header.Algorithm].
HeaderAlgES256 = "ES256"
// HeaderType is the standard [Header.Type].
HeaderType = "JWT"
)
// Header represents a JWT header.
type Header struct {
Algorithm string `json:"alg"`
Type string `json:"typ"`
KeyID string `json:"kid"`
}
func (h *Header) encode() (string, error) {
b, err := json.Marshal(h)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Claims represents the claims set of a JWT.
type Claims struct {
// Iss is the issuer JWT claim.
Iss string `json:"iss"`
// Scope is the scope JWT claim.
Scope string `json:"scope,omitempty"`
// Exp is the expiry JWT claim. If unset, default is in one hour from now.
Exp int64 `json:"exp"`
// Iat is the subject issued at claim. If unset, default is now.
Iat int64 `json:"iat"`
// Aud is the audience JWT claim. Optional.
Aud string `json:"aud"`
// Sub is the subject JWT claim. Optional.
Sub string `json:"sub,omitempty"`
// AdditionalClaims contains any additional non-standard JWT claims. Optional.
AdditionalClaims map[string]interface{} `json:"-"`
}
func (c *Claims) encode() (string, error) {
// Compensate for skew
now := time.Now().Add(-10 * time.Second)
if c.Iat == 0 {
c.Iat = now.Unix()
}
if c.Exp == 0 {
c.Exp = now.Add(time.Hour).Unix()
}
if c.Exp < c.Iat {
return "", fmt.Errorf("jwt: invalid Exp = %d; must be later than Iat = %d", c.Exp, c.Iat)
}
b, err := json.Marshal(c)
if err != nil {
return "", err
}
if len(c.AdditionalClaims) == 0 {
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Marshal private claim set and then append it to b.
prv, err := json.Marshal(c.AdditionalClaims)
if err != nil {
return "", fmt.Errorf("invalid map of additional claims %v: %w", c.AdditionalClaims, err)
}
// Concatenate public and private claim JSON objects.
if !bytes.HasSuffix(b, []byte{'}'}) {
return "", fmt.Errorf("invalid JSON %s", b)
}
if !bytes.HasPrefix(prv, []byte{'{'}) {
return "", fmt.Errorf("invalid JSON %s", prv)
}
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
b = append(b, prv[1:]...) // Append private claims.
return base64.RawURLEncoding.EncodeToString(b), nil
}
// EncodeJWS encodes the data using the provided key as a JSON web signature.
func EncodeJWS(header *Header, c *Claims, signer crypto.Signer) (string, error) {
head, err := header.encode()
if err != nil {
return "", err
}
claims, err := c.encode()
if err != nil {
return "", err
}
ss := fmt.Sprintf("%s.%s", head, claims)
h := sha256.New()
h.Write([]byte(ss))
sig, err := signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA256)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
}
// DecodeJWS decodes a claim set from a JWS payload.
func DecodeJWS(payload string) (*Claims, error) {
// decode returned id token to get expiry
s := strings.Split(payload, ".")
if len(s) < 2 {
return nil, errors.New("invalid token received")
}
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return nil, err
}
c := &Claims{}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c); err != nil {
return nil, err
}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&c.AdditionalClaims); err != nil {
return nil, err
}
return c, err
}
// VerifyJWS tests whether the provided JWT token's signature was produced by
// the private key associated with the provided public key.
func VerifyJWS(token string, key *rsa.PublicKey) error {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return errors.New("jwt: invalid token received, token must have 3 parts")
}
signedContent := parts[0] + "." + parts[1]
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return err
}
h := sha256.New()
h.Write([]byte(signedContent))
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
}

View File

@ -1,368 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"log"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport/cert"
"github.com/google/s2a-go"
"github.com/google/s2a-go/fallback"
"google.golang.org/grpc/credentials"
)
const (
mTLSModeAlways = "always"
mTLSModeNever = "never"
mTLSModeAuto = "auto"
// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
googleAPIUseCertSource = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
googleAPIUseMTLS = "GOOGLE_API_USE_MTLS_ENDPOINT"
googleAPIUseMTLSOld = "GOOGLE_API_USE_MTLS"
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
mtlsMDSRoot = "/run/google-mds-mtls/root.crt"
mtlsMDSKey = "/run/google-mds-mtls/client.key"
)
// Options is a struct that is duplicated information from the individual
// transport packages in order to avoid cyclic deps. It correlates 1:1 with
// fields on httptransport.Options and grpctransport.Options.
type Options struct {
Endpoint string
DefaultEndpointTemplate string
DefaultMTLSEndpoint string
ClientCertProvider cert.Provider
Client *http.Client
UniverseDomain string
EnableDirectPath bool
EnableDirectPathXds bool
Logger *slog.Logger
}
// getUniverseDomain returns the default service domain for a given Cloud
// universe.
func (o *Options) getUniverseDomain() string {
if o.UniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return o.UniverseDomain
}
// isUniverseDomainGDU returns true if the universe domain is the default Google
// universe.
func (o *Options) isUniverseDomainGDU() bool {
return o.getUniverseDomain() == internal.DefaultUniverseDomain
}
// defaultEndpoint returns the DefaultEndpointTemplate merged with the
// universe domain if the DefaultEndpointTemplate is set, otherwise returns an
// empty string.
func (o *Options) defaultEndpoint() string {
if o.DefaultEndpointTemplate == "" {
return ""
}
return strings.Replace(o.DefaultEndpointTemplate, universeDomainPlaceholder, o.getUniverseDomain(), 1)
}
// defaultMTLSEndpoint returns the DefaultMTLSEndpointTemplate merged with the
// universe domain if the DefaultMTLSEndpointTemplate is set, otherwise returns an
// empty string.
func (o *Options) defaultMTLSEndpoint() string {
if o.DefaultMTLSEndpoint == "" {
return ""
}
return strings.Replace(o.DefaultMTLSEndpoint, universeDomainPlaceholder, o.getUniverseDomain(), 1)
}
// mergedEndpoint merges a user-provided Endpoint of format host[:port] with the
// default endpoint.
func (o *Options) mergedEndpoint() (string, error) {
defaultEndpoint := o.defaultEndpoint()
u, err := url.Parse(fixScheme(defaultEndpoint))
if err != nil {
return "", err
}
return strings.Replace(defaultEndpoint, u.Host, o.Endpoint, 1), nil
}
func fixScheme(baseURL string) string {
if !strings.Contains(baseURL, "://") {
baseURL = "https://" + baseURL
}
return baseURL
}
// GetGRPCTransportCredsAndEndpoint returns an instance of
// [google.golang.org/grpc/credentials.TransportCredentials], and the
// corresponding endpoint to use for GRPC client.
func GetGRPCTransportCredsAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, "", err
}
defaultTransportCreds := credentials.NewTLS(&tls.Config{
GetClientCertificate: config.clientCertSource,
})
var s2aAddr string
var transportCredsForS2A credentials.TransportCredentials
if config.mtlsS2AAddress != "" {
s2aAddr = config.mtlsS2AAddress
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
if err != nil {
log.Printf("Loading MTLS MDS credentials failed: %v", err)
if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return defaultTransportCreds, config.endpoint, nil
}
}
} else if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return defaultTransportCreds, config.endpoint, nil
}
var fallbackOpts *s2a.FallbackOptions
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
if fallbackHandshake, err := fallback.DefaultFallbackClientHandshakeFunc(config.endpoint); err == nil {
fallbackOpts = &s2a.FallbackOptions{
FallbackClientHandshakeFunc: fallbackHandshake,
}
}
s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
S2AAddress: s2aAddr,
TransportCreds: transportCredsForS2A,
FallbackOpts: fallbackOpts,
})
if err != nil {
// Use default if we cannot initialize S2A client transport credentials.
return defaultTransportCreds, config.endpoint, nil
}
return s2aTransportCreds, config.s2aMTLSEndpoint, nil
}
// GetHTTPTransportConfig returns a client certificate source and a function for
// dialing MTLS with S2A.
func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, nil, err
}
var s2aAddr string
var transportCredsForS2A credentials.TransportCredentials
if config.mtlsS2AAddress != "" {
s2aAddr = config.mtlsS2AAddress
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
if err != nil {
log.Printf("Loading MTLS MDS credentials failed: %v", err)
if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return config.clientCertSource, nil, nil
}
}
} else if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return config.clientCertSource, nil, nil
}
var fallbackOpts *s2a.FallbackOptions
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
if fallbackURL, err := url.Parse(config.endpoint); err == nil {
if fallbackDialer, fallbackServerAddr, err := fallback.DefaultFallbackDialerAndAddress(fallbackURL.Hostname()); err == nil {
fallbackOpts = &s2a.FallbackOptions{
FallbackDialer: &s2a.FallbackDialer{
Dialer: fallbackDialer,
ServerAddr: fallbackServerAddr,
},
}
}
}
dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
S2AAddress: s2aAddr,
TransportCreds: transportCredsForS2A,
FallbackOpts: fallbackOpts,
})
return nil, dialTLSContextFunc, nil
}
func loadMTLSMDSTransportCreds(mtlsMDSRootFile, mtlsMDSKeyFile string) (credentials.TransportCredentials, error) {
rootPEM, err := os.ReadFile(mtlsMDSRootFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(rootPEM)
if !ok {
return nil, errors.New("failed to load MTLS MDS root certificate")
}
// The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
// followed by a PEM-encoded private key. For this reason, the concatenation is passed in to the
// tls.X509KeyPair function as both the certificate chain and private key arguments.
cert, err := tls.LoadX509KeyPair(mtlsMDSKeyFile, mtlsMDSKeyFile)
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
return credentials.NewTLS(&tlsConfig), nil
}
func getTransportConfig(opts *Options) (*transportConfig, error) {
clientCertSource, err := GetClientCertificateProvider(opts)
if err != nil {
return nil, err
}
endpoint, err := getEndpoint(opts, clientCertSource)
if err != nil {
return nil, err
}
defaultTransportConfig := transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
}
if !shouldUseS2A(clientCertSource, opts) {
return &defaultTransportConfig, nil
}
s2aAddress := GetS2AAddress(opts.Logger)
mtlsS2AAddress := GetMTLSS2AAddress(opts.Logger)
if s2aAddress == "" && mtlsS2AAddress == "" {
return &defaultTransportConfig, nil
}
return &transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
s2aAddress: s2aAddress,
mtlsS2AAddress: mtlsS2AAddress,
s2aMTLSEndpoint: opts.defaultMTLSEndpoint(),
}, nil
}
// GetClientCertificateProvider returns a default client certificate source, if
// not provided by the user.
//
// A nil default source can be returned if the source does not exist. Any exceptions
// encountered while initializing the default source will be reported as client
// error (ex. corrupt metadata file).
func GetClientCertificateProvider(opts *Options) (cert.Provider, error) {
if !isClientCertificateEnabled(opts) {
return nil, nil
} else if opts.ClientCertProvider != nil {
return opts.ClientCertProvider, nil
}
return cert.DefaultProvider()
}
// isClientCertificateEnabled returns true by default for all GDU universe domain, unless explicitly overridden by env var
func isClientCertificateEnabled(opts *Options) bool {
if value, ok := os.LookupEnv(googleAPIUseCertSource); ok {
// error as false is OK
b, _ := strconv.ParseBool(value)
return b
}
return opts.isUniverseDomainGDU()
}
type transportConfig struct {
// The client certificate source.
clientCertSource cert.Provider
// The corresponding endpoint to use based on client certificate source.
endpoint string
// The plaintext S2A address if it can be used, otherwise an empty string.
s2aAddress string
// The MTLS S2A address if it can be used, otherwise an empty string.
mtlsS2AAddress string
// The MTLS endpoint to use with S2A.
s2aMTLSEndpoint string
}
// getEndpoint returns the endpoint for the service, taking into account the
// user-provided endpoint override "settings.Endpoint".
//
// If no endpoint override is specified, we will either return the default
// endpoint or the default mTLS endpoint if a client certificate is available.
//
// You can override the default endpoint choice (mTLS vs. regular) by setting
// the GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
//
// If the endpoint override is an address (host:port) rather than full base
// URL (ex. https://...), then the user-provided address will be merged into
// the default endpoint. For example, WithEndpoint("myhost:8000") and
// DefaultEndpointTemplate("https://UNIVERSE_DOMAIN/bar/baz") will return
// "https://myhost:8080/bar/baz". Note that this does not apply to the mTLS
// endpoint.
func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
if opts.Endpoint == "" {
mtlsMode := getMTLSMode()
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
return opts.defaultMTLSEndpoint(), nil
}
return opts.defaultEndpoint(), nil
}
if strings.Contains(opts.Endpoint, "://") {
// User passed in a full URL path, use it verbatim.
return opts.Endpoint, nil
}
if opts.defaultEndpoint() == "" {
// If DefaultEndpointTemplate is not configured,
// use the user provided endpoint verbatim. This allows a naked
// "host[:port]" URL to be used with GRPC Direct Path.
return opts.Endpoint, nil
}
// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
return opts.mergedEndpoint()
}
func getMTLSMode() string {
mode := os.Getenv(googleAPIUseMTLS)
if mode == "" {
mode = os.Getenv(googleAPIUseMTLSOld) // Deprecated.
}
if mode == "" {
return mTLSModeAuto
}
return strings.ToLower(mode)
}

View File

@ -1,65 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"errors"
"sync"
)
// defaultCertData holds all the variables pertaining to
// the default certificate provider created by [DefaultProvider].
//
// A singleton model is used to allow the provider to be reused
// by the transport layer. As mentioned in [DefaultProvider] (provider nil, nil)
// may be returned to indicate a default provider could not be found, which
// will skip extra tls config in the transport layer .
type defaultCertData struct {
once sync.Once
provider Provider
err error
}
var (
defaultCert defaultCertData
)
// Provider is a function that can be passed into crypto/tls.Config.GetClientCertificate.
type Provider func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable.
var errSourceUnavailable = errors.New("certificate source is unavailable")
// DefaultProvider returns a certificate source using the preferred EnterpriseCertificateProxySource.
// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource.
//
// If neither source is available (due to missing configurations), a nil Source and a nil Error are
// returned to indicate that a default certificate source is unavailable.
func DefaultProvider() (Provider, error) {
defaultCert.once.Do(func() {
defaultCert.provider, defaultCert.err = NewWorkloadX509CertProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = NewEnterpriseCertificateProxyProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = NewSecureConnectProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = nil, nil
}
}
}
})
return defaultCert.provider, defaultCert.err
}

View File

@ -1,54 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"github.com/googleapis/enterprise-certificate-proxy/client"
)
type ecpSource struct {
key *client.Key
}
// NewEnterpriseCertificateProxyProvider creates a certificate source
// using the Enterprise Certificate Proxy client, which delegates
// certifcate related operations to an OS-specific "signer binary"
// that communicates with the native keystore (ex. keychain on MacOS).
//
// The configFilePath points to a config file containing relevant parameters
// such as the certificate issuer and the location of the signer binary.
// If configFilePath is empty, the client will attempt to load the config from
// a well-known gcloud location.
func NewEnterpriseCertificateProxyProvider(configFilePath string) (Provider, error) {
key, err := client.Cred(configFilePath)
if err != nil {
// TODO(codyoss): once this is fixed upstream can handle this error a
// little better here. But be safe for now and assume unavailable.
return nil, errSourceUnavailable
}
return (&ecpSource{
key: key,
}).getClientCertificate, nil
}
func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
var cert tls.Certificate
cert.PrivateKey = s.key
cert.Certificate = s.key.CertificateChain()
return &cert, nil
}

View File

@ -1,124 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"sync"
"time"
)
const (
metadataPath = ".secureConnect"
metadataFile = "context_aware_metadata.json"
)
type secureConnectSource struct {
metadata secureConnectMetadata
// Cache the cert to avoid executing helper command repeatedly.
cachedCertMutex sync.Mutex
cachedCert *tls.Certificate
}
type secureConnectMetadata struct {
Cmd []string `json:"cert_provider_command"`
}
// NewSecureConnectProvider creates a certificate source using
// the Secure Connect Helper and its associated metadata file.
//
// The configFilePath points to the location of the context aware metadata file.
// If configFilePath is empty, use the default context aware metadata location.
func NewSecureConnectProvider(configFilePath string) (Provider, error) {
if configFilePath == "" {
user, err := user.Current()
if err != nil {
// Error locating the default config means Secure Connect is not supported.
return nil, errSourceUnavailable
}
configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
}
file, err := os.ReadFile(configFilePath)
if err != nil {
// Config file missing means Secure Connect is not supported.
// There are non-os.ErrNotExist errors that may be returned.
// (e.g. if the home directory is /dev/null, *nix systems will
// return ENOTDIR instead of ENOENT)
return nil, errSourceUnavailable
}
var metadata secureConnectMetadata
if err := json.Unmarshal(file, &metadata); err != nil {
return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
}
if err := validateMetadata(metadata); err != nil {
return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
}
return (&secureConnectSource{
metadata: metadata,
}).getClientCertificate, nil
}
func validateMetadata(metadata secureConnectMetadata) error {
if len(metadata.Cmd) == 0 {
return errors.New("empty cert_provider_command")
}
return nil
}
func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
s.cachedCertMutex.Lock()
defer s.cachedCertMutex.Unlock()
if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
return s.cachedCert, nil
}
// Expand OS environment variables in the cert provider command such as "$HOME".
for i := 0; i < len(s.metadata.Cmd); i++ {
s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
}
command := s.metadata.Cmd
data, err := exec.Command(command[0], command[1:]...).Output()
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(data, data)
if err != nil {
return nil, err
}
s.cachedCert = &cert
return &cert, nil
}
// isCertificateExpired returns true if the given cert is expired or invalid.
func isCertificateExpired(cert *tls.Certificate) bool {
if len(cert.Certificate) == 0 {
return true
}
parsed, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return true
}
return time.Now().After(parsed.NotAfter)
}

View File

@ -1,114 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"encoding/json"
"errors"
"io"
"os"
"github.com/googleapis/enterprise-certificate-proxy/client/util"
)
type certConfigs struct {
Workload *workloadSource `json:"workload"`
}
type workloadSource struct {
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
}
type certificateConfig struct {
CertConfigs certConfigs `json:"cert_configs"`
}
// NewWorkloadX509CertProvider creates a certificate source
// that reads a certificate and private key file from the local file system.
// This is intended to be used for workload identity federation.
//
// The configFilePath points to a config file containing relevant parameters
// such as the certificate and key file paths.
// If configFilePath is empty, the client will attempt to load the config from
// a well-known gcloud location.
func NewWorkloadX509CertProvider(configFilePath string) (Provider, error) {
if configFilePath == "" {
envFilePath := util.GetConfigFilePathFromEnv()
if envFilePath != "" {
configFilePath = envFilePath
} else {
configFilePath = util.GetDefaultConfigFilePath()
}
}
certFile, keyFile, err := getCertAndKeyFiles(configFilePath)
if err != nil {
return nil, err
}
source := &workloadSource{
CertPath: certFile,
KeyPath: keyFile,
}
return source.getClientCertificate, nil
}
// getClientCertificate attempts to load the certificate and key from the files specified in the
// certificate config.
func (s *workloadSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(s.CertPath, s.KeyPath)
if err != nil {
return nil, err
}
return &cert, nil
}
// getCertAndKeyFiles attempts to read the provided config file and return the certificate and private
// key file paths.
func getCertAndKeyFiles(configFilePath string) (string, string, error) {
jsonFile, err := os.Open(configFilePath)
if err != nil {
return "", "", errSourceUnavailable
}
byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return "", "", err
}
var config certificateConfig
if err := json.Unmarshal(byteValue, &config); err != nil {
return "", "", err
}
if config.CertConfigs.Workload == nil {
return "", "", errSourceUnavailable
}
certFile := config.CertConfigs.Workload.CertPath
keyFile := config.CertConfigs.Workload.KeyPath
if certFile == "" {
return "", "", errors.New("certificate configuration is missing the certificate file location")
}
if keyFile == "" {
return "", "", errors.New("certificate configuration is missing the key file location")
}
return certFile, keyFile, nil
}

View File

@ -1,138 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"os"
"strconv"
"sync"
"cloud.google.com/go/auth/internal/transport/cert"
"cloud.google.com/go/compute/metadata"
)
const (
configEndpointSuffix = "instance/platform-security/auto-mtls-configuration"
)
var (
mtlsConfiguration *mtlsConfig
mtlsOnce sync.Once
)
// GetS2AAddress returns the S2A address to be reached via plaintext connection.
// Returns empty string if not set or invalid.
func GetS2AAddress(logger *slog.Logger) string {
getMetadataMTLSAutoConfig(logger)
if !mtlsConfiguration.valid() {
return ""
}
return mtlsConfiguration.S2A.PlaintextAddress
}
// GetMTLSS2AAddress returns the S2A address to be reached via MTLS connection.
// Returns empty string if not set or invalid.
func GetMTLSS2AAddress(logger *slog.Logger) string {
getMetadataMTLSAutoConfig(logger)
if !mtlsConfiguration.valid() {
return ""
}
return mtlsConfiguration.S2A.MTLSAddress
}
// mtlsConfig contains the configuration for establishing MTLS connections with Google APIs.
type mtlsConfig struct {
S2A *s2aAddresses `json:"s2a"`
}
func (c *mtlsConfig) valid() bool {
return c != nil && c.S2A != nil
}
// s2aAddresses contains the plaintext and/or MTLS S2A addresses.
type s2aAddresses struct {
// PlaintextAddress is the plaintext address to reach S2A
PlaintextAddress string `json:"plaintext_address"`
// MTLSAddress is the MTLS address to reach S2A
MTLSAddress string `json:"mtls_address"`
}
func getMetadataMTLSAutoConfig(logger *slog.Logger) {
var err error
mtlsOnce.Do(func() {
mtlsConfiguration, err = queryConfig(logger)
if err != nil {
log.Printf("Getting MTLS config failed: %v", err)
}
})
}
var httpGetMetadataMTLSConfig = func(logger *slog.Logger) (string, error) {
metadataClient := metadata.NewWithOptions(&metadata.Options{
Logger: logger,
})
return metadataClient.GetWithContext(context.Background(), configEndpointSuffix)
}
func queryConfig(logger *slog.Logger) (*mtlsConfig, error) {
resp, err := httpGetMetadataMTLSConfig(logger)
if err != nil {
return nil, fmt.Errorf("querying MTLS config from MDS endpoint failed: %w", err)
}
var config mtlsConfig
err = json.Unmarshal([]byte(resp), &config)
if err != nil {
return nil, fmt.Errorf("unmarshalling MTLS config from MDS endpoint failed: %w", err)
}
if config.S2A == nil {
return nil, fmt.Errorf("returned MTLS config from MDS endpoint is invalid: %v", config)
}
return &config, nil
}
func shouldUseS2A(clientCertSource cert.Provider, opts *Options) bool {
// If client cert is found, use that over S2A.
if clientCertSource != nil {
return false
}
// If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A.
if !isGoogleS2AEnabled() {
return false
}
// If DefaultMTLSEndpoint is not set or has endpoint override, skip S2A.
if opts.DefaultMTLSEndpoint == "" || opts.Endpoint != "" {
return false
}
// If custom HTTP client is provided, skip S2A.
if opts.Client != nil {
return false
}
// If directPath is enabled, skip S2A.
return !opts.EnableDirectPath && !opts.EnableDirectPathXds
}
func isGoogleS2AEnabled() bool {
b, err := strconv.ParseBool(os.Getenv(googleAPIUseS2AEnv))
if err != nil {
return false
}
return b
}

View File

@ -1,106 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package transport provided internal helpers for the two transport packages
// (grpctransport and httptransport).
package transport
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
"cloud.google.com/go/auth/credentials"
)
// CloneDetectOptions clones a user set detect option into some new memory that
// we can internally manipulate before sending onto the detect package.
func CloneDetectOptions(oldDo *credentials.DetectOptions) *credentials.DetectOptions {
if oldDo == nil {
// it is valid for users not to set this, but we will need to to default
// some options for them in this case so return some initialized memory
// to work with.
return &credentials.DetectOptions{}
}
newDo := &credentials.DetectOptions{
// Simple types
Audience: oldDo.Audience,
Subject: oldDo.Subject,
EarlyTokenRefresh: oldDo.EarlyTokenRefresh,
TokenURL: oldDo.TokenURL,
STSAudience: oldDo.STSAudience,
CredentialsFile: oldDo.CredentialsFile,
UseSelfSignedJWT: oldDo.UseSelfSignedJWT,
UniverseDomain: oldDo.UniverseDomain,
// These fields are are pointer types that we just want to use exactly
// as the user set, copy the ref
Client: oldDo.Client,
Logger: oldDo.Logger,
AuthHandlerOptions: oldDo.AuthHandlerOptions,
}
// Smartly size this memory and copy below.
if len(oldDo.CredentialsJSON) > 0 {
newDo.CredentialsJSON = make([]byte, len(oldDo.CredentialsJSON))
copy(newDo.CredentialsJSON, oldDo.CredentialsJSON)
}
if len(oldDo.Scopes) > 0 {
newDo.Scopes = make([]string, len(oldDo.Scopes))
copy(newDo.Scopes, oldDo.Scopes)
}
return newDo
}
// ValidateUniverseDomain verifies that the universe domain configured for the
// client matches the universe domain configured for the credentials.
func ValidateUniverseDomain(clientUniverseDomain, credentialsUniverseDomain string) error {
if clientUniverseDomain != credentialsUniverseDomain {
return fmt.Errorf(
"the configured universe domain (%q) does not match the universe "+
"domain found in the credentials (%q). If you haven't configured "+
"the universe domain explicitly, \"googleapis.com\" is the default",
clientUniverseDomain,
credentialsUniverseDomain)
}
return nil
}
// DefaultHTTPClientWithTLS constructs an HTTPClient using the provided tlsConfig, to support mTLS.
func DefaultHTTPClientWithTLS(tlsConfig *tls.Config) *http.Client {
trans := BaseTransport()
trans.TLSClientConfig = tlsConfig
return &http.Client{Transport: trans}
}
// BaseTransport returns a default [http.Transport] which can be used if
// [http.DefaultTransport] has been overwritten.
func BaseTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}

View File

@ -1,75 +0,0 @@
# Changelog
## [0.2.7](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.6...auth/oauth2adapt/v0.2.7) (2025-01-09)
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.33.0 ([e9b0b69](https://github.com/googleapis/google-cloud-go/commit/e9b0b69644ea5b276cacff0a707e8a5e87efafc9))
## [0.2.6](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.5...auth/oauth2adapt/v0.2.6) (2024-11-21)
### Bug Fixes
* **auth/oauth2adapt:** Copy map in tokenSourceAdapter.Token ([#11164](https://github.com/googleapis/google-cloud-go/issues/11164)) ([8cb0cbc](https://github.com/googleapis/google-cloud-go/commit/8cb0cbccdc32886dfb3af49fee04012937d114d2)), refs [#11161](https://github.com/googleapis/google-cloud-go/issues/11161)
## [0.2.5](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.4...auth/oauth2adapt/v0.2.5) (2024-10-30)
### Bug Fixes
* **auth/oauth2adapt:** Convert token metadata where possible ([#11062](https://github.com/googleapis/google-cloud-go/issues/11062)) ([34bf1c1](https://github.com/googleapis/google-cloud-go/commit/34bf1c164465d66745c0cfdf7cd10a8e2da92e52))
## [0.2.4](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.3...auth/oauth2adapt/v0.2.4) (2024-08-08)
### Bug Fixes
* **auth/oauth2adapt:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.2...auth/oauth2adapt/v0.2.3) (2024-07-10)
### Bug Fixes
* **auth/oauth2adapt:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.1...auth/oauth2adapt/v0.2.2) (2024-04-23)
### Bug Fixes
* **auth/oauth2adapt:** Bump x/net to v0.24.0 ([ba31ed5](https://github.com/googleapis/google-cloud-go/commit/ba31ed5fda2c9664f2e1cf972469295e63deb5b4))
## [0.2.1](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.0...auth/oauth2adapt/v0.2.1) (2024-04-18)
### Bug Fixes
* **auth/oauth2adapt:** Adapt Token Types to be translated ([#9801](https://github.com/googleapis/google-cloud-go/issues/9801)) ([70f4115](https://github.com/googleapis/google-cloud-go/commit/70f411555ebbf2b71e6d425cc8d2030644c6b438)), refs [#9800](https://github.com/googleapis/google-cloud-go/issues/9800)
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.1.0...auth/oauth2adapt/v0.2.0) (2024-04-16)
### Features
* **auth/oauth2adapt:** Add helpers for working with credentials types ([#9694](https://github.com/googleapis/google-cloud-go/issues/9694)) ([cf33b55](https://github.com/googleapis/google-cloud-go/commit/cf33b5514423a2ac5c2a323a1cd99aac34fd4233))
### Bug Fixes
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
## 0.1.0 (2023-10-19)
### Features
* **auth/oauth2adapt:** Adds a new module to translate types ([#8595](https://github.com/googleapis/google-cloud-go/issues/8595)) ([6933c5a](https://github.com/googleapis/google-cloud-go/commit/6933c5a0c1fc8e58cbfff8bbca439d671b94672f))
* **auth/oauth2adapt:** Fixup deps for release ([#8747](https://github.com/googleapis/google-cloud-go/issues/8747)) ([749d243](https://github.com/googleapis/google-cloud-go/commit/749d243862b025a6487a4d2d339219889b4cfe70))
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,200 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package oauth2adapt helps converts types used in [cloud.google.com/go/auth]
// and [golang.org/x/oauth2].
package oauth2adapt
import (
"context"
"encoding/json"
"errors"
"cloud.google.com/go/auth"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
oauth2TokenSourceKey = "oauth2.google.tokenSource"
oauth2ServiceAccountKey = "oauth2.google.serviceAccount"
authTokenSourceKey = "auth.google.tokenSource"
authServiceAccountKey = "auth.google.serviceAccount"
)
// TokenProviderFromTokenSource converts any [golang.org/x/oauth2.TokenSource]
// into a [cloud.google.com/go/auth.TokenProvider].
func TokenProviderFromTokenSource(ts oauth2.TokenSource) auth.TokenProvider {
return &tokenProviderAdapter{ts: ts}
}
type tokenProviderAdapter struct {
ts oauth2.TokenSource
}
// Token fulfills the [cloud.google.com/go/auth.TokenProvider] interface. It
// is a light wrapper around the underlying TokenSource.
func (tp *tokenProviderAdapter) Token(context.Context) (*auth.Token, error) {
tok, err := tp.ts.Token()
if err != nil {
var err2 *oauth2.RetrieveError
if ok := errors.As(err, &err2); ok {
return nil, AuthErrorFromRetrieveError(err2)
}
return nil, err
}
// Preserve compute token metadata, for both types of tokens.
metadata := map[string]interface{}{}
if val, ok := tok.Extra(oauth2TokenSourceKey).(string); ok {
metadata[authTokenSourceKey] = val
metadata[oauth2TokenSourceKey] = val
}
if val, ok := tok.Extra(oauth2ServiceAccountKey).(string); ok {
metadata[authServiceAccountKey] = val
metadata[oauth2ServiceAccountKey] = val
}
return &auth.Token{
Value: tok.AccessToken,
Type: tok.Type(),
Expiry: tok.Expiry,
Metadata: metadata,
}, nil
}
// TokenSourceFromTokenProvider converts any
// [cloud.google.com/go/auth.TokenProvider] into a
// [golang.org/x/oauth2.TokenSource].
func TokenSourceFromTokenProvider(tp auth.TokenProvider) oauth2.TokenSource {
return &tokenSourceAdapter{tp: tp}
}
type tokenSourceAdapter struct {
tp auth.TokenProvider
}
// Token fulfills the [golang.org/x/oauth2.TokenSource] interface. It
// is a light wrapper around the underlying TokenProvider.
func (ts *tokenSourceAdapter) Token() (*oauth2.Token, error) {
tok, err := ts.tp.Token(context.Background())
if err != nil {
var err2 *auth.Error
if ok := errors.As(err, &err2); ok {
return nil, AddRetrieveErrorToAuthError(err2)
}
return nil, err
}
tok2 := &oauth2.Token{
AccessToken: tok.Value,
TokenType: tok.Type,
Expiry: tok.Expiry,
}
// Preserve token metadata.
m := tok.Metadata
if m != nil {
// Copy map to avoid concurrent map writes error (#11161).
metadata := make(map[string]interface{}, len(m)+2)
for k, v := range m {
metadata[k] = v
}
// Append compute token metadata in converted form.
if val, ok := metadata[authTokenSourceKey].(string); ok && val != "" {
metadata[oauth2TokenSourceKey] = val
}
if val, ok := metadata[authServiceAccountKey].(string); ok && val != "" {
metadata[oauth2ServiceAccountKey] = val
}
tok2 = tok2.WithExtra(metadata)
}
return tok2, nil
}
// AuthCredentialsFromOauth2Credentials converts a [golang.org/x/oauth2/google.Credentials]
// to a [cloud.google.com/go/auth.Credentials].
func AuthCredentialsFromOauth2Credentials(creds *google.Credentials) *auth.Credentials {
if creds == nil {
return nil
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: TokenProviderFromTokenSource(creds.TokenSource),
JSON: creds.JSON,
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return creds.ProjectID, nil
}),
UniverseDomainProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return creds.GetUniverseDomain()
}),
})
}
// Oauth2CredentialsFromAuthCredentials converts a [cloud.google.com/go/auth.Credentials]
// to a [golang.org/x/oauth2/google.Credentials].
func Oauth2CredentialsFromAuthCredentials(creds *auth.Credentials) *google.Credentials {
if creds == nil {
return nil
}
// Throw away errors as old credentials are not request aware. Also, no
// network requests are currently happening for this use case.
projectID, _ := creds.ProjectID(context.Background())
return &google.Credentials{
TokenSource: TokenSourceFromTokenProvider(creds.TokenProvider),
ProjectID: projectID,
JSON: creds.JSON(),
UniverseDomainProvider: func() (string, error) {
return creds.UniverseDomain(context.Background())
},
}
}
type oauth2Error struct {
ErrorCode string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
}
// AddRetrieveErrorToAuthError returns the same error provided and adds a
// [golang.org/x/oauth2.RetrieveError] to the error chain by setting the `Err` field on the
// [cloud.google.com/go/auth.Error].
func AddRetrieveErrorToAuthError(err *auth.Error) *auth.Error {
if err == nil {
return nil
}
e := &oauth2.RetrieveError{
Response: err.Response,
Body: err.Body,
}
err.Err = e
if len(err.Body) > 0 {
var oErr oauth2Error
// ignore the error as it only fills in extra details
json.Unmarshal(err.Body, &oErr)
e.ErrorCode = oErr.ErrorCode
e.ErrorDescription = oErr.ErrorDescription
e.ErrorURI = oErr.ErrorURI
}
return err
}
// AuthErrorFromRetrieveError returns an [cloud.google.com/go/auth.Error] that
// wraps the provided [golang.org/x/oauth2.RetrieveError].
func AuthErrorFromRetrieveError(err *oauth2.RetrieveError) *auth.Error {
if err == nil {
return nil
}
return &auth.Error{
Response: err.Response,
Body: err.Body,
Err: err,
}
}

View File

@ -1,382 +0,0 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
// AuthorizationHandler is a 3-legged-OAuth helper that prompts the user for
// OAuth consent at the specified auth code URL and returns an auth code and
// state upon approval.
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
// Options3LO are the options for doing a 3-legged OAuth2 flow.
type Options3LO struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret. Not required if AuthHandlerOpts
// is set.
ClientSecret string
// AuthURL is the URL for authenticating.
AuthURL string
// TokenURL is the URL for retrieving a token.
TokenURL string
// AuthStyle is used to describe how to client info in the token request.
AuthStyle Style
// RefreshToken is the token used to refresh the credential. Not required
// if AuthHandlerOpts is set.
RefreshToken string
// RedirectURL is the URL to redirect users to. Optional.
RedirectURL string
// Scopes specifies requested permissions for the Token. Optional.
Scopes []string
// URLParams are the set of values to apply to the token exchange. Optional.
URLParams url.Values
// Client is the client to be used to make the underlying token requests.
// Optional.
Client *http.Client
// EarlyTokenExpiry is the time before the token expires that it should be
// refreshed. If not set the default value is 3 minutes and 45 seconds.
// Optional.
EarlyTokenExpiry time.Duration
// AuthHandlerOpts provides a set of options for doing a
// 3-legged OAuth2 flow with a custom [AuthorizationHandler]. Optional.
AuthHandlerOpts *AuthorizationHandlerOptions
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *Options3LO) validate() error {
if o == nil {
return errors.New("auth: options must be provided")
}
if o.ClientID == "" {
return errors.New("auth: client ID must be provided")
}
if o.AuthHandlerOpts == nil && o.ClientSecret == "" {
return errors.New("auth: client secret must be provided")
}
if o.AuthURL == "" {
return errors.New("auth: auth URL must be provided")
}
if o.TokenURL == "" {
return errors.New("auth: token URL must be provided")
}
if o.AuthStyle == StyleUnknown {
return errors.New("auth: auth style must be provided")
}
if o.AuthHandlerOpts == nil && o.RefreshToken == "" {
return errors.New("auth: refresh token must be provided")
}
return nil
}
func (o *Options3LO) logger() *slog.Logger {
return internallog.New(o.Logger)
}
// PKCEOptions holds parameters to support PKCE.
type PKCEOptions struct {
// Challenge is the un-padded, base64-url-encoded string of the encrypted code verifier.
Challenge string // The un-padded, base64-url-encoded string of the encrypted code verifier.
// ChallengeMethod is the encryption method (ex. S256).
ChallengeMethod string
// Verifier is the original, non-encrypted secret.
Verifier string // The original, non-encrypted secret.
}
type tokenJSON struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
// error fields
ErrorCode string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
}
func (e *tokenJSON) expiry() (t time.Time) {
if v := e.ExpiresIn; v != 0 {
return time.Now().Add(time.Duration(v) * time.Second)
}
return
}
func (o *Options3LO) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.DefaultClient()
}
// authCodeURL returns a URL that points to a OAuth2 consent page.
func (o *Options3LO) authCodeURL(state string, values url.Values) string {
var buf bytes.Buffer
buf.WriteString(o.AuthURL)
v := url.Values{
"response_type": {"code"},
"client_id": {o.ClientID},
}
if o.RedirectURL != "" {
v.Set("redirect_uri", o.RedirectURL)
}
if len(o.Scopes) > 0 {
v.Set("scope", strings.Join(o.Scopes, " "))
}
if state != "" {
v.Set("state", state)
}
if o.AuthHandlerOpts != nil {
if o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.Challenge != "" {
v.Set(codeChallengeKey, o.AuthHandlerOpts.PKCEOpts.Challenge)
}
if o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.ChallengeMethod != "" {
v.Set(codeChallengeMethodKey, o.AuthHandlerOpts.PKCEOpts.ChallengeMethod)
}
}
for k := range values {
v.Set(k, v.Get(k))
}
if strings.Contains(o.AuthURL, "?") {
buf.WriteByte('&')
} else {
buf.WriteByte('?')
}
buf.WriteString(v.Encode())
return buf.String()
}
// New3LOTokenProvider returns a [TokenProvider] based on the 3-legged OAuth2
// configuration. The TokenProvider is caches and auto-refreshes tokens by
// default.
func New3LOTokenProvider(opts *Options3LO) (TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if opts.AuthHandlerOpts != nil {
return new3LOTokenProviderWithAuthHandler(opts), nil
}
return NewCachedTokenProvider(&tokenProvider3LO{opts: opts, refreshToken: opts.RefreshToken, client: opts.client()}, &CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenExpiry,
}), nil
}
// AuthorizationHandlerOptions provides a set of options to specify for doing a
// 3-legged OAuth2 flow with a custom [AuthorizationHandler].
type AuthorizationHandlerOptions struct {
// AuthorizationHandler specifies the handler used to for the authorization
// part of the flow.
Handler AuthorizationHandler
// State is used verify that the "state" is identical in the request and
// response before exchanging the auth code for OAuth2 token.
State string
// PKCEOpts allows setting configurations for PKCE. Optional.
PKCEOpts *PKCEOptions
}
func new3LOTokenProviderWithAuthHandler(opts *Options3LO) TokenProvider {
return NewCachedTokenProvider(&tokenProviderWithHandler{opts: opts, state: opts.AuthHandlerOpts.State}, &CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenExpiry,
})
}
// exchange handles the final exchange portion of the 3lo flow. Returns a Token,
// refreshToken, and error.
func (o *Options3LO) exchange(ctx context.Context, code string) (*Token, string, error) {
// Build request
v := url.Values{
"grant_type": {"authorization_code"},
"code": {code},
}
if o.RedirectURL != "" {
v.Set("redirect_uri", o.RedirectURL)
}
if o.AuthHandlerOpts != nil &&
o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.Verifier != "" {
v.Set(codeVerifierKey, o.AuthHandlerOpts.PKCEOpts.Verifier)
}
for k := range o.URLParams {
v.Set(k, o.URLParams.Get(k))
}
return fetchToken(ctx, o, v)
}
// This struct is not safe for concurrent access alone, but the way it is used
// in this package by wrapping it with a cachedTokenProvider makes it so.
type tokenProvider3LO struct {
opts *Options3LO
client *http.Client
refreshToken string
}
func (tp *tokenProvider3LO) Token(ctx context.Context) (*Token, error) {
if tp.refreshToken == "" {
return nil, errors.New("auth: token expired and refresh token is not set")
}
v := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {tp.refreshToken},
}
for k := range tp.opts.URLParams {
v.Set(k, tp.opts.URLParams.Get(k))
}
tk, rt, err := fetchToken(ctx, tp.opts, v)
if err != nil {
return nil, err
}
if tp.refreshToken != rt && rt != "" {
tp.refreshToken = rt
}
return tk, err
}
type tokenProviderWithHandler struct {
opts *Options3LO
state string
}
func (tp tokenProviderWithHandler) Token(ctx context.Context) (*Token, error) {
url := tp.opts.authCodeURL(tp.state, nil)
code, state, err := tp.opts.AuthHandlerOpts.Handler(url)
if err != nil {
return nil, err
}
if state != tp.state {
return nil, errors.New("auth: state mismatch in 3-legged-OAuth flow")
}
tok, _, err := tp.opts.exchange(ctx, code)
return tok, err
}
// fetchToken returns a Token, refresh token, and/or an error.
func fetchToken(ctx context.Context, o *Options3LO, v url.Values) (*Token, string, error) {
var refreshToken string
if o.AuthStyle == StyleInParams {
if o.ClientID != "" {
v.Set("client_id", o.ClientID)
}
if o.ClientSecret != "" {
v.Set("client_secret", o.ClientSecret)
}
}
req, err := http.NewRequestWithContext(ctx, "POST", o.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
return nil, refreshToken, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if o.AuthStyle == StyleInHeader {
req.SetBasicAuth(url.QueryEscape(o.ClientID), url.QueryEscape(o.ClientSecret))
}
logger := o.logger()
logger.DebugContext(ctx, "3LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
// Make request
resp, body, err := internal.DoRequest(o.client(), req)
if err != nil {
return nil, refreshToken, err
}
logger.DebugContext(ctx, "3LO token response", "response", internallog.HTTPResponse(resp, body))
failureStatus := resp.StatusCode < 200 || resp.StatusCode > 299
tokError := &Error{
Response: resp,
Body: body,
}
var token *Token
// errors ignored because of default switch on content
content, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
// some endpoints return a query string
vals, err := url.ParseQuery(string(body))
if err != nil {
if failureStatus {
return nil, refreshToken, tokError
}
return nil, refreshToken, fmt.Errorf("auth: cannot parse response: %w", err)
}
tokError.code = vals.Get("error")
tokError.description = vals.Get("error_description")
tokError.uri = vals.Get("error_uri")
token = &Token{
Value: vals.Get("access_token"),
Type: vals.Get("token_type"),
Metadata: make(map[string]interface{}, len(vals)),
}
for k, v := range vals {
token.Metadata[k] = v
}
refreshToken = vals.Get("refresh_token")
e := vals.Get("expires_in")
expires, _ := strconv.Atoi(e)
if expires != 0 {
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
}
default:
var tj tokenJSON
if err = json.Unmarshal(body, &tj); err != nil {
if failureStatus {
return nil, refreshToken, tokError
}
return nil, refreshToken, fmt.Errorf("auth: cannot parse json: %w", err)
}
tokError.code = tj.ErrorCode
tokError.description = tj.ErrorDescription
tokError.uri = tj.ErrorURI
token = &Token{
Value: tj.AccessToken,
Type: tj.TokenType,
Expiry: tj.expiry(),
Metadata: make(map[string]interface{}),
}
json.Unmarshal(body, &token.Metadata) // optional field, skip err check
refreshToken = tj.RefreshToken
}
// according to spec, servers should respond status 400 in error case
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
// but some unorthodox servers respond 200 in error case
if failureStatus || tokError.code != "" {
return nil, refreshToken, tokError
}
if token.Value == "" {
return nil, refreshToken, errors.New("auth: server response missing access_token")
}
return token, refreshToken, nil
}

View File

@ -1,66 +0,0 @@
# Changes
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.2...compute/metadata/v0.6.0) (2024-12-13)
### Features
* **compute/metadata:** Add debug logging ([#11078](https://github.com/googleapis/google-cloud-go/issues/11078)) ([a816814](https://github.com/googleapis/google-cloud-go/commit/a81681463906e4473570a2f426eb0dc2de64e53f))
## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.1...compute/metadata/v0.5.2) (2024-09-20)
### Bug Fixes
* **compute/metadata:** Close Response Body for failed request ([#10891](https://github.com/googleapis/google-cloud-go/issues/10891)) ([e91d45e](https://github.com/googleapis/google-cloud-go/commit/e91d45e4757a9e354114509ba9800085d9e0ff1f))
## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.0...compute/metadata/v0.5.1) (2024-09-12)
### Bug Fixes
* **compute/metadata:** Check error chain for retryable error ([#10840](https://github.com/googleapis/google-cloud-go/issues/10840)) ([2bdedef](https://github.com/googleapis/google-cloud-go/commit/2bdedeff621b223d63cebc4355fcf83bc68412cd))
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.4.0...compute/metadata/v0.5.0) (2024-07-10)
### Features
* **compute/metadata:** Add sys check for windows OnGCE ([#10521](https://github.com/googleapis/google-cloud-go/issues/10521)) ([3b9a830](https://github.com/googleapis/google-cloud-go/commit/3b9a83063960d2a2ac20beb47cc15818a68bd302))
## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.3.0...compute/metadata/v0.4.0) (2024-07-01)
### Features
* **compute/metadata:** Add context for all functions/methods ([#10370](https://github.com/googleapis/google-cloud-go/issues/10370)) ([66b8efe](https://github.com/googleapis/google-cloud-go/commit/66b8efe7ad877e052b2987bb4475477e38c67bb3))
### Documentation
* **compute/metadata:** Update OnGCE description ([#10408](https://github.com/googleapis/google-cloud-go/issues/10408)) ([6a46dca](https://github.com/googleapis/google-cloud-go/commit/6a46dca4eae4f88ec6f88822e01e5bf8aeca787f))
## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.3...compute/metadata/v0.3.0) (2024-04-15)
### Features
* **compute/metadata:** Add context aware functions ([#9733](https://github.com/googleapis/google-cloud-go/issues/9733)) ([e4eb5b4](https://github.com/googleapis/google-cloud-go/commit/e4eb5b46ee2aec9d2fc18300bfd66015e25a0510))
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.2...compute/metadata/v0.2.3) (2022-12-15)
### Bug Fixes
* **compute/metadata:** Switch DNS lookup to an absolute lookup ([119b410](https://github.com/googleapis/google-cloud-go/commit/119b41060c7895e45e48aee5621ad35607c4d021)), refs [#7165](https://github.com/googleapis/google-cloud-go/issues/7165)
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.1...compute/metadata/v0.2.2) (2022-12-01)
### Bug Fixes
* **compute/metadata:** Set IdleConnTimeout for http.Client ([#7084](https://github.com/googleapis/google-cloud-go/issues/7084)) ([766516a](https://github.com/googleapis/google-cloud-go/commit/766516aaf3816bfb3159efeea65aa3d1d205a3e2)), refs [#5430](https://github.com/googleapis/google-cloud-go/issues/5430)
## [0.1.0] (2022-10-26)
Initial release of metadata being it's own module.

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,27 +0,0 @@
# Compute API
[![Go Reference](https://pkg.go.dev/badge/cloud.google.com/go/compute.svg)](https://pkg.go.dev/cloud.google.com/go/compute/metadata)
This is a utility library for communicating with Google Cloud metadata service
on Google Cloud.
## Install
```bash
go get cloud.google.com/go/compute/metadata
```
## Go Version Support
See the [Go Versions Supported](https://github.com/googleapis/google-cloud-go#go-versions-supported)
section in the root directory's README.
## Contributing
Contributions are welcome. Please, see the [CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
document for details.
Please note that this project is released with a Contributor Code of Conduct.
By participating in this project you agree to abide by its terms. See
[Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
for more information.

View File

@ -1,149 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
)
// Code below this point is copied from github.com/googleapis/gax-go/v2/internallog
// to avoid the dependency. The compute/metadata module is used by too many
// non-client library modules that can't justify the dependency.
// The handler returned if logging is not enabled.
type noOpHandler struct{}
func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
return false
}
func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
return nil
}
func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h noOpHandler) WithGroup(_ string) slog.Handler {
return h
}
// httpRequest returns a lazily evaluated [slog.LogValuer] for a
// [http.Request] and the associated body.
func httpRequest(req *http.Request, body []byte) slog.LogValuer {
return &request{
req: req,
payload: body,
}
}
type request struct {
req *http.Request
payload []byte
}
func (r *request) LogValue() slog.Value {
if r == nil || r.req == nil {
return slog.Value{}
}
var groupValueAttrs []slog.Attr
groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method))
groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String()))
var headerAttr []slog.Attr
for k, val := range r.req.Header {
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
}
if len(headerAttr) > 0 {
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
}
if len(r.payload) > 0 {
if attr, ok := processPayload(r.payload); ok {
groupValueAttrs = append(groupValueAttrs, attr)
}
}
return slog.GroupValue(groupValueAttrs...)
}
// httpResponse returns a lazily evaluated [slog.LogValuer] for a
// [http.Response] and the associated body.
func httpResponse(resp *http.Response, body []byte) slog.LogValuer {
return &response{
resp: resp,
payload: body,
}
}
type response struct {
resp *http.Response
payload []byte
}
func (r *response) LogValue() slog.Value {
if r == nil {
return slog.Value{}
}
var groupValueAttrs []slog.Attr
groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode)))
var headerAttr []slog.Attr
for k, val := range r.resp.Header {
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
}
if len(headerAttr) > 0 {
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
}
if len(r.payload) > 0 {
if attr, ok := processPayload(r.payload); ok {
groupValueAttrs = append(groupValueAttrs, attr)
}
}
return slog.GroupValue(groupValueAttrs...)
}
func processPayload(payload []byte) (slog.Attr, bool) {
peekChar := payload[0]
if peekChar == '{' {
// JSON object
var m map[string]any
if err := json.Unmarshal(payload, &m); err == nil {
return slog.Any("payload", m), true
}
} else if peekChar == '[' {
// JSON array
var m []any
if err := json.Unmarshal(payload, &m); err == nil {
return slog.Any("payload", m), true
}
} else {
// Everything else
buf := &bytes.Buffer{}
if err := json.Compact(buf, payload); err != nil {
// Write raw payload incase of error
buf.Write(payload)
}
return slog.String("payload", buf.String()), true
}
return slog.Attr{}, false
}

View File

@ -1,872 +0,0 @@
// Copyright 2014 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package metadata provides access to Google Compute Engine (GCE)
// metadata and API service accounts.
//
// This package is a wrapper around the GCE metadata service,
// as documented at https://cloud.google.com/compute/docs/metadata/overview.
package metadata // import "cloud.google.com/go/compute/metadata"
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
)
const (
// metadataIP is the documented metadata server IP address.
metadataIP = "169.254.169.254"
// metadataHostEnv is the environment variable specifying the
// GCE metadata hostname. If empty, the default value of
// metadataIP ("169.254.169.254") is used instead.
// This is variable name is not defined by any spec, as far as
// I know; it was made up for the Go package.
metadataHostEnv = "GCE_METADATA_HOST"
userAgent = "gcloud-golang/0.1"
)
type cachedValue struct {
k string
trim bool
mu sync.Mutex
v string
}
var (
projID = &cachedValue{k: "project/project-id", trim: true}
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
instID = &cachedValue{k: "instance/id", trim: true}
)
var defaultClient = &Client{
hc: newDefaultHTTPClient(),
logger: slog.New(noOpHandler{}),
}
func newDefaultHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
IdleConnTimeout: 60 * time.Second,
},
Timeout: 5 * time.Second,
}
}
// NotDefinedError is returned when requested metadata is not defined.
//
// The underlying string is the suffix after "/computeMetadata/v1/".
//
// This error is not returned if the value is defined to be the empty
// string.
type NotDefinedError string
func (suffix NotDefinedError) Error() string {
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
}
func (c *cachedValue) get(ctx context.Context, cl *Client) (v string, err error) {
defer c.mu.Unlock()
c.mu.Lock()
if c.v != "" {
return c.v, nil
}
if c.trim {
v, err = cl.getTrimmed(ctx, c.k)
} else {
v, err = cl.GetWithContext(ctx, c.k)
}
if err == nil {
c.v = v
}
return
}
var (
onGCEOnce sync.Once
onGCE bool
)
// OnGCE reports whether this process is running on Google Compute Platforms.
// NOTE: True returned from `OnGCE` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func OnGCE() bool {
onGCEOnce.Do(initOnGCE)
return onGCE
}
func initOnGCE() {
onGCE = testOnGCE()
}
func testOnGCE() bool {
// The user explicitly said they're on GCE, so trust them.
if os.Getenv(metadataHostEnv) != "" {
return true
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resc := make(chan bool, 2)
// Try two strategies in parallel.
// See https://github.com/googleapis/google-cloud-go/issues/194
go func() {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent)
res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
if err != nil {
resc <- false
return
}
defer res.Body.Close()
resc <- res.Header.Get("Metadata-Flavor") == "Google"
}()
go func() {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
if err != nil || len(addrs) == 0 {
resc <- false
return
}
resc <- strsContains(addrs, metadataIP)
}()
tryHarder := systemInfoSuggestsGCE()
if tryHarder {
res := <-resc
if res {
// The first strategy succeeded, so let's use it.
return true
}
// Wait for either the DNS or metadata server probe to
// contradict the other one and say we are running on
// GCE. Give it a lot of time to do so, since the system
// info already suggests we're running on a GCE BIOS.
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case res = <-resc:
return res
case <-timer.C:
// Too slow. Who knows what this system is.
return false
}
}
// There's no hint from the system info that we're running on
// GCE, so use the first probe's result as truth, whether it's
// true or false. The goal here is to optimize for speed for
// users who are NOT running on GCE. We can't assume that
// either a DNS lookup or an HTTP request to a blackholed IP
// address is fast. Worst case this should return when the
// metaClient's Transport.ResponseHeaderTimeout or
// Transport.Dial.Timeout fires (in two seconds).
return <-resc
}
// Subscribe calls Client.SubscribeWithContext on the default client.
//
// Deprecated: Please use the context aware variant [SubscribeWithContext].
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
}
// SubscribeWithContext calls Client.SubscribeWithContext on the default client.
func SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
return defaultClient.SubscribeWithContext(ctx, suffix, fn)
}
// Get calls Client.GetWithContext on the default client.
//
// Deprecated: Please use the context aware variant [GetWithContext].
func Get(suffix string) (string, error) {
return defaultClient.GetWithContext(context.Background(), suffix)
}
// GetWithContext calls Client.GetWithContext on the default client.
func GetWithContext(ctx context.Context, suffix string) (string, error) {
return defaultClient.GetWithContext(ctx, suffix)
}
// ProjectID returns the current instance's project ID string.
//
// Deprecated: Please use the context aware variant [ProjectIDWithContext].
func ProjectID() (string, error) {
return defaultClient.ProjectIDWithContext(context.Background())
}
// ProjectIDWithContext returns the current instance's project ID string.
func ProjectIDWithContext(ctx context.Context) (string, error) {
return defaultClient.ProjectIDWithContext(ctx)
}
// NumericProjectID returns the current instance's numeric project ID.
//
// Deprecated: Please use the context aware variant [NumericProjectIDWithContext].
func NumericProjectID() (string, error) {
return defaultClient.NumericProjectIDWithContext(context.Background())
}
// NumericProjectIDWithContext returns the current instance's numeric project ID.
func NumericProjectIDWithContext(ctx context.Context) (string, error) {
return defaultClient.NumericProjectIDWithContext(ctx)
}
// InternalIP returns the instance's primary internal IP address.
//
// Deprecated: Please use the context aware variant [InternalIPWithContext].
func InternalIP() (string, error) {
return defaultClient.InternalIPWithContext(context.Background())
}
// InternalIPWithContext returns the instance's primary internal IP address.
func InternalIPWithContext(ctx context.Context) (string, error) {
return defaultClient.InternalIPWithContext(ctx)
}
// ExternalIP returns the instance's primary external (public) IP address.
//
// Deprecated: Please use the context aware variant [ExternalIPWithContext].
func ExternalIP() (string, error) {
return defaultClient.ExternalIPWithContext(context.Background())
}
// ExternalIPWithContext returns the instance's primary external (public) IP address.
func ExternalIPWithContext(ctx context.Context) (string, error) {
return defaultClient.ExternalIPWithContext(ctx)
}
// Email calls Client.EmailWithContext on the default client.
//
// Deprecated: Please use the context aware variant [EmailWithContext].
func Email(serviceAccount string) (string, error) {
return defaultClient.EmailWithContext(context.Background(), serviceAccount)
}
// EmailWithContext calls Client.EmailWithContext on the default client.
func EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
return defaultClient.EmailWithContext(ctx, serviceAccount)
}
// Hostname returns the instance's hostname. This will be of the form
// "<instanceID>.c.<projID>.internal".
//
// Deprecated: Please use the context aware variant [HostnameWithContext].
func Hostname() (string, error) {
return defaultClient.HostnameWithContext(context.Background())
}
// HostnameWithContext returns the instance's hostname. This will be of the form
// "<instanceID>.c.<projID>.internal".
func HostnameWithContext(ctx context.Context) (string, error) {
return defaultClient.HostnameWithContext(ctx)
}
// InstanceTags returns the list of user-defined instance tags,
// assigned when initially creating a GCE instance.
//
// Deprecated: Please use the context aware variant [InstanceTagsWithContext].
func InstanceTags() ([]string, error) {
return defaultClient.InstanceTagsWithContext(context.Background())
}
// InstanceTagsWithContext returns the list of user-defined instance tags,
// assigned when initially creating a GCE instance.
func InstanceTagsWithContext(ctx context.Context) ([]string, error) {
return defaultClient.InstanceTagsWithContext(ctx)
}
// InstanceID returns the current VM's numeric instance ID.
//
// Deprecated: Please use the context aware variant [InstanceIDWithContext].
func InstanceID() (string, error) {
return defaultClient.InstanceIDWithContext(context.Background())
}
// InstanceIDWithContext returns the current VM's numeric instance ID.
func InstanceIDWithContext(ctx context.Context) (string, error) {
return defaultClient.InstanceIDWithContext(ctx)
}
// InstanceName returns the current VM's instance ID string.
//
// Deprecated: Please use the context aware variant [InstanceNameWithContext].
func InstanceName() (string, error) {
return defaultClient.InstanceNameWithContext(context.Background())
}
// InstanceNameWithContext returns the current VM's instance ID string.
func InstanceNameWithContext(ctx context.Context) (string, error) {
return defaultClient.InstanceNameWithContext(ctx)
}
// Zone returns the current VM's zone, such as "us-central1-b".
//
// Deprecated: Please use the context aware variant [ZoneWithContext].
func Zone() (string, error) {
return defaultClient.ZoneWithContext(context.Background())
}
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
func ZoneWithContext(ctx context.Context) (string, error) {
return defaultClient.ZoneWithContext(ctx)
}
// InstanceAttributes calls Client.InstanceAttributesWithContext on the default client.
//
// Deprecated: Please use the context aware variant [InstanceAttributesWithContext.
func InstanceAttributes() ([]string, error) {
return defaultClient.InstanceAttributesWithContext(context.Background())
}
// InstanceAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
func InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
return defaultClient.InstanceAttributesWithContext(ctx)
}
// ProjectAttributes calls Client.ProjectAttributesWithContext on the default client.
//
// Deprecated: Please use the context aware variant [ProjectAttributesWithContext].
func ProjectAttributes() ([]string, error) {
return defaultClient.ProjectAttributesWithContext(context.Background())
}
// ProjectAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
func ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
return defaultClient.ProjectAttributesWithContext(ctx)
}
// InstanceAttributeValue calls Client.InstanceAttributeValueWithContext on the default client.
//
// Deprecated: Please use the context aware variant [InstanceAttributeValueWithContext].
func InstanceAttributeValue(attr string) (string, error) {
return defaultClient.InstanceAttributeValueWithContext(context.Background(), attr)
}
// InstanceAttributeValueWithContext calls Client.InstanceAttributeValueWithContext on the default client.
func InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
return defaultClient.InstanceAttributeValueWithContext(ctx, attr)
}
// ProjectAttributeValue calls Client.ProjectAttributeValueWithContext on the default client.
//
// Deprecated: Please use the context aware variant [ProjectAttributeValueWithContext].
func ProjectAttributeValue(attr string) (string, error) {
return defaultClient.ProjectAttributeValueWithContext(context.Background(), attr)
}
// ProjectAttributeValueWithContext calls Client.ProjectAttributeValueWithContext on the default client.
func ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
return defaultClient.ProjectAttributeValueWithContext(ctx, attr)
}
// Scopes calls Client.ScopesWithContext on the default client.
//
// Deprecated: Please use the context aware variant [ScopesWithContext].
func Scopes(serviceAccount string) ([]string, error) {
return defaultClient.ScopesWithContext(context.Background(), serviceAccount)
}
// ScopesWithContext calls Client.ScopesWithContext on the default client.
func ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
return defaultClient.ScopesWithContext(ctx, serviceAccount)
}
func strsContains(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}
// A Client provides metadata.
type Client struct {
hc *http.Client
logger *slog.Logger
}
// Options for configuring a [Client].
type Options struct {
// Client is the HTTP client used to make requests. Optional.
Client *http.Client
// Logger is used to log information about HTTP request and responses.
// If not provided, nothing will be logged. Optional.
Logger *slog.Logger
}
// NewClient returns a Client that can be used to fetch metadata.
// Returns the client that uses the specified http.Client for HTTP requests.
// If nil is specified, returns the default client.
func NewClient(c *http.Client) *Client {
return NewWithOptions(&Options{
Client: c,
})
}
// NewWithOptions returns a Client that is configured with the provided Options.
func NewWithOptions(opts *Options) *Client {
if opts == nil {
return defaultClient
}
client := opts.Client
if client == nil {
client = newDefaultHTTPClient()
}
logger := opts.Logger
if logger == nil {
logger = slog.New(noOpHandler{})
}
return &Client{hc: client, logger: logger}
}
// getETag returns a value from the metadata service as well as the associated ETag.
// This func is otherwise equivalent to Get.
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
// Using a fixed IP makes it very difficult to spoof the metadata service in
// a container, which is an important use-case for local testing of cloud
// deployments. To enable spoofing of the metadata service, the environment
// variable GCE_METADATA_HOST is first inspected to decide where metadata
// requests shall go.
host := os.Getenv(metadataHostEnv)
if host == "" {
// Using 169.254.169.254 instead of "metadata" here because Go
// binaries built with the "netgo" tag and without cgo won't
// know the search suffix for "metadata" is
// ".google.internal", and this IP address is documented as
// being stable anyway.
host = metadataIP
}
suffix = strings.TrimLeft(suffix, "/")
u := "http://" + host + "/computeMetadata/v1/" + suffix
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return "", "", err
}
req.Header.Set("Metadata-Flavor", "Google")
req.Header.Set("User-Agent", userAgent)
var res *http.Response
var reqErr error
var body []byte
retryer := newRetryer()
for {
c.logger.DebugContext(ctx, "metadata request", "request", httpRequest(req, nil))
res, reqErr = c.hc.Do(req)
var code int
if res != nil {
code = res.StatusCode
body, err = io.ReadAll(res.Body)
if err != nil {
res.Body.Close()
return "", "", err
}
c.logger.DebugContext(ctx, "metadata response", "response", httpResponse(res, body))
res.Body.Close()
}
if delay, shouldRetry := retryer.Retry(code, reqErr); shouldRetry {
if res != nil && res.Body != nil {
res.Body.Close()
}
if err := sleep(ctx, delay); err != nil {
return "", "", err
}
continue
}
break
}
if reqErr != nil {
return "", "", reqErr
}
if res.StatusCode == http.StatusNotFound {
return "", "", NotDefinedError(suffix)
}
if res.StatusCode != 200 {
return "", "", &Error{Code: res.StatusCode, Message: string(body)}
}
return string(body), res.Header.Get("Etag"), nil
}
// Get returns a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
//
// If the GCE_METADATA_HOST environment variable is not defined, a default of
// 169.254.169.254 will be used instead.
//
// If the requested metadata is not defined, the returned error will
// be of type NotDefinedError.
//
// Deprecated: Please use the context aware variant [Client.GetWithContext].
func (c *Client) Get(suffix string) (string, error) {
return c.GetWithContext(context.Background(), suffix)
}
// GetWithContext returns a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
//
// If the GCE_METADATA_HOST environment variable is not defined, a default of
// 169.254.169.254 will be used instead.
//
// If the requested metadata is not defined, the returned error will
// be of type NotDefinedError.
//
// NOTE: Without an extra deadline in the context this call can take in the
// worst case, with internal backoff retries, up to 15 seconds (e.g. when server
// is responding slowly). Pass context with additional timeouts when needed.
func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
val, _, err := c.getETag(ctx, suffix)
return val, err
}
func (c *Client) getTrimmed(ctx context.Context, suffix string) (s string, err error) {
s, err = c.GetWithContext(ctx, suffix)
s = strings.TrimSpace(s)
return
}
func (c *Client) lines(ctx context.Context, suffix string) ([]string, error) {
j, err := c.GetWithContext(ctx, suffix)
if err != nil {
return nil, err
}
s := strings.Split(strings.TrimSpace(j), "\n")
for i := range s {
s[i] = strings.TrimSpace(s[i])
}
return s, nil
}
// ProjectID returns the current instance's project ID string.
//
// Deprecated: Please use the context aware variant [Client.ProjectIDWithContext].
func (c *Client) ProjectID() (string, error) { return c.ProjectIDWithContext(context.Background()) }
// ProjectIDWithContext returns the current instance's project ID string.
func (c *Client) ProjectIDWithContext(ctx context.Context) (string, error) { return projID.get(ctx, c) }
// NumericProjectID returns the current instance's numeric project ID.
//
// Deprecated: Please use the context aware variant [Client.NumericProjectIDWithContext].
func (c *Client) NumericProjectID() (string, error) {
return c.NumericProjectIDWithContext(context.Background())
}
// NumericProjectIDWithContext returns the current instance's numeric project ID.
func (c *Client) NumericProjectIDWithContext(ctx context.Context) (string, error) {
return projNum.get(ctx, c)
}
// InstanceID returns the current VM's numeric instance ID.
//
// Deprecated: Please use the context aware variant [Client.InstanceIDWithContext].
func (c *Client) InstanceID() (string, error) {
return c.InstanceIDWithContext(context.Background())
}
// InstanceIDWithContext returns the current VM's numeric instance ID.
func (c *Client) InstanceIDWithContext(ctx context.Context) (string, error) {
return instID.get(ctx, c)
}
// InternalIP returns the instance's primary internal IP address.
//
// Deprecated: Please use the context aware variant [Client.InternalIPWithContext].
func (c *Client) InternalIP() (string, error) {
return c.InternalIPWithContext(context.Background())
}
// InternalIPWithContext returns the instance's primary internal IP address.
func (c *Client) InternalIPWithContext(ctx context.Context) (string, error) {
return c.getTrimmed(ctx, "instance/network-interfaces/0/ip")
}
// Email returns the email address associated with the service account.
//
// Deprecated: Please use the context aware variant [Client.EmailWithContext].
func (c *Client) Email(serviceAccount string) (string, error) {
return c.EmailWithContext(context.Background(), serviceAccount)
}
// EmailWithContext returns the email address associated with the service account.
// The serviceAccount parameter default value (empty string or "default" value)
// will use the instance's main account.
func (c *Client) EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
if serviceAccount == "" {
serviceAccount = "default"
}
return c.getTrimmed(ctx, "instance/service-accounts/"+serviceAccount+"/email")
}
// ExternalIP returns the instance's primary external (public) IP address.
//
// Deprecated: Please use the context aware variant [Client.ExternalIPWithContext].
func (c *Client) ExternalIP() (string, error) {
return c.ExternalIPWithContext(context.Background())
}
// ExternalIPWithContext returns the instance's primary external (public) IP address.
func (c *Client) ExternalIPWithContext(ctx context.Context) (string, error) {
return c.getTrimmed(ctx, "instance/network-interfaces/0/access-configs/0/external-ip")
}
// Hostname returns the instance's hostname. This will be of the form
// "<instanceID>.c.<projID>.internal".
//
// Deprecated: Please use the context aware variant [Client.HostnameWithContext].
func (c *Client) Hostname() (string, error) {
return c.HostnameWithContext(context.Background())
}
// HostnameWithContext returns the instance's hostname. This will be of the form
// "<instanceID>.c.<projID>.internal".
func (c *Client) HostnameWithContext(ctx context.Context) (string, error) {
return c.getTrimmed(ctx, "instance/hostname")
}
// InstanceTags returns the list of user-defined instance tags.
//
// Deprecated: Please use the context aware variant [Client.InstanceTagsWithContext].
func (c *Client) InstanceTags() ([]string, error) {
return c.InstanceTagsWithContext(context.Background())
}
// InstanceTagsWithContext returns the list of user-defined instance tags,
// assigned when initially creating a GCE instance.
func (c *Client) InstanceTagsWithContext(ctx context.Context) ([]string, error) {
var s []string
j, err := c.GetWithContext(ctx, "instance/tags")
if err != nil {
return nil, err
}
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
return nil, err
}
return s, nil
}
// InstanceName returns the current VM's instance ID string.
//
// Deprecated: Please use the context aware variant [Client.InstanceNameWithContext].
func (c *Client) InstanceName() (string, error) {
return c.InstanceNameWithContext(context.Background())
}
// InstanceNameWithContext returns the current VM's instance ID string.
func (c *Client) InstanceNameWithContext(ctx context.Context) (string, error) {
return c.getTrimmed(ctx, "instance/name")
}
// Zone returns the current VM's zone, such as "us-central1-b".
//
// Deprecated: Please use the context aware variant [Client.ZoneWithContext].
func (c *Client) Zone() (string, error) {
return c.ZoneWithContext(context.Background())
}
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
func (c *Client) ZoneWithContext(ctx context.Context) (string, error) {
zone, err := c.getTrimmed(ctx, "instance/zone")
// zone is of the form "projects/<projNum>/zones/<zoneName>".
if err != nil {
return "", err
}
return zone[strings.LastIndex(zone, "/")+1:], nil
}
// InstanceAttributes returns the list of user-defined attributes,
// assigned when initially creating a GCE VM instance. The value of an
// attribute can be obtained with InstanceAttributeValue.
//
// Deprecated: Please use the context aware variant [Client.InstanceAttributesWithContext].
func (c *Client) InstanceAttributes() ([]string, error) {
return c.InstanceAttributesWithContext(context.Background())
}
// InstanceAttributesWithContext returns the list of user-defined attributes,
// assigned when initially creating a GCE VM instance. The value of an
// attribute can be obtained with InstanceAttributeValue.
func (c *Client) InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
return c.lines(ctx, "instance/attributes/")
}
// ProjectAttributes returns the list of user-defined attributes
// applying to the project as a whole, not just this VM. The value of
// an attribute can be obtained with ProjectAttributeValue.
//
// Deprecated: Please use the context aware variant [Client.ProjectAttributesWithContext].
func (c *Client) ProjectAttributes() ([]string, error) {
return c.ProjectAttributesWithContext(context.Background())
}
// ProjectAttributesWithContext returns the list of user-defined attributes
// applying to the project as a whole, not just this VM. The value of
// an attribute can be obtained with ProjectAttributeValue.
func (c *Client) ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
return c.lines(ctx, "project/attributes/")
}
// InstanceAttributeValue returns the value of the provided VM
// instance attribute.
//
// If the requested attribute is not defined, the returned error will
// be of type NotDefinedError.
//
// InstanceAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
//
// Deprecated: Please use the context aware variant [Client.InstanceAttributeValueWithContext].
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
return c.InstanceAttributeValueWithContext(context.Background(), attr)
}
// InstanceAttributeValueWithContext returns the value of the provided VM
// instance attribute.
//
// If the requested attribute is not defined, the returned error will
// be of type NotDefinedError.
//
// InstanceAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
func (c *Client) InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
return c.GetWithContext(ctx, "instance/attributes/"+attr)
}
// ProjectAttributeValue returns the value of the provided
// project attribute.
//
// If the requested attribute is not defined, the returned error will
// be of type NotDefinedError.
//
// ProjectAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
//
// Deprecated: Please use the context aware variant [Client.ProjectAttributeValueWithContext].
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
return c.ProjectAttributeValueWithContext(context.Background(), attr)
}
// ProjectAttributeValueWithContext returns the value of the provided
// project attribute.
//
// If the requested attribute is not defined, the returned error will
// be of type NotDefinedError.
//
// ProjectAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
func (c *Client) ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
return c.GetWithContext(ctx, "project/attributes/"+attr)
}
// Scopes returns the service account scopes for the given account.
// The account may be empty or the string "default" to use the instance's
// main account.
//
// Deprecated: Please use the context aware variant [Client.ScopesWithContext].
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
return c.ScopesWithContext(context.Background(), serviceAccount)
}
// ScopesWithContext returns the service account scopes for the given account.
// The account may be empty or the string "default" to use the instance's
// main account.
func (c *Client) ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
if serviceAccount == "" {
serviceAccount = "default"
}
return c.lines(ctx, "instance/service-accounts/"+serviceAccount+"/scopes")
}
// Subscribe subscribes to a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
// The suffix may contain query parameters.
//
// Deprecated: Please use the context aware variant [Client.SubscribeWithContext].
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
return c.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
}
// SubscribeWithContext subscribes to a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
// The suffix may contain query parameters.
//
// SubscribeWithContext calls fn with the latest metadata value indicated by the
// provided suffix. If the metadata value is deleted, fn is called with the
// empty string and ok false. Subscribe blocks until fn returns a non-nil error
// or the value is deleted. Subscribe returns the error value returned from the
// last call to fn, which may be nil when ok == false.
func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
const failedSubscribeSleep = time.Second * 5
// First check to see if the metadata value exists at all.
val, lastETag, err := c.getETag(ctx, suffix)
if err != nil {
return err
}
if err := fn(ctx, val, true); err != nil {
return err
}
ok := true
if strings.ContainsRune(suffix, '?') {
suffix += "&wait_for_change=true&last_etag="
} else {
suffix += "?wait_for_change=true&last_etag="
}
for {
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
if err != nil {
if _, deleted := err.(NotDefinedError); !deleted {
time.Sleep(failedSubscribeSleep)
continue // Retry on other errors.
}
ok = false
}
lastETag = etag
if err := fn(ctx, val, ok); err != nil || !ok {
return err
}
}
}
// Error contains an error response from the server.
type Error struct {
// Code is the HTTP response status code.
Code int
// Message is the server response message.
Message string
}
func (e *Error) Error() string {
return fmt.Sprintf("compute: Received %d `%s`", e.Code, e.Message)
}

View File

@ -1,114 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"io"
"math/rand"
"net/http"
"time"
)
const (
maxRetryAttempts = 5
)
var (
syscallRetryable = func(error) bool { return false }
)
// defaultBackoff is basically equivalent to gax.Backoff without the need for
// the dependency.
type defaultBackoff struct {
max time.Duration
mul float64
cur time.Duration
}
func (b *defaultBackoff) Pause() time.Duration {
d := time.Duration(1 + rand.Int63n(int64(b.cur)))
b.cur = time.Duration(float64(b.cur) * b.mul)
if b.cur > b.max {
b.cur = b.max
}
return d
}
// sleep is the equivalent of gax.Sleep without the need for the dependency.
func sleep(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return ctx.Err()
case <-t.C:
return nil
}
}
func newRetryer() *metadataRetryer {
return &metadataRetryer{bo: &defaultBackoff{
cur: 100 * time.Millisecond,
max: 30 * time.Second,
mul: 2,
}}
}
type backoff interface {
Pause() time.Duration
}
type metadataRetryer struct {
bo backoff
attempts int
}
func (r *metadataRetryer) Retry(status int, err error) (time.Duration, bool) {
if status == http.StatusOK {
return 0, false
}
retryOk := shouldRetry(status, err)
if !retryOk {
return 0, false
}
if r.attempts == maxRetryAttempts {
return 0, false
}
r.attempts++
return r.bo.Pause(), true
}
func shouldRetry(status int, err error) bool {
if 500 <= status && status <= 599 {
return true
}
if err == io.ErrUnexpectedEOF {
return true
}
// Transient network errors should be retried.
if syscallRetryable(err) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
if err.Temporary() {
return true
}
}
if err, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, err.Unwrap())
}
return false
}

View File

@ -1,31 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package metadata
import (
"errors"
"syscall"
)
func init() {
// Initialize syscallRetryable to return true on transient socket-level
// errors. These errors are specific to Linux.
syscallRetryable = func(err error) bool {
return errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNREFUSED)
}
}

View File

@ -1,26 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows && !linux
package metadata
// systemInfoSuggestsGCE reports whether the local system (without
// doing network requests) suggests that we're running on GCE. If this
// returns true, testOnGCE tries a bit harder to reach its metadata
// server.
func systemInfoSuggestsGCE() bool {
// We don't currently have checks for other GOOS
return false
}

View File

@ -1,28 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
package metadata
import (
"os"
"strings"
)
func systemInfoSuggestsGCE() bool {
b, _ := os.ReadFile("/sys/class/dmi/id/product_name")
name := strings.TrimSpace(string(b))
return name == "Google" || name == "Google Compute Engine"
}

View File

@ -1,38 +0,0 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package metadata
import (
"strings"
"golang.org/x/sys/windows/registry"
)
func systemInfoSuggestsGCE() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
s, _, err := k.GetStringValue("SystemProductName")
if err != nil {
return false
}
s = strings.TrimSpace(s)
return strings.HasPrefix(s, "Google")
}

View File

@ -1,19 +0,0 @@
Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,10 +0,0 @@
.PHONY: ci generate clean
ci: clean generate
go test -race -v ./...
generate:
go generate .
clean:
rm -rf *_generated*.go

View File

@ -1,95 +0,0 @@
# httpsnoop
Package httpsnoop provides an easy way to capture http related metrics (i.e.
response time, bytes written, and http status code) from your application's
http.Handlers.
Doing this requires non-trivial wrapping of the http.ResponseWriter interface,
which is also exposed for users interested in a more low-level API.
[![Go Reference](https://pkg.go.dev/badge/github.com/felixge/httpsnoop.svg)](https://pkg.go.dev/github.com/felixge/httpsnoop)
[![Build Status](https://github.com/felixge/httpsnoop/actions/workflows/main.yaml/badge.svg)](https://github.com/felixge/httpsnoop/actions/workflows/main.yaml)
## Usage Example
```go
// myH is your app's http handler, perhaps a http.ServeMux or similar.
var myH http.Handler
// wrappedH wraps myH in order to log every request.
wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m := httpsnoop.CaptureMetrics(myH, w, r)
log.Printf(
"%s %s (code=%d dt=%s written=%d)",
r.Method,
r.URL,
m.Code,
m.Duration,
m.Written,
)
})
http.ListenAndServe(":8080", wrappedH)
```
## Why this package exists
Instrumenting an application's http.Handler is surprisingly difficult.
However if you google for e.g. "capture ResponseWriter status code" you'll find
lots of advise and code examples that suggest it to be a fairly trivial
undertaking. Unfortunately everything I've seen so far has a high chance of
breaking your application.
The main problem is that a `http.ResponseWriter` often implements additional
interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and
`io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter`
in your own struct that also implements the `http.ResponseWriter` interface
will hide the additional interfaces mentioned above. This has a high change of
introducing subtle bugs into any non-trivial application.
Another approach I've seen people take is to return a struct that implements
all of the interfaces above. However, that's also problematic, because it's
difficult to fake some of these interfaces behaviors when the underlying
`http.ResponseWriter` doesn't have an implementation. It's also dangerous,
because an application may choose to operate differently, merely because it
detects the presence of these additional interfaces.
This package solves this problem by checking which additional interfaces a
`http.ResponseWriter` implements, returning a wrapped version implementing the
exact same set of interfaces.
Additionally this package properly handles edge cases such as `WriteHeader` not
being called, or called more than once, as well as concurrent calls to
`http.ResponseWriter` methods, and even calls happening after the wrapped
`ServeHTTP` has already returned.
Unfortunately this package is not perfect either. It's possible that it is
still missing some interfaces provided by the go core (let me know if you find
one), and it won't work for applications adding their own interfaces into the
mix. You can however use `httpsnoop.Unwrap(w)` to access the underlying
`http.ResponseWriter` and type-assert the result to its other interfaces.
However, hopefully the explanation above has sufficiently scared you of rolling
your own solution to this problem. httpsnoop may still break your application,
but at least it tries to avoid it as much as possible.
Anyway, the real problem here is that smuggling additional interfaces inside
`http.ResponseWriter` is a problematic design choice, but it probably goes as
deep as the Go language specification itself. But that's okay, I still prefer
Go over the alternatives ;).
## Performance
```
BenchmarkBaseline-8 20000 94912 ns/op
BenchmarkCaptureMetrics-8 20000 95461 ns/op
```
As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an
overhead of ~500 ns per http request on my machine. However, the margin of
error appears to be larger than that, therefor it should be reasonable to
assume that the overhead introduced by `CaptureMetrics` is absolutely
negligible.
## License
MIT

View File

@ -1,86 +0,0 @@
package httpsnoop
import (
"io"
"net/http"
"time"
)
// Metrics holds metrics captured from CaptureMetrics.
type Metrics struct {
// Code is the first http response code passed to the WriteHeader func of
// the ResponseWriter. If no such call is made, a default code of 200 is
// assumed instead.
Code int
// Duration is the time it took to execute the handler.
Duration time.Duration
// Written is the number of bytes successfully written by the Write or
// ReadFrom function of the ResponseWriter. ResponseWriters may also write
// data to their underlaying connection directly (e.g. headers), but those
// are not tracked. Therefor the number of Written bytes will usually match
// the size of the response body.
Written int64
}
// CaptureMetrics wraps the given hnd, executes it with the given w and r, and
// returns the metrics it captured from it.
func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics {
return CaptureMetricsFn(w, func(ww http.ResponseWriter) {
hnd.ServeHTTP(ww, r)
})
}
// CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the
// resulting metrics. This is very similar to CaptureMetrics (which is just
// sugar on top of this func), but is a more usable interface if your
// application doesn't use the Go http.Handler interface.
func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics {
m := Metrics{Code: http.StatusOK}
m.CaptureMetrics(w, fn)
return m
}
// CaptureMetrics wraps w and calls fn with the wrapped w and updates
// Metrics m with the resulting metrics. This is similar to CaptureMetricsFn,
// but allows one to customize starting Metrics object.
func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWriter)) {
var (
start = time.Now()
headerWritten bool
hooks = Hooks{
WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc {
return func(code int) {
next(code)
if !(code >= 100 && code <= 199) && !headerWritten {
m.Code = code
headerWritten = true
}
}
},
Write: func(next WriteFunc) WriteFunc {
return func(p []byte) (int, error) {
n, err := next(p)
m.Written += int64(n)
headerWritten = true
return n, err
}
},
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
return func(src io.Reader) (int64, error) {
n, err := next(src)
headerWritten = true
m.Written += n
return n, err
}
},
}
)
fn(Wrap(w, hooks))
m.Duration += time.Since(start)
}

View File

@ -1,10 +0,0 @@
// Package httpsnoop provides an easy way to capture http related metrics (i.e.
// response time, bytes written, and http status code) from your application's
// http.Handlers.
//
// Doing this requires non-trivial wrapping of the http.ResponseWriter
// interface, which is also exposed for users interested in a more low-level
// API.
package httpsnoop
//go:generate go run codegen/main.go

View File

@ -1,436 +0,0 @@
// +build go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// PushFunc is part of the http.Pusher interface.
type PushFunc func(target string, opts *http.PushOptions) error
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
Push func(PushFunc) PushFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
// - http.Pusher
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
_, i4 := w.(http.Pusher)
switch {
// combination 1/32
case !i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/32
case !i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Pusher
}{rw, rw, rw}
// combination 3/32
case !i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 4/32
case !i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw}
// combination 5/32
case !i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 6/32
case !i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
http.Pusher
}{rw, rw, rw, rw}
// combination 7/32
case !i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 8/32
case !i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 9/32
case !i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 10/32
case !i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw}
// combination 11/32
case !i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 12/32
case !i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 13/32
case !i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 14/32
case !i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 15/32
case !i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 16/32
case !i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 17/32
case i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 18/32
case i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Pusher
}{rw, rw, rw, rw}
// combination 19/32
case i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 20/32
case i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 21/32
case i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 22/32
case i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 23/32
case i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 24/32
case i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 25/32
case i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 26/32
case i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 27/32
case i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 28/32
case i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 29/32
case i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 30/32
case i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 31/32
case i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
// combination 32/32
case i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
func (w *rw) Push(target string, opts *http.PushOptions) error {
f := w.w.(http.Pusher).Push
if w.h.Push != nil {
f = w.h.Push(f)
}
return f(target, opts)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}

View File

@ -1,278 +0,0 @@
// +build !go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
switch {
// combination 1/16
case !i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/16
case !i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 3/16
case !i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 4/16
case !i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 5/16
case !i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 6/16
case !i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 7/16
case !i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 8/16
case !i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 9/16
case i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 10/16
case i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 11/16
case i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 12/16
case i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 13/16
case i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 14/16
case i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 15/16
case i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 16/16
case i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}

View File

@ -1,26 +0,0 @@
run:
timeout: 1m
tests: true
linters:
disable-all: true
enable:
- asciicheck
- errcheck
- forcetypeassert
- gocritic
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- misspell
- revive
- staticcheck
- typecheck
- unused
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 10

View File

@ -1,6 +0,0 @@
# CHANGELOG
## v1.0.0-rc1
This is the first logged release. Major changes (including breaking changes)
have occurred since earlier tags.

View File

@ -1,17 +0,0 @@
# Contributing
Logr is open to pull-requests, provided they fit within the intended scope of
the project. Specifically, this library aims to be VERY small and minimalist,
with no external dependencies.
## Compatibility
This project intends to follow [semantic versioning](http://semver.org) and
is very strict about compatibility. Any proposed changes MUST follow those
rules.
## Performance
As a logging library, logr must be as light-weight as possible. Any proposed
code change must include results of running the [benchmark](./benchmark)
before and after the change.

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,407 +0,0 @@
# A minimal logging API for Go
[![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/logr.svg)](https://pkg.go.dev/github.com/go-logr/logr)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-logr/logr)](https://goreportcard.com/report/github.com/go-logr/logr)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-logr/logr/badge)](https://securityscorecards.dev/viewer/?platform=github.com&org=go-logr&repo=logr)
logr offers an(other) opinion on how Go programs and libraries can do logging
without becoming coupled to a particular logging implementation. This is not
an implementation of logging - it is an API. In fact it is two APIs with two
different sets of users.
The `Logger` type is intended for application and library authors. It provides
a relatively small API which can be used everywhere you want to emit logs. It
defers the actual act of writing logs (to files, to stdout, or whatever) to the
`LogSink` interface.
The `LogSink` interface is intended for logging library implementers. It is a
pure interface which can be implemented by logging frameworks to provide the actual logging
functionality.
This decoupling allows application and library developers to write code in
terms of `logr.Logger` (which has very low dependency fan-out) while the
implementation of logging is managed "up stack" (e.g. in or near `main()`.)
Application developers can then switch out implementations as necessary.
Many people assert that libraries should not be logging, and as such efforts
like this are pointless. Those people are welcome to convince the authors of
the tens-of-thousands of libraries that *DO* write logs that they are all
wrong. In the meantime, logr takes a more practical approach.
## Typical usage
Somewhere, early in an application's life, it will make a decision about which
logging library (implementation) it actually wants to use. Something like:
```
func main() {
// ... other setup code ...
// Create the "root" logger. We have chosen the "logimpl" implementation,
// which takes some initial parameters and returns a logr.Logger.
logger := logimpl.New(param1, param2)
// ... other setup code ...
```
Most apps will call into other libraries, create structures to govern the flow,
etc. The `logr.Logger` object can be passed to these other libraries, stored
in structs, or even used as a package-global variable, if needed. For example:
```
app := createTheAppObject(logger)
app.Run()
```
Outside of this early setup, no other packages need to know about the choice of
implementation. They write logs in terms of the `logr.Logger` that they
received:
```
type appObject struct {
// ... other fields ...
logger logr.Logger
// ... other fields ...
}
func (app *appObject) Run() {
app.logger.Info("starting up", "timestamp", time.Now())
// ... app code ...
```
## Background
If the Go standard library had defined an interface for logging, this project
probably would not be needed. Alas, here we are.
When the Go developers started developing such an interface with
[slog](https://github.com/golang/go/issues/56345), they adopted some of the
logr design but also left out some parts and changed others:
| Feature | logr | slog |
|---------|------|------|
| High-level API | `Logger` (passed by value) | `Logger` (passed by [pointer](https://github.com/golang/go/issues/59126)) |
| Low-level API | `LogSink` | `Handler` |
| Stack unwinding | done by `LogSink` | done by `Logger` |
| Skipping helper functions | `WithCallDepth`, `WithCallStackHelper` | [not supported by Logger](https://github.com/golang/go/issues/59145) |
| Generating a value for logging on demand | `Marshaler` | `LogValuer` |
| Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" |
| Error log entries | always logged, don't have a verbosity level | normal log entries with level >= `LevelError` |
| Passing logger via context | `NewContext`, `FromContext` | no API |
| Adding a name to a logger | `WithName` | no API |
| Modify verbosity of log entries in a call chain | `V` | no API |
| Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` |
| Pass context for extracting additional values | no API | API variants like `InfoCtx` |
The high-level slog API is explicitly meant to be one of many different APIs
that can be layered on top of a shared `slog.Handler`. logr is one such
alternative API, with [interoperability](#slog-interoperability) provided by
some conversion functions.
### Inspiration
Before you consider this package, please read [this blog post by the
inimitable Dave Cheney][warning-makes-no-sense]. We really appreciate what
he has to say, and it largely aligns with our own experiences.
### Differences from Dave's ideas
The main differences are:
1. Dave basically proposes doing away with the notion of a logging API in favor
of `fmt.Printf()`. We disagree, especially when you consider things like output
locations, timestamps, file and line decorations, and structured logging. This
package restricts the logging API to just 2 types of logs: info and error.
Info logs are things you want to tell the user which are not errors. Error
logs are, well, errors. If your code receives an `error` from a subordinate
function call and is logging that `error` *and not returning it*, use error
logs.
2. Verbosity-levels on info logs. This gives developers a chance to indicate
arbitrary grades of importance for info logs, without assigning names with
semantic meaning such as "warning", "trace", and "debug." Superficially this
may feel very similar, but the primary difference is the lack of semantics.
Because verbosity is a numerical value, it's safe to assume that an app running
with higher verbosity means more (and less important) logs will be generated.
## Implementations (non-exhaustive)
There are implementations for the following logging libraries:
- **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr)
- **a testing.T** (for use in Go tests, with JSON-like output): [testr](https://github.com/go-logr/logr/tree/master/testr)
- **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr)
- **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr)
- **a testing.T** (with klog-like text output): [ktesting](https://git.k8s.io/klog/ktesting)
- **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr)
- **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr)
- **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr)
- **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend)
- **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr)
- **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr)
- **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0)
- **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing)
## slog interoperability
Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler`
and using the `slog.Logger` API with a `logr.LogSink`. `FromSlogHandler` and
`ToSlogHandler` convert between a `logr.Logger` and a `slog.Handler`.
As usual, `slog.New` can be used to wrap such a `slog.Handler` in the high-level
slog API.
### Using a `logr.LogSink` as backend for slog
Ideally, a logr sink implementation should support both logr and slog by
implementing both the normal logr interface(s) and `SlogSink`. Because
of a conflict in the parameters of the common `Enabled` method, it is [not
possible to implement both slog.Handler and logr.Sink in the same
type](https://github.com/golang/go/issues/59110).
If both are supported, log calls can go from the high-level APIs to the backend
without the need to convert parameters. `FromSlogHandler` and `ToSlogHandler` can
convert back and forth without adding additional wrappers, with one exception:
when `Logger.V` was used to adjust the verbosity for a `slog.Handler`, then
`ToSlogHandler` has to use a wrapper which adjusts the verbosity for future
log calls.
Such an implementation should also support values that implement specific
interfaces from both packages for logging (`logr.Marshaler`, `slog.LogValuer`,
`slog.GroupValue`). logr does not convert those.
Not supporting slog has several drawbacks:
- Recording source code locations works correctly if the handler gets called
through `slog.Logger`, but may be wrong in other cases. That's because a
`logr.Sink` does its own stack unwinding instead of using the program counter
provided by the high-level API.
- slog levels <= 0 can be mapped to logr levels by negating the level without a
loss of information. But all slog levels > 0 (e.g. `slog.LevelWarning` as
used by `slog.Logger.Warn`) must be mapped to 0 before calling the sink
because logr does not support "more important than info" levels.
- The slog group concept is supported by prefixing each key in a key/value
pair with the group names, separated by a dot. For structured output like
JSON it would be better to group the key/value pairs inside an object.
- Special slog values and interfaces don't work as expected.
- The overhead is likely to be higher.
These drawbacks are severe enough that applications using a mixture of slog and
logr should switch to a different backend.
### Using a `slog.Handler` as backend for logr
Using a plain `slog.Handler` without support for logr works better than the
other direction:
- All logr verbosity levels can be mapped 1:1 to their corresponding slog level
by negating them.
- Stack unwinding is done by the `SlogSink` and the resulting program
counter is passed to the `slog.Handler`.
- Names added via `Logger.WithName` are gathered and recorded in an additional
attribute with `logger` as key and the names separated by slash as value.
- `Logger.Error` is turned into a log record with `slog.LevelError` as level
and an additional attribute with `err` as key, if an error was provided.
The main drawback is that `logr.Marshaler` will not be supported. Types should
ideally support both `logr.Marshaler` and `slog.Valuer`. If compatibility
with logr implementations without slog support is not important, then
`slog.Valuer` is sufficient.
### Context support for slog
Storing a logger in a `context.Context` is not supported by
slog. `NewContextWithSlogLogger` and `FromContextAsSlogLogger` can be
used to fill this gap. They store and retrieve a `slog.Logger` pointer
under the same context key that is also used by `NewContext` and
`FromContext` for `logr.Logger` value.
When `NewContextWithSlogLogger` is followed by `FromContext`, the latter will
automatically convert the `slog.Logger` to a
`logr.Logger`. `FromContextAsSlogLogger` does the same for the other direction.
With this approach, binaries which use either slog or logr are as efficient as
possible with no unnecessary allocations. This is also why the API stores a
`slog.Logger` pointer: when storing a `slog.Handler`, creating a `slog.Logger`
on retrieval would need to allocate one.
The downside is that switching back and forth needs more allocations. Because
logr is the API that is already in use by different packages, in particular
Kubernetes, the recommendation is to use the `logr.Logger` API in code which
uses contextual logging.
An alternative to adding values to a logger and storing that logger in the
context is to store the values in the context and to configure a logging
backend to extract those values when emitting log entries. This only works when
log calls are passed the context, which is not supported by the logr API.
With the slog API, it is possible, but not
required. https://github.com/veqryn/slog-context is a package for slog which
provides additional support code for this approach. It also contains wrappers
for the context functions in logr, so developers who prefer to not use the logr
APIs directly can use those instead and the resulting code will still be
interoperable with logr.
## FAQ
### Conceptual
#### Why structured logging?
- **Structured logs are more easily queryable**: Since you've got
key-value pairs, it's much easier to query your structured logs for
particular values by filtering on the contents of a particular key --
think searching request logs for error codes, Kubernetes reconcilers for
the name and namespace of the reconciled object, etc.
- **Structured logging makes it easier to have cross-referenceable logs**:
Similarly to searchability, if you maintain conventions around your
keys, it becomes easy to gather all log lines related to a particular
concept.
- **Structured logs allow better dimensions of filtering**: if you have
structure to your logs, you've got more precise control over how much
information is logged -- you might choose in a particular configuration
to log certain keys but not others, only log lines where a certain key
matches a certain value, etc., instead of just having v-levels and names
to key off of.
- **Structured logs better represent structured data**: sometimes, the
data that you want to log is inherently structured (think tuple-link
objects.) Structured logs allow you to preserve that structure when
outputting.
#### Why V-levels?
**V-levels give operators an easy way to control the chattiness of log
operations**. V-levels provide a way for a given package to distinguish
the relative importance or verbosity of a given log message. Then, if
a particular logger or package is logging too many messages, the user
of the package can simply change the v-levels for that library.
#### Why not named levels, like Info/Warning/Error?
Read [Dave Cheney's post][warning-makes-no-sense]. Then read [Differences
from Dave's ideas](#differences-from-daves-ideas).
#### Why not allow format strings, too?
**Format strings negate many of the benefits of structured logs**:
- They're not easily searchable without resorting to fuzzy searching,
regular expressions, etc.
- They don't store structured data well, since contents are flattened into
a string.
- They're not cross-referenceable.
- They don't compress easily, since the message is not constant.
(Unless you turn positional parameters into key-value pairs with numerical
keys, at which point you've gotten key-value logging with meaningless
keys.)
### Practical
#### Why key-value pairs, and not a map?
Key-value pairs are *much* easier to optimize, especially around
allocations. Zap (a structured logger that inspired logr's interface) has
[performance measurements](https://github.com/uber-go/zap#performance)
that show this quite nicely.
While the interface ends up being a little less obvious, you get
potentially better performance, plus avoid making users type
`map[string]string{}` every time they want to log.
#### What if my V-levels differ between libraries?
That's fine. Control your V-levels on a per-logger basis, and use the
`WithName` method to pass different loggers to different libraries.
Generally, you should take care to ensure that you have relatively
consistent V-levels within a given logger, however, as this makes deciding
on what verbosity of logs to request easier.
#### But I really want to use a format string!
That's not actually a question. Assuming your question is "how do
I convert my mental model of logging with format strings to logging with
constant messages":
1. Figure out what the error actually is, as you'd write in a TL;DR style,
and use that as a message.
2. For every place you'd write a format specifier, look to the word before
it, and add that as a key value pair.
For instance, consider the following examples (all taken from spots in the
Kubernetes codebase):
- `klog.V(4).Infof("Client is returning errors: code %v, error %v",
responseCode, err)` becomes `logger.Error(err, "client returned an
error", "code", responseCode)`
- `klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v",
seconds, retries, url)` becomes `logger.V(4).Info("got a retry-after
response when requesting url", "attempt", retries, "after
seconds", seconds, "url", url)`
If you *really* must use a format string, use it in a key's value, and
call `fmt.Sprintf` yourself. For instance: `log.Printf("unable to
reflect over type %T")` becomes `logger.Info("unable to reflect over
type", "type", fmt.Sprintf("%T"))`. In general though, the cases where
this is necessary should be few and far between.
#### How do I choose my V-levels?
This is basically the only hard constraint: increase V-levels to denote
more verbose or more debug-y logs.
Otherwise, you can start out with `0` as "you always want to see this",
`1` as "common logging that you might *possibly* want to turn off", and
`10` as "I would like to performance-test your log collection stack."
Then gradually choose levels in between as you need them, working your way
down from 10 (for debug and trace style logs) and up from 1 (for chattier
info-type logs). For reference, slog pre-defines -4 for debug logs
(corresponds to 4 in logr), which matches what is
[recommended for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use).
#### How do I choose my keys?
Keys are fairly flexible, and can hold more or less any string
value. For best compatibility with implementations and consistency
with existing code in other projects, there are a few conventions you
should consider.
- Make your keys human-readable.
- Constant keys are generally a good idea.
- Be consistent across your codebase.
- Keys should naturally match parts of the message string.
- Use lower case for simple keys and
[lowerCamelCase](https://en.wiktionary.org/wiki/lowerCamelCase) for
more complex ones. Kubernetes is one example of a project that has
[adopted that
convention](https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments).
While key names are mostly unrestricted (and spaces are acceptable),
it's generally a good idea to stick to printable ascii characters, or at
least match the general character set of your log lines.
#### Why should keys be constant values?
The point of structured logging is to make later log processing easier. Your
keys are, effectively, the schema of each log message. If you use different
keys across instances of the same log line, you will make your structured logs
much harder to use. `Sprintf()` is for values, not for keys!
#### Why is this not a pure interface?
The Logger type is implemented as a struct in order to allow the Go compiler to
optimize things like high-V `Info` logs that are not triggered. Not all of
these implementations are implemented yet, but this structure was suggested as
a way to ensure they *can* be implemented. All of the real work is behind the
`LogSink` interface.
[warning-makes-no-sense]: http://dave.cheney.net/2015/11/05/lets-talk-about-logging

View File

@ -1,18 +0,0 @@
# Security Policy
If you have discovered a security vulnerability in this project, please report it
privately. **Do not disclose it as a public issue.** This gives us time to work with you
to fix the issue before public exposure, reducing the chance that the exploit will be
used before a patch is released.
You may submit the report in the following ways:
- send an email to go-logr-security@googlegroups.com
- send us a [private vulnerability report](https://github.com/go-logr/logr/security/advisories/new)
Please provide the following information in your report:
- A description of the vulnerability and its impact
- How to reproduce the issue
We ask that you give us 90 days to work on a fix before public exposure.

View File

@ -1,33 +0,0 @@
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
// contextKey is how we find Loggers in a context.Context. With Go < 1.21,
// the value is always a Logger value. With Go >= 1.21, the value can be a
// Logger value or a slog.Logger pointer.
type contextKey struct{}
// notFoundError exists to carry an IsNotFound method.
type notFoundError struct{}
func (notFoundError) Error() string {
return "no logr.Logger was present"
}
func (notFoundError) IsNotFound() bool {
return true
}

View File

@ -1,49 +0,0 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
)
// FromContext returns a Logger from ctx or an error if no Logger is found.
func FromContext(ctx context.Context) (Logger, error) {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v, nil
}
return Logger{}, notFoundError{}
}
// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this
// returns a Logger that discards all log messages.
func FromContextOrDiscard(ctx context.Context) Logger {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v
}
return Discard()
}
// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
func NewContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}

View File

@ -1,83 +0,0 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"fmt"
"log/slog"
)
// FromContext returns a Logger from ctx or an error if no Logger is found.
func FromContext(ctx context.Context) (Logger, error) {
v := ctx.Value(contextKey{})
if v == nil {
return Logger{}, notFoundError{}
}
switch v := v.(type) {
case Logger:
return v, nil
case *slog.Logger:
return FromSlogHandler(v.Handler()), nil
default:
// Not reached.
panic(fmt.Sprintf("unexpected value type for logr context key: %T", v))
}
}
// FromContextAsSlogLogger returns a slog.Logger from ctx or nil if no such Logger is found.
func FromContextAsSlogLogger(ctx context.Context) *slog.Logger {
v := ctx.Value(contextKey{})
if v == nil {
return nil
}
switch v := v.(type) {
case Logger:
return slog.New(ToSlogHandler(v))
case *slog.Logger:
return v
default:
// Not reached.
panic(fmt.Sprintf("unexpected value type for logr context key: %T", v))
}
}
// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this
// returns a Logger that discards all log messages.
func FromContextOrDiscard(ctx context.Context) Logger {
if logger, err := FromContext(ctx); err == nil {
return logger
}
return Discard()
}
// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
func NewContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}
// NewContextWithSlogLogger returns a new Context, derived from ctx, which carries the
// provided slog.Logger.
func NewContextWithSlogLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}

View File

@ -1,24 +0,0 @@
/*
Copyright 2020 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
// Discard returns a Logger that discards all messages logged to it. It can be
// used whenever the caller is not interested in the logs. Logger instances
// produced by this function always compare as equal.
func Discard() Logger {
return New(nil)
}

View File

@ -1,914 +0,0 @@
/*
Copyright 2021 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package funcr implements formatting of structured log messages and
// optionally captures the call site and timestamp.
//
// The simplest way to use it is via its implementation of a
// github.com/go-logr/logr.LogSink with output through an arbitrary
// "write" function. See New and NewJSON for details.
//
// # Custom LogSinks
//
// For users who need more control, a funcr.Formatter can be embedded inside
// your own custom LogSink implementation. This is useful when the LogSink
// needs to implement additional methods, for example.
//
// # Formatting
//
// This will respect logr.Marshaler, fmt.Stringer, and error interfaces for
// values which are being logged. When rendering a struct, funcr will use Go's
// standard JSON tags (all except "string").
package funcr
import (
"bytes"
"encoding"
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"github.com/go-logr/logr"
)
// New returns a logr.Logger which is implemented by an arbitrary function.
func New(fn func(prefix, args string), opts Options) logr.Logger {
return logr.New(newSink(fn, NewFormatter(opts)))
}
// NewJSON returns a logr.Logger which is implemented by an arbitrary function
// and produces JSON output.
func NewJSON(fn func(obj string), opts Options) logr.Logger {
fnWrapper := func(_, obj string) {
fn(obj)
}
return logr.New(newSink(fnWrapper, NewFormatterJSON(opts)))
}
// Underlier exposes access to the underlying logging function. Since
// callers only have a logr.Logger, they have to know which
// implementation is in use, so this interface is less of an
// abstraction and more of a way to test type conversion.
type Underlier interface {
GetUnderlying() func(prefix, args string)
}
func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink {
l := &fnlogger{
Formatter: formatter,
write: fn,
}
// For skipping fnlogger.Info and fnlogger.Error.
l.Formatter.AddCallDepth(1)
return l
}
// Options carries parameters which influence the way logs are generated.
type Options struct {
// LogCaller tells funcr to add a "caller" key to some or all log lines.
// This has some overhead, so some users might not want it.
LogCaller MessageClass
// LogCallerFunc tells funcr to also log the calling function name. This
// has no effect if caller logging is not enabled (see Options.LogCaller).
LogCallerFunc bool
// LogTimestamp tells funcr to add a "ts" key to log lines. This has some
// overhead, so some users might not want it.
LogTimestamp bool
// TimestampFormat tells funcr how to render timestamps when LogTimestamp
// is enabled. If not specified, a default format will be used. For more
// details, see docs for Go's time.Layout.
TimestampFormat string
// LogInfoLevel tells funcr what key to use to log the info level.
// If not specified, the info level will be logged as "level".
// If this is set to "", the info level will not be logged at all.
LogInfoLevel *string
// Verbosity tells funcr which V logs to produce. Higher values enable
// more logs. Info logs at or below this level will be written, while logs
// above this level will be discarded.
Verbosity int
// RenderBuiltinsHook allows users to mutate the list of key-value pairs
// while a log line is being rendered. The kvList argument follows logr
// conventions - each pair of slice elements is comprised of a string key
// and an arbitrary value (verified and sanitized before calling this
// hook). The value returned must follow the same conventions. This hook
// can be used to audit or modify logged data. For example, you might want
// to prefix all of funcr's built-in keys with some string. This hook is
// only called for built-in (provided by funcr itself) key-value pairs.
// Equivalent hooks are offered for key-value pairs saved via
// logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and
// for user-provided pairs (see RenderArgsHook).
RenderBuiltinsHook func(kvList []any) []any
// RenderValuesHook is the same as RenderBuiltinsHook, except that it is
// only called for key-value pairs saved via logr.Logger.WithValues. See
// RenderBuiltinsHook for more details.
RenderValuesHook func(kvList []any) []any
// RenderArgsHook is the same as RenderBuiltinsHook, except that it is only
// called for key-value pairs passed directly to Info and Error. See
// RenderBuiltinsHook for more details.
RenderArgsHook func(kvList []any) []any
// MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct
// that contains a struct, etc.) it may log. Every time it finds a struct,
// slice, array, or map the depth is increased by one. When the maximum is
// reached, the value will be converted to a string indicating that the max
// depth has been exceeded. If this field is not specified, a default
// value will be used.
MaxLogDepth int
}
// MessageClass indicates which category or categories of messages to consider.
type MessageClass int
const (
// None ignores all message classes.
None MessageClass = iota
// All considers all message classes.
All
// Info only considers info messages.
Info
// Error only considers error messages.
Error
)
// fnlogger inherits some of its LogSink implementation from Formatter
// and just needs to add some glue code.
type fnlogger struct {
Formatter
write func(prefix, args string)
}
func (l fnlogger) WithName(name string) logr.LogSink {
l.Formatter.AddName(name)
return &l
}
func (l fnlogger) WithValues(kvList ...any) logr.LogSink {
l.Formatter.AddValues(kvList)
return &l
}
func (l fnlogger) WithCallDepth(depth int) logr.LogSink {
l.Formatter.AddCallDepth(depth)
return &l
}
func (l fnlogger) Info(level int, msg string, kvList ...any) {
prefix, args := l.FormatInfo(level, msg, kvList)
l.write(prefix, args)
}
func (l fnlogger) Error(err error, msg string, kvList ...any) {
prefix, args := l.FormatError(err, msg, kvList)
l.write(prefix, args)
}
func (l fnlogger) GetUnderlying() func(prefix, args string) {
return l.write
}
// Assert conformance to the interfaces.
var _ logr.LogSink = &fnlogger{}
var _ logr.CallDepthLogSink = &fnlogger{}
var _ Underlier = &fnlogger{}
// NewFormatter constructs a Formatter which emits a JSON-like key=value format.
func NewFormatter(opts Options) Formatter {
return newFormatter(opts, outputKeyValue)
}
// NewFormatterJSON constructs a Formatter which emits strict JSON.
func NewFormatterJSON(opts Options) Formatter {
return newFormatter(opts, outputJSON)
}
// Defaults for Options.
const defaultTimestampFormat = "2006-01-02 15:04:05.000000"
const defaultMaxLogDepth = 16
func newFormatter(opts Options, outfmt outputFormat) Formatter {
if opts.TimestampFormat == "" {
opts.TimestampFormat = defaultTimestampFormat
}
if opts.MaxLogDepth == 0 {
opts.MaxLogDepth = defaultMaxLogDepth
}
if opts.LogInfoLevel == nil {
opts.LogInfoLevel = new(string)
*opts.LogInfoLevel = "level"
}
f := Formatter{
outputFormat: outfmt,
prefix: "",
values: nil,
depth: 0,
opts: &opts,
}
return f
}
// Formatter is an opaque struct which can be embedded in a LogSink
// implementation. It should be constructed with NewFormatter. Some of
// its methods directly implement logr.LogSink.
type Formatter struct {
outputFormat outputFormat
prefix string
values []any
valuesStr string
depth int
opts *Options
groupName string // for slog groups
groups []groupDef
}
// outputFormat indicates which outputFormat to use.
type outputFormat int
const (
// outputKeyValue emits a JSON-like key=value format, but not strict JSON.
outputKeyValue outputFormat = iota
// outputJSON emits strict JSON.
outputJSON
)
// groupDef represents a saved group. The values may be empty, but we don't
// know if we need to render the group until the final record is rendered.
type groupDef struct {
name string
values string
}
// PseudoStruct is a list of key-value pairs that gets logged as a struct.
type PseudoStruct []any
// render produces a log line, ready to use.
func (f Formatter) render(builtins, args []any) string {
// Empirically bytes.Buffer is faster than strings.Builder for this.
buf := bytes.NewBuffer(make([]byte, 0, 1024))
if f.outputFormat == outputJSON {
buf.WriteByte('{') // for the whole record
}
// Render builtins
vals := builtins
if hook := f.opts.RenderBuiltinsHook; hook != nil {
vals = hook(f.sanitize(vals))
}
f.flatten(buf, vals, false) // keys are ours, no need to escape
continuing := len(builtins) > 0
// Turn the inner-most group into a string
argsStr := func() string {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
vals = args
if hook := f.opts.RenderArgsHook; hook != nil {
vals = hook(f.sanitize(vals))
}
f.flatten(buf, vals, true) // escape user-provided keys
return buf.String()
}()
// Render the stack of groups from the inside out.
bodyStr := f.renderGroup(f.groupName, f.valuesStr, argsStr)
for i := len(f.groups) - 1; i >= 0; i-- {
grp := &f.groups[i]
if grp.values == "" && bodyStr == "" {
// no contents, so we must elide the whole group
continue
}
bodyStr = f.renderGroup(grp.name, grp.values, bodyStr)
}
if bodyStr != "" {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(bodyStr)
}
if f.outputFormat == outputJSON {
buf.WriteByte('}') // for the whole record
}
return buf.String()
}
// renderGroup returns a string representation of the named group with rendered
// values and args. If the name is empty, this will return the values and args,
// joined. If the name is not empty, this will return a single key-value pair,
// where the value is a grouping of the values and args. If the values and
// args are both empty, this will return an empty string, even if the name was
// specified.
func (f Formatter) renderGroup(name string, values string, args string) string {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
needClosingBrace := false
if name != "" && (values != "" || args != "") {
buf.WriteString(f.quoted(name, true)) // escape user-provided keys
buf.WriteByte(f.colon())
buf.WriteByte('{')
needClosingBrace = true
}
continuing := false
if values != "" {
buf.WriteString(values)
continuing = true
}
if args != "" {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(args)
}
if needClosingBrace {
buf.WriteByte('}')
}
return buf.String()
}
// flatten renders a list of key-value pairs into a buffer. If escapeKeys is
// true, the keys are assumed to have non-JSON-compatible characters in them
// and must be evaluated for escapes.
//
// This function returns a potentially modified version of kvList, which
// ensures that there is a value for every key (adding a value if needed) and
// that each key is a string (substituting a key if needed).
func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, escapeKeys bool) []any {
// This logic overlaps with sanitize() but saves one type-cast per key,
// which can be measurable.
if len(kvList)%2 != 0 {
kvList = append(kvList, noValue)
}
copied := false
for i := 0; i < len(kvList); i += 2 {
k, ok := kvList[i].(string)
if !ok {
if !copied {
newList := make([]any, len(kvList))
copy(newList, kvList)
kvList = newList
copied = true
}
k = f.nonStringKey(kvList[i])
kvList[i] = k
}
v := kvList[i+1]
if i > 0 {
if f.outputFormat == outputJSON {
buf.WriteByte(f.comma())
} else {
// In theory the format could be something we don't understand. In
// practice, we control it, so it won't be.
buf.WriteByte(' ')
}
}
buf.WriteString(f.quoted(k, escapeKeys))
buf.WriteByte(f.colon())
buf.WriteString(f.pretty(v))
}
return kvList
}
func (f Formatter) quoted(str string, escape bool) string {
if escape {
return prettyString(str)
}
// this is faster
return `"` + str + `"`
}
func (f Formatter) comma() byte {
if f.outputFormat == outputJSON {
return ','
}
return ' '
}
func (f Formatter) colon() byte {
if f.outputFormat == outputJSON {
return ':'
}
return '='
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0)
}
const (
flagRawStruct = 0x1 // do not print braces on structs
)
// TODO: This is not fast. Most of the overhead goes here.
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}
// Handle types that take full control of logging.
if v, ok := value.(logr.Marshaler); ok {
// Replace the value with what the type wants to get logged.
// That then gets handled below via reflection.
value = invokeMarshaler(v)
}
// Handle types that want to format themselves.
switch v := value.(type) {
case fmt.Stringer:
value = invokeStringer(v)
case error:
value = invokeError(v)
}
// Handling the most common types without reflect is a small perf win.
switch v := value.(type) {
case bool:
return strconv.FormatBool(v)
case string:
return prettyString(v)
case int:
return strconv.FormatInt(int64(v), 10)
case int8:
return strconv.FormatInt(int64(v), 10)
case int16:
return strconv.FormatInt(int64(v), 10)
case int32:
return strconv.FormatInt(int64(v), 10)
case int64:
return strconv.FormatInt(int64(v), 10)
case uint:
return strconv.FormatUint(uint64(v), 10)
case uint8:
return strconv.FormatUint(uint64(v), 10)
case uint16:
return strconv.FormatUint(uint64(v), 10)
case uint32:
return strconv.FormatUint(uint64(v), 10)
case uint64:
return strconv.FormatUint(v, 10)
case uintptr:
return strconv.FormatUint(uint64(v), 10)
case float32:
return strconv.FormatFloat(float64(v), 'f', -1, 32)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case complex64:
return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"`
case complex128:
return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"`
case PseudoStruct:
buf := bytes.NewBuffer(make([]byte, 0, 1024))
v = f.sanitize(v)
if flags&flagRawStruct == 0 {
buf.WriteByte('{')
}
for i := 0; i < len(v); i += 2 {
if i > 0 {
buf.WriteByte(f.comma())
}
k, _ := v[i].(string) // sanitize() above means no need to check success
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
}
return buf.String()
}
buf := bytes.NewBuffer(make([]byte, 0, 256))
t := reflect.TypeOf(value)
if t == nil {
return "null"
}
v := reflect.ValueOf(value)
switch t.Kind() {
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return prettyString(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(int64(v.Int()), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(uint64(v.Uint()), 10)
case reflect.Float32:
return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32)
case reflect.Float64:
return strconv.FormatFloat(v.Float(), 'f', -1, 64)
case reflect.Complex64:
return `"` + strconv.FormatComplex(complex128(v.Complex()), 'f', -1, 64) + `"`
case reflect.Complex128:
return `"` + strconv.FormatComplex(v.Complex(), 'f', -1, 128) + `"`
case reflect.Struct:
if flags&flagRawStruct == 0 {
buf.WriteByte('{')
}
printComma := false // testing i>0 is not enough because of JSON omitted fields
for i := 0; i < t.NumField(); i++ {
fld := t.Field(i)
if fld.PkgPath != "" {
// reflect says this field is only defined for non-exported fields.
continue
}
if !v.Field(i).CanInterface() {
// reflect isn't clear exactly what this means, but we can't use it.
continue
}
name := ""
omitempty := false
if tag, found := fld.Tag.Lookup("json"); found {
if tag == "-" {
continue
}
if comma := strings.Index(tag, ","); comma != -1 {
if n := tag[:comma]; n != "" {
name = n
}
rest := tag[comma:]
if strings.Contains(rest, ",omitempty,") || strings.HasSuffix(rest, ",omitempty") {
omitempty = true
}
} else {
name = tag
}
}
if omitempty && isEmpty(v.Field(i)) {
continue
}
if printComma {
buf.WriteByte(f.comma())
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
continue
}
if name == "" {
name = fld.Name
}
// field names can't contain characters which need escaping
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
}
return buf.String()
case reflect.Slice, reflect.Array:
// If this is outputing as JSON make sure this isn't really a json.RawMessage.
// If so just emit "as-is" and don't pretty it as that will just print
// it as [X,Y,Z,...] which isn't terribly useful vs the string form you really want.
if f.outputFormat == outputJSON {
if rm, ok := value.(json.RawMessage); ok {
// If it's empty make sure we emit an empty value as the array style would below.
if len(rm) > 0 {
buf.Write(rm)
} else {
buf.WriteString("null")
}
return buf.String()
}
}
buf.WriteByte('[')
for i := 0; i < v.Len(); i++ {
if i > 0 {
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
}
buf.WriteByte(']')
return buf.String()
case reflect.Map:
buf.WriteByte('{')
// This does not sort the map keys, for best perf.
it := v.MapRange()
i := 0
for it.Next() {
if i > 0 {
buf.WriteByte(f.comma())
}
// If a map key supports TextMarshaler, use it.
keystr := ""
if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok {
txt, err := m.MarshalText()
if err != nil {
keystr = fmt.Sprintf("<error-MarshalText: %s>", err.Error())
} else {
keystr = string(txt)
}
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
keystr = prettyString(keystr)
}
}
buf.WriteString(keystr)
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
i++
}
buf.WriteByte('}')
return buf.String()
case reflect.Ptr, reflect.Interface:
if v.IsNil() {
return "null"
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
func prettyString(s string) string {
// Avoid escaping (which does allocations) if we can.
if needsEscape(s) {
return strconv.Quote(s)
}
b := bytes.NewBuffer(make([]byte, 0, 1024))
b.WriteByte('"')
b.WriteString(s)
b.WriteByte('"')
return b.String()
}
// needsEscape determines whether the input string needs to be escaped or not,
// without doing any allocations.
func needsEscape(s string) bool {
for _, r := range s {
if !strconv.IsPrint(r) || r == '\\' || r == '"' {
return true
}
}
return false
}
func isEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func invokeMarshaler(m logr.Marshaler) (ret any) {
defer func() {
if r := recover(); r != nil {
ret = fmt.Sprintf("<panic: %s>", r)
}
}()
return m.MarshalLog()
}
func invokeStringer(s fmt.Stringer) (ret string) {
defer func() {
if r := recover(); r != nil {
ret = fmt.Sprintf("<panic: %s>", r)
}
}()
return s.String()
}
func invokeError(e error) (ret string) {
defer func() {
if r := recover(); r != nil {
ret = fmt.Sprintf("<panic: %s>", r)
}
}()
return e.Error()
}
// Caller represents the original call site for a log line, after considering
// logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and
// Line fields will always be provided, while the Func field is optional.
// Users can set the render hook fields in Options to examine logged key-value
// pairs, one of which will be {"caller", Caller} if the Options.LogCaller
// field is enabled for the given MessageClass.
type Caller struct {
// File is the basename of the file for this call site.
File string `json:"file"`
// Line is the line number in the file for this call site.
Line int `json:"line"`
// Func is the function name for this call site, or empty if
// Options.LogCallerFunc is not enabled.
Func string `json:"function,omitempty"`
}
func (f Formatter) caller() Caller {
// +1 for this frame, +1 for Info/Error.
pc, file, line, ok := runtime.Caller(f.depth + 2)
if !ok {
return Caller{"<unknown>", 0, ""}
}
fn := ""
if f.opts.LogCallerFunc {
if fp := runtime.FuncForPC(pc); fp != nil {
fn = fp.Name()
}
}
return Caller{filepath.Base(file), line, fn}
}
const noValue = "<no-value>"
func (f Formatter) nonStringKey(v any) string {
return fmt.Sprintf("<non-string-key: %s>", f.snippet(v))
}
// snippet produces a short snippet string of an arbitrary value.
func (f Formatter) snippet(v any) string {
const snipLen = 16
snip := f.pretty(v)
if len(snip) > snipLen {
snip = snip[:snipLen]
}
return snip
}
// sanitize ensures that a list of key-value pairs has a value for every key
// (adding a value if needed) and that each key is a string (substituting a key
// if needed).
func (f Formatter) sanitize(kvList []any) []any {
if len(kvList)%2 != 0 {
kvList = append(kvList, noValue)
}
for i := 0; i < len(kvList); i += 2 {
_, ok := kvList[i].(string)
if !ok {
kvList[i] = f.nonStringKey(kvList[i])
}
}
return kvList
}
// startGroup opens a new group scope (basically a sub-struct), which locks all
// the current saved values and starts them anew. This is needed to satisfy
// slog.
func (f *Formatter) startGroup(name string) {
// Unnamed groups are just inlined.
if name == "" {
return
}
n := len(f.groups)
f.groups = append(f.groups[:n:n], groupDef{f.groupName, f.valuesStr})
// Start collecting new values.
f.groupName = name
f.valuesStr = ""
f.values = nil
}
// Init configures this Formatter from runtime info, such as the call depth
// imposed by logr itself.
// Note that this receiver is a pointer, so depth can be saved.
func (f *Formatter) Init(info logr.RuntimeInfo) {
f.depth += info.CallDepth
}
// Enabled checks whether an info message at the given level should be logged.
func (f Formatter) Enabled(level int) bool {
return level <= f.opts.Verbosity
}
// GetDepth returns the current depth of this Formatter. This is useful for
// implementations which do their own caller attribution.
func (f Formatter) GetDepth() int {
return f.depth
}
// FormatInfo renders an Info log message into strings. The prefix will be
// empty when no names were set (via AddNames), or when the output is
// configured for JSON.
func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, argsStr string) {
args := make([]any, 0, 64) // using a constant here impacts perf
prefix = f.prefix
if f.outputFormat == outputJSON {
args = append(args, "logger", prefix)
prefix = ""
}
if f.opts.LogTimestamp {
args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat))
}
if policy := f.opts.LogCaller; policy == All || policy == Info {
args = append(args, "caller", f.caller())
}
if key := *f.opts.LogInfoLevel; key != "" {
args = append(args, key, level)
}
args = append(args, "msg", msg)
return prefix, f.render(args, kvList)
}
// FormatError renders an Error log message into strings. The prefix will be
// empty when no names were set (via AddNames), or when the output is
// configured for JSON.
func (f Formatter) FormatError(err error, msg string, kvList []any) (prefix, argsStr string) {
args := make([]any, 0, 64) // using a constant here impacts perf
prefix = f.prefix
if f.outputFormat == outputJSON {
args = append(args, "logger", prefix)
prefix = ""
}
if f.opts.LogTimestamp {
args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat))
}
if policy := f.opts.LogCaller; policy == All || policy == Error {
args = append(args, "caller", f.caller())
}
args = append(args, "msg", msg)
var loggableErr any
if err != nil {
loggableErr = err.Error()
}
args = append(args, "error", loggableErr)
return prefix, f.render(args, kvList)
}
// AddName appends the specified name. funcr uses '/' characters to separate
// name elements. Callers should not pass '/' in the provided name string, but
// this library does not actually enforce that.
func (f *Formatter) AddName(name string) {
if len(f.prefix) > 0 {
f.prefix += "/"
}
f.prefix += name
}
// AddValues adds key-value pairs to the set of saved values to be logged with
// each log line.
func (f *Formatter) AddValues(kvList []any) {
// Three slice args forces a copy.
n := len(f.values)
f.values = append(f.values[:n:n], kvList...)
vals := f.values
if hook := f.opts.RenderValuesHook; hook != nil {
vals = hook(f.sanitize(vals))
}
// Pre-render values, so we don't have to do it on each Info/Error call.
buf := bytes.NewBuffer(make([]byte, 0, 1024))
f.flatten(buf, vals, true) // escape user-provided keys
f.valuesStr = buf.String()
}
// AddCallDepth increases the number of stack-frames to skip when attributing
// the log line to a file and line.
func (f *Formatter) AddCallDepth(depth int) {
f.depth += depth
}

View File

@ -1,105 +0,0 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package funcr
import (
"context"
"log/slog"
"github.com/go-logr/logr"
)
var _ logr.SlogSink = &fnlogger{}
const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList)
return true
})
if record.Level >= slog.LevelError {
l.WithCallDepth(extraSlogSinkDepth).Error(nil, record.Message, kvList...)
} else {
level := l.levelFromSlog(record.Level)
l.WithCallDepth(extraSlogSinkDepth).Info(level, record.Message, kvList...)
}
return nil
}
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList)
}
l.AddValues(kvList)
return &l
}
func (l fnlogger) WithGroup(name string) logr.SlogSink {
l.startGroup(name)
return &l
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs)
}
if attr.Key == "" {
// slog says we have to inline these
kvList = append(kvList, grpKVs...)
} else {
kvList = append(kvList, attr.Key, PseudoStruct(grpKVs))
}
} else if attr.Key != "" {
kvList = append(kvList, attr.Key, attrVal.Any())
}
return kvList
}
// levelFromSlog adjusts the level by the logger's verbosity and negates it.
// It ensures that the result is >= 0. This is necessary because the result is
// passed to a LogSink and that API did not historically document whether
// levels could be negative or what that meant.
//
// Some example usage:
//
// logrV0 := getMyLogger()
// logrV2 := logrV0.V(2)
// slogV2 := slog.New(logr.ToSlogHandler(logrV2))
// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2)
// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0)
func (l fnlogger) levelFromSlog(level slog.Level) int {
result := -level
if result < 0 {
result = 0 // because LogSink doesn't expect negative V levels
}
return int(result)
}

View File

@ -1,520 +0,0 @@
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This design derives from Dave Cheney's blog:
// http://dave.cheney.net/2015/11/05/lets-talk-about-logging
// Package logr defines a general-purpose logging API and abstract interfaces
// to back that API. Packages in the Go ecosystem can depend on this package,
// while callers can implement logging with whatever backend is appropriate.
//
// # Usage
//
// Logging is done using a Logger instance. Logger is a concrete type with
// methods, which defers the actual logging to a LogSink interface. The main
// methods of Logger are Info() and Error(). Arguments to Info() and Error()
// are key/value pairs rather than printf-style formatted strings, emphasizing
// "structured logging".
//
// With Go's standard log package, we might write:
//
// log.Printf("setting target value %s", targetValue)
//
// With logr's structured logging, we'd write:
//
// logger.Info("setting target", "value", targetValue)
//
// Errors are much the same. Instead of:
//
// log.Printf("failed to open the pod bay door for user %s: %v", user, err)
//
// We'd write:
//
// logger.Error(err, "failed to open the pod bay door", "user", user)
//
// Info() and Error() are very similar, but they are separate methods so that
// LogSink implementations can choose to do things like attach additional
// information (such as stack traces) on calls to Error(). Error() messages are
// always logged, regardless of the current verbosity. If there is no error
// instance available, passing nil is valid.
//
// # Verbosity
//
// Often we want to log information only when the application in "verbose
// mode". To write log lines that are more verbose, Logger has a V() method.
// The higher the V-level of a log line, the less critical it is considered.
// Log-lines with V-levels that are not enabled (as per the LogSink) will not
// be written. Level V(0) is the default, and logger.V(0).Info() has the same
// meaning as logger.Info(). Negative V-levels have the same meaning as V(0).
// Error messages do not have a verbosity level and are always logged.
//
// Where we might have written:
//
// if flVerbose >= 2 {
// log.Printf("an unusual thing happened")
// }
//
// We can write:
//
// logger.V(2).Info("an unusual thing happened")
//
// # Logger Names
//
// Logger instances can have name strings so that all messages logged through
// that instance have additional context. For example, you might want to add
// a subsystem name:
//
// logger.WithName("compactor").Info("started", "time", time.Now())
//
// The WithName() method returns a new Logger, which can be passed to
// constructors or other functions for further use. Repeated use of WithName()
// will accumulate name "segments". These name segments will be joined in some
// way by the LogSink implementation. It is strongly recommended that name
// segments contain simple identifiers (letters, digits, and hyphen), and do
// not contain characters that could muddle the log output or confuse the
// joining operation (e.g. whitespace, commas, periods, slashes, brackets,
// quotes, etc).
//
// # Saved Values
//
// Logger instances can store any number of key/value pairs, which will be
// logged alongside all messages logged through that instance. For example,
// you might want to create a Logger instance per managed object:
//
// With the standard log package, we might write:
//
// log.Printf("decided to set field foo to value %q for object %s/%s",
// targetValue, object.Namespace, object.Name)
//
// With logr we'd write:
//
// // Elsewhere: set up the logger to log the object name.
// obj.logger = mainLogger.WithValues(
// "name", obj.name, "namespace", obj.namespace)
//
// // later on...
// obj.logger.Info("setting foo", "value", targetValue)
//
// # Best Practices
//
// Logger has very few hard rules, with the goal that LogSink implementations
// might have a lot of freedom to differentiate. There are, however, some
// things to consider.
//
// The log message consists of a constant message attached to the log line.
// This should generally be a simple description of what's occurring, and should
// never be a format string. Variable information can then be attached using
// named values.
//
// Keys are arbitrary strings, but should generally be constant values. Values
// may be any Go value, but how the value is formatted is determined by the
// LogSink implementation.
//
// Logger instances are meant to be passed around by value. Code that receives
// such a value can call its methods without having to check whether the
// instance is ready for use.
//
// The zero logger (= Logger{}) is identical to Discard() and discards all log
// entries. Code that receives a Logger by value can simply call it, the methods
// will never crash. For cases where passing a logger is optional, a pointer to Logger
// should be used.
//
// # Key Naming Conventions
//
// Keys are not strictly required to conform to any specification or regex, but
// it is recommended that they:
// - be human-readable and meaningful (not auto-generated or simple ordinals)
// - be constant (not dependent on input data)
// - contain only printable characters
// - not contain whitespace or punctuation
// - use lower case for simple keys and lowerCamelCase for more complex ones
//
// These guidelines help ensure that log data is processed properly regardless
// of the log implementation. For example, log implementations will try to
// output JSON data or will store data for later database (e.g. SQL) queries.
//
// While users are generally free to use key names of their choice, it's
// generally best to avoid using the following keys, as they're frequently used
// by implementations:
// - "caller": the calling information (file/line) of a particular log line
// - "error": the underlying error value in the `Error` method
// - "level": the log level
// - "logger": the name of the associated logger
// - "msg": the log message
// - "stacktrace": the stack trace associated with a particular log line or
// error (often from the `Error` message)
// - "ts": the timestamp for a log line
//
// Implementations are encouraged to make use of these keys to represent the
// above concepts, when necessary (for example, in a pure-JSON output form, it
// would be necessary to represent at least message and timestamp as ordinary
// named values).
//
// # Break Glass
//
// Implementations may choose to give callers access to the underlying
// logging implementation. The recommended pattern for this is:
//
// // Underlier exposes access to the underlying logging implementation.
// // Since callers only have a logr.Logger, they have to know which
// // implementation is in use, so this interface is less of an abstraction
// // and more of way to test type conversion.
// type Underlier interface {
// GetUnderlying() <underlying-type>
// }
//
// Logger grants access to the sink to enable type assertions like this:
//
// func DoSomethingWithImpl(log logr.Logger) {
// if underlier, ok := log.GetSink().(impl.Underlier); ok {
// implLogger := underlier.GetUnderlying()
// ...
// }
// }
//
// Custom `With*` functions can be implemented by copying the complete
// Logger struct and replacing the sink in the copy:
//
// // WithFooBar changes the foobar parameter in the log sink and returns a
// // new logger with that modified sink. It does nothing for loggers where
// // the sink doesn't support that parameter.
// func WithFoobar(log logr.Logger, foobar int) logr.Logger {
// if foobarLogSink, ok := log.GetSink().(FoobarSink); ok {
// log = log.WithSink(foobarLogSink.WithFooBar(foobar))
// }
// return log
// }
//
// Don't use New to construct a new Logger with a LogSink retrieved from an
// existing Logger. Source code attribution might not work correctly and
// unexported fields in Logger get lost.
//
// Beware that the same LogSink instance may be shared by different logger
// instances. Calling functions that modify the LogSink will affect all of
// those.
package logr
// New returns a new Logger instance. This is primarily used by libraries
// implementing LogSink, rather than end users. Passing a nil sink will create
// a Logger which discards all log lines.
func New(sink LogSink) Logger {
logger := Logger{}
logger.setSink(sink)
if sink != nil {
sink.Init(runtimeInfo)
}
return logger
}
// setSink stores the sink and updates any related fields. It mutates the
// logger and thus is only safe to use for loggers that are not currently being
// used concurrently.
func (l *Logger) setSink(sink LogSink) {
l.sink = sink
}
// GetSink returns the stored sink.
func (l Logger) GetSink() LogSink {
return l.sink
}
// WithSink returns a copy of the logger with the new sink.
func (l Logger) WithSink(sink LogSink) Logger {
l.setSink(sink)
return l
}
// Logger is an interface to an abstract logging implementation. This is a
// concrete type for performance reasons, but all the real work is passed on to
// a LogSink. Implementations of LogSink should provide their own constructors
// that return Logger, not LogSink.
//
// The underlying sink can be accessed through GetSink and be modified through
// WithSink. This enables the implementation of custom extensions (see "Break
// Glass" in the package documentation). Normally the sink should be used only
// indirectly.
type Logger struct {
sink LogSink
level int
}
// Enabled tests whether this Logger is enabled. For example, commandline
// flags might be used to set the logging verbosity and disable some info logs.
func (l Logger) Enabled() bool {
// Some implementations of LogSink look at the caller in Enabled (e.g.
// different verbosity levels per package or file), but we only pass one
// CallDepth in (via Init). This means that all calls from Logger to the
// LogSink's Enabled, Info, and Error methods must have the same number of
// frames. In other words, Logger methods can't call other Logger methods
// which call these LogSink methods unless we do it the same in all paths.
return l.sink != nil && l.sink.Enabled(l.level)
}
// Info logs a non-error message with the given key/value pairs as context.
//
// The msg argument should be used to add some constant description to the log
// line. The key/value pairs can then be used to add additional variable
// information. The key/value pairs must alternate string keys and arbitrary
// values.
func (l Logger) Info(msg string, keysAndValues ...any) {
if l.sink == nil {
return
}
if l.sink.Enabled(l.level) { // see comment in Enabled
if withHelper, ok := l.sink.(CallStackHelperLogSink); ok {
withHelper.GetCallStackHelper()()
}
l.sink.Info(l.level, msg, keysAndValues...)
}
}
// Error logs an error, with the given message and key/value pairs as context.
// It functions similarly to Info, but may have unique behavior, and should be
// preferred for logging errors (see the package documentations for more
// information). The log message will always be emitted, regardless of
// verbosity level.
//
// The msg argument should be used to add context to any underlying error,
// while the err argument should be used to attach the actual error that
// triggered this log line, if present. The err parameter is optional
// and nil may be passed instead of an error instance.
func (l Logger) Error(err error, msg string, keysAndValues ...any) {
if l.sink == nil {
return
}
if withHelper, ok := l.sink.(CallStackHelperLogSink); ok {
withHelper.GetCallStackHelper()()
}
l.sink.Error(err, msg, keysAndValues...)
}
// V returns a new Logger instance for a specific verbosity level, relative to
// this Logger. In other words, V-levels are additive. A higher verbosity
// level means a log message is less important. Negative V-levels are treated
// as 0.
func (l Logger) V(level int) Logger {
if l.sink == nil {
return l
}
if level < 0 {
level = 0
}
l.level += level
return l
}
// GetV returns the verbosity level of the logger. If the logger's LogSink is
// nil as in the Discard logger, this will always return 0.
func (l Logger) GetV() int {
// 0 if l.sink nil because of the if check in V above.
return l.level
}
// WithValues returns a new Logger instance with additional key/value pairs.
// See Info for documentation on how key/value pairs work.
func (l Logger) WithValues(keysAndValues ...any) Logger {
if l.sink == nil {
return l
}
l.setSink(l.sink.WithValues(keysAndValues...))
return l
}
// WithName returns a new Logger instance with the specified name element added
// to the Logger's name. Successive calls with WithName append additional
// suffixes to the Logger's name. It's strongly recommended that name segments
// contain only letters, digits, and hyphens (see the package documentation for
// more information).
func (l Logger) WithName(name string) Logger {
if l.sink == nil {
return l
}
l.setSink(l.sink.WithName(name))
return l
}
// WithCallDepth returns a Logger instance that offsets the call stack by the
// specified number of frames when logging call site information, if possible.
// This is useful for users who have helper functions between the "real" call
// site and the actual calls to Logger methods. If depth is 0 the attribution
// should be to the direct caller of this function. If depth is 1 the
// attribution should skip 1 call frame, and so on. Successive calls to this
// are additive.
//
// If the underlying log implementation supports a WithCallDepth(int) method,
// it will be called and the result returned. If the implementation does not
// support CallDepthLogSink, the original Logger will be returned.
//
// To skip one level, WithCallStackHelper() should be used instead of
// WithCallDepth(1) because it works with implementions that support the
// CallDepthLogSink and/or CallStackHelperLogSink interfaces.
func (l Logger) WithCallDepth(depth int) Logger {
if l.sink == nil {
return l
}
if withCallDepth, ok := l.sink.(CallDepthLogSink); ok {
l.setSink(withCallDepth.WithCallDepth(depth))
}
return l
}
// WithCallStackHelper returns a new Logger instance that skips the direct
// caller when logging call site information, if possible. This is useful for
// users who have helper functions between the "real" call site and the actual
// calls to Logger methods and want to support loggers which depend on marking
// each individual helper function, like loggers based on testing.T.
//
// In addition to using that new logger instance, callers also must call the
// returned function.
//
// If the underlying log implementation supports a WithCallDepth(int) method,
// WithCallDepth(1) will be called to produce a new logger. If it supports a
// WithCallStackHelper() method, that will be also called. If the
// implementation does not support either of these, the original Logger will be
// returned.
func (l Logger) WithCallStackHelper() (func(), Logger) {
if l.sink == nil {
return func() {}, l
}
var helper func()
if withCallDepth, ok := l.sink.(CallDepthLogSink); ok {
l.setSink(withCallDepth.WithCallDepth(1))
}
if withHelper, ok := l.sink.(CallStackHelperLogSink); ok {
helper = withHelper.GetCallStackHelper()
} else {
helper = func() {}
}
return helper, l
}
// IsZero returns true if this logger is an uninitialized zero value
func (l Logger) IsZero() bool {
return l.sink == nil
}
// RuntimeInfo holds information that the logr "core" library knows which
// LogSinks might want to know.
type RuntimeInfo struct {
// CallDepth is the number of call frames the logr library adds between the
// end-user and the LogSink. LogSink implementations which choose to print
// the original logging site (e.g. file & line) should climb this many
// additional frames to find it.
CallDepth int
}
// runtimeInfo is a static global. It must not be changed at run time.
var runtimeInfo = RuntimeInfo{
CallDepth: 1,
}
// LogSink represents a logging implementation. End-users will generally not
// interact with this type.
type LogSink interface {
// Init receives optional information about the logr library for LogSink
// implementations that need it.
Init(info RuntimeInfo)
// Enabled tests whether this LogSink is enabled at the specified V-level.
// For example, commandline flags might be used to set the logging
// verbosity and disable some info logs.
Enabled(level int) bool
// Info logs a non-error message with the given key/value pairs as context.
// The level argument is provided for optional logging. This method will
// only be called when Enabled(level) is true. See Logger.Info for more
// details.
Info(level int, msg string, keysAndValues ...any)
// Error logs an error, with the given message and key/value pairs as
// context. See Logger.Error for more details.
Error(err error, msg string, keysAndValues ...any)
// WithValues returns a new LogSink with additional key/value pairs. See
// Logger.WithValues for more details.
WithValues(keysAndValues ...any) LogSink
// WithName returns a new LogSink with the specified name appended. See
// Logger.WithName for more details.
WithName(name string) LogSink
}
// CallDepthLogSink represents a LogSink that knows how to climb the call stack
// to identify the original call site and can offset the depth by a specified
// number of frames. This is useful for users who have helper functions
// between the "real" call site and the actual calls to Logger methods.
// Implementations that log information about the call site (such as file,
// function, or line) would otherwise log information about the intermediate
// helper functions.
//
// This is an optional interface and implementations are not required to
// support it.
type CallDepthLogSink interface {
// WithCallDepth returns a LogSink that will offset the call
// stack by the specified number of frames when logging call
// site information.
//
// If depth is 0, the LogSink should skip exactly the number
// of call frames defined in RuntimeInfo.CallDepth when Info
// or Error are called, i.e. the attribution should be to the
// direct caller of Logger.Info or Logger.Error.
//
// If depth is 1 the attribution should skip 1 call frame, and so on.
// Successive calls to this are additive.
WithCallDepth(depth int) LogSink
}
// CallStackHelperLogSink represents a LogSink that knows how to climb
// the call stack to identify the original call site and can skip
// intermediate helper functions if they mark themselves as
// helper. Go's testing package uses that approach.
//
// This is useful for users who have helper functions between the
// "real" call site and the actual calls to Logger methods.
// Implementations that log information about the call site (such as
// file, function, or line) would otherwise log information about the
// intermediate helper functions.
//
// This is an optional interface and implementations are not required
// to support it. Implementations that choose to support this must not
// simply implement it as WithCallDepth(1), because
// Logger.WithCallStackHelper will call both methods if they are
// present. This should only be implemented for LogSinks that actually
// need it, as with testing.T.
type CallStackHelperLogSink interface {
// GetCallStackHelper returns a function that must be called
// to mark the direct caller as helper function when logging
// call site information.
GetCallStackHelper() func()
}
// Marshaler is an optional interface that logged values may choose to
// implement. Loggers with structured output, such as JSON, should
// log the object return by the MarshalLog method instead of the
// original value.
type Marshaler interface {
// MarshalLog can be used to:
// - ensure that structs are not logged as strings when the original
// value has a String method: return a different type without a
// String method
// - select which fields of a complex type should get logged:
// return a simpler struct with fewer fields
// - log unexported fields: return a different struct
// with exported fields
//
// It may return any value of any type.
MarshalLog() any
}

View File

@ -1,192 +0,0 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
)
type slogHandler struct {
// May be nil, in which case all logs get discarded.
sink LogSink
// Non-nil if sink is non-nil and implements SlogSink.
slogSink SlogSink
// groupPrefix collects values from WithGroup calls. It gets added as
// prefix to value keys when handling a log record.
groupPrefix string
// levelBias can be set when constructing the handler to influence the
// slog.Level of log records. A positive levelBias reduces the
// slog.Level value. slog has no API to influence this value after the
// handler got created, so it can only be set indirectly through
// Logger.V.
levelBias slog.Level
}
var _ slog.Handler = &slogHandler{}
// groupSeparator is used to concatenate WithGroup names and attribute keys.
const groupSeparator = "."
// GetLevel is used for black box unit testing.
func (l *slogHandler) GetLevel() slog.Level {
return l.levelBias
}
func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level)))
}
func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error {
if l.slogSink != nil {
// Only adjust verbosity level of log entries < slog.LevelError.
if record.Level < slog.LevelError {
record.Level -= l.levelBias
}
return l.slogSink.Handle(ctx, record)
}
// No need to check for nil sink here because Handle will only be called
// when Enabled returned true.
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, l.groupPrefix, kvList)
return true
})
if record.Level >= slog.LevelError {
l.sinkWithCallDepth().Error(nil, record.Message, kvList...)
} else {
level := l.levelFromSlog(record.Level)
l.sinkWithCallDepth().Info(level, record.Message, kvList...)
}
return nil
}
// sinkWithCallDepth adjusts the stack unwinding so that when Error or Info
// are called by Handle, code in slog gets skipped.
//
// This offset currently (Go 1.21.0) works for calls through
// slog.New(ToSlogHandler(...)). There's no guarantee that the call
// chain won't change. Wrapping the handler will also break unwinding. It's
// still better than not adjusting at all....
//
// This cannot be done when constructing the handler because FromSlogHandler needs
// access to the original sink without this adjustment. A second copy would
// work, but then WithAttrs would have to be called for both of them.
func (l *slogHandler) sinkWithCallDepth() LogSink {
if sink, ok := l.sink.(CallDepthLogSink); ok {
return sink.WithCallDepth(2)
}
return l.sink
}
func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if l.sink == nil || len(attrs) == 0 {
return l
}
clone := *l
if l.slogSink != nil {
clone.slogSink = l.slogSink.WithAttrs(attrs)
clone.sink = clone.slogSink
} else {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, l.groupPrefix, kvList)
}
clone.sink = l.sink.WithValues(kvList...)
}
return &clone
}
func (l *slogHandler) WithGroup(name string) slog.Handler {
if l.sink == nil {
return l
}
if name == "" {
// slog says to inline empty groups
return l
}
clone := *l
if l.slogSink != nil {
clone.slogSink = l.slogSink.WithGroup(name)
clone.sink = clone.slogSink
} else {
clone.groupPrefix = addPrefix(clone.groupPrefix, name)
}
return &clone
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
prefix := groupPrefix
if attr.Key != "" {
prefix = addPrefix(groupPrefix, attr.Key)
}
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, prefix, grpKVs)
}
kvList = append(kvList, grpKVs...)
} else if attr.Key != "" {
kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any())
}
return kvList
}
func addPrefix(prefix, name string) string {
if prefix == "" {
return name
}
if name == "" {
return prefix
}
return prefix + groupSeparator + name
}
// levelFromSlog adjusts the level by the logger's verbosity and negates it.
// It ensures that the result is >= 0. This is necessary because the result is
// passed to a LogSink and that API did not historically document whether
// levels could be negative or what that meant.
//
// Some example usage:
//
// logrV0 := getMyLogger()
// logrV2 := logrV0.V(2)
// slogV2 := slog.New(logr.ToSlogHandler(logrV2))
// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2)
// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0)
func (l *slogHandler) levelFromSlog(level slog.Level) int {
result := -level
result += l.levelBias // in case the original Logger had a V level
if result < 0 {
result = 0 // because LogSink doesn't expect negative V levels
}
return int(result)
}

View File

@ -1,100 +0,0 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
)
// FromSlogHandler returns a Logger which writes to the slog.Handler.
//
// The logr verbosity level is mapped to slog levels such that V(0) becomes
// slog.LevelInfo and V(4) becomes slog.LevelDebug.
func FromSlogHandler(handler slog.Handler) Logger {
if handler, ok := handler.(*slogHandler); ok {
if handler.sink == nil {
return Discard()
}
return New(handler.sink).V(int(handler.levelBias))
}
return New(&slogSink{handler: handler})
}
// ToSlogHandler returns a slog.Handler which writes to the same sink as the Logger.
//
// The returned logger writes all records with level >= slog.LevelError as
// error log entries with LogSink.Error, regardless of the verbosity level of
// the Logger:
//
// logger := <some Logger with 0 as verbosity level>
// slog.New(ToSlogHandler(logger.V(10))).Error(...) -> logSink.Error(...)
//
// The level of all other records gets reduced by the verbosity
// level of the Logger and the result is negated. If it happens
// to be negative, then it gets replaced by zero because a LogSink
// is not expected to handled negative levels:
//
// slog.New(ToSlogHandler(logger)).Debug(...) -> logger.GetSink().Info(level=4, ...)
// slog.New(ToSlogHandler(logger)).Warning(...) -> logger.GetSink().Info(level=0, ...)
// slog.New(ToSlogHandler(logger)).Info(...) -> logger.GetSink().Info(level=0, ...)
// slog.New(ToSlogHandler(logger.V(4))).Info(...) -> logger.GetSink().Info(level=4, ...)
func ToSlogHandler(logger Logger) slog.Handler {
if sink, ok := logger.GetSink().(*slogSink); ok && logger.GetV() == 0 {
return sink.handler
}
handler := &slogHandler{sink: logger.GetSink(), levelBias: slog.Level(logger.GetV())}
if slogSink, ok := handler.sink.(SlogSink); ok {
handler.slogSink = slogSink
}
return handler
}
// SlogSink is an optional interface that a LogSink can implement to support
// logging through the slog.Logger or slog.Handler APIs better. It then should
// also support special slog values like slog.Group. When used as a
// slog.Handler, the advantages are:
//
// - stack unwinding gets avoided in favor of logging the pre-recorded PC,
// as intended by slog
// - proper grouping of key/value pairs via WithGroup
// - verbosity levels > slog.LevelInfo can be recorded
// - less overhead
//
// Both APIs (Logger and slog.Logger/Handler) then are supported equally
// well. Developers can pick whatever API suits them better and/or mix
// packages which use either API in the same binary with a common logging
// implementation.
//
// This interface is necessary because the type implementing the LogSink
// interface cannot also implement the slog.Handler interface due to the
// different prototype of the common Enabled method.
//
// An implementation could support both interfaces in two different types, but then
// additional interfaces would be needed to convert between those types in FromSlogHandler
// and ToSlogHandler.
type SlogSink interface {
LogSink
Handle(ctx context.Context, record slog.Record) error
WithAttrs(attrs []slog.Attr) SlogSink
WithGroup(name string) SlogSink
}

View File

@ -1,120 +0,0 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
"runtime"
"time"
)
var (
_ LogSink = &slogSink{}
_ CallDepthLogSink = &slogSink{}
_ Underlier = &slogSink{}
)
// Underlier is implemented by the LogSink returned by NewFromLogHandler.
type Underlier interface {
// GetUnderlying returns the Handler used by the LogSink.
GetUnderlying() slog.Handler
}
const (
// nameKey is used to log the `WithName` values as an additional attribute.
nameKey = "logger"
// errKey is used to log the error parameter of Error as an additional attribute.
errKey = "err"
)
type slogSink struct {
callDepth int
name string
handler slog.Handler
}
func (l *slogSink) Init(info RuntimeInfo) {
l.callDepth = info.CallDepth
}
func (l *slogSink) GetUnderlying() slog.Handler {
return l.handler
}
func (l *slogSink) WithCallDepth(depth int) LogSink {
newLogger := *l
newLogger.callDepth += depth
return &newLogger
}
func (l *slogSink) Enabled(level int) bool {
return l.handler.Enabled(context.Background(), slog.Level(-level))
}
func (l *slogSink) Info(level int, msg string, kvList ...interface{}) {
l.log(nil, msg, slog.Level(-level), kvList...)
}
func (l *slogSink) Error(err error, msg string, kvList ...interface{}) {
l.log(err, msg, slog.LevelError, kvList...)
}
func (l *slogSink) log(err error, msg string, level slog.Level, kvList ...interface{}) {
var pcs [1]uintptr
// skip runtime.Callers, this function, Info/Error, and all helper functions above that.
runtime.Callers(3+l.callDepth, pcs[:])
record := slog.NewRecord(time.Now(), level, msg, pcs[0])
if l.name != "" {
record.AddAttrs(slog.String(nameKey, l.name))
}
if err != nil {
record.AddAttrs(slog.Any(errKey, err))
}
record.Add(kvList...)
_ = l.handler.Handle(context.Background(), record)
}
func (l slogSink) WithName(name string) LogSink {
if l.name != "" {
l.name += "/"
}
l.name += name
return &l
}
func (l slogSink) WithValues(kvList ...interface{}) LogSink {
l.handler = l.handler.WithAttrs(kvListToAttrs(kvList...))
return &l
}
func kvListToAttrs(kvList ...interface{}) []slog.Attr {
// We don't need the record itself, only its Add method.
record := slog.NewRecord(time.Time{}, 0, "", 0)
record.Add(kvList...)
attrs := make([]slog.Attr, 0, record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
attrs = append(attrs, attr)
return true
})
return attrs
}

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,6 +0,0 @@
# Minimal Go logging using logr and Go's standard library
[![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/stdr.svg)](https://pkg.go.dev/github.com/go-logr/stdr)
This package implements the [logr interface](https://github.com/go-logr/logr)
in terms of Go's standard log package(https://pkg.go.dev/log).

View File

@ -1,170 +0,0 @@
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package stdr implements github.com/go-logr/logr.Logger in terms of
// Go's standard log package.
package stdr
import (
"log"
"os"
"github.com/go-logr/logr"
"github.com/go-logr/logr/funcr"
)
// The global verbosity level. See SetVerbosity().
var globalVerbosity int
// SetVerbosity sets the global level against which all info logs will be
// compared. If this is greater than or equal to the "V" of the logger, the
// message will be logged. A higher value here means more logs will be written.
// The previous verbosity value is returned. This is not concurrent-safe -
// callers must be sure to call it from only one goroutine.
func SetVerbosity(v int) int {
old := globalVerbosity
globalVerbosity = v
return old
}
// New returns a logr.Logger which is implemented by Go's standard log package,
// or something like it. If std is nil, this will use a default logger
// instead.
//
// Example: stdr.New(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)))
func New(std StdLogger) logr.Logger {
return NewWithOptions(std, Options{})
}
// NewWithOptions returns a logr.Logger which is implemented by Go's standard
// log package, or something like it. See New for details.
func NewWithOptions(std StdLogger, opts Options) logr.Logger {
if std == nil {
// Go's log.Default() is only available in 1.16 and higher.
std = log.New(os.Stderr, "", log.LstdFlags)
}
if opts.Depth < 0 {
opts.Depth = 0
}
fopts := funcr.Options{
LogCaller: funcr.MessageClass(opts.LogCaller),
}
sl := &logger{
Formatter: funcr.NewFormatter(fopts),
std: std,
}
// For skipping our own logger.Info/Error.
sl.Formatter.AddCallDepth(1 + opts.Depth)
return logr.New(sl)
}
// Options carries parameters which influence the way logs are generated.
type Options struct {
// Depth biases the assumed number of call frames to the "true" caller.
// This is useful when the calling code calls a function which then calls
// stdr (e.g. a logging shim to another API). Values less than zero will
// be treated as zero.
Depth int
// LogCaller tells stdr to add a "caller" key to some or all log lines.
// Go's log package has options to log this natively, too.
LogCaller MessageClass
// TODO: add an option to log the date/time
}
// MessageClass indicates which category or categories of messages to consider.
type MessageClass int
const (
// None ignores all message classes.
None MessageClass = iota
// All considers all message classes.
All
// Info only considers info messages.
Info
// Error only considers error messages.
Error
)
// StdLogger is the subset of the Go stdlib log.Logger API that is needed for
// this adapter.
type StdLogger interface {
// Output is the same as log.Output and log.Logger.Output.
Output(calldepth int, logline string) error
}
type logger struct {
funcr.Formatter
std StdLogger
}
var _ logr.LogSink = &logger{}
var _ logr.CallDepthLogSink = &logger{}
func (l logger) Enabled(level int) bool {
return globalVerbosity >= level
}
func (l logger) Info(level int, msg string, kvList ...interface{}) {
prefix, args := l.FormatInfo(level, msg, kvList)
if prefix != "" {
args = prefix + ": " + args
}
_ = l.std.Output(l.Formatter.GetDepth()+1, args)
}
func (l logger) Error(err error, msg string, kvList ...interface{}) {
prefix, args := l.FormatError(err, msg, kvList)
if prefix != "" {
args = prefix + ": " + args
}
_ = l.std.Output(l.Formatter.GetDepth()+1, args)
}
func (l logger) WithName(name string) logr.LogSink {
l.Formatter.AddName(name)
return &l
}
func (l logger) WithValues(kvList ...interface{}) logr.LogSink {
l.Formatter.AddValues(kvList)
return &l
}
func (l logger) WithCallDepth(depth int) logr.LogSink {
l.Formatter.AddCallDepth(depth)
return &l
}
// Underlier exposes access to the underlying logging implementation. Since
// callers only have a logr.Logger, they have to know which implementation is
// in use, so this interface is less of an abstraction and more of way to test
// type conversion.
type Underlier interface {
GetUnderlying() StdLogger
}
// GetUnderlying returns the StdLogger underneath this logger. Since StdLogger
// is itself an interface, the result may or may not be a Go log.Logger.
func (l logger) GetUnderlying() StdLogger {
return l.std
}

View File

@ -1,6 +0,0 @@
# Ignore binaries without extension
//example/client/client
//example/server/server
//internal/v2/fakes2av2_server/fakes2av2_server
.idea/

View File

@ -1,93 +0,0 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct also applies outside the project spaces when the Project
Steward has a reasonable belief that an individual's behavior may have a
negative impact on the project or its community.
## Conflict Resolution
We do not believe that all conflict is bad; healthy debate and disagreement
often yield positive results. However, it is never okay to be disrespectful or
to engage in behavior that violates the projects code of conduct.
If you see someone violating the code of conduct, you are encouraged to address
the behavior directly with those involved. Many issues can be resolved quickly
and easily, and this gives people more control over the outcome of their
dispute. If you are unable to resolve the matter for any reason, or if the
behavior is threatening or harassing, report it. We are dedicated to providing
an environment where participants feel welcome and safe.
Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
Project Steward(s) for *[PROJECT NAME]*. It is the Project Stewards duty to
receive and address reported violations of the code of conduct. They will then
work with a committee consisting of representatives from the Open Source
Programs Office and the Google Open Source Strategy team. If for any reason you
are uncomfortable reaching out to the Project Steward, please email
opensource@google.com.
We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is taken.
The identity of the reporter will be omitted from the details of the report
supplied to the accused. In potentially harmful situations, such as ongoing
harassment or threats to anyone's safety, we may take action without notice.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

View File

@ -1,29 +0,0 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement (CLA). You (or your employer) retain the copyright to your
contribution; this simply gives us permission to use and redistribute your
contributions as part of the project. Head over to
<https://cla.developers.google.com/> to see your current agreements on file or
to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,14 +0,0 @@
# Secure Session Agent Client Libraries
The Secure Session Agent is a service that enables a workload to offload select
operations from the mTLS handshake and protects a workload's private key
material from exfiltration. Specifically, the workload asks the Secure Session
Agent for the TLS configuration to use during the handshake, to perform private
key operations, and to validate the peer certificate chain. The Secure Session
Agent's client libraries enable applications to communicate with the Secure
Session Agent during the TLS handshake, and to encrypt traffic to the peer
after the TLS handshake is complete.
This repository contains the source code for the Secure Session Agent's Go
client libraries, which allow gRPC and HTTP Go applications to use the Secure Session
Agent.

View File

@ -1,167 +0,0 @@
/*
*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package fallback provides default implementations of fallback options when S2A fails.
package fallback
import (
"context"
"crypto/tls"
"fmt"
"net"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
)
const (
alpnProtoStrH2 = "h2"
alpnProtoStrHTTP = "http/1.1"
defaultHTTPSPort = "443"
)
// FallbackTLSConfigGRPC is a tls.Config used by the DefaultFallbackClientHandshakeFunc function.
// It supports GRPC use case, thus the alpn is set to 'h2'.
var FallbackTLSConfigGRPC = tls.Config{
MinVersion: tls.VersionTLS13,
ClientSessionCache: nil,
NextProtos: []string{alpnProtoStrH2},
}
// FallbackTLSConfigHTTP is a tls.Config used by the DefaultFallbackDialerAndAddress func.
// It supports the HTTP use case and the alpn is set to both 'http/1.1' and 'h2'.
var FallbackTLSConfigHTTP = tls.Config{
MinVersion: tls.VersionTLS13,
ClientSessionCache: nil,
NextProtos: []string{alpnProtoStrH2, alpnProtoStrHTTP},
}
// ClientHandshake establishes a TLS connection and returns it, plus its auth info.
// Inputs:
//
// targetServer: the server attempted with S2A.
// conn: the tcp connection to the server at address targetServer that was passed into S2A's ClientHandshake func.
// If fallback is successful, the `conn` should be closed.
// err: the error encountered when performing the client-side TLS handshake with S2A.
type ClientHandshake func(ctx context.Context, targetServer string, conn net.Conn, err error) (net.Conn, credentials.AuthInfo, error)
// DefaultFallbackClientHandshakeFunc returns a ClientHandshake function,
// which establishes a TLS connection to the provided fallbackAddr, returns the new connection and its auth info.
// Example use:
//
// transportCreds, _ = s2a.NewClientCreds(&s2a.ClientOptions{
// S2AAddress: s2aAddress,
// FallbackOpts: &s2a.FallbackOptions{ // optional
// FallbackClientHandshakeFunc: fallback.DefaultFallbackClientHandshakeFunc(fallbackAddr),
// },
// })
//
// The fallback server's certificate must be verifiable using OS root store.
// The fallbackAddr is expected to be a network address, e.g. example.com:port. If port is not specified,
// it uses default port 443.
// In the returned function's TLS config, ClientSessionCache is explicitly set to nil to disable TLS resumption,
// and min TLS version is set to 1.3.
func DefaultFallbackClientHandshakeFunc(fallbackAddr string) (ClientHandshake, error) {
var fallbackDialer = tls.Dialer{Config: &FallbackTLSConfigGRPC}
return defaultFallbackClientHandshakeFuncInternal(fallbackAddr, fallbackDialer.DialContext)
}
func defaultFallbackClientHandshakeFuncInternal(fallbackAddr string, dialContextFunc func(context.Context, string, string) (net.Conn, error)) (ClientHandshake, error) {
fallbackServerAddr, err := processFallbackAddr(fallbackAddr)
if err != nil {
if grpclog.V(1) {
grpclog.Infof("error processing fallback address [%s]: %v", fallbackAddr, err)
}
return nil, err
}
return func(ctx context.Context, targetServer string, conn net.Conn, s2aErr error) (net.Conn, credentials.AuthInfo, error) {
fbConn, fbErr := dialContextFunc(ctx, "tcp", fallbackServerAddr)
if fbErr != nil {
grpclog.Infof("dialing to fallback server %s failed: %v", fallbackServerAddr, fbErr)
return nil, nil, fmt.Errorf("dialing to fallback server %s failed: %v; S2A client handshake with %s error: %w", fallbackServerAddr, fbErr, targetServer, s2aErr)
}
tc, success := fbConn.(*tls.Conn)
if !success {
grpclog.Infof("the connection with fallback server is expected to be tls but isn't")
return nil, nil, fmt.Errorf("the connection with fallback server is expected to be tls but isn't; S2A client handshake with %s error: %w", targetServer, s2aErr)
}
tlsInfo := credentials.TLSInfo{
State: tc.ConnectionState(),
CommonAuthInfo: credentials.CommonAuthInfo{
SecurityLevel: credentials.PrivacyAndIntegrity,
},
}
if grpclog.V(1) {
grpclog.Infof("ConnectionState.NegotiatedProtocol: %v", tc.ConnectionState().NegotiatedProtocol)
grpclog.Infof("ConnectionState.HandshakeComplete: %v", tc.ConnectionState().HandshakeComplete)
grpclog.Infof("ConnectionState.ServerName: %v", tc.ConnectionState().ServerName)
}
conn.Close()
return fbConn, tlsInfo, nil
}, nil
}
// DefaultFallbackDialerAndAddress returns a TLS dialer and the network address to dial.
// Example use:
//
// fallbackDialer, fallbackServerAddr := fallback.DefaultFallbackDialerAndAddress(fallbackAddr)
// dialTLSContext := s2a.NewS2aDialTLSContextFunc(&s2a.ClientOptions{
// S2AAddress: s2aAddress, // required
// FallbackOpts: &s2a.FallbackOptions{
// FallbackDialer: &s2a.FallbackDialer{
// Dialer: fallbackDialer,
// ServerAddr: fallbackServerAddr,
// },
// },
// })
//
// The fallback server's certificate should be verifiable using OS root store.
// The fallbackAddr is expected to be a network address, e.g. example.com:port. If port is not specified,
// it uses default port 443.
// In the returned function's TLS config, ClientSessionCache is explicitly set to nil to disable TLS resumption,
// and min TLS version is set to 1.3.
func DefaultFallbackDialerAndAddress(fallbackAddr string) (*tls.Dialer, string, error) {
fallbackServerAddr, err := processFallbackAddr(fallbackAddr)
if err != nil {
if grpclog.V(1) {
grpclog.Infof("error processing fallback address [%s]: %v", fallbackAddr, err)
}
return nil, "", err
}
return &tls.Dialer{Config: &FallbackTLSConfigHTTP}, fallbackServerAddr, nil
}
func processFallbackAddr(fallbackAddr string) (string, error) {
var fallbackServerAddr string
var err error
if fallbackAddr == "" {
return "", fmt.Errorf("empty fallback address")
}
_, _, err = net.SplitHostPort(fallbackAddr)
if err != nil {
// fallbackAddr does not have port suffix
fallbackServerAddr = net.JoinHostPort(fallbackAddr, defaultHTTPSPort)
} else {
// FallbackServerAddr already has port suffix
fallbackServerAddr = fallbackAddr
}
return fallbackServerAddr, nil
}

View File

@ -1,119 +0,0 @@
/*
*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package authinfo provides authentication and authorization information that
// results from the TLS handshake.
package authinfo
import (
"errors"
commonpb "github.com/google/s2a-go/internal/proto/common_go_proto"
contextpb "github.com/google/s2a-go/internal/proto/s2a_context_go_proto"
grpcpb "github.com/google/s2a-go/internal/proto/s2a_go_proto"
"google.golang.org/grpc/credentials"
)
var _ credentials.AuthInfo = (*S2AAuthInfo)(nil)
const s2aAuthType = "s2a"
// S2AAuthInfo exposes authentication and authorization information from the
// S2A session result to the gRPC stack.
type S2AAuthInfo struct {
s2aContext *contextpb.S2AContext
commonAuthInfo credentials.CommonAuthInfo
}
// NewS2AAuthInfo returns a new S2AAuthInfo object from the S2A session result.
func NewS2AAuthInfo(result *grpcpb.SessionResult) (credentials.AuthInfo, error) {
return newS2AAuthInfo(result)
}
func newS2AAuthInfo(result *grpcpb.SessionResult) (*S2AAuthInfo, error) {
if result == nil {
return nil, errors.New("NewS2aAuthInfo given nil session result")
}
return &S2AAuthInfo{
s2aContext: &contextpb.S2AContext{
ApplicationProtocol: result.GetApplicationProtocol(),
TlsVersion: result.GetState().GetTlsVersion(),
Ciphersuite: result.GetState().GetTlsCiphersuite(),
PeerIdentity: result.GetPeerIdentity(),
LocalIdentity: result.GetLocalIdentity(),
PeerCertFingerprint: result.GetPeerCertFingerprint(),
LocalCertFingerprint: result.GetLocalCertFingerprint(),
IsHandshakeResumed: result.GetState().GetIsHandshakeResumed(),
},
commonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity},
}, nil
}
// AuthType returns the authentication type.
func (s *S2AAuthInfo) AuthType() string {
return s2aAuthType
}
// ApplicationProtocol returns the application protocol, e.g. "grpc".
func (s *S2AAuthInfo) ApplicationProtocol() string {
return s.s2aContext.GetApplicationProtocol()
}
// TLSVersion returns the TLS version negotiated during the handshake.
func (s *S2AAuthInfo) TLSVersion() commonpb.TLSVersion {
return s.s2aContext.GetTlsVersion()
}
// Ciphersuite returns the ciphersuite negotiated during the handshake.
func (s *S2AAuthInfo) Ciphersuite() commonpb.Ciphersuite {
return s.s2aContext.GetCiphersuite()
}
// PeerIdentity returns the authenticated identity of the peer.
func (s *S2AAuthInfo) PeerIdentity() *commonpb.Identity {
return s.s2aContext.GetPeerIdentity()
}
// LocalIdentity returns the local identity of the application used during
// session setup.
func (s *S2AAuthInfo) LocalIdentity() *commonpb.Identity {
return s.s2aContext.GetLocalIdentity()
}
// PeerCertFingerprint returns the SHA256 hash of the peer certificate used in
// the S2A handshake.
func (s *S2AAuthInfo) PeerCertFingerprint() []byte {
return s.s2aContext.GetPeerCertFingerprint()
}
// LocalCertFingerprint returns the SHA256 hash of the local certificate used
// in the S2A handshake.
func (s *S2AAuthInfo) LocalCertFingerprint() []byte {
return s.s2aContext.GetLocalCertFingerprint()
}
// IsHandshakeResumed returns true if a cached session was used to resume
// the handshake.
func (s *S2AAuthInfo) IsHandshakeResumed() bool {
return s.s2aContext.GetIsHandshakeResumed()
}
// SecurityLevel returns the security level of the connection.
func (s *S2AAuthInfo) SecurityLevel() credentials.SecurityLevel {
return s.commonAuthInfo.SecurityLevel
}

View File

@ -1,438 +0,0 @@
/*
*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package handshaker communicates with the S2A handshaker service.
package handshaker
import (
"context"
"errors"
"fmt"
"io"
"net"
"sync"
"github.com/google/s2a-go/internal/authinfo"
commonpb "github.com/google/s2a-go/internal/proto/common_go_proto"
s2apb "github.com/google/s2a-go/internal/proto/s2a_go_proto"
"github.com/google/s2a-go/internal/record"
"github.com/google/s2a-go/internal/tokenmanager"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
)
var (
// appProtocol contains the application protocol accepted by the handshaker.
appProtocol = "grpc"
// frameLimit is the maximum size of a frame in bytes.
frameLimit = 1024 * 64
// peerNotRespondingError is the error thrown when the peer doesn't respond.
errPeerNotResponding = errors.New("peer is not responding and re-connection should be attempted")
)
// Handshaker defines a handshaker interface.
type Handshaker interface {
// ClientHandshake starts and completes a TLS handshake from the client side,
// and returns a secure connection along with additional auth information.
ClientHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error)
// ServerHandshake starts and completes a TLS handshake from the server side,
// and returns a secure connection along with additional auth information.
ServerHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error)
// Close terminates the Handshaker. It should be called when the handshake
// is complete.
Close() error
}
// ClientHandshakerOptions contains the options needed to configure the S2A
// handshaker service on the client-side.
type ClientHandshakerOptions struct {
// MinTLSVersion specifies the min TLS version supported by the client.
MinTLSVersion commonpb.TLSVersion
// MaxTLSVersion specifies the max TLS version supported by the client.
MaxTLSVersion commonpb.TLSVersion
// TLSCiphersuites is the ordered list of ciphersuites supported by the
// client.
TLSCiphersuites []commonpb.Ciphersuite
// TargetIdentities contains a list of allowed server identities. One of the
// target identities should match the peer identity in the handshake
// result; otherwise, the handshake fails.
TargetIdentities []*commonpb.Identity
// LocalIdentity is the local identity of the client application. If none is
// provided, then the S2A will choose the default identity.
LocalIdentity *commonpb.Identity
// TargetName is the allowed server name, which may be used for server
// authorization check by the S2A if it is provided.
TargetName string
// EnsureProcessSessionTickets allows users to wait and ensure that all
// available session tickets are sent to S2A before a process completes.
EnsureProcessSessionTickets *sync.WaitGroup
}
// ServerHandshakerOptions contains the options needed to configure the S2A
// handshaker service on the server-side.
type ServerHandshakerOptions struct {
// MinTLSVersion specifies the min TLS version supported by the server.
MinTLSVersion commonpb.TLSVersion
// MaxTLSVersion specifies the max TLS version supported by the server.
MaxTLSVersion commonpb.TLSVersion
// TLSCiphersuites is the ordered list of ciphersuites supported by the
// server.
TLSCiphersuites []commonpb.Ciphersuite
// LocalIdentities is the list of local identities that may be assumed by
// the server. If no local identity is specified, then the S2A chooses a
// default local identity.
LocalIdentities []*commonpb.Identity
}
// s2aHandshaker performs a TLS handshake using the S2A handshaker service.
type s2aHandshaker struct {
// stream is used to communicate with the S2A handshaker service.
stream s2apb.S2AService_SetUpSessionClient
// conn is the connection to the peer.
conn net.Conn
// clientOpts should be non-nil iff the handshaker is client-side.
clientOpts *ClientHandshakerOptions
// serverOpts should be non-nil iff the handshaker is server-side.
serverOpts *ServerHandshakerOptions
// isClient determines if the handshaker is client or server side.
isClient bool
// hsAddr stores the address of the S2A handshaker service.
hsAddr string
// tokenManager manages access tokens for authenticating to S2A.
tokenManager tokenmanager.AccessTokenManager
// localIdentities is the set of local identities for whom the
// tokenManager should fetch a token when preparing a request to be
// sent to S2A.
localIdentities []*commonpb.Identity
}
// NewClientHandshaker creates an s2aHandshaker instance that performs a
// client-side TLS handshake using the S2A handshaker service.
func NewClientHandshaker(ctx context.Context, conn *grpc.ClientConn, c net.Conn, hsAddr string, opts *ClientHandshakerOptions) (Handshaker, error) {
stream, err := s2apb.NewS2AServiceClient(conn).SetUpSession(ctx, grpc.WaitForReady(true))
if err != nil {
return nil, err
}
tokenManager, err := tokenmanager.NewSingleTokenAccessTokenManager()
if err != nil {
grpclog.Infof("failed to create single token access token manager: %v", err)
}
return newClientHandshaker(stream, c, hsAddr, opts, tokenManager), nil
}
func newClientHandshaker(stream s2apb.S2AService_SetUpSessionClient, c net.Conn, hsAddr string, opts *ClientHandshakerOptions, tokenManager tokenmanager.AccessTokenManager) *s2aHandshaker {
var localIdentities []*commonpb.Identity
if opts != nil {
localIdentities = []*commonpb.Identity{opts.LocalIdentity}
}
return &s2aHandshaker{
stream: stream,
conn: c,
clientOpts: opts,
isClient: true,
hsAddr: hsAddr,
tokenManager: tokenManager,
localIdentities: localIdentities,
}
}
// NewServerHandshaker creates an s2aHandshaker instance that performs a
// server-side TLS handshake using the S2A handshaker service.
func NewServerHandshaker(ctx context.Context, conn *grpc.ClientConn, c net.Conn, hsAddr string, opts *ServerHandshakerOptions) (Handshaker, error) {
stream, err := s2apb.NewS2AServiceClient(conn).SetUpSession(ctx, grpc.WaitForReady(true))
if err != nil {
return nil, err
}
tokenManager, err := tokenmanager.NewSingleTokenAccessTokenManager()
if err != nil {
grpclog.Infof("failed to create single token access token manager: %v", err)
}
return newServerHandshaker(stream, c, hsAddr, opts, tokenManager), nil
}
func newServerHandshaker(stream s2apb.S2AService_SetUpSessionClient, c net.Conn, hsAddr string, opts *ServerHandshakerOptions, tokenManager tokenmanager.AccessTokenManager) *s2aHandshaker {
var localIdentities []*commonpb.Identity
if opts != nil {
localIdentities = opts.LocalIdentities
}
return &s2aHandshaker{
stream: stream,
conn: c,
serverOpts: opts,
isClient: false,
hsAddr: hsAddr,
tokenManager: tokenManager,
localIdentities: localIdentities,
}
}
// ClientHandshake performs a client-side TLS handshake using the S2A handshaker
// service. When complete, returns a TLS connection.
func (h *s2aHandshaker) ClientHandshake(_ context.Context) (net.Conn, credentials.AuthInfo, error) {
if !h.isClient {
return nil, nil, errors.New("only handshakers created using NewClientHandshaker can perform a client-side handshake")
}
// Extract the hostname from the target name. The target name is assumed to be an authority.
hostname, _, err := net.SplitHostPort(h.clientOpts.TargetName)
if err != nil {
// If the target name had no host port or could not be parsed, use it as is.
hostname = h.clientOpts.TargetName
}
// Prepare a client start message to send to the S2A handshaker service.
req := &s2apb.SessionReq{
ReqOneof: &s2apb.SessionReq_ClientStart{
ClientStart: &s2apb.ClientSessionStartReq{
ApplicationProtocols: []string{appProtocol},
MinTlsVersion: h.clientOpts.MinTLSVersion,
MaxTlsVersion: h.clientOpts.MaxTLSVersion,
TlsCiphersuites: h.clientOpts.TLSCiphersuites,
TargetIdentities: h.clientOpts.TargetIdentities,
LocalIdentity: h.clientOpts.LocalIdentity,
TargetName: hostname,
},
},
AuthMechanisms: h.getAuthMechanisms(),
}
conn, result, err := h.setUpSession(req)
if err != nil {
return nil, nil, err
}
authInfo, err := authinfo.NewS2AAuthInfo(result)
if err != nil {
return nil, nil, err
}
return conn, authInfo, nil
}
// ServerHandshake performs a server-side TLS handshake using the S2A handshaker
// service. When complete, returns a TLS connection.
func (h *s2aHandshaker) ServerHandshake(_ context.Context) (net.Conn, credentials.AuthInfo, error) {
if h.isClient {
return nil, nil, errors.New("only handshakers created using NewServerHandshaker can perform a server-side handshake")
}
p := make([]byte, frameLimit)
n, err := h.conn.Read(p)
if err != nil {
return nil, nil, err
}
// Prepare a server start message to send to the S2A handshaker service.
req := &s2apb.SessionReq{
ReqOneof: &s2apb.SessionReq_ServerStart{
ServerStart: &s2apb.ServerSessionStartReq{
ApplicationProtocols: []string{appProtocol},
MinTlsVersion: h.serverOpts.MinTLSVersion,
MaxTlsVersion: h.serverOpts.MaxTLSVersion,
TlsCiphersuites: h.serverOpts.TLSCiphersuites,
LocalIdentities: h.serverOpts.LocalIdentities,
InBytes: p[:n],
},
},
AuthMechanisms: h.getAuthMechanisms(),
}
conn, result, err := h.setUpSession(req)
if err != nil {
return nil, nil, err
}
authInfo, err := authinfo.NewS2AAuthInfo(result)
if err != nil {
return nil, nil, err
}
return conn, authInfo, nil
}
// setUpSession proxies messages between the peer and the S2A handshaker
// service.
func (h *s2aHandshaker) setUpSession(req *s2apb.SessionReq) (net.Conn, *s2apb.SessionResult, error) {
resp, err := h.accessHandshakerService(req)
if err != nil {
return nil, nil, err
}
// Check if the returned status is an error.
if resp.GetStatus() != nil {
if got, want := resp.GetStatus().Code, uint32(codes.OK); got != want {
return nil, nil, fmt.Errorf("%v", resp.GetStatus().Details)
}
}
// Calculate the extra unread bytes from the Session. Attempting to consume
// more than the bytes sent will throw an error.
var extra []byte
if req.GetServerStart() != nil {
if resp.GetBytesConsumed() > uint32(len(req.GetServerStart().GetInBytes())) {
return nil, nil, errors.New("handshaker service consumed bytes value is out-of-bounds")
}
extra = req.GetServerStart().GetInBytes()[resp.GetBytesConsumed():]
}
result, extra, err := h.processUntilDone(resp, extra)
if err != nil {
return nil, nil, err
}
if result.GetLocalIdentity() == nil {
return nil, nil, errors.New("local identity must be populated in session result")
}
// Create a new TLS record protocol using the Session Result.
newConn, err := record.NewConn(&record.ConnParameters{
NetConn: h.conn,
Ciphersuite: result.GetState().GetTlsCiphersuite(),
TLSVersion: result.GetState().GetTlsVersion(),
InTrafficSecret: result.GetState().GetInKey(),
OutTrafficSecret: result.GetState().GetOutKey(),
UnusedBuf: extra,
InSequence: result.GetState().GetInSequence(),
OutSequence: result.GetState().GetOutSequence(),
HSAddr: h.hsAddr,
ConnectionID: result.GetState().GetConnectionId(),
LocalIdentity: result.GetLocalIdentity(),
EnsureProcessSessionTickets: h.ensureProcessSessionTickets(),
})
if err != nil {
return nil, nil, err
}
return newConn, result, nil
}
func (h *s2aHandshaker) ensureProcessSessionTickets() *sync.WaitGroup {
if h.clientOpts == nil {
return nil
}
return h.clientOpts.EnsureProcessSessionTickets
}
// accessHandshakerService sends the session request to the S2A handshaker
// service and returns the session response.
func (h *s2aHandshaker) accessHandshakerService(req *s2apb.SessionReq) (*s2apb.SessionResp, error) {
if err := h.stream.Send(req); err != nil {
return nil, err
}
resp, err := h.stream.Recv()
if err != nil {
return nil, err
}
return resp, nil
}
// processUntilDone continues proxying messages between the peer and the S2A
// handshaker service until the handshaker service returns the SessionResult at
// the end of the handshake or an error occurs.
func (h *s2aHandshaker) processUntilDone(resp *s2apb.SessionResp, unusedBytes []byte) (*s2apb.SessionResult, []byte, error) {
for {
if len(resp.OutFrames) > 0 {
if _, err := h.conn.Write(resp.OutFrames); err != nil {
return nil, nil, err
}
}
if resp.Result != nil {
return resp.Result, unusedBytes, nil
}
buf := make([]byte, frameLimit)
n, err := h.conn.Read(buf)
if err != nil && err != io.EOF {
return nil, nil, err
}
// If there is nothing to send to the handshaker service and nothing is
// received from the peer, then we are stuck. This covers the case when
// the peer is not responding. Note that handshaker service connection
// issues are caught in accessHandshakerService before we even get
// here.
if len(resp.OutFrames) == 0 && n == 0 {
return nil, nil, errPeerNotResponding
}
// Append extra bytes from the previous interaction with the handshaker
// service with the current buffer read from conn.
p := append(unusedBytes, buf[:n]...)
// From here on, p and unusedBytes point to the same slice.
resp, err = h.accessHandshakerService(&s2apb.SessionReq{
ReqOneof: &s2apb.SessionReq_Next{
Next: &s2apb.SessionNextReq{
InBytes: p,
},
},
AuthMechanisms: h.getAuthMechanisms(),
})
if err != nil {
return nil, nil, err
}
// Cache the local identity returned by S2A, if it is populated. This
// overwrites any existing local identities. This is done because, once the
// S2A has selected a local identity, then only that local identity should
// be asserted in future requests until the end of the current handshake.
if resp.GetLocalIdentity() != nil {
h.localIdentities = []*commonpb.Identity{resp.GetLocalIdentity()}
}
// Set unusedBytes based on the handshaker service response.
if resp.GetBytesConsumed() > uint32(len(p)) {
return nil, nil, errors.New("handshaker service consumed bytes value is out-of-bounds")
}
unusedBytes = p[resp.GetBytesConsumed():]
}
}
// Close shuts down the handshaker and the stream to the S2A handshaker service
// when the handshake is complete. It should be called when the caller obtains
// the secure connection at the end of the handshake.
func (h *s2aHandshaker) Close() error {
return h.stream.CloseSend()
}
func (h *s2aHandshaker) getAuthMechanisms() []*s2apb.AuthenticationMechanism {
if h.tokenManager == nil {
return nil
}
// First handle the special case when no local identities have been provided
// by the application. In this case, an AuthenticationMechanism with no local
// identity will be sent.
if len(h.localIdentities) == 0 {
token, err := h.tokenManager.DefaultToken()
if err != nil {
grpclog.Infof("unable to get token for empty local identity: %v", err)
return nil
}
return []*s2apb.AuthenticationMechanism{
{
MechanismOneof: &s2apb.AuthenticationMechanism_Token{
Token: token,
},
},
}
}
// Next, handle the case where the application (or the S2A) has provided
// one or more local identities.
var authMechanisms []*s2apb.AuthenticationMechanism
for _, localIdentity := range h.localIdentities {
token, err := h.tokenManager.Token(localIdentity)
if err != nil {
grpclog.Infof("unable to get token for local identity %v: %v", localIdentity, err)
continue
}
authMechanism := &s2apb.AuthenticationMechanism{
Identity: localIdentity,
MechanismOneof: &s2apb.AuthenticationMechanism_Token{
Token: token,
},
}
authMechanisms = append(authMechanisms, authMechanism)
}
return authMechanisms
}

View File

@ -1,66 +0,0 @@
/*
*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package service is a utility for calling the S2A handshaker service.
package service
import (
"context"
"sync"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
var (
// mu guards hsConnMap and hsDialer.
mu sync.Mutex
// hsConnMap represents a mapping from an S2A handshaker service address
// to a corresponding connection to an S2A handshaker service instance.
hsConnMap = make(map[string]*grpc.ClientConn)
// hsDialer will be reassigned in tests.
hsDialer = grpc.DialContext
)
// Dial dials the S2A handshaker service. If a connection has already been
// established, this function returns it. Otherwise, a new connection is
// created.
func Dial(ctx context.Context, handshakerServiceAddress string, transportCreds credentials.TransportCredentials) (*grpc.ClientConn, error) {
mu.Lock()
defer mu.Unlock()
hsConn, ok := hsConnMap[handshakerServiceAddress]
if !ok {
// Create a new connection to the S2A handshaker service. Note that
// this connection stays open until the application is closed.
var grpcOpts []grpc.DialOption
if transportCreds != nil {
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(transportCreds))
} else {
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
var err error
hsConn, err = hsDialer(ctx, handshakerServiceAddress, grpcOpts...)
if err != nil {
return nil, err
}
hsConnMap[handshakerServiceAddress] = hsConn
}
return hsConn, nil
}

View File

@ -1,388 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v3.21.12
// source: internal/proto/common/common.proto
package common_go_proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The ciphersuites supported by S2A. The name determines the confidentiality,
// and authentication ciphers as well as the hash algorithm used for PRF in
// TLS 1.2 or HKDF in TLS 1.3. Thus, the components of the name are:
// - AEAD -- for encryption and authentication, e.g., AES_128_GCM.
// - Hash algorithm -- used in PRF or HKDF, e.g., SHA256.
type Ciphersuite int32
const (
Ciphersuite_AES_128_GCM_SHA256 Ciphersuite = 0
Ciphersuite_AES_256_GCM_SHA384 Ciphersuite = 1
Ciphersuite_CHACHA20_POLY1305_SHA256 Ciphersuite = 2
)
// Enum value maps for Ciphersuite.
var (
Ciphersuite_name = map[int32]string{
0: "AES_128_GCM_SHA256",
1: "AES_256_GCM_SHA384",
2: "CHACHA20_POLY1305_SHA256",
}
Ciphersuite_value = map[string]int32{
"AES_128_GCM_SHA256": 0,
"AES_256_GCM_SHA384": 1,
"CHACHA20_POLY1305_SHA256": 2,
}
)
func (x Ciphersuite) Enum() *Ciphersuite {
p := new(Ciphersuite)
*p = x
return p
}
func (x Ciphersuite) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Ciphersuite) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_common_common_proto_enumTypes[0].Descriptor()
}
func (Ciphersuite) Type() protoreflect.EnumType {
return &file_internal_proto_common_common_proto_enumTypes[0]
}
func (x Ciphersuite) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Ciphersuite.Descriptor instead.
func (Ciphersuite) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_common_common_proto_rawDescGZIP(), []int{0}
}
// The TLS versions supported by S2A's handshaker module.
type TLSVersion int32
const (
TLSVersion_TLS1_2 TLSVersion = 0
TLSVersion_TLS1_3 TLSVersion = 1
)
// Enum value maps for TLSVersion.
var (
TLSVersion_name = map[int32]string{
0: "TLS1_2",
1: "TLS1_3",
}
TLSVersion_value = map[string]int32{
"TLS1_2": 0,
"TLS1_3": 1,
}
)
func (x TLSVersion) Enum() *TLSVersion {
p := new(TLSVersion)
*p = x
return p
}
func (x TLSVersion) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (TLSVersion) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_common_common_proto_enumTypes[1].Descriptor()
}
func (TLSVersion) Type() protoreflect.EnumType {
return &file_internal_proto_common_common_proto_enumTypes[1]
}
func (x TLSVersion) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use TLSVersion.Descriptor instead.
func (TLSVersion) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_common_common_proto_rawDescGZIP(), []int{1}
}
type Identity struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to IdentityOneof:
//
// *Identity_SpiffeId
// *Identity_Hostname
// *Identity_Uid
// *Identity_Username
// *Identity_GcpId
IdentityOneof isIdentity_IdentityOneof `protobuf_oneof:"identity_oneof"`
// Additional identity-specific attributes.
Attributes map[string]string `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *Identity) Reset() {
*x = Identity{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_proto_common_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Identity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Identity) ProtoMessage() {}
func (x *Identity) ProtoReflect() protoreflect.Message {
mi := &file_internal_proto_common_common_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Identity.ProtoReflect.Descriptor instead.
func (*Identity) Descriptor() ([]byte, []int) {
return file_internal_proto_common_common_proto_rawDescGZIP(), []int{0}
}
func (m *Identity) GetIdentityOneof() isIdentity_IdentityOneof {
if m != nil {
return m.IdentityOneof
}
return nil
}
func (x *Identity) GetSpiffeId() string {
if x, ok := x.GetIdentityOneof().(*Identity_SpiffeId); ok {
return x.SpiffeId
}
return ""
}
func (x *Identity) GetHostname() string {
if x, ok := x.GetIdentityOneof().(*Identity_Hostname); ok {
return x.Hostname
}
return ""
}
func (x *Identity) GetUid() string {
if x, ok := x.GetIdentityOneof().(*Identity_Uid); ok {
return x.Uid
}
return ""
}
func (x *Identity) GetUsername() string {
if x, ok := x.GetIdentityOneof().(*Identity_Username); ok {
return x.Username
}
return ""
}
func (x *Identity) GetGcpId() string {
if x, ok := x.GetIdentityOneof().(*Identity_GcpId); ok {
return x.GcpId
}
return ""
}
func (x *Identity) GetAttributes() map[string]string {
if x != nil {
return x.Attributes
}
return nil
}
type isIdentity_IdentityOneof interface {
isIdentity_IdentityOneof()
}
type Identity_SpiffeId struct {
// The SPIFFE ID of a connection endpoint.
SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3,oneof"`
}
type Identity_Hostname struct {
// The hostname of a connection endpoint.
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3,oneof"`
}
type Identity_Uid struct {
// The UID of a connection endpoint.
Uid string `protobuf:"bytes,4,opt,name=uid,proto3,oneof"`
}
type Identity_Username struct {
// The username of a connection endpoint.
Username string `protobuf:"bytes,5,opt,name=username,proto3,oneof"`
}
type Identity_GcpId struct {
// The GCP ID of a connection endpoint.
GcpId string `protobuf:"bytes,6,opt,name=gcp_id,json=gcpId,proto3,oneof"`
}
func (*Identity_SpiffeId) isIdentity_IdentityOneof() {}
func (*Identity_Hostname) isIdentity_IdentityOneof() {}
func (*Identity_Uid) isIdentity_IdentityOneof() {}
func (*Identity_Username) isIdentity_IdentityOneof() {}
func (*Identity_GcpId) isIdentity_IdentityOneof() {}
var File_internal_proto_common_common_proto protoreflect.FileDescriptor
var file_internal_proto_common_common_proto_rawDesc = []byte{
0x0a, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0xa8, 0x02, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x09,
0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48,
0x00, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x68,
0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52,
0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x03, 0x75, 0x69, 0x64,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48,
0x00, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x06, 0x67,
0x63, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x67,
0x63, 0x70, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x74,
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x61,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x69, 0x64, 0x65, 0x6e,
0x74, 0x69, 0x74, 0x79, 0x5f, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x2a, 0x5b, 0x0a, 0x0b, 0x43, 0x69,
0x70, 0x68, 0x65, 0x72, 0x73, 0x75, 0x69, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x45, 0x53,
0x5f, 0x31, 0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, 0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10,
0x00, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x47, 0x43, 0x4d,
0x5f, 0x53, 0x48, 0x41, 0x33, 0x38, 0x34, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x48, 0x41,
0x43, 0x48, 0x41, 0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x5f, 0x53,
0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x02, 0x2a, 0x24, 0x0a, 0x0a, 0x54, 0x4c, 0x53, 0x56, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x32, 0x10,
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x33, 0x10, 0x01, 0x42, 0x36, 0x5a,
0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2f, 0x73, 0x32, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x5f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_proto_common_common_proto_rawDescOnce sync.Once
file_internal_proto_common_common_proto_rawDescData = file_internal_proto_common_common_proto_rawDesc
)
func file_internal_proto_common_common_proto_rawDescGZIP() []byte {
file_internal_proto_common_common_proto_rawDescOnce.Do(func() {
file_internal_proto_common_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_proto_common_common_proto_rawDescData)
})
return file_internal_proto_common_common_proto_rawDescData
}
var file_internal_proto_common_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_proto_common_common_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_internal_proto_common_common_proto_goTypes = []any{
(Ciphersuite)(0), // 0: s2a.proto.Ciphersuite
(TLSVersion)(0), // 1: s2a.proto.TLSVersion
(*Identity)(nil), // 2: s2a.proto.Identity
nil, // 3: s2a.proto.Identity.AttributesEntry
}
var file_internal_proto_common_common_proto_depIdxs = []int32{
3, // 0: s2a.proto.Identity.attributes:type_name -> s2a.proto.Identity.AttributesEntry
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_internal_proto_common_common_proto_init() }
func file_internal_proto_common_common_proto_init() {
if File_internal_proto_common_common_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_proto_common_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Identity); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_internal_proto_common_common_proto_msgTypes[0].OneofWrappers = []any{
(*Identity_SpiffeId)(nil),
(*Identity_Hostname)(nil),
(*Identity_Uid)(nil),
(*Identity_Username)(nil),
(*Identity_GcpId)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_proto_common_common_proto_rawDesc,
NumEnums: 2,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_proto_common_common_proto_goTypes,
DependencyIndexes: file_internal_proto_common_common_proto_depIdxs,
EnumInfos: file_internal_proto_common_common_proto_enumTypes,
MessageInfos: file_internal_proto_common_common_proto_msgTypes,
}.Build()
File_internal_proto_common_common_proto = out.File
file_internal_proto_common_common_proto_rawDesc = nil
file_internal_proto_common_common_proto_goTypes = nil
file_internal_proto_common_common_proto_depIdxs = nil
}

View File

@ -1,267 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v3.21.12
// source: internal/proto/s2a_context/s2a_context.proto
package s2a_context_go_proto
import (
common_go_proto "github.com/google/s2a-go/internal/proto/common_go_proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type S2AContext struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The application protocol negotiated for this connection, e.g., 'grpc'.
ApplicationProtocol string `protobuf:"bytes,1,opt,name=application_protocol,json=applicationProtocol,proto3" json:"application_protocol,omitempty"`
// The TLS version number that the S2A's handshaker module used to set up the
// session.
TlsVersion common_go_proto.TLSVersion `protobuf:"varint,2,opt,name=tls_version,json=tlsVersion,proto3,enum=s2a.proto.TLSVersion" json:"tls_version,omitempty"`
// The TLS ciphersuite negotiated by the S2A's handshaker module.
Ciphersuite common_go_proto.Ciphersuite `protobuf:"varint,3,opt,name=ciphersuite,proto3,enum=s2a.proto.Ciphersuite" json:"ciphersuite,omitempty"`
// The authenticated identity of the peer.
PeerIdentity *common_go_proto.Identity `protobuf:"bytes,4,opt,name=peer_identity,json=peerIdentity,proto3" json:"peer_identity,omitempty"`
// The local identity used during session setup. This could be:
// - The local identity that the client specifies in ClientSessionStartReq.
// - One of the local identities that the server specifies in
// ServerSessionStartReq.
// - If neither client or server specifies local identities, the S2A picks the
// default one. In this case, this field will contain that identity.
LocalIdentity *common_go_proto.Identity `protobuf:"bytes,5,opt,name=local_identity,json=localIdentity,proto3" json:"local_identity,omitempty"`
// The SHA256 hash of the peer certificate used in the handshake.
PeerCertFingerprint []byte `protobuf:"bytes,6,opt,name=peer_cert_fingerprint,json=peerCertFingerprint,proto3" json:"peer_cert_fingerprint,omitempty"`
// The SHA256 hash of the local certificate used in the handshake.
LocalCertFingerprint []byte `protobuf:"bytes,7,opt,name=local_cert_fingerprint,json=localCertFingerprint,proto3" json:"local_cert_fingerprint,omitempty"`
// Set to true if a cached session was reused to resume the handshake.
IsHandshakeResumed bool `protobuf:"varint,8,opt,name=is_handshake_resumed,json=isHandshakeResumed,proto3" json:"is_handshake_resumed,omitempty"`
}
func (x *S2AContext) Reset() {
*x = S2AContext{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_proto_s2a_context_s2a_context_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *S2AContext) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*S2AContext) ProtoMessage() {}
func (x *S2AContext) ProtoReflect() protoreflect.Message {
mi := &file_internal_proto_s2a_context_s2a_context_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use S2AContext.ProtoReflect.Descriptor instead.
func (*S2AContext) Descriptor() ([]byte, []int) {
return file_internal_proto_s2a_context_s2a_context_proto_rawDescGZIP(), []int{0}
}
func (x *S2AContext) GetApplicationProtocol() string {
if x != nil {
return x.ApplicationProtocol
}
return ""
}
func (x *S2AContext) GetTlsVersion() common_go_proto.TLSVersion {
if x != nil {
return x.TlsVersion
}
return common_go_proto.TLSVersion(0)
}
func (x *S2AContext) GetCiphersuite() common_go_proto.Ciphersuite {
if x != nil {
return x.Ciphersuite
}
return common_go_proto.Ciphersuite(0)
}
func (x *S2AContext) GetPeerIdentity() *common_go_proto.Identity {
if x != nil {
return x.PeerIdentity
}
return nil
}
func (x *S2AContext) GetLocalIdentity() *common_go_proto.Identity {
if x != nil {
return x.LocalIdentity
}
return nil
}
func (x *S2AContext) GetPeerCertFingerprint() []byte {
if x != nil {
return x.PeerCertFingerprint
}
return nil
}
func (x *S2AContext) GetLocalCertFingerprint() []byte {
if x != nil {
return x.LocalCertFingerprint
}
return nil
}
func (x *S2AContext) GetIsHandshakeResumed() bool {
if x != nil {
return x.IsHandshakeResumed
}
return false
}
var File_internal_proto_s2a_context_s2a_context_proto protoreflect.FileDescriptor
var file_internal_proto_s2a_context_s2a_context_proto_rawDesc = []byte{
0x0a, 0x2c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x73, 0x32, 0x61, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x73, 0x32, 0x61,
0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09,
0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x03,
0x0a, 0x0a, 0x53, 0x32, 0x41, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x31, 0x0a, 0x14,
0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x70, 0x70, 0x6c,
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
0x36, 0x0a, 0x0b, 0x74, 0x6c, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x54, 0x4c, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x74, 0x6c, 0x73,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x0b, 0x63, 0x69, 0x70, 0x68, 0x65,
0x72, 0x73, 0x75, 0x69, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x73,
0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73,
0x75, 0x69, 0x74, 0x65, 0x52, 0x0b, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73, 0x75, 0x69, 0x74,
0x65, 0x12, 0x38, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69,
0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x70,
0x65, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x0e, 0x6c,
0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x65, 0x65, 0x72, 0x5f,
0x63, 0x65, 0x72, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x70, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74,
0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x6c,
0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72,
0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, 0x6c, 0x6f, 0x63,
0x61, 0x6c, 0x43, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e,
0x74, 0x12, 0x30, 0x0a, 0x14, 0x69, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b,
0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
0x12, 0x69, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x75,
0x6d, 0x65, 0x64, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x73, 0x32, 0x61, 0x2f, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x32, 0x61, 0x5f,
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_proto_s2a_context_s2a_context_proto_rawDescOnce sync.Once
file_internal_proto_s2a_context_s2a_context_proto_rawDescData = file_internal_proto_s2a_context_s2a_context_proto_rawDesc
)
func file_internal_proto_s2a_context_s2a_context_proto_rawDescGZIP() []byte {
file_internal_proto_s2a_context_s2a_context_proto_rawDescOnce.Do(func() {
file_internal_proto_s2a_context_s2a_context_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_proto_s2a_context_s2a_context_proto_rawDescData)
})
return file_internal_proto_s2a_context_s2a_context_proto_rawDescData
}
var file_internal_proto_s2a_context_s2a_context_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_internal_proto_s2a_context_s2a_context_proto_goTypes = []any{
(*S2AContext)(nil), // 0: s2a.proto.S2AContext
(common_go_proto.TLSVersion)(0), // 1: s2a.proto.TLSVersion
(common_go_proto.Ciphersuite)(0), // 2: s2a.proto.Ciphersuite
(*common_go_proto.Identity)(nil), // 3: s2a.proto.Identity
}
var file_internal_proto_s2a_context_s2a_context_proto_depIdxs = []int32{
1, // 0: s2a.proto.S2AContext.tls_version:type_name -> s2a.proto.TLSVersion
2, // 1: s2a.proto.S2AContext.ciphersuite:type_name -> s2a.proto.Ciphersuite
3, // 2: s2a.proto.S2AContext.peer_identity:type_name -> s2a.proto.Identity
3, // 3: s2a.proto.S2AContext.local_identity:type_name -> s2a.proto.Identity
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_internal_proto_s2a_context_s2a_context_proto_init() }
func file_internal_proto_s2a_context_s2a_context_proto_init() {
if File_internal_proto_s2a_context_s2a_context_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_proto_s2a_context_s2a_context_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*S2AContext); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_proto_s2a_context_s2a_context_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_proto_s2a_context_s2a_context_proto_goTypes,
DependencyIndexes: file_internal_proto_s2a_context_s2a_context_proto_depIdxs,
MessageInfos: file_internal_proto_s2a_context_s2a_context_proto_msgTypes,
}.Build()
File_internal_proto_s2a_context_s2a_context_proto = out.File
file_internal_proto_s2a_context_s2a_context_proto_rawDesc = nil
file_internal_proto_s2a_context_s2a_context_proto_goTypes = nil
file_internal_proto_s2a_context_s2a_context_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,174 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.4.0
// - protoc v3.21.12
// source: internal/proto/s2a/s2a.proto
package s2a_go_proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.62.0 or later.
const _ = grpc.SupportPackageIsVersion8
const (
S2AService_SetUpSession_FullMethodName = "/s2a.proto.S2AService/SetUpSession"
)
// S2AServiceClient is the client API for S2AService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type S2AServiceClient interface {
// S2A service accepts a stream of session setup requests and returns a stream
// of session setup responses. The client of this service is expected to send
// exactly one client_start or server_start message followed by at least one
// next message. Applications running TLS clients can send requests with
// resumption_ticket messages only after the session is successfully set up.
//
// Every time S2A client sends a request, this service sends a response.
// However, clients do not have to wait for service response before sending
// the next request.
SetUpSession(ctx context.Context, opts ...grpc.CallOption) (S2AService_SetUpSessionClient, error)
}
type s2AServiceClient struct {
cc grpc.ClientConnInterface
}
func NewS2AServiceClient(cc grpc.ClientConnInterface) S2AServiceClient {
return &s2AServiceClient{cc}
}
func (c *s2AServiceClient) SetUpSession(ctx context.Context, opts ...grpc.CallOption) (S2AService_SetUpSessionClient, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &S2AService_ServiceDesc.Streams[0], S2AService_SetUpSession_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &s2AServiceSetUpSessionClient{ClientStream: stream}
return x, nil
}
type S2AService_SetUpSessionClient interface {
Send(*SessionReq) error
Recv() (*SessionResp, error)
grpc.ClientStream
}
type s2AServiceSetUpSessionClient struct {
grpc.ClientStream
}
func (x *s2AServiceSetUpSessionClient) Send(m *SessionReq) error {
return x.ClientStream.SendMsg(m)
}
func (x *s2AServiceSetUpSessionClient) Recv() (*SessionResp, error) {
m := new(SessionResp)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// S2AServiceServer is the server API for S2AService service.
// All implementations must embed UnimplementedS2AServiceServer
// for forward compatibility
type S2AServiceServer interface {
// S2A service accepts a stream of session setup requests and returns a stream
// of session setup responses. The client of this service is expected to send
// exactly one client_start or server_start message followed by at least one
// next message. Applications running TLS clients can send requests with
// resumption_ticket messages only after the session is successfully set up.
//
// Every time S2A client sends a request, this service sends a response.
// However, clients do not have to wait for service response before sending
// the next request.
SetUpSession(S2AService_SetUpSessionServer) error
mustEmbedUnimplementedS2AServiceServer()
}
// UnimplementedS2AServiceServer must be embedded to have forward compatible implementations.
type UnimplementedS2AServiceServer struct {
}
func (UnimplementedS2AServiceServer) SetUpSession(S2AService_SetUpSessionServer) error {
return status.Errorf(codes.Unimplemented, "method SetUpSession not implemented")
}
func (UnimplementedS2AServiceServer) mustEmbedUnimplementedS2AServiceServer() {}
// UnsafeS2AServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to S2AServiceServer will
// result in compilation errors.
type UnsafeS2AServiceServer interface {
mustEmbedUnimplementedS2AServiceServer()
}
func RegisterS2AServiceServer(s grpc.ServiceRegistrar, srv S2AServiceServer) {
s.RegisterService(&S2AService_ServiceDesc, srv)
}
func _S2AService_SetUpSession_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(S2AServiceServer).SetUpSession(&s2AServiceSetUpSessionServer{ServerStream: stream})
}
type S2AService_SetUpSessionServer interface {
Send(*SessionResp) error
Recv() (*SessionReq, error)
grpc.ServerStream
}
type s2AServiceSetUpSessionServer struct {
grpc.ServerStream
}
func (x *s2AServiceSetUpSessionServer) Send(m *SessionResp) error {
return x.ServerStream.SendMsg(m)
}
func (x *s2AServiceSetUpSessionServer) Recv() (*SessionReq, error) {
m := new(SessionReq)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// S2AService_ServiceDesc is the grpc.ServiceDesc for S2AService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var S2AService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "s2a.proto.S2AService",
HandlerType: (*S2AServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "SetUpSession",
Handler: _S2AService_SetUpSession_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "internal/proto/s2a/s2a.proto",
}

View File

@ -1,549 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v3.21.12
// source: internal/proto/v2/common/common.proto
package common_go_proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The TLS 1.0-1.2 ciphersuites that the application can negotiate when using
// S2A.
type Ciphersuite int32
const (
Ciphersuite_CIPHERSUITE_UNSPECIFIED Ciphersuite = 0
Ciphersuite_CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 Ciphersuite = 1
Ciphersuite_CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 Ciphersuite = 2
Ciphersuite_CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 Ciphersuite = 3
Ciphersuite_CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Ciphersuite = 4
Ciphersuite_CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384 Ciphersuite = 5
Ciphersuite_CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Ciphersuite = 6
)
// Enum value maps for Ciphersuite.
var (
Ciphersuite_name = map[int32]string{
0: "CIPHERSUITE_UNSPECIFIED",
1: "CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
2: "CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
3: "CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
4: "CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
5: "CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
6: "CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
}
Ciphersuite_value = map[string]int32{
"CIPHERSUITE_UNSPECIFIED": 0,
"CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": 1,
"CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": 2,
"CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": 3,
"CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256": 4,
"CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384": 5,
"CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": 6,
}
)
func (x Ciphersuite) Enum() *Ciphersuite {
p := new(Ciphersuite)
*p = x
return p
}
func (x Ciphersuite) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Ciphersuite) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_v2_common_common_proto_enumTypes[0].Descriptor()
}
func (Ciphersuite) Type() protoreflect.EnumType {
return &file_internal_proto_v2_common_common_proto_enumTypes[0]
}
func (x Ciphersuite) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Ciphersuite.Descriptor instead.
func (Ciphersuite) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_v2_common_common_proto_rawDescGZIP(), []int{0}
}
// The TLS versions supported by S2A's handshaker module.
type TLSVersion int32
const (
TLSVersion_TLS_VERSION_UNSPECIFIED TLSVersion = 0
TLSVersion_TLS_VERSION_1_0 TLSVersion = 1
TLSVersion_TLS_VERSION_1_1 TLSVersion = 2
TLSVersion_TLS_VERSION_1_2 TLSVersion = 3
TLSVersion_TLS_VERSION_1_3 TLSVersion = 4
)
// Enum value maps for TLSVersion.
var (
TLSVersion_name = map[int32]string{
0: "TLS_VERSION_UNSPECIFIED",
1: "TLS_VERSION_1_0",
2: "TLS_VERSION_1_1",
3: "TLS_VERSION_1_2",
4: "TLS_VERSION_1_3",
}
TLSVersion_value = map[string]int32{
"TLS_VERSION_UNSPECIFIED": 0,
"TLS_VERSION_1_0": 1,
"TLS_VERSION_1_1": 2,
"TLS_VERSION_1_2": 3,
"TLS_VERSION_1_3": 4,
}
)
func (x TLSVersion) Enum() *TLSVersion {
p := new(TLSVersion)
*p = x
return p
}
func (x TLSVersion) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (TLSVersion) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_v2_common_common_proto_enumTypes[1].Descriptor()
}
func (TLSVersion) Type() protoreflect.EnumType {
return &file_internal_proto_v2_common_common_proto_enumTypes[1]
}
func (x TLSVersion) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use TLSVersion.Descriptor instead.
func (TLSVersion) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_v2_common_common_proto_rawDescGZIP(), []int{1}
}
// The side in the TLS connection.
type ConnectionSide int32
const (
ConnectionSide_CONNECTION_SIDE_UNSPECIFIED ConnectionSide = 0
ConnectionSide_CONNECTION_SIDE_CLIENT ConnectionSide = 1
ConnectionSide_CONNECTION_SIDE_SERVER ConnectionSide = 2
)
// Enum value maps for ConnectionSide.
var (
ConnectionSide_name = map[int32]string{
0: "CONNECTION_SIDE_UNSPECIFIED",
1: "CONNECTION_SIDE_CLIENT",
2: "CONNECTION_SIDE_SERVER",
}
ConnectionSide_value = map[string]int32{
"CONNECTION_SIDE_UNSPECIFIED": 0,
"CONNECTION_SIDE_CLIENT": 1,
"CONNECTION_SIDE_SERVER": 2,
}
)
func (x ConnectionSide) Enum() *ConnectionSide {
p := new(ConnectionSide)
*p = x
return p
}
func (x ConnectionSide) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConnectionSide) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_v2_common_common_proto_enumTypes[2].Descriptor()
}
func (ConnectionSide) Type() protoreflect.EnumType {
return &file_internal_proto_v2_common_common_proto_enumTypes[2]
}
func (x ConnectionSide) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConnectionSide.Descriptor instead.
func (ConnectionSide) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_v2_common_common_proto_rawDescGZIP(), []int{2}
}
// The ALPN protocols that the application can negotiate during a TLS handshake.
type AlpnProtocol int32
const (
AlpnProtocol_ALPN_PROTOCOL_UNSPECIFIED AlpnProtocol = 0
AlpnProtocol_ALPN_PROTOCOL_GRPC AlpnProtocol = 1
AlpnProtocol_ALPN_PROTOCOL_HTTP2 AlpnProtocol = 2
AlpnProtocol_ALPN_PROTOCOL_HTTP1_1 AlpnProtocol = 3
)
// Enum value maps for AlpnProtocol.
var (
AlpnProtocol_name = map[int32]string{
0: "ALPN_PROTOCOL_UNSPECIFIED",
1: "ALPN_PROTOCOL_GRPC",
2: "ALPN_PROTOCOL_HTTP2",
3: "ALPN_PROTOCOL_HTTP1_1",
}
AlpnProtocol_value = map[string]int32{
"ALPN_PROTOCOL_UNSPECIFIED": 0,
"ALPN_PROTOCOL_GRPC": 1,
"ALPN_PROTOCOL_HTTP2": 2,
"ALPN_PROTOCOL_HTTP1_1": 3,
}
)
func (x AlpnProtocol) Enum() *AlpnProtocol {
p := new(AlpnProtocol)
*p = x
return p
}
func (x AlpnProtocol) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (AlpnProtocol) Descriptor() protoreflect.EnumDescriptor {
return file_internal_proto_v2_common_common_proto_enumTypes[3].Descriptor()
}
func (AlpnProtocol) Type() protoreflect.EnumType {
return &file_internal_proto_v2_common_common_proto_enumTypes[3]
}
func (x AlpnProtocol) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use AlpnProtocol.Descriptor instead.
func (AlpnProtocol) EnumDescriptor() ([]byte, []int) {
return file_internal_proto_v2_common_common_proto_rawDescGZIP(), []int{3}
}
type Identity struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to IdentityOneof:
//
// *Identity_SpiffeId
// *Identity_Hostname
// *Identity_Uid
// *Identity_Username
// *Identity_GcpId
IdentityOneof isIdentity_IdentityOneof `protobuf_oneof:"identity_oneof"`
// Additional identity-specific attributes.
Attributes map[string]string `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *Identity) Reset() {
*x = Identity{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_proto_v2_common_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Identity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Identity) ProtoMessage() {}
func (x *Identity) ProtoReflect() protoreflect.Message {
mi := &file_internal_proto_v2_common_common_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Identity.ProtoReflect.Descriptor instead.
func (*Identity) Descriptor() ([]byte, []int) {
return file_internal_proto_v2_common_common_proto_rawDescGZIP(), []int{0}
}
func (m *Identity) GetIdentityOneof() isIdentity_IdentityOneof {
if m != nil {
return m.IdentityOneof
}
return nil
}
func (x *Identity) GetSpiffeId() string {
if x, ok := x.GetIdentityOneof().(*Identity_SpiffeId); ok {
return x.SpiffeId
}
return ""
}
func (x *Identity) GetHostname() string {
if x, ok := x.GetIdentityOneof().(*Identity_Hostname); ok {
return x.Hostname
}
return ""
}
func (x *Identity) GetUid() string {
if x, ok := x.GetIdentityOneof().(*Identity_Uid); ok {
return x.Uid
}
return ""
}
func (x *Identity) GetUsername() string {
if x, ok := x.GetIdentityOneof().(*Identity_Username); ok {
return x.Username
}
return ""
}
func (x *Identity) GetGcpId() string {
if x, ok := x.GetIdentityOneof().(*Identity_GcpId); ok {
return x.GcpId
}
return ""
}
func (x *Identity) GetAttributes() map[string]string {
if x != nil {
return x.Attributes
}
return nil
}
type isIdentity_IdentityOneof interface {
isIdentity_IdentityOneof()
}
type Identity_SpiffeId struct {
// The SPIFFE ID of a connection endpoint.
SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3,oneof"`
}
type Identity_Hostname struct {
// The hostname of a connection endpoint.
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3,oneof"`
}
type Identity_Uid struct {
// The UID of a connection endpoint.
Uid string `protobuf:"bytes,4,opt,name=uid,proto3,oneof"`
}
type Identity_Username struct {
// The username of a connection endpoint.
Username string `protobuf:"bytes,5,opt,name=username,proto3,oneof"`
}
type Identity_GcpId struct {
// The GCP ID of a connection endpoint.
GcpId string `protobuf:"bytes,6,opt,name=gcp_id,json=gcpId,proto3,oneof"`
}
func (*Identity_SpiffeId) isIdentity_IdentityOneof() {}
func (*Identity_Hostname) isIdentity_IdentityOneof() {}
func (*Identity_Uid) isIdentity_IdentityOneof() {}
func (*Identity_Username) isIdentity_IdentityOneof() {}
func (*Identity_GcpId) isIdentity_IdentityOneof() {}
var File_internal_proto_v2_common_common_proto protoreflect.FileDescriptor
var file_internal_proto_v2_common_common_proto_rawDesc = []byte{
0x0a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f,
0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x76, 0x32, 0x22, 0xab, 0x02, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69,
0x74, 0x79, 0x12, 0x1d, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49,
0x64, 0x12, 0x1c, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x12, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03,
0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x17, 0x0a, 0x06, 0x67, 0x63, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x48, 0x00, 0x52, 0x05, 0x67, 0x63, 0x70, 0x49, 0x64, 0x12, 0x46, 0x0a, 0x0a, 0x61, 0x74,
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26,
0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
0x01, 0x42, 0x10, 0x0a, 0x0e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6f, 0x6e,
0x65, 0x6f, 0x66, 0x2a, 0xee, 0x02, 0x0a, 0x0b, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73, 0x75,
0x69, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x53, 0x55, 0x49,
0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
0x12, 0x33, 0x0a, 0x2f, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x53, 0x55, 0x49, 0x54, 0x45, 0x5f,
0x45, 0x43, 0x44, 0x48, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x53, 0x41, 0x5f, 0x57, 0x49, 0x54, 0x48,
0x5f, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, 0x5f, 0x53, 0x48, 0x41,
0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x33, 0x0a, 0x2f, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x53,
0x55, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x48, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x53, 0x41,
0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x47, 0x43,
0x4d, 0x5f, 0x53, 0x48, 0x41, 0x33, 0x38, 0x34, 0x10, 0x02, 0x12, 0x39, 0x0a, 0x35, 0x43, 0x49,
0x50, 0x48, 0x45, 0x52, 0x53, 0x55, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x48, 0x45, 0x5f,
0x45, 0x43, 0x44, 0x53, 0x41, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x41, 0x43, 0x48,
0x41, 0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x5f, 0x53, 0x48, 0x41,
0x32, 0x35, 0x36, 0x10, 0x03, 0x12, 0x31, 0x0a, 0x2d, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x53,
0x55, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x48, 0x45, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x57,
0x49, 0x54, 0x48, 0x5f, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, 0x5f,
0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x04, 0x12, 0x31, 0x0a, 0x2d, 0x43, 0x49, 0x50, 0x48,
0x45, 0x52, 0x53, 0x55, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x48, 0x45, 0x5f, 0x52, 0x53,
0x41, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x47,
0x43, 0x4d, 0x5f, 0x53, 0x48, 0x41, 0x33, 0x38, 0x34, 0x10, 0x05, 0x12, 0x37, 0x0a, 0x33, 0x43,
0x49, 0x50, 0x48, 0x45, 0x52, 0x53, 0x55, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x43, 0x44, 0x48, 0x45,
0x5f, 0x52, 0x53, 0x41, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41,
0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x5f, 0x53, 0x48, 0x41, 0x32,
0x35, 0x36, 0x10, 0x06, 0x2a, 0x7d, 0x0a, 0x0a, 0x54, 0x4c, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x4c, 0x53, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f,
0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
0x13, 0x0a, 0x0f, 0x54, 0x4c, 0x53, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x31,
0x5f, 0x30, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x4c, 0x53, 0x5f, 0x56, 0x45, 0x52, 0x53,
0x49, 0x4f, 0x4e, 0x5f, 0x31, 0x5f, 0x31, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x4c, 0x53,
0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x31, 0x5f, 0x32, 0x10, 0x03, 0x12, 0x13,
0x0a, 0x0f, 0x54, 0x4c, 0x53, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x31, 0x5f,
0x33, 0x10, 0x04, 0x2a, 0x69, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x53, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54,
0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43,
0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54,
0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e,
0x5f, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x02, 0x2a, 0x79,
0x0a, 0x0c, 0x41, 0x6c, 0x70, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1d,
0x0a, 0x19, 0x41, 0x4c, 0x50, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f,
0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a,
0x12, 0x41, 0x4c, 0x50, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x47,
0x52, 0x50, 0x43, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x4c, 0x50, 0x4e, 0x5f, 0x50, 0x52,
0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x32, 0x10, 0x02, 0x12, 0x19,
0x0a, 0x15, 0x41, 0x4c, 0x50, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f,
0x48, 0x54, 0x54, 0x50, 0x31, 0x5f, 0x31, 0x10, 0x03, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x73,
0x32, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x5f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_proto_v2_common_common_proto_rawDescOnce sync.Once
file_internal_proto_v2_common_common_proto_rawDescData = file_internal_proto_v2_common_common_proto_rawDesc
)
func file_internal_proto_v2_common_common_proto_rawDescGZIP() []byte {
file_internal_proto_v2_common_common_proto_rawDescOnce.Do(func() {
file_internal_proto_v2_common_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_proto_v2_common_common_proto_rawDescData)
})
return file_internal_proto_v2_common_common_proto_rawDescData
}
var file_internal_proto_v2_common_common_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_internal_proto_v2_common_common_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_internal_proto_v2_common_common_proto_goTypes = []any{
(Ciphersuite)(0), // 0: s2a.proto.v2.Ciphersuite
(TLSVersion)(0), // 1: s2a.proto.v2.TLSVersion
(ConnectionSide)(0), // 2: s2a.proto.v2.ConnectionSide
(AlpnProtocol)(0), // 3: s2a.proto.v2.AlpnProtocol
(*Identity)(nil), // 4: s2a.proto.v2.Identity
nil, // 5: s2a.proto.v2.Identity.AttributesEntry
}
var file_internal_proto_v2_common_common_proto_depIdxs = []int32{
5, // 0: s2a.proto.v2.Identity.attributes:type_name -> s2a.proto.v2.Identity.AttributesEntry
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_internal_proto_v2_common_common_proto_init() }
func file_internal_proto_v2_common_common_proto_init() {
if File_internal_proto_v2_common_common_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_proto_v2_common_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Identity); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_internal_proto_v2_common_common_proto_msgTypes[0].OneofWrappers = []any{
(*Identity_SpiffeId)(nil),
(*Identity_Hostname)(nil),
(*Identity_Uid)(nil),
(*Identity_Username)(nil),
(*Identity_GcpId)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_proto_v2_common_common_proto_rawDesc,
NumEnums: 4,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_proto_v2_common_common_proto_goTypes,
DependencyIndexes: file_internal_proto_v2_common_common_proto_depIdxs,
EnumInfos: file_internal_proto_v2_common_common_proto_enumTypes,
MessageInfos: file_internal_proto_v2_common_common_proto_msgTypes,
}.Build()
File_internal_proto_v2_common_common_proto = out.File
file_internal_proto_v2_common_common_proto_rawDesc = nil
file_internal_proto_v2_common_common_proto_goTypes = nil
file_internal_proto_v2_common_common_proto_depIdxs = nil
}

View File

@ -1,249 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v3.21.12
// source: internal/proto/v2/s2a_context/s2a_context.proto
package s2a_context_go_proto
import (
common_go_proto "github.com/google/s2a-go/internal/proto/v2/common_go_proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type S2AContext struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The SPIFFE ID from the peer leaf certificate, if present.
//
// This field is only populated if the leaf certificate is a valid SPIFFE
// SVID; in particular, there is a unique URI SAN and this URI SAN is a valid
// SPIFFE ID.
LeafCertSpiffeId string `protobuf:"bytes,1,opt,name=leaf_cert_spiffe_id,json=leafCertSpiffeId,proto3" json:"leaf_cert_spiffe_id,omitempty"`
// The URIs that are present in the SubjectAltName extension of the peer leaf
// certificate.
//
// Note that the extracted URIs are not validated and may not be properly
// formatted.
LeafCertUris []string `protobuf:"bytes,2,rep,name=leaf_cert_uris,json=leafCertUris,proto3" json:"leaf_cert_uris,omitempty"`
// The DNSNames that are present in the SubjectAltName extension of the peer
// leaf certificate.
LeafCertDnsnames []string `protobuf:"bytes,3,rep,name=leaf_cert_dnsnames,json=leafCertDnsnames,proto3" json:"leaf_cert_dnsnames,omitempty"`
// The (ordered) list of fingerprints in the certificate chain used to verify
// the given leaf certificate. The order MUST be from leaf certificate
// fingerprint to root certificate fingerprint.
//
// A fingerprint is the base-64 encoding of the SHA256 hash of the
// DER-encoding of a certificate. The list MAY be populated even if the peer
// certificate chain was NOT validated successfully.
PeerCertificateChainFingerprints []string `protobuf:"bytes,4,rep,name=peer_certificate_chain_fingerprints,json=peerCertificateChainFingerprints,proto3" json:"peer_certificate_chain_fingerprints,omitempty"`
// The local identity used during session setup.
LocalIdentity *common_go_proto.Identity `protobuf:"bytes,9,opt,name=local_identity,json=localIdentity,proto3" json:"local_identity,omitempty"`
// The SHA256 hash of the DER-encoding of the local leaf certificate used in
// the handshake.
LocalLeafCertFingerprint []byte `protobuf:"bytes,6,opt,name=local_leaf_cert_fingerprint,json=localLeafCertFingerprint,proto3" json:"local_leaf_cert_fingerprint,omitempty"`
}
func (x *S2AContext) Reset() {
*x = S2AContext{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_proto_v2_s2a_context_s2a_context_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *S2AContext) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*S2AContext) ProtoMessage() {}
func (x *S2AContext) ProtoReflect() protoreflect.Message {
mi := &file_internal_proto_v2_s2a_context_s2a_context_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use S2AContext.ProtoReflect.Descriptor instead.
func (*S2AContext) Descriptor() ([]byte, []int) {
return file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescGZIP(), []int{0}
}
func (x *S2AContext) GetLeafCertSpiffeId() string {
if x != nil {
return x.LeafCertSpiffeId
}
return ""
}
func (x *S2AContext) GetLeafCertUris() []string {
if x != nil {
return x.LeafCertUris
}
return nil
}
func (x *S2AContext) GetLeafCertDnsnames() []string {
if x != nil {
return x.LeafCertDnsnames
}
return nil
}
func (x *S2AContext) GetPeerCertificateChainFingerprints() []string {
if x != nil {
return x.PeerCertificateChainFingerprints
}
return nil
}
func (x *S2AContext) GetLocalIdentity() *common_go_proto.Identity {
if x != nil {
return x.LocalIdentity
}
return nil
}
func (x *S2AContext) GetLocalLeafCertFingerprint() []byte {
if x != nil {
return x.LocalLeafCertFingerprint
}
return nil
}
var File_internal_proto_v2_s2a_context_s2a_context_proto protoreflect.FileDescriptor
var file_internal_proto_v2_s2a_context_s2a_context_proto_rawDesc = []byte{
0x0a, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x76, 0x32, 0x2f, 0x73, 0x32, 0x61, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2f,
0x73, 0x32, 0x61, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x0c, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x32, 0x1a,
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
0x76, 0x32, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xee, 0x02, 0x0a, 0x0a, 0x53, 0x32, 0x41, 0x43, 0x6f,
0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, 0x13, 0x6c, 0x65, 0x61, 0x66, 0x5f, 0x63, 0x65,
0x72, 0x74, 0x5f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x10, 0x6c, 0x65, 0x61, 0x66, 0x43, 0x65, 0x72, 0x74, 0x53, 0x70, 0x69, 0x66,
0x66, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x65, 0x61, 0x66, 0x5f, 0x63, 0x65, 0x72,
0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x65,
0x61, 0x66, 0x43, 0x65, 0x72, 0x74, 0x55, 0x72, 0x69, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65,
0x61, 0x66, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x64, 0x6e, 0x73, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x6c, 0x65, 0x61, 0x66, 0x43, 0x65, 0x72, 0x74,
0x44, 0x6e, 0x73, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x23, 0x70, 0x65, 0x65, 0x72,
0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61,
0x69, 0x6e, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x18,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x20, 0x70, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x67, 0x65,
0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x16, 0x2e, 0x73, 0x32, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x32, 0x2e, 0x49,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x1b, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f,
0x6c, 0x65, 0x61, 0x66, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72,
0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, 0x6c, 0x6f, 0x63,
0x61, 0x6c, 0x4c, 0x65, 0x61, 0x66, 0x43, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72,
0x70, 0x72, 0x69, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10,
0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x73, 0x32, 0x61,
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
0x76, 0x32, 0x2f, 0x73, 0x32, 0x61, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x67,
0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescOnce sync.Once
file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescData = file_internal_proto_v2_s2a_context_s2a_context_proto_rawDesc
)
func file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescGZIP() []byte {
file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescOnce.Do(func() {
file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescData)
})
return file_internal_proto_v2_s2a_context_s2a_context_proto_rawDescData
}
var file_internal_proto_v2_s2a_context_s2a_context_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_internal_proto_v2_s2a_context_s2a_context_proto_goTypes = []any{
(*S2AContext)(nil), // 0: s2a.proto.v2.S2AContext
(*common_go_proto.Identity)(nil), // 1: s2a.proto.v2.Identity
}
var file_internal_proto_v2_s2a_context_s2a_context_proto_depIdxs = []int32{
1, // 0: s2a.proto.v2.S2AContext.local_identity:type_name -> s2a.proto.v2.Identity
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_internal_proto_v2_s2a_context_s2a_context_proto_init() }
func file_internal_proto_v2_s2a_context_s2a_context_proto_init() {
if File_internal_proto_v2_s2a_context_s2a_context_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_proto_v2_s2a_context_s2a_context_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*S2AContext); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_proto_v2_s2a_context_s2a_context_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_proto_v2_s2a_context_s2a_context_proto_goTypes,
DependencyIndexes: file_internal_proto_v2_s2a_context_s2a_context_proto_depIdxs,
MessageInfos: file_internal_proto_v2_s2a_context_s2a_context_proto_msgTypes,
}.Build()
File_internal_proto_v2_s2a_context_s2a_context_proto = out.File
file_internal_proto_v2_s2a_context_s2a_context_proto_rawDesc = nil
file_internal_proto_v2_s2a_context_s2a_context_proto_goTypes = nil
file_internal_proto_v2_s2a_context_s2a_context_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More