diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-02-12 04:32:43 -0800 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-02-12 04:32:43 -0800 |
| commit | e02b9ef3ee4394db1a387b146aa5faeea95efa3a (patch) | |
| tree | e5b6f2c7c21dda6a5e4ea1617e10e3437c93979c | |
| parent | 0eb26108f65b68aa4e99786f7f7a0b28cd356f72 (diff) | |
| download | xesite-e02b9ef3ee4394db1a387b146aa5faeea95efa3a.tar.xz xesite-e02b9ef3ee4394db1a387b146aa5faeea95efa3a.zip | |
cmd/patreon-saasproxy: use twirp/protobuf instead of yolo json
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | cmd/patreon-saasproxy/main.go | 22 | ||||
| -rw-r--r-- | flake.nix | 4 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | gomod2nix.toml | 3 | ||||
| -rw-r--r-- | internal/adminpb/generate.go | 5 | ||||
| -rw-r--r-- | internal/adminpb/internal.pb.go | 201 | ||||
| -rw-r--r-- | internal/adminpb/internal.proto | 22 | ||||
| -rw-r--r-- | internal/adminpb/internal.twirp.go | 1607 | ||||
| -rw-r--r-- | internal/saasproxytoken/tokensource.go | 24 | ||||
| -rw-r--r-- | pb/generate.go | 5 | ||||
| -rw-r--r-- | pb/xesite.pb.go | 189 | ||||
| -rw-r--r-- | pb/xesite.proto | 17 | ||||
| -rw-r--r-- | pb/xesite.twirp.go | 1109 |
14 files changed, 3202 insertions, 13 deletions
diff --git a/cmd/patreon-saasproxy/main.go b/cmd/patreon-saasproxy/main.go index cfe9717..b44a697 100644 --- a/cmd/patreon-saasproxy/main.go +++ b/cmd/patreon-saasproxy/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/base64" "encoding/json" "expvar" @@ -14,6 +15,8 @@ import ( "github.com/facebookgo/flagenv" _ "github.com/joho/godotenv/autoload" "golang.org/x/oauth2" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" "gopkg.in/mxpv/patreon-go.v1" "tailscale.com/client/tailscale" "tailscale.com/hostinfo" @@ -21,6 +24,7 @@ import ( "tailscale.com/tsnet" "tailscale.com/tsweb" "xeiaso.net/v4/internal" + "xeiaso.net/v4/internal/adminpb" ) var ( @@ -110,6 +114,9 @@ func main() { log.Fatal(err) } + ph := adminpb.NewPatreonServer(s) + http.Handle(adminpb.PatreonPathPrefix, ph) + slog.Info("listening over tailscale", "hostname", *tailscaleHostname) log.Fatal(http.Serve(ln, nil)) @@ -120,6 +127,21 @@ type Server struct { cts oauth2.TokenSource } +func (s *Server) GetToken(ctx context.Context, _ *emptypb.Empty) (*adminpb.PatreonToken, error) { + token, err := s.cts.Token() + if err != nil { + slog.Error("token fetch failed", "err", err) + return nil, err + } + + return &adminpb.PatreonToken{ + AccessToken: token.AccessToken, + TokenType: token.TokenType, + RefreshToken: token.RefreshToken, + Expiry: timestamppb.New(token.Expiry), + }, nil +} + func (s *Server) GiveToken(w http.ResponseWriter, r *http.Request) { whois, err := s.lc.WhoIs(r.Context(), r.RemoteAddr) if err != nil { @@ -192,6 +192,10 @@ zig nodejs + protobuf + protoc-gen-go + protoc-gen-twirp + jq jo @@ -6,9 +6,12 @@ require ( github.com/bep/debounce v1.2.1 github.com/donatj/hmacsig v1.1.0 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 + github.com/go-faker/faker/v4 v4.2.0 github.com/go-git/go-git/v5 v5.10.0 github.com/joho/godotenv v1.5.1 + github.com/twitchtv/twirp v8.1.3+incompatible golang.org/x/oauth2 v0.12.0 + google.golang.org/protobuf v1.31.0 gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/mxpv/patreon-go.v1 v1.0.0-20171031001022-1d2f253ac700 tailscale.com v1.58.2 @@ -48,7 +51,6 @@ require ( github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect - github.com/go-faker/faker/v4 v4.2.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -117,7 +119,6 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.31.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect @@ -276,6 +276,8 @@ github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4Ca github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= +github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= +github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= diff --git a/gomod2nix.toml b/gomod2nix.toml index cd59d19..cfbe714 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -259,6 +259,9 @@ schema = 3 [mod."github.com/tcnksm/go-httpstat"] version = "v0.2.0" hash = "sha256-bCWn8E+DcZY6+yPu07AF3hCcDZx3CFdD74qfpDIgVqI=" + [mod."github.com/twitchtv/twirp"] + version = "v8.1.3+incompatible" + hash = "sha256-j1h9YE3wl9h36DfPf92To1H/PwNk4CgerOARNO3HK1w=" [mod."github.com/u-root/uio"] version = "v0.0.0-20230305220412-3e8cd9d6bf63" hash = "sha256-y0VT9PLROozi6wNMgnX706ifumQxlMc8y4/XZDhdfMY=" diff --git a/internal/adminpb/generate.go b/internal/adminpb/generate.go new file mode 100644 index 0000000..29a5216 --- /dev/null +++ b/internal/adminpb/generate.go @@ -0,0 +1,5 @@ +package adminpb + +func init() {} + +//go:generate protoc --proto_path=. --proto_path=../../pb --go_out=. --go_opt=paths=source_relative --twirp_out=. --twirp_opt=paths=source_relative internal.proto diff --git a/internal/adminpb/internal.pb.go b/internal/adminpb/internal.pb.go new file mode 100644 index 0000000..fc47e2b --- /dev/null +++ b/internal/adminpb/internal.pb.go @@ -0,0 +1,201 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.24.4 +// source: internal.proto + +package adminpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + pb "xeiaso.net/v4/pb" +) + +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 PatreonToken struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + Expiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiry,proto3" json:"expiry,omitempty"` +} + +func (x *PatreonToken) Reset() { + *x = PatreonToken{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PatreonToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PatreonToken) ProtoMessage() {} + +func (x *PatreonToken) ProtoReflect() protoreflect.Message { + mi := &file_internal_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 PatreonToken.ProtoReflect.Descriptor instead. +func (*PatreonToken) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{0} +} + +func (x *PatreonToken) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *PatreonToken) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + +func (x *PatreonToken) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +func (x *PatreonToken) GetExpiry() *timestamppb.Timestamp { + if x != nil { + return x.Expiry + } + return nil +} + +var File_internal_proto protoreflect.FileDescriptor + +var file_internal_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x13, 0x78, 0x65, 0x69, 0x61, 0x73, 0x6f, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x78, 0x65, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0xa9, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x74, 0x72, 0x65, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x32, 0x50, 0x0a, + 0x07, 0x50, 0x61, 0x74, 0x72, 0x65, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x78, + 0x65, 0x69, 0x61, 0x73, 0x6f, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x50, 0x61, 0x74, 0x72, 0x65, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32, + 0x41, 0x0a, 0x05, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x38, 0x0a, 0x07, 0x52, 0x65, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x15, 0x2e, 0x78, 0x65, + 0x69, 0x61, 0x73, 0x6f, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, + 0x66, 0x6f, 0x42, 0x20, 0x5a, 0x1e, 0x78, 0x65, 0x69, 0x61, 0x73, 0x6f, 0x2e, 0x6e, 0x65, 0x74, + 0x2f, 0x76, 0x34, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x64, 0x6d, + 0x69, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_internal_proto_rawDescOnce sync.Once + file_internal_proto_rawDescData = file_internal_proto_rawDesc +) + +func file_internal_proto_rawDescGZIP() []byte { + file_internal_proto_rawDescOnce.Do(func() { + file_internal_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_proto_rawDescData) + }) + return file_internal_proto_rawDescData +} + +var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_internal_proto_goTypes = []interface{}{ + (*PatreonToken)(nil), // 0: xeiaso.net.internal.PatreonToken + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty + (*pb.BuildInfo)(nil), // 3: xeiaso.net.BuildInfo +} +var file_internal_proto_depIdxs = []int32{ + 1, // 0: xeiaso.net.internal.PatreonToken.expiry:type_name -> google.protobuf.Timestamp + 2, // 1: xeiaso.net.internal.Patreon.GetToken:input_type -> google.protobuf.Empty + 2, // 2: xeiaso.net.internal.Admin.Rebuild:input_type -> google.protobuf.Empty + 0, // 3: xeiaso.net.internal.Patreon.GetToken:output_type -> xeiaso.net.internal.PatreonToken + 3, // 4: xeiaso.net.internal.Admin.Rebuild:output_type -> xeiaso.net.BuildInfo + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] 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_init() } +func file_internal_proto_init() { + if File_internal_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_internal_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PatreonToken); 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_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_internal_proto_goTypes, + DependencyIndexes: file_internal_proto_depIdxs, + MessageInfos: file_internal_proto_msgTypes, + }.Build() + File_internal_proto = out.File + file_internal_proto_rawDesc = nil + file_internal_proto_goTypes = nil + file_internal_proto_depIdxs = nil +} diff --git a/internal/adminpb/internal.proto b/internal/adminpb/internal.proto new file mode 100644 index 0000000..a0755ff --- /dev/null +++ b/internal/adminpb/internal.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package xeiaso.net.internal.admin; +option go_package = "xeiaso.net/v4/internal/adminpb"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "xesite.proto"; + +service Patreon { + rpc GetToken (google.protobuf.Empty) returns (PatreonToken); +} + +message PatreonToken { + string access_token = 1; + string token_type = 2; + string refresh_token = 3; + google.protobuf.Timestamp expiry = 4; +} + +service Admin { + rpc Rebuild(google.protobuf.Empty) returns (xeiaso.net.BuildInfo); +}
\ No newline at end of file diff --git a/internal/adminpb/internal.twirp.go b/internal/adminpb/internal.twirp.go new file mode 100644 index 0000000..3ad3281 --- /dev/null +++ b/internal/adminpb/internal.twirp.go @@ -0,0 +1,1607 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: internal.proto + +package adminpb + +import context "context" +import fmt "fmt" +import http "net/http" +import io "io" +import json "encoding/json" +import strconv "strconv" +import strings "strings" + +import protojson "google.golang.org/protobuf/encoding/protojson" +import proto "google.golang.org/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +import google_protobuf "google.golang.org/protobuf/types/known/emptypb" +import xeiaso_net "xeiaso.net/v4/pb" + +import bytes "bytes" +import errors "errors" +import path "path" +import url "net/url" + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// ================= +// Patreon Interface +// ================= + +type Patreon interface { + GetToken(context.Context, *google_protobuf.Empty) (*PatreonToken, error) +} + +// ======================= +// Patreon Protobuf Client +// ======================= + +type patreonProtobufClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewPatreonProtobufClient creates a Protobuf client that implements the Patreon interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewPatreonProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Patreon { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "xeiaso.net.internal", "Patreon") + urls := [1]string{ + serviceURL + "GetToken", + } + + return &patreonProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *patreonProtobufClient) GetToken(ctx context.Context, in *google_protobuf.Empty) (*PatreonToken, error) { + ctx = ctxsetters.WithPackageName(ctx, "xeiaso.net.internal") + ctx = ctxsetters.WithServiceName(ctx, "Patreon") + ctx = ctxsetters.WithMethodName(ctx, "GetToken") + caller := c.callGetToken + if c.interceptor != nil { + caller = func(ctx context.Context, req *google_protobuf.Empty) (*PatreonToken, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*google_protobuf.Empty) when calling interceptor") + } + return c.callGetToken(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*PatreonToken) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*PatreonToken) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *patreonProtobufClient) callGetToken(ctx context.Context, in *google_protobuf.Empty) (*PatreonToken, error) { + out := new(PatreonToken) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// =================== +// Patreon JSON Client +// =================== + +type patreonJSONClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewPatreonJSONClient creates a JSON client that implements the Patreon interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewPatreonJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Patreon { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "xeiaso.net.internal", "Patreon") + urls := [1]string{ + serviceURL + "GetToken", + } + + return &patreonJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *patreonJSONClient) GetToken(ctx context.Context, in *google_protobuf.Empty) (*PatreonToken, error) { + ctx = ctxsetters.WithPackageName(ctx, "xeiaso.net.internal") + ctx = ctxsetters.WithServiceName(ctx, "Patreon") + ctx = ctxsetters.WithMethodName(ctx, "GetToken") + caller := c.callGetToken + if c.interceptor != nil { + caller = func(ctx context.Context, req *google_protobuf.Empty) (*PatreonToken, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*google_protobuf.Empty) when calling interceptor") + } + return c.callGetToken(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*PatreonToken) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*PatreonToken) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *patreonJSONClient) callGetToken(ctx context.Context, in *google_protobuf.Empty) (*PatreonToken, error) { + out := new(PatreonToken) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ====================== +// Patreon Server Handler +// ====================== + +type patreonServer struct { + Patreon + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewPatreonServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewPatreonServer(svc Patreon, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &patreonServer{ + Patreon: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *patreonServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *patreonServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// PatreonPathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const PatreonPathPrefix = "/twirp/xeiaso.net.internal.Patreon/" + +func (s *patreonServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "xeiaso.net.internal") + ctx = ctxsetters.WithServiceName(ctx, "Patreon") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: [<prefix>]/<package>.<Service>/<Method> + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "xeiaso.net.internal.Patreon" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "GetToken": + s.serveGetToken(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *patreonServer) serveGetToken(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveGetTokenJSON(ctx, resp, req) + case "application/protobuf": + s.serveGetTokenProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *patreonServer) serveGetTokenJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "GetToken") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(google_protobuf.Empty) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Patreon.GetToken + if s.interceptor != nil { + handler = func(ctx context.Context, req *google_protobuf.Empty) (*PatreonToken, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*google_protobuf.Empty) when calling interceptor") + } + return s.Patreon.GetToken(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*PatreonToken) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*PatreonToken) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *PatreonToken + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *PatreonToken and nil error while calling GetToken. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *patreonServer) serveGetTokenProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "GetToken") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(google_protobuf.Empty) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Patreon.GetToken + if s.interceptor != nil { + handler = func(ctx context.Context, req *google_protobuf.Empty) (*PatreonToken, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*google_protobuf.Empty) when calling interceptor") + } + return s.Patreon.GetToken(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*PatreonToken) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*PatreonToken) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *PatreonToken + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *PatreonToken and nil error while calling GetToken. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.Write |
