diff --git a/xrootd/client/client.go b/xrootd/client/client.go index cacc5a37f..30d6d0593 100644 --- a/xrootd/client/client.go +++ b/xrootd/client/client.go @@ -35,10 +35,11 @@ import ( // Concurrent requests are supported. // Zero value is invalid, Client should be instantiated using NewClient. type Client struct { - cancel context.CancelFunc - conn net.Conn - mux *mux.Mux - protocolVersion int32 + cancel context.CancelFunc + conn net.Conn + mux *mux.Mux + protocolVersion int32 + signRequirements protocol.SignRequirements } // NewClient creates a new xrootd client that connects to the given address. @@ -54,7 +55,7 @@ func NewClient(ctx context.Context, address string) (*Client, error) { return nil, err } - client := &Client{cancel, conn, mux.New(), 0} + client := &Client{cancel: cancel, conn: conn, mux: mux.New()} go client.consume(ctx) @@ -63,6 +64,14 @@ func NewClient(ctx context.Context, address string) (*Client, error) { return nil, err } + protocolInfo, err := client.Protocol(ctx) + if err != nil { + client.Close() + return nil, err + } + + client.signRequirements = protocol.NewSignRequirements(protocolInfo.SecurityLevel, protocolInfo.SecurityOverrides) + return client, nil } diff --git a/xrootd/client/main_mock_test.go b/xrootd/client/main_mock_test.go new file mode 100644 index 000000000..b785d8191 --- /dev/null +++ b/xrootd/client/main_mock_test.go @@ -0,0 +1,91 @@ +// Copyright 2018 The go-hep 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 client // import "go-hep.org/x/hep/xrootd/client" + +import ( + "context" + "encoding/binary" + "io" + "net" + + "github.com/pkg/errors" + "go-hep.org/x/hep/xrootd/internal/mux" + "go-hep.org/x/hep/xrootd/protocol" +) + +func testClientWithMockServer(serverFunc func(cancel func(), conn net.Conn), clientFunc func(cancel func(), client *Client)) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + server, conn := net.Pipe() + defer server.Close() + defer conn.Close() + + client := &Client{cancel: cancel, conn: conn, mux: mux.New(), signRequirements: protocol.DefaultSignRequirements()} + defer client.Close() + + go serverFunc(func() { client.Close() }, server) + go client.consume(ctx) + + clientFunc(cancel, client) +} + +func readRequest(conn net.Conn) ([]byte, error) { + // 16 is for the request options and 4 is for the data length + const requestSize = protocol.RequestHeaderLength + 16 + 4 + var request = make([]byte, requestSize) + if _, err := io.ReadFull(conn, request); err != nil { + return nil, err + } + + dataLength := binary.BigEndian.Uint32(request[protocol.RequestHeaderLength+16:]) + if dataLength == 0 { + return request, nil + } + + var data = make([]byte, dataLength) + if _, err := io.ReadFull(conn, data); err != nil { + return nil, err + } + + return append(request, data...), nil +} + +func writeResponse(conn net.Conn, data []byte) error { + n, err := conn.Write(data) + if err != nil { + return err + } + if n != len(data) { + return errors.Errorf("could not write all %d bytes: wrote %d", len(data), n) + } + return nil +} + +// TODO: move marshalResponse outside of main_mock_test.go and use it for server implementation. +func marshalResponse(responseParts ...interface{}) ([]byte, error) { + var data []byte + for _, p := range responseParts { + pData, err := protocol.Marshal(p) + if err != nil { + return nil, err + } + data = append(data, pData...) + } + return data, nil +} + +// TODO: move unmarshalRequest outside of main_mock_test.go and use it for server implementation. +func unmarshalRequest(data []byte, request interface{}) (protocol.RequestHeader, error) { + var header protocol.RequestHeader + if err := protocol.Unmarshal(data[:protocol.RequestHeaderLength], &header); err != nil { + return protocol.RequestHeader{}, err + } + if err := protocol.Unmarshal(data[protocol.RequestHeaderLength:], request); err != nil { + return protocol.RequestHeader{}, err + } + + return header, nil +} diff --git a/xrootd/client/protocol.go b/xrootd/client/protocol.go new file mode 100644 index 000000000..365ee3e64 --- /dev/null +++ b/xrootd/client/protocol.go @@ -0,0 +1,93 @@ +// Copyright 2018 The go-hep 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 client // import "go-hep.org/x/hep/xrootd/client" + +import ( + "context" + + xrdproto "go-hep.org/x/hep/xrootd/protocol" + "go-hep.org/x/hep/xrootd/protocol/protocol" +) + +// ProtocolInfo is a response for the `Protocol` request. See details in the xrootd protocol specification. +type ProtocolInfo struct { + BinaryProtocolVersion int32 + ServerType xrdproto.ServerType + IsManager bool + IsServer bool + IsMeta bool + IsProxy bool + IsSupervisor bool + SecurityVersion byte + ForceSecurity bool + SecurityLevel protocol.SecurityLevel + SecurityOverrides []protocol.SecurityOverride +} + +// Protocol obtains the protocol version number, type of the server and security information, such as: +// the security version, the security options, the security level, and the list of alterations +// needed to the specified predefined security level. +func (client *Client) Protocol(ctx context.Context) (ProtocolInfo, error) { + resp, err := client.call(ctx, protocol.RequestID, protocol.NewRequest(client.protocolVersion, true)) + if err != nil { + return ProtocolInfo{}, err + } + + var generalResp protocol.GeneralResponse + + if err = xrdproto.Unmarshal(resp, &generalResp); err != nil { + return ProtocolInfo{}, err + } + + var securityInfo *protocol.SecurityInfo + if len(resp) > protocol.GeneralResponseLength { + securityInfo = &protocol.SecurityInfo{} + err = xrdproto.Unmarshal(resp[protocol.GeneralResponseLength:], securityInfo) + if err != nil { + return ProtocolInfo{}, err + } + } + + var info = ProtocolInfo{ + BinaryProtocolVersion: generalResp.BinaryProtocolVersion, + ServerType: extractServerType(generalResp.Flags), + + // TODO: implement bit-encoded fields support in Unmarshal. + IsManager: generalResp.Flags&protocol.IsManager != 0, + IsServer: generalResp.Flags&protocol.IsServer != 0, + IsMeta: generalResp.Flags&protocol.IsMeta != 0, + IsProxy: generalResp.Flags&protocol.IsProxy != 0, + IsSupervisor: generalResp.Flags&protocol.IsSupervisor != 0, + } + + if securityInfo != nil { + info.SecurityVersion = securityInfo.SecurityVersion + info.ForceSecurity = securityInfo.SecurityOptions&protocol.ForceSecurity != 0 + info.SecurityLevel = securityInfo.SecurityLevel + + if securityInfo.SecurityOverridesSize > 0 { + info.SecurityOverrides = make([]protocol.SecurityOverride, securityInfo.SecurityOverridesSize) + + const offset = protocol.GeneralResponseLength + protocol.SecurityInfoLength + const elementSize = protocol.SecurityOverrideLength + + for i := byte(0); i < securityInfo.SecurityOverridesSize; i++ { + err = xrdproto.Unmarshal(resp[offset+elementSize*int(i):], &info.SecurityOverrides[i]) + if err != nil { + return ProtocolInfo{}, err + } + } + } + } + + return info, nil +} + +func extractServerType(flags protocol.Flags) xrdproto.ServerType { + if int32(flags)&int32(xrdproto.DataServer) != 0 { + return xrdproto.DataServer + } + return xrdproto.LoadBalancingServer +} diff --git a/xrootd/client/protocol_mock_test.go b/xrootd/client/protocol_mock_test.go new file mode 100644 index 000000000..993755150 --- /dev/null +++ b/xrootd/client/protocol_mock_test.go @@ -0,0 +1,93 @@ +// Copyright 2018 The go-hep 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 client + +import ( + "context" + "net" + "reflect" + "testing" + + xrdproto "go-hep.org/x/hep/xrootd/protocol" + "go-hep.org/x/hep/xrootd/protocol/protocol" +) + +func TestClient_Protocol_WithSecurityInfo(t *testing.T) { + var protocolVersion int32 = 0x310 + + serverFunc := func(cancel func(), conn net.Conn) { + defer cancel() + + data, err := readRequest(conn) + if err != nil { + t.Fatalf("could not read request: %v", err) + } + + var gotRequest protocol.Request + gotHeader, err := unmarshalRequest(data, &gotRequest) + if err != nil { + t.Fatalf("could not unmarshal request: %v", err) + } + + if gotHeader.RequestID != protocol.RequestID { + t.Fatalf("invalid request id was specified:\nwant = %d\ngot = %d\n", protocol.RequestID, gotHeader.RequestID) + } + + if gotRequest.ClientProtocolVersion != protocolVersion { + t.Fatalf("invalid client protocol version was specified:\nwant = %d\ngot = %d\n", protocolVersion, gotRequest.ClientProtocolVersion) + } + + flags := protocol.IsManager | protocol.IsServer | protocol.IsMeta | protocol.IsProxy | protocol.IsSupervisor + + responseHeader := xrdproto.ResponseHeader{ + StreamID: gotHeader.StreamID, + DataLength: protocol.GeneralResponseLength + protocol.SecurityInfoLength + protocol.SecurityOverrideLength, + } + + protocolResponse := protocol.GeneralResponse{protocolVersion, flags} + + protocolSecurityInfo := protocol.SecurityInfo{ + SecurityOptions: protocol.None, + SecurityLevel: protocol.Pedantic, + SecurityOverridesSize: 1, + } + + securityOverride := protocol.SecurityOverride{1, protocol.SignNeeded} + + response, err := marshalResponse(responseHeader, protocolResponse, protocolSecurityInfo, securityOverride) + if err != nil { + t.Fatalf("could not marshal response: %v", err) + } + + if err := writeResponse(conn, response); err != nil { + t.Fatalf("invalid write: %s", err) + } + } + + var want = ProtocolInfo{ + BinaryProtocolVersion: protocolVersion, + ServerType: xrdproto.DataServer, + IsManager: true, + IsServer: true, + IsMeta: true, + IsProxy: true, + IsSupervisor: true, + SecurityLevel: protocol.Pedantic, + SecurityOverrides: []protocol.SecurityOverride{{1, protocol.SignNeeded}}, + } + + clientFunc := func(cancel func(), client *Client) { + client.protocolVersion = protocolVersion + got, err := client.Protocol(context.Background()) + if err != nil { + t.Fatalf("invalid protocol call: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("protocol info does not match:\ngot = %v\nwant = %v", got, want) + } + } + + testClientWithMockServer(serverFunc, clientFunc) +} diff --git a/xrootd/client/protocol_test.go b/xrootd/client/protocol_test.go new file mode 100644 index 000000000..1a63e17bb --- /dev/null +++ b/xrootd/client/protocol_test.go @@ -0,0 +1,46 @@ +// Copyright 2018 The go-hep 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 xrootd_test_with_server + +package client + +import ( + "context" + "reflect" + "testing" + + "go-hep.org/x/hep/xrootd/protocol" +) + +func testClient_Protocol(t *testing.T, addr string) { + var want = ProtocolInfo{ + BinaryProtocolVersion: 784, + ServerType: protocol.DataServer, + IsServer: true, + } + + client, err := NewClient(context.Background(), addr) + if err != nil { + t.Fatalf("could not create client: %v", err) + } + + got, err := client.Protocol(context.Background()) + if err != nil { + t.Fatalf("invalid protocol call: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Client.Protocol()\ngot = %v\nwant = %v", got, want) + } + + client.Close() +} + +func TestClient_Protocol(t *testing.T) { + for _, addr := range testClientAddrs { + t.Run(addr, func(t *testing.T) { + testClient_Protocol(t, addr) + }) + } +} diff --git a/xrootd/protocol/handshake/handshake.go b/xrootd/protocol/handshake/handshake.go index d0e1dfa88..904a70720 100644 --- a/xrootd/protocol/handshake/handshake.go +++ b/xrootd/protocol/handshake/handshake.go @@ -6,22 +6,13 @@ // for handshake request (see XRootD specification). package handshake // import "go-hep.org/x/hep/xrootd/protocol/handshake" -// ServerType is the general server type kept for compatibility -// with 2.0 protocol version (see xrootd protocol specification v3.1.0, p. 5). -type ServerType int32 - -const ( - // LoadBalancingServer indicates whether this is a load-balancing server. - LoadBalancingServer ServerType = iota - // DataServer indicates whether this is a data server. - DataServer ServerType = iota -) +import "go-hep.org/x/hep/xrootd/protocol" // Response is a response for the handshake request, // which contains protocol version and server type. type Response struct { ProtocolVersion int32 - ServerType ServerType + ServerType protocol.ServerType } // Request holds the handshake request parameters. diff --git a/xrootd/protocol/protocol.go b/xrootd/protocol/protocol.go index 8f881be3a..81d3400fd 100644 --- a/xrootd/protocol/protocol.go +++ b/xrootd/protocol/protocol.go @@ -50,6 +50,16 @@ type ResponseHeader struct { DataLength int32 } +// RequestHeaderLength is the length of the RequestHeader in bytes. +const RequestHeaderLength = 2 + 2 + +// ResponseHeader is the header that precedes all requests (we are interested in StreamID and RequestID, actual request +// parameters are a part of specific request). +type RequestHeader struct { + StreamID StreamID + RequestID uint16 +} + // Error returns an error received from the server or nil if request hasn't failed. func (hdr ResponseHeader) Error(data []byte) error { if hdr.Status == Error { @@ -64,3 +74,14 @@ func (hdr ResponseHeader) Error(data []byte) error { } return nil } + +// ServerType is the general server type kept for compatibility +// with 2.0 protocol version (see xrootd protocol specification v3.1.0, p. 5). +type ServerType int32 + +const ( + // LoadBalancingServer indicates whether this is a load-balancing server. + LoadBalancingServer ServerType = iota + // DataServer indicates whether this is a data server. + DataServer +) diff --git a/xrootd/protocol/protocol/protocol.go b/xrootd/protocol/protocol/protocol.go new file mode 100644 index 000000000..73611f69b --- /dev/null +++ b/xrootd/protocol/protocol/protocol.go @@ -0,0 +1,140 @@ +// Copyright 2018 The go-hep 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 protocol contains the structures describing request and response +// for protocol request (see XRootD specification). +// +// A response consists of 3 parts: +// +// 1) GeneralResponse - general response that is always returned and specifies protocol version and flags describing server type. +// +// 2) SecurityInfo - a response part that is added to the general response +// if `ReturnSecurityRequirements` is provided and server supports it. +// It contains the security version, the security options, the security level, +// and the number of following security overrides, if any. +// +// 3) A list of SecurityOverride - alterations needed to the specified predefined security level. +package protocol // import "go-hep.org/x/hep/xrootd/protocol/protocol" + +// RequestID is the id of the request, it is sent as part of message. +// See xrootd protocol specification for details: http://xrootd.org/doc/dev45/XRdv310.pdf, 2.3 Client Request Format. +const RequestID uint16 = 3006 + +// GeneralResponseLength is the length of GeneralResponse in bytes. +const GeneralResponseLength = 8 + +// General response is the response that is always returned from xrootd server. +// It contains protocol version and flags that describe server type. +type GeneralResponse struct { + BinaryProtocolVersion int32 + Flags Flags +} + +// Flags are the flags that define xrootd server type. See xrootd protocol specification for further info. +type Flags int32 + +const ( + IsServer Flags = 0x00000001 // IsServer indicates whether this server has server role. + IsManager Flags = 0x00000002 // IsManager indicates whether this server has manager role. + IsMeta Flags = 0x00000100 // IsMeta indicates whether this server has meta attribute. + IsProxy Flags = 0x00000200 // IsProxy indicates whether this server has proxy attribute. + IsSupervisor Flags = 0x00000400 // IsSupervisor indicates whether this server has supervisor attribute. +) + +// SecurityOptions are the security-related options. +// See specification for details: http://xrootd.org/doc/dev45/XRdv310.pdf, p. 72. +type SecurityOptions byte + +const ( + // None specifies that no security options are provided. + None SecurityOptions = 0 + // ForceSecurity specifies that signing is required even if the authentication + // protocol does not support generic encryption. + ForceSecurity SecurityOptions = 0x02 +) + +// SecurityInfoLength is the length of SecurityInfo in bytes. +const SecurityInfoLength = 6 + +// SecurityInfo is a response part that is provided when required (if server supports that). +// It contains the security version, the security options, the security level, +// and the number of following security overrides, if any. +type SecurityInfo struct { + // FIXME: Rename Reserved* fields to _ when automatically generated (un)marshalling will be available. + Reserved1 byte + Reserved2 byte + SecurityVersion byte + SecurityOptions SecurityOptions + SecurityLevel SecurityLevel + SecurityOverridesSize byte +} + +// SecurityLevel is the predefined security level that specifies which requests should be signed. +// See specification for details: http://xrootd.org/doc/dev45/XRdv310.pdf, p. 75. +type SecurityLevel byte + +const ( + // NoneLevel indicates that no request needs to be signed. + NoneLevel SecurityLevel = 0 + // Compatible indicates that only potentially destructive requests need to be signed. + Compatible SecurityLevel = 1 + // Standard indicates that potentially destructive requests + // as well as certain non-destructive requests need to be signed. + Standard SecurityLevel = 2 + // Intense indicates that request that may reveal metadata or modify data need to be signed. + Intense SecurityLevel = 3 + // Pedantic indicates that all requests need to be signed. + Pedantic SecurityLevel = 4 +) + +// RequestLevel is the security requirement that the associated request is to have. +type RequestLevel byte + +const ( + SignNone RequestLevel = 0 // SignNone indicates that the request need not to be signed. + SignLikely RequestLevel = 1 // SignLikely indicates that the request must be signed if it modifies data. + SignNeeded RequestLevel = 2 // SignNeeded indicates that the request mush be signed. +) + +// SecurityOverrideLength is the length of SecurityOverride in bytes. +const SecurityOverrideLength = 2 + +// SecurityOverride is an alteration needed to the specified predefined security level. +// It consists of the request index and the security requirement the associated request should have. +// Request index is calculated as: +// (request code) - (request code of Auth request) +// according to xrootd protocol specification. +type SecurityOverride struct { + RequestIndex byte + RequestLevel RequestLevel +} + +// RequestOptions specifies what should be returned as part of response. +type RequestOptions byte + +const ( + // RequestOptionsNone specifies that only general response should be returned. + RequestOptionsNone RequestOptions = 0 + // ReturnSecurityRequirements specifies that security requirements should be returned + // if that's supported by the server. + ReturnSecurityRequirements RequestOptions = 1 +) + +// Request holds protocol request parameters. +type Request struct { + ClientProtocolVersion int32 + Options RequestOptions + // FIXME: Rename Reserved* fields to _ when automatically generated (un)marshalling will be available. + Reserved1 [11]byte + Reserved2 int32 +} + +// NewRequest forms a Request according to provided parameters. +func NewRequest(protocolVersion int32, withSecurityRequirements bool) Request { + var options = RequestOptionsNone + if withSecurityRequirements { + options |= ReturnSecurityRequirements + } + return Request{ClientProtocolVersion: protocolVersion, Options: options} +} diff --git a/xrootd/protocol/signing.go b/xrootd/protocol/signing.go new file mode 100644 index 000000000..3681ed6df --- /dev/null +++ b/xrootd/protocol/signing.go @@ -0,0 +1,62 @@ +// Copyright 2018 The go-hep 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 protocol // import "go-hep.org/x/hep/xrootd/protocol" + +import ( + "go-hep.org/x/hep/xrootd/protocol/protocol" +) + +// SignRequirements implements a way to check if request should be signed +// according to XRootD protocol specification v. 3.1.0, p.75-76. +type SignRequirements struct { + requirements map[uint16]protocol.RequestLevel +} + +// Needed return whether the request should be signed. +// "Modifies" indicates that request modifies data or metadata +// and is used to handle the "signLikely" level which specifies that +// request should be signed only if it modifies data. +// For the list of actual examples see XRootD protocol specification v. 3.1.0, p.76. +func (sr *SignRequirements) Needed(requestID uint16, modifies bool) bool { + v, exist := sr.requirements[requestID] + if !exist || v == protocol.SignNone { + return false + } + if v == protocol.SignLikely && !modifies { + return false + } + return true +} + +// DefaultSignRequirements creates a default SignRequirements with "None" security level. +func DefaultSignRequirements() SignRequirements { + return NewSignRequirements(protocol.NoneLevel, nil) +} + +// NewSignRequirements creates a SignRequirements according to provided security level and security overrides. +func NewSignRequirements(level protocol.SecurityLevel, overrides []protocol.SecurityOverride) SignRequirements { + var sr = SignRequirements{make(map[uint16]protocol.RequestLevel)} + + if level >= protocol.Compatible { + // TODO: set requirements + } + if level >= protocol.Standard { + // TODO: set requirements + } + if level >= protocol.Intense { + // TODO: set requirements + } + if level >= protocol.Pedantic { + // TODO: set requirements + } + + for _, override := range overrides { + // TODO: use auth.RequestID instead of 3000. + requestID := 3000 + uint16(override.RequestIndex) + sr.requirements[requestID] = override.RequestLevel + } + + return sr +}