From c41861749f252ced5d8a2eaab76eab66459016fb Mon Sep 17 00:00:00 2001 From: Mikhail Ivchenko Date: Fri, 25 May 2018 19:08:31 +0400 Subject: [PATCH] xrootd: add protocol request Protocol request is used for obtaining the protocol version number, type of server and possible security requirements. These security requirements determine which requests should be signed. This info is provided to Client via protocol.SignRequirements. The Client will use sr.Needed to determine if the request should be signed via Sigver request. Actual sign requirements depending on the security level will be added as part of the request implementation in the following form: if level >= protocol.Pedantic { sr.requirements[dirlist.RequestID] = protocol.SignNeeded // ... } Updates go-hep/hep#170. --- xrootd/client/client.go | 19 +++- xrootd/client/main_mock_test.go | 91 ++++++++++++++++ xrootd/client/protocol.go | 93 ++++++++++++++++ xrootd/client/protocol_mock_test.go | 93 ++++++++++++++++ xrootd/client/protocol_test.go | 46 ++++++++ xrootd/protocol/handshake/handshake.go | 13 +-- xrootd/protocol/protocol.go | 21 ++++ xrootd/protocol/protocol/protocol.go | 140 +++++++++++++++++++++++++ xrootd/protocol/signing.go | 62 +++++++++++ 9 files changed, 562 insertions(+), 16 deletions(-) create mode 100644 xrootd/client/main_mock_test.go create mode 100644 xrootd/client/protocol.go create mode 100644 xrootd/client/protocol_mock_test.go create mode 100644 xrootd/client/protocol_test.go create mode 100644 xrootd/protocol/protocol/protocol.go create mode 100644 xrootd/protocol/signing.go 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 +}