Commit 59d86caf authored by Ahmad Sherif's avatar Ahmad Sherif

Communicate with Gitaly via gRPC

parent 468b4fe7
{ {
"ImportPath": "gitlab.com/gitlab-org/gitlab-workhorse", "ImportPath": "gitlab.com/gitlab-org/gitlab-workhorse",
"GoVersion": "go1.5", "GoVersion": "go1.5",
"GodepVersion": "v74", "GodepVersion": "v78",
"Packages": [
"./..."
],
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/beorn7/perks/quantile", "ImportPath": "github.com/beorn7/perks/quantile",
...@@ -72,6 +69,99 @@ ...@@ -72,6 +69,99 @@
{ {
"ImportPath": "github.com/prometheus/procfs", "ImportPath": "github.com/prometheus/procfs",
"Rev": "abf152e5f3e97f2fafac028d2cc06c1feb87ffa5" "Rev": "abf152e5f3e97f2fafac028d2cc06c1feb87ffa5"
},
{
"ImportPath": "gitlab.com/gitlab-org/gitaly/protos/go",
"Comment": "v0.2.0",
"Rev": "833b069cd41ec76f56091da40722535735431d2c"
},
{
"ImportPath": "gitlab.com/gitlab-org/gitaly/protos/go/helper",
"Comment": "v0.2.0",
"Rev": "833b069cd41ec76f56091da40722535735431d2c"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/http2",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/http2/hpack",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/idna",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/internal/timeseries",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/lex/httplex",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "golang.org/x/net/trace",
"Rev": "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
},
{
"ImportPath": "google.golang.org/grpc",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/codes",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/credentials",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/grpclog",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/internal",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/metadata",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/naming",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/peer",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/stats",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/tap",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
},
{
"ImportPath": "google.golang.org/grpc/transport",
"Comment": "v1.0.5-14-g5095579",
"Rev": "50955793b0183f9de69bd78e2ec251cf20aab121"
} }
] ]
} }
...@@ -3,33 +3,14 @@ package git ...@@ -3,33 +3,14 @@ package git
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/gitaly" "gitlab.com/gitlab-org/gitlab-workhorse/internal/gitaly"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
) )
func GetInfoRefsHandler(a *api.API, cfg *config.Config) http.Handler { func GetInfoRefsHandler(a *api.API) http.Handler {
return repoPreAuthorizeHandler(a, func(rw http.ResponseWriter, r *http.Request, apiResponse *api.Response) { return repoPreAuthorizeHandler(a, handleGetInfoRefs)
if apiResponse.GitalySocketPath == "" {
handleGetInfoRefs(rw, r, apiResponse)
} else {
handleGetInfoRefsWithGitaly(rw, r, apiResponse, gitaly.NewClient(apiResponse.GitalySocketPath, cfg))
}
})
}
func handleGetInfoRefsWithGitaly(rw http.ResponseWriter, r *http.Request, a *api.Response, gitalyClient *gitaly.Client) {
req := *r // Make a copy of r
req.Header = helper.HeaderClone(r.Header)
req.Header.Add("Gitaly-Repo-Path", a.RepoPath)
req.Header.Add("Gitaly-GL-Id", a.GL_ID)
req.URL.Path = path.Join(a.GitalyResourcePath, subCommand(getService(r)))
req.URL.RawQuery = ""
gitalyClient.Proxy.ServeHTTP(rw, &req)
} }
func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) { func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) {
...@@ -47,12 +28,19 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) ...@@ -47,12 +28,19 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", rpc)) w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", rpc))
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
if err := writeBody(w, a, rpc); err != nil { var err error
if a.GitalySocketPath == "" {
err = handleGetInfoRefsLocally(w, a, rpc)
} else {
err = handleGetInfoRefsWithGitaly(w, a, rpc)
}
if err != nil {
helper.LogError(r, fmt.Errorf("handleGetInfoRefs: %v", err)) helper.LogError(r, fmt.Errorf("handleGetInfoRefs: %v", err))
} }
} }
func writeBody(w http.ResponseWriter, a *api.Response, rpc string) error { func handleGetInfoRefsLocally(w http.ResponseWriter, a *api.Response, rpc string) error {
if err := pktLine(w, fmt.Sprintf("# service=%s\n", rpc)); err != nil { if err := pktLine(w, fmt.Sprintf("# service=%s\n", rpc)); err != nil {
return fmt.Errorf("pktLine: %v", err) return fmt.Errorf("pktLine: %v", err)
} }
...@@ -72,3 +60,21 @@ func writeBody(w http.ResponseWriter, a *api.Response, rpc string) error { ...@@ -72,3 +60,21 @@ func writeBody(w http.ResponseWriter, a *api.Response, rpc string) error {
return nil return nil
} }
func handleGetInfoRefsWithGitaly(w http.ResponseWriter, a *api.Response, rpc string) error {
smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalySocketPath)
if err != nil {
return fmt.Errorf("GetInfoRefsHandler: %v", err)
}
infoRefsResponseWriter, err := smarthttp.InfoRefsResponseWriterTo(a.RepoPath, rpc)
if err != nil {
return fmt.Errorf("GetInfoRefsHandler: %v", err)
}
if _, err = infoRefsResponseWriter.WriteTo(w); err != nil {
return fmt.Errorf("handleGetInfoRefsWithGitaly: Error receiving response from server: %v", err)
}
return nil
}
package gitaly package gitaly
import ( import (
"net"
"sync" "sync"
"time"
pb "gitlab.com/gitlab-org/gitaly/protos/go"
"google.golang.org/grpc"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/badgateway" "gitlab.com/gitlab-org/gitlab-workhorse/internal/badgateway"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/proxy"
) )
type Client struct { type connectionsCache struct {
Proxy *proxy.Proxy
}
type clientCache struct {
sync.RWMutex sync.RWMutex
clients map[string]*Client connections map[string]*grpc.ClientConn
} }
var cache = clientCache{ var cache = connectionsCache{
clients: make(map[string]*Client), connections: make(map[string]*grpc.ClientConn),
} }
func NewClient(socketPath string, cfg *config.Config) *Client { func NewSmartHTTPClient(socketPath string) (*SmartHTTPClient, error) {
if client := getClient(socketPath); client != nil { conn, err := getOrCreateConnection(socketPath)
return client if err != nil {
return nil, err
} }
grpcClient := pb.NewSmartHTTPClient(conn)
return &SmartHTTPClient{grpcClient}, nil
}
func getOrCreateConnection(socketPath string) (*grpc.ClientConn, error) {
cache.Lock() cache.Lock()
defer cache.Unlock() defer cache.Unlock()
if client := cache.clients[socketPath]; client != nil { if conn := cache.connections[socketPath]; conn != nil {
return client return conn, nil
} }
client := &Client{} conn, err := newConnection(socketPath)
roundTripper := badgateway.NewRoundTripper(nil, socketPath, cfg.ProxyHeadersTimeout, cfg.DevelopmentMode) if err != nil {
client.Proxy = proxy.NewProxy(nil, cfg.Version, roundTripper) return nil, err
client.Proxy.AllowResponseBuffering = false }
cache.clients[socketPath] = client cache.connections[socketPath] = conn
return client return conn, nil
} }
func getClient(socketPath string) *Client { func CloseConnections() {
cache.RLock() cache.Lock()
defer cache.RUnlock() defer cache.Unlock()
for _, conn := range cache.connections {
conn.Close()
}
}
func newConnection(socketPath string) (*grpc.ClientConn, error) {
connOpts := []grpc.DialOption{
grpc.WithInsecure(), // Since we're connecting to Gitaly over UNIX, we don't need to use TLS credentials.
grpc.WithDialer(func(addr string, _ time.Duration) (net.Conn, error) {
return badgateway.DefaultDialer.Dial("unix", addr)
}),
}
conn, err := grpc.Dial(socketPath, connOpts...)
if err != nil {
return nil, err
}
return cache.clients[socketPath] return conn, nil
} }
package gitaly
import (
"fmt"
"io"
pb "gitlab.com/gitlab-org/gitaly/protos/go"
pbhelper "gitlab.com/gitlab-org/gitaly/protos/go/helper"
"golang.org/x/net/context"
)
type SmartHTTPClient struct {
pb.SmartHTTPClient
}
func (client *SmartHTTPClient) InfoRefsResponseWriterTo(repoPath, rpc string) (io.WriterTo, error) {
repo := &pb.Repository{Path: repoPath}
rpcRequest := &pb.InfoRefsRequest{Repository: repo}
var c pbhelper.InfoRefsClient
var err error
switch rpc {
case "git-upload-pack":
c, err = client.InfoRefsUploadPack(context.Background(), rpcRequest)
case "git-receive-pack":
c, err = client.InfoRefsReceivePack(context.Background(), rpcRequest)
default:
return nil, fmt.Errorf("InfoRefsResponseWriterTo: Unsupported RPC: %q", rpc)
}
if err != nil {
return nil, fmt.Errorf("InfoRefsResponseWriterTo: RPC call failed: %v", err)
}
return &pbhelper.InfoRefsClientWriterTo{c}, nil
}
package testhelper
import (
pb "gitlab.com/gitlab-org/gitaly/protos/go"
)
type GitalyTestServer struct{}
const GitalyInfoRefsResponseMock = "Mock Gitaly InfoRefsResponse data"
func NewGitalyServer() *GitalyTestServer {
return &GitalyTestServer{}
}
func (s *GitalyTestServer) InfoRefsUploadPack(in *pb.InfoRefsRequest, stream pb.SmartHTTP_InfoRefsUploadPackServer) error {
response := &pb.InfoRefsResponse{
Data: []byte(GitalyInfoRefsResponseMock),
}
return stream.Send(response)
}
func (s *GitalyTestServer) InfoRefsReceivePack(in *pb.InfoRefsRequest, stream pb.SmartHTTP_InfoRefsReceivePackServer) error {
response := &pb.InfoRefsResponse{
Data: []byte(GitalyInfoRefsResponseMock),
}
return stream.Send(response)
}
...@@ -121,7 +121,7 @@ func (u *Upstream) configureRoutes() { ...@@ -121,7 +121,7 @@ func (u *Upstream) configureRoutes() {
u.Routes = []routeEntry{ u.Routes = []routeEntry{
// Git Clone // Git Clone
route("GET", gitProjectPattern+`info/refs\z`, git.GetInfoRefsHandler(api, &u.Config)), route("GET", gitProjectPattern+`info/refs\z`, git.GetInfoRefsHandler(api)),
route("POST", gitProjectPattern+`git-upload-pack\z`, contentEncodingHandler(git.UploadPack(api)), isContentType("application/x-git-upload-pack-request")), route("POST", gitProjectPattern+`git-upload-pack\z`, contentEncodingHandler(git.UploadPack(api)), isContentType("application/x-git-upload-pack-request")),
route("POST", gitProjectPattern+`git-receive-pack\z`, contentEncodingHandler(git.ReceivePack(api)), isContentType("application/x-git-receive-pack-request")), route("POST", gitProjectPattern+`git-receive-pack\z`, contentEncodingHandler(git.ReceivePack(api)), isContentType("application/x-git-receive-pack-request")),
route("PUT", gitProjectPattern+`gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`, lfs.PutStore(api, proxy), isContentType("application/octet-stream")), route("PUT", gitProjectPattern+`gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`, lfs.PutStore(api, proxy), isContentType("application/octet-stream")),
......
...@@ -21,9 +21,13 @@ import ( ...@@ -21,9 +21,13 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config" "gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/gitaly"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream" "gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream"
pb "gitlab.com/gitlab-org/gitaly/protos/go"
"google.golang.org/grpc"
) )
const scratchDir = "testdata/scratch" const scratchDir = "testdata/scratch"
...@@ -34,6 +38,7 @@ const testProject = "group/test" ...@@ -34,6 +38,7 @@ const testProject = "group/test"
var checkoutDir = path.Join(scratchDir, "test") var checkoutDir = path.Join(scratchDir, "test")
var cacheDir = path.Join(scratchDir, "cache") var cacheDir = path.Join(scratchDir, "cache")
var gitalySocketPath = path.Join(scratchDir, "gitaly.sock")
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
source := "https://gitlab.com/gitlab-org/gitlab-test.git" source := "https://gitlab.com/gitlab-org/gitlab-test.git"
...@@ -561,15 +566,16 @@ func TestApiContentTypeBlock(t *testing.T) { ...@@ -561,15 +566,16 @@ func TestApiContentTypeBlock(t *testing.T) {
} }
func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) { func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) {
content := "0000"
apiResponse := gitOkBody(t) apiResponse := gitOkBody(t)
apiResponse.GitalyResourcePath = "/projects/1/git-http/info-refs" apiResponse.GitalyResourcePath = "/projects/1/git-http/info-refs"
gitalyPath := path.Join(apiResponse.GitalyResourcePath, "upload-pack") gitalyServer := startGitalyServer(t)
gitaly := startGitalyServer(regexp.MustCompile(gitalyPath), content) defer func() {
defer gitaly.Close() gitalyServer.Stop()
gitaly.CloseConnections()
}()
apiResponse.GitalySocketPath = gitaly.Listener.Addr().String() apiResponse.GitalySocketPath = gitalySocketPath
ts := testAuthServer(nil, 200, apiResponse) ts := testAuthServer(nil, 200, apiResponse)
defer ts.Close() defer ts.Close()
...@@ -587,14 +593,18 @@ func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) { ...@@ -587,14 +593,18 @@ func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) {
t.Error(err) t.Error(err)
} }
if !bytes.Equal(responseBody, []byte(content)) { expectedContent := testhelper.GitalyInfoRefsResponseMock
t.Errorf("GET %q: Expected %q, got %q", resource, content, responseBody) if !bytes.Equal(responseBody, []byte(expectedContent)) {
t.Errorf("GET %q: Expected %q, got %q", resource, expectedContent, responseBody)
} }
} }
func TestGetInfoRefsHandledLocallyDueToEmptyGitalySocketPath(t *testing.T) { func TestGetInfoRefsHandledLocallyDueToEmptyGitalySocketPath(t *testing.T) {
gitaly := startGitalyServer(nil, "Gitaly response: should never reach the client") gitalyServer := startGitalyServer(t)
defer gitaly.Close() defer func() {
gitalyServer.Stop()
gitaly.CloseConnections()
}()
apiResponse := gitOkBody(t) apiResponse := gitOkBody(t)
apiResponse.GitalySocketPath = "" apiResponse.GitalySocketPath = ""
...@@ -619,7 +629,8 @@ func TestGetInfoRefsHandledLocallyDueToEmptyGitalySocketPath(t *testing.T) { ...@@ -619,7 +629,8 @@ func TestGetInfoRefsHandledLocallyDueToEmptyGitalySocketPath(t *testing.T) {
t.Errorf("GET %q: expected 200, got %d", resource, resp.StatusCode) t.Errorf("GET %q: expected 200, got %d", resource, resp.StatusCode)
} }
if bytes.Contains(responseBody, []byte("Gitaly response")) { expectedContent := testhelper.GitalyInfoRefsResponseMock
if bytes.Contains(responseBody, []byte(expectedContent)) {
t.Errorf("GET %q: request should not have been proxied to Gitaly", resource) t.Errorf("GET %q: request should not have been proxied to Gitaly", resource)
} }
} }
...@@ -797,26 +808,18 @@ func startWorkhorseServerWithConfig(cfg *config.Config) *httptest.Server { ...@@ -797,26 +808,18 @@ func startWorkhorseServerWithConfig(cfg *config.Config) *httptest.Server {
return httptest.NewServer(u) return httptest.NewServer(u)
} }
func startGitalyServer(url *regexp.Regexp, body string) *httptest.Server { func startGitalyServer(t *testing.T) *grpc.Server {
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := grpc.NewServer()
listener, err := net.Listen("unix", gitalySocketPath)
if url != nil && !url.MatchString(r.URL.Path) {
log.Println("Gitaly", r.Method, r.URL, "DENY")
w.WriteHeader(404)
return
}
fmt.Fprint(w, body)
}))
listener, err := net.Listen("unix", path.Join(scratchDir, "gitaly.sock"))
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
ts.Listener = listener
ts.Start() pb.RegisterSmartHTTPServer(server, testhelper.NewGitalyServer())
return ts
go server.Serve(listener)
return server
} }
func runOrFail(t *testing.T, cmd *exec.Cmd) { func runOrFail(t *testing.T, cmd *exec.Cmd) {
......
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
The MIT License (MIT)
Copyright (c) 2016-2017 GitLab B.V.
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.
This diff is collapsed.
# Auto-generated Go gRPC bindings for gitaly
This Go package is used both by the Gitaly server itself and by Go
Gitaly clients (such as gitlab-workhorse).
// Code generated by protoc-gen-go.
// source: gitaly.proto
// DO NOT EDIT!
/*
Package gitaly is a generated protocol buffer package.
It is generated from these files:
gitaly.proto
It has these top-level messages:
InfoRefsRequest
InfoRefsResponse
Repository
*/
package gitaly
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type InfoRefsRequest struct {
Repository *Repository `protobuf:"bytes,1,opt,name=repository" json:"repository,omitempty"`
}
func (m *InfoRefsRequest) Reset() { *m = InfoRefsRequest{} }
func (m *InfoRefsRequest) String() string { return proto.CompactTextString(m) }
func (*InfoRefsRequest) ProtoMessage() {}
func (*InfoRefsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *InfoRefsRequest) GetRepository() *Repository {
if m != nil {
return m.Repository
}
return nil
}
type InfoRefsResponse struct {
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}
func (m *InfoRefsResponse) Reset() { *m = InfoRefsResponse{} }
func (m *InfoRefsResponse) String() string { return proto.CompactTextString(m) }
func (*InfoRefsResponse) ProtoMessage() {}
func (*InfoRefsResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *InfoRefsResponse) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
type Repository struct {
Path string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
}
func (m *Repository) Reset() { *m = Repository{} }
func (m *Repository) String() string { return proto.CompactTextString(m) }
func (*Repository) ProtoMessage() {}
func (*Repository) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *Repository) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func init() {
proto.RegisterType((*InfoRefsRequest)(nil), "gitaly.InfoRefsRequest")
proto.RegisterType((*InfoRefsResponse)(nil), "gitaly.InfoRefsResponse")
proto.RegisterType((*Repository)(nil), "gitaly.Repository")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for SmartHTTP service
type SmartHTTPClient interface {
// The response body for GET /info/refs?service=git-upload-pack
InfoRefsUploadPack(ctx context.Context, in *InfoRefsRequest, opts ...grpc.CallOption) (SmartHTTP_InfoRefsUploadPackClient, error)
// The response body for GET /info/refs?service=git-receive-pack
InfoRefsReceivePack(ctx context.Context, in *InfoRefsRequest, opts ...grpc.CallOption) (SmartHTTP_InfoRefsReceivePackClient, error)
}
type smartHTTPClient struct {
cc *grpc.ClientConn
}
func NewSmartHTTPClient(cc *grpc.ClientConn) SmartHTTPClient {
return &smartHTTPClient{cc}
}
func (c *smartHTTPClient) InfoRefsUploadPack(ctx context.Context, in *InfoRefsRequest, opts ...grpc.CallOption) (SmartHTTP_InfoRefsUploadPackClient, error) {
stream, err := grpc.NewClientStream(ctx, &_SmartHTTP_serviceDesc.Streams[0], c.cc, "/gitaly.SmartHTTP/InfoRefsUploadPack", opts...)
if err != nil {
return nil, err
}
x := &smartHTTPInfoRefsUploadPackClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type SmartHTTP_InfoRefsUploadPackClient interface {
Recv() (*InfoRefsResponse, error)
grpc.ClientStream
}
type smartHTTPInfoRefsUploadPackClient struct {
grpc.ClientStream
}
func (x *smartHTTPInfoRefsUploadPackClient) Recv() (*InfoRefsResponse, error) {
m := new(InfoRefsResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *smartHTTPClient) InfoRefsReceivePack(ctx context.Context, in *InfoRefsRequest, opts ...grpc.CallOption) (SmartHTTP_InfoRefsReceivePackClient, error) {
stream, err := grpc.NewClientStream(ctx, &_SmartHTTP_serviceDesc.Streams[1], c.cc, "/gitaly.SmartHTTP/InfoRefsReceivePack", opts...)
if err != nil {
return nil, err
}
x := &smartHTTPInfoRefsReceivePackClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type SmartHTTP_InfoRefsReceivePackClient interface {
Recv() (*InfoRefsResponse, error)
grpc.ClientStream
}
type smartHTTPInfoRefsReceivePackClient struct {
grpc.ClientStream
}
func (x *smartHTTPInfoRefsReceivePackClient) Recv() (*InfoRefsResponse, error) {
m := new(InfoRefsResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// Server API for SmartHTTP service
type SmartHTTPServer interface {
// The response body for GET /info/refs?service=git-upload-pack
InfoRefsUploadPack(*InfoRefsRequest, SmartHTTP_InfoRefsUploadPackServer) error
// The response body for GET /info/refs?service=git-receive-pack
InfoRefsReceivePack(*InfoRefsRequest, SmartHTTP_InfoRefsReceivePackServer) error
}
func RegisterSmartHTTPServer(s *grpc.Server, srv SmartHTTPServer) {
s.RegisterService(&_SmartHTTP_serviceDesc, srv)
}
func _SmartHTTP_InfoRefsUploadPack_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(InfoRefsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(SmartHTTPServer).InfoRefsUploadPack(m, &smartHTTPInfoRefsUploadPackServer{stream})
}
type SmartHTTP_InfoRefsUploadPackServer interface {
Send(*InfoRefsResponse) error
grpc.ServerStream
}
type smartHTTPInfoRefsUploadPackServer struct {
grpc.ServerStream
}
func (x *smartHTTPInfoRefsUploadPackServer) Send(m *InfoRefsResponse) error {
return x.ServerStream.SendMsg(m)
}
func _SmartHTTP_InfoRefsReceivePack_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(InfoRefsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(SmartHTTPServer).InfoRefsReceivePack(m, &smartHTTPInfoRefsReceivePackServer{stream})
}
type SmartHTTP_InfoRefsReceivePackServer interface {
Send(*InfoRefsResponse) error
grpc.ServerStream
}
type smartHTTPInfoRefsReceivePackServer struct {
grpc.ServerStream
}
func (x *smartHTTPInfoRefsReceivePackServer) Send(m *InfoRefsResponse) error {
return x.ServerStream.SendMsg(m)
}
var _SmartHTTP_serviceDesc = grpc.ServiceDesc{
ServiceName: "gitaly.SmartHTTP",
HandlerType: (*SmartHTTPServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "InfoRefsUploadPack",
Handler: _SmartHTTP_InfoRefsUploadPack_Handler,
ServerStreams: true,
},
{
StreamName: "InfoRefsReceivePack",
Handler: _SmartHTTP_InfoRefsReceivePack_Handler,
ServerStreams: true,
},
},
Metadata: "gitaly.proto",
}
func init() { proto.RegisterFile("gitaly.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 202 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x49, 0xcf, 0x2c, 0x49,
0xcc, 0xa9, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x83, 0xf0, 0x94, 0x5c, 0xb9, 0xf8,
0x3d, 0xf3, 0xd2, 0xf2, 0x83, 0x52, 0xd3, 0x8a, 0x83, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84,
0x8c, 0xb8, 0xb8, 0x8a, 0x52, 0x0b, 0xf2, 0x8b, 0x33, 0x4b, 0xf2, 0x8b, 0x2a, 0x25, 0x18, 0x15,
0x18, 0x35, 0xb8, 0x8d, 0x84, 0xf4, 0xa0, 0xba, 0x83, 0xe0, 0x32, 0x41, 0x48, 0xaa, 0x94, 0xd4,
0xb8, 0x04, 0x10, 0xc6, 0x14, 0x17, 0xe4, 0xe7, 0x15, 0xa7, 0x0a, 0x09, 0x71, 0xb1, 0xa4, 0x24,
0x96, 0x24, 0x82, 0x4d, 0xe0, 0x09, 0x02, 0xb3, 0x95, 0x14, 0xb8, 0xb8, 0x10, 0x26, 0x80, 0x54,
0x14, 0x24, 0x96, 0x64, 0x80, 0x55, 0x70, 0x06, 0x81, 0xd9, 0x46, 0xcb, 0x18, 0xb9, 0x38, 0x83,
0x73, 0x13, 0x8b, 0x4a, 0x3c, 0x42, 0x42, 0x02, 0x84, 0xbc, 0xb9, 0x84, 0x60, 0xe6, 0x86, 0x16,
0xe4, 0xe4, 0x27, 0xa6, 0x04, 0x24, 0x26, 0x67, 0x0b, 0x89, 0xc3, 0x5c, 0x83, 0xe6, 0x74, 0x29,
0x09, 0x4c, 0x09, 0x88, 0x63, 0x94, 0x18, 0x0c, 0x18, 0x85, 0x7c, 0xb8, 0x84, 0x11, 0xe2, 0xc9,
0xa9, 0x99, 0x65, 0xa9, 0x14, 0x98, 0x96, 0xc4, 0x06, 0x0e, 0x48, 0x63, 0x40, 0x00, 0x00, 0x00,
0xff, 0xff, 0x35, 0xf6, 0x6a, 0x74, 0x58, 0x01, 0x00, 0x00,
}
package helper
import (
"io"
pb "gitlab.com/gitlab-org/gitaly/protos/go"
)
type InfoRefsClient interface {
Recv() (*pb.InfoRefsResponse, error)
}
type InfoRefsClientWriterTo struct {
InfoRefsClient
}
func (clientReader *InfoRefsClientWriterTo) WriteTo(w io.Writer) (total int64, err error) {
for {
response, err := clientReader.Recv()
if err == io.EOF {
return total, nil
} else if err != nil {
return total, err
}
n, err := w.Write(response.GetData())
total += int64(n)
if err != nil {
return total, err
}
}
}
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google 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,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package context defines the Context type, which carries deadlines,
// cancelation signals, and other request-scoped values across API boundaries
// and between processes.
//
// Incoming requests to a server should create a Context, and outgoing calls to
// servers should accept a Context. The chain of function calls between must
// propagate the Context, optionally replacing it with a modified copy created
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
//
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
// func DoSomething(ctx context.Context, arg Arg) error {
// // ... use ctx ...
// }
//
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
// if you are unsure about which Context to use.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The same Context may be passed to functions running in different goroutines;
// Contexts are safe for simultaneous use by multiple goroutines.
//
// See http://blog.golang.org/context for example code for a server that uses
// Contexts.
package context
import "time"
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
Done() <-chan struct{}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stores using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "golang.org/x/net/context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key = 0
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter). TODO is recognized by static analysis tools that determine
// whether Contexts are propagated correctly in a program.
func TODO() Context {
return todo
}
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.7
package context
import (
"context" // standard library's context, as of Go 1.7
"time"
)
var (
todo = context.TODO()
background = context.Background()
)
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = context.Canceled
// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded = context.DeadlineExceeded
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
ctx, f := context.WithCancel(parent)
return ctx, CancelFunc(f)
}
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
ctx, f := context.WithDeadline(parent, deadline)
return ctx, CancelFunc(f)
}
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key interface{}, val interface{}) Context {
return context.WithValue(parent, key, val)
}
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.7
package context
import (
"errors"
"fmt"
"sync"
"time"
)
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")
// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded = errors.New("context deadline exceeded")
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]bool)
}
p.children[child] = true
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
close(c.done)
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
propagateCancel(parent, c)
d := deadline.Sub(time.Now())
if d <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key interface{}, val interface{}) Context {
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
#
# This Dockerfile builds a recent curl with HTTP/2 client support, using
# a recent nghttp2 build.
#
# See the Makefile for how to tag it. If Docker and that image is found, the
# Go tests use this curl binary for integration tests.
#
FROM ubuntu:trusty
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git-core build-essential wget
RUN apt-get install -y --no-install-recommends \
autotools-dev libtool pkg-config zlib1g-dev \
libcunit1-dev libssl-dev libxml2-dev libevent-dev \
automake autoconf
# The list of packages nghttp2 recommends for h2load:
RUN apt-get install -y --no-install-recommends make binutils \
autoconf automake autotools-dev \
libtool pkg-config zlib1g-dev libcunit1-dev libssl-dev libxml2-dev \
libev-dev libevent-dev libjansson-dev libjemalloc-dev \
cython python3.4-dev python-setuptools
# Note: setting NGHTTP2_VER before the git clone, so an old git clone isn't cached:
ENV NGHTTP2_VER 895da9a
RUN cd /root && git clone https://github.com/tatsuhiro-t/nghttp2.git
WORKDIR /root/nghttp2
RUN git reset --hard $NGHTTP2_VER
RUN autoreconf -i
RUN automake
RUN autoconf
RUN ./configure
RUN make
RUN make install
WORKDIR /root
RUN wget http://curl.haxx.se/download/curl-7.45.0.tar.gz
RUN tar -zxvf curl-7.45.0.tar.gz
WORKDIR /root/curl-7.45.0
RUN ./configure --with-ssl --with-nghttp2=/usr/local
RUN make
RUN make install
RUN ldconfig
CMD ["-h"]
ENTRYPOINT ["/usr/local/bin/curl"]
curlimage:
docker build -t gohttp2/curl .
This is a work-in-progress HTTP/2 implementation for Go.
It will eventually live in the Go standard library and won't require
any changes to your code to use. It will just be automatic.
Status:
* The server support is pretty good. A few things are missing
but are being worked on.
* The client work has just started but shares a lot of code
is coming along much quicker.
Docs are at https://godoc.org/golang.org/x/net/http2
Demo test server at https://http2.golang.org/
Help & bug reports welcome!
Contributing: https://golang.org/doc/contribute.html
Bugs: https://golang.org/issue/new?title=x/net/http2:+
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Transport code's client connection pooling.
package http2
import (
"crypto/tls"
"net/http"
"sync"
)
// ClientConnPool manages a pool of HTTP/2 client connections.
type ClientConnPool interface {
GetClientConn(req *http.Request, addr string) (*ClientConn, error)
MarkDead(*ClientConn)
}
// clientConnPoolIdleCloser is the interface implemented by ClientConnPool
// implementations which can close their idle connections.
type clientConnPoolIdleCloser interface {
ClientConnPool
closeIdleConnections()
}
var (
_ clientConnPoolIdleCloser = (*clientConnPool)(nil)
_ clientConnPoolIdleCloser = noDialClientConnPool{}
)
// TODO: use singleflight for dialing and addConnCalls?
type clientConnPool struct {
t *Transport
mu sync.Mutex // TODO: maybe switch to RWMutex
// TODO: add support for sharing conns based on cert names
// (e.g. share conn for googleapis.com and appspot.com)
conns map[string][]*ClientConn // key is host:port
dialing map[string]*dialCall // currently in-flight dials
keys map[*ClientConn][]string
addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls
}
func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
return p.getClientConn(req, addr, dialOnMiss)
}
const (
dialOnMiss = true
noDialOnMiss = false
)
func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
if isConnectionCloseRequest(req) && dialOnMiss {
// It gets its own connection.
const singleUse = true
cc, err := p.t.dialClientConn(addr, singleUse)
if err != nil {
return nil, err
}
return cc, nil
}
p.mu.Lock()
for _, cc := range p.conns[addr] {
if cc.CanTakeNewRequest() {
p.mu.Unlock()
return cc, nil
}
}
if !dialOnMiss {
p.mu.Unlock()
return nil, ErrNoCachedConn
}
call := p.getStartDialLocked(addr)
p.mu.Unlock()
<-call.done
return call.res, call.err
}
// dialCall is an in-flight Transport dial call to a host.
type dialCall struct {
p *clientConnPool
done chan struct{} // closed when done
res *ClientConn // valid after done is closed
err error // valid after done is closed
}
// requires p.mu is held.
func (p *clientConnPool) getStartDialLocked(addr string) *dialCall {
if call, ok := p.dialing[addr]; ok {
// A dial is already in-flight. Don't start another.
return call
}
call := &dialCall{p: p, done: make(chan struct{})}
if p.dialing == nil {
p.dialing = make(map[string]*dialCall)
}
p.dialing[addr] = call
go call.dial(addr)
return call
}
// run in its own goroutine.
func (c *dialCall) dial(addr string) {
const singleUse = false // shared conn
c.res, c.err = c.p.t.dialClientConn(addr, singleUse)
close(c.done)
c.p.mu.Lock()
delete(c.p.dialing, addr)
if c.err == nil {
c.p.addConnLocked(addr, c.res)
}
c.p.mu.Unlock()
}
// addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't
// already exist. It coalesces concurrent calls with the same key.
// This is used by the http1 Transport code when it creates a new connection. Because
// the http1 Transport doesn't de-dup TCP dials to outbound hosts (because it doesn't know
// the protocol), it can get into a situation where it has multiple TLS connections.
// This code decides which ones live or die.
// The return value used is whether c was used.
// c is never closed.
func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) {
p.mu.Lock()
for _, cc := range p.conns[key] {
if cc.CanTakeNewRequest() {
p.mu.Unlock()
return false, nil
}
}
call, dup := p.addConnCalls[key]
if !dup {
if p.addConnCalls == nil {
p.addConnCalls = make(map[string]*addConnCall)
}
call = &addConnCall{
p: p,
done: make(chan struct{}),
}
p.addConnCalls[key] = call
go call.run(t, key, c)
}
p.mu.Unlock()
<-call.done
if call.err != nil {
return false, call.err
}
return !dup, nil
}
type addConnCall struct {
p *clientConnPool
done chan struct{} // closed when done
err error
}
func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
cc, err := t.NewClientConn(tc)
p := c.p
p.mu.Lock()
if err != nil {
c.err = err
} else {
p.addConnLocked(key, cc)
}
delete(p.addConnCalls, key)
p.mu.Unlock()
close(c.done)
}
func (p *clientConnPool) addConn(key string, cc *ClientConn) {
p.mu.Lock()
p.addConnLocked(key, cc)
p.mu.Unlock()
}
// p.mu must be held
func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) {
for _, v := range p.conns[key] {
if v == cc {
return
}
}
if p.conns == nil {
p.conns = make(map[string][]*ClientConn)
}
if p.keys == nil {
p.keys = make(map[*ClientConn][]string)
}
p.conns[key] = append(p.conns[key], cc)
p.keys[cc] = append(p.keys[cc], key)
}
func (p *clientConnPool) MarkDead(cc *ClientConn) {
p.mu.Lock()
defer p.mu.Unlock()
for _, key := range p.keys[cc] {
vv, ok := p.conns[key]
if !ok {
continue
}
newList := filterOutClientConn(vv, cc)
if len(newList) > 0 {
p.conns[key] = newList
} else {
delete(p.conns, key)
}
}
delete(p.keys, cc)
}
func (p *clientConnPool) closeIdleConnections() {
p.mu.Lock()
defer p.mu.Unlock()
// TODO: don't close a cc if it was just added to the pool
// milliseconds ago and has never been used. There's currently
// a small race window with the HTTP/1 Transport's integration
// where it can add an idle conn just before using it, and
// somebody else can concurrently call CloseIdleConns and
// break some caller's RoundTrip.
for _, vv := range p.conns {
for _, cc := range vv {
cc.closeIfIdle()
}
}
}
func filterOutClientConn(in []*ClientConn, exclude *ClientConn) []*ClientConn {
out := in[:0]
for _, v := range in {
if v != exclude {
out = append(out, v)
}
}
// If we filtered it out, zero out the last item to prevent
// the GC from seeing it.
if len(in) != len(out) {
in[len(in)-1] = nil
}
return out
}
// noDialClientConnPool is an implementation of http2.ClientConnPool
// which never dials. We let the HTTP/1.1 client dial and use its TLS
// connection instead.
type noDialClientConnPool struct{ *clientConnPool }
func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
return p.getClientConn(req, addr, noDialOnMiss)
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.6
package http2
import (
"crypto/tls"
"fmt"
"net/http"
)
func configureTransport(t1 *http.Transport) (*Transport, error) {
connPool := new(clientConnPool)
t2 := &Transport{
ConnPool: noDialClientConnPool{connPool},
t1: t1,
}
connPool.t = t2
if err := registerHTTPSProtocol(t1, noDialH2RoundTripper{t2}); err != nil {
return nil, err
}
if t1.TLSClientConfig == nil {
t1.TLSClientConfig = new(tls.Config)
}
if !strSliceContains(t1.TLSClientConfig.NextProtos, "h2") {
t1.TLSClientConfig.NextProtos = append([]string{"h2"}, t1.TLSClientConfig.NextProtos...)
}
if !strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") {
t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
}
upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper {
addr := authorityAddr("https", authority)
if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
go c.Close()
return erringRoundTripper{err}
} else if !used {
// Turns out we don't need this c.
// For example, two goroutines made requests to the same host
// at the same time, both kicking off TCP dials. (since protocol
// was unknown)
go c.Close()
}
return t2
}
if m := t1.TLSNextProto; len(m) == 0 {
t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{
"h2": upgradeFn,
}
} else {
m["h2"] = upgradeFn
}
return t2, nil
}
// registerHTTPSProtocol calls Transport.RegisterProtocol but
// convering panics into errors.
func registerHTTPSProtocol(t *http.Transport, rt http.RoundTripper) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
t.RegisterProtocol("https", rt)
return nil
}
// noDialH2RoundTripper is a RoundTripper which only tries to complete the request
// if there's already has a cached connection to the host.
type noDialH2RoundTripper struct{ t *Transport }
func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
res, err := rt.t.RoundTrip(req)
if err == ErrNoCachedConn {
return nil, http.ErrSkipAltProtocol
}
return res, err
}
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http2
import (
"errors"
"fmt"
)
// An ErrCode is an unsigned 32-bit error code as defined in the HTTP/2 spec.
type ErrCode uint32
const (
ErrCodeNo ErrCode = 0x0
ErrCodeProtocol ErrCode = 0x1
ErrCodeInternal ErrCode = 0x2
ErrCodeFlowControl ErrCode = 0x3
ErrCodeSettingsTimeout ErrCode = 0x4
ErrCodeStreamClosed ErrCode = 0x5
ErrCodeFrameSize ErrCode = 0x6
ErrCodeRefusedStream ErrCode = 0x7
ErrCodeCancel ErrCode = 0x8
ErrCodeCompression ErrCode = 0x9
ErrCodeConnect ErrCode = 0xa
ErrCodeEnhanceYourCalm ErrCode = 0xb
ErrCodeInadequateSecurity ErrCode = 0xc
ErrCodeHTTP11Required ErrCode = 0xd
)
var errCodeName = map[ErrCode]string{
ErrCodeNo: "NO_ERROR",
ErrCodeProtocol: "PROTOCOL_ERROR",
ErrCodeInternal: "INTERNAL_ERROR",
ErrCodeFlowControl: "FLOW_CONTROL_ERROR",
ErrCodeSettingsTimeout: "SETTINGS_TIMEOUT",
ErrCodeStreamClosed: "STREAM_CLOSED",
ErrCodeFrameSize: "FRAME_SIZE_ERROR",
ErrCodeRefusedStream: "REFUSED_STREAM",
ErrCodeCancel: "CANCEL",
ErrCodeCompression: "COMPRESSION_ERROR",
ErrCodeConnect: "CONNECT_ERROR",
ErrCodeEnhanceYourCalm: "ENHANCE_YOUR_CALM",
ErrCodeInadequateSecurity: "INADEQUATE_SECURITY",
ErrCodeHTTP11Required: "HTTP_1_1_REQUIRED",
}
func (e ErrCode) String() string {
if s, ok := errCodeName[e]; ok {
return s
}
return fmt.Sprintf("unknown error code 0x%x", uint32(e))
}
// ConnectionError is an error that results in the termination of the
// entire connection.
type ConnectionError ErrCode
func (e ConnectionError) Error() string { return fmt.Sprintf("connection error: %s", ErrCode(e)) }
// StreamError is an error that only affects one stream within an
// HTTP/2 connection.
type StreamError struct {
StreamID uint32
Code ErrCode
Cause error // optional additional detail
}
func streamError(id uint32, code ErrCode) StreamError {
return StreamError{StreamID: id, Code: code}
}
func (e StreamError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("stream error: stream ID %d; %v; %v", e.StreamID, e.Code, e.Cause)
}
return fmt.Sprintf("stream error: stream ID %d; %v", e.StreamID, e.Code)
}
// 6.9.1 The Flow Control Window
// "If a sender receives a WINDOW_UPDATE that causes a flow control
// window to exceed this maximum it MUST terminate either the stream
// or the connection, as appropriate. For streams, [...]; for the
// connection, a GOAWAY frame with a FLOW_CONTROL_ERROR code."
type goAwayFlowError struct{}
func (goAwayFlowError) Error() string { return "connection exceeded flow control window size" }
// connErrorReason wraps a ConnectionError with an informative error about why it occurs.
// Errors of this type are only returned by the frame parser functions
// and converted into ConnectionError(ErrCodeProtocol).
type connError struct {
Code ErrCode
Reason string
}
func (e connError) Error() string {
return fmt.Sprintf("http2: connection error: %v: %v", e.Code, e.Reason)
}
type pseudoHeaderError string
func (e pseudoHeaderError) Error() string {
return fmt.Sprintf("invalid pseudo-header %q", string(e))
}
type duplicatePseudoHeaderError string
func (e duplicatePseudoHeaderError) Error() string {
return fmt.Sprintf("duplicate pseudo-header %q", string(e))
}
type headerFieldNameError string
func (e headerFieldNameError) Error() string {
return fmt.Sprintf("invalid header field name %q", string(e))
}
type headerFieldValueError string
func (e headerFieldValueError) Error() string {
return fmt.Sprintf("invalid header field value %q", string(e))
}
var (
errMixPseudoHeaderTypes = errors.New("mix of request and response pseudo headers")
errPseudoAfterRegular = errors.New("pseudo header field after regular")
)
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http2
import (
"errors"
)
// fixedBuffer is an io.ReadWriter backed by a fixed size buffer.
// It never allocates, but moves old data as new data is written.
type fixedBuffer struct {
buf []byte
r, w int
}
var (
errReadEmpty = errors.New("read from empty fixedBuffer")
errWriteFull = errors.New("write on full fixedBuffer")
)
// Read copies bytes from the buffer into p.
// It is an error to read when no data is available.
func (b *fixedBuffer) Read(p []byte) (n int, err error) {
if b.r == b.w {
return 0, errReadEmpty
}
n = copy(p, b.buf[b.r:b.w])
b.r += n
if b.r == b.w {
b.r = 0
b.w = 0
}
return n, nil
}
// Len returns the number of bytes of the unread portion of the buffer.
func (b *fixedBuffer) Len() int {
return b.w - b.r
}
// Write copies bytes from p into the buffer.
// It is an error to write more data than the buffer can hold.
func (b *fixedBuffer) Write(p []byte) (n int, err error) {
// Slide existing data to beginning.
if b.r > 0 && len(p) > len(b.buf)-b.w {
copy(b.buf, b.buf[b.r:b.w])
b.w -= b.r
b.r = 0
}
// Write new data.
n = copy(b.buf[b.w:], p)
b.w += n
if n < len(p) {
err = errWriteFull
}
return n, err
}
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Flow control
package http2
// flow is the flow control window's size.
type flow struct {
// n is the number of DATA bytes we're allowed to send.
// A flow is kept both on a conn and a per-stream.
n int32
// conn points to the shared connection-level flow that is
// shared by all streams on that conn. It is nil for the flow
// that's on the conn directly.
conn *flow
}
func (f *flow) setConnFlow(cf *flow) { f.conn = cf }
func (f *flow) available() int32 {
n := f.n
if f.conn != nil && f.conn.n < n {
n = f.conn.n
}
return n
}
func (f *flow) take(n int32) {
if n > f.available() {
panic("internal error: took too much")
}
f.n -= n
if f.conn != nil {
f.conn.n -= n
}
}
// add adds n bytes (positive or negative) to the flow control window.
// It returns false if the sum would exceed 2^31-1.
func (f *flow) add(n int32) bool {
remain := (1<<31 - 1) - f.n
if n > remain {
return false
}
f.n += n
return true
}
This diff is collapsed.
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.6
package http2
import (
"crypto/tls"
"net/http"
"time"
)
func transportExpectContinueTimeout(t1 *http.Transport) time.Duration {
return t1.ExpectContinueTimeout
}
// isBadCipher reports whether the cipher is blacklisted by the HTTP/2 spec.
func isBadCipher(cipher uint16) bool {
switch cipher {
case tls.TLS_RSA_WITH_RC4_128_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
// Reject cipher suites from Appendix A.
// "This list includes those cipher suites that do not
// offer an ephemeral key exchange and those that are
// based on the TLS null, stream or block cipher type"
return true
default:
return false
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment