9351ec1d2c
Every RPC failure surfaced as a GatewayError with no way to tell a rejected key from a missing scope, though docs/ClientLibrariesDesign.md requires the two be treated distinctly. GatewayError now matches the new sentinels ErrUnauthenticated and ErrPermissionDenied via errors.Is (by gRPC status code only, so it works through the secret-redaction wrapper) and exposes Code(). Tests: errors_auth_test.go — sentinel matching per code, Code(), and a bufconn AcknowledgeAlarm round trip proving the match survives the wire and the password stays redacted (both behavioural tests fail without GatewayError.Is).
206 lines
5.9 KiB
Markdown
206 lines
5.9 KiB
Markdown
# Go Client Detailed Design
|
|
|
|
## Purpose
|
|
|
|
Provide an idiomatic Go client module for MXAccess Gateway, plus a test CLI and
|
|
unit tests. The Go client should be suitable for services and command-line
|
|
automation.
|
|
|
|
Follow the [Go Style Guide](../../docs/style-guides/GoStyleGuide.md) for handwritten
|
|
code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md) for
|
|
generated contract inputs.
|
|
|
|
## Module Layout
|
|
|
|
Recommended layout:
|
|
|
|
```text
|
|
clients/go/
|
|
go.mod
|
|
mxgateway/
|
|
client.go
|
|
session.go
|
|
options.go
|
|
auth.go
|
|
values.go
|
|
errors.go
|
|
internal/generated/
|
|
mxaccess_gateway.pb.go
|
|
mxaccess_gateway_grpc.pb.go
|
|
cmd/mxgw-go/
|
|
main.go
|
|
tests/
|
|
```
|
|
|
|
Generated code should come from `protoc` plus:
|
|
|
|
- `protoc-gen-go`
|
|
- `protoc-gen-go-grpc`
|
|
|
|
## Library API
|
|
|
|
Suggested API:
|
|
|
|
```go
|
|
type Client struct {
|
|
// owns grpc.ClientConn
|
|
}
|
|
|
|
type Options struct {
|
|
Endpoint string
|
|
APIKey string
|
|
Plaintext bool
|
|
CACertFile string
|
|
ServerNameOverride string
|
|
DialTimeout time.Duration
|
|
CallTimeout time.Duration
|
|
}
|
|
|
|
func Dial(ctx context.Context, opts Options) (*Client, error)
|
|
func (c *Client) OpenSession(ctx context.Context, opts OpenSessionOptions) (*Session, error)
|
|
func (c *Client) Invoke(ctx context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error)
|
|
func (c *Client) Close() error
|
|
```
|
|
|
|
Session:
|
|
|
|
```go
|
|
type Session struct {
|
|
ID string
|
|
}
|
|
|
|
func (s *Session) Register(ctx context.Context, clientName string) (int32, error)
|
|
func (s *Session) Unregister(ctx context.Context, serverHandle int32) error
|
|
func (s *Session) AddItem(ctx context.Context, serverHandle int32, item string) (int32, error)
|
|
func (s *Session) AddItem2(ctx context.Context, serverHandle int32, item, context string) (int32, error)
|
|
func (s *Session) Advise(ctx context.Context, serverHandle, itemHandle int32) error
|
|
func (s *Session) AddItemBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) AdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) RemoveItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) UnAdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) SubscribeBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) UnsubscribeBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
|
|
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value Value, userID int32) error
|
|
func (s *Session) Events(ctx context.Context) (<-chan EventResult, error)
|
|
func (s *Session) Close(ctx context.Context) error
|
|
```
|
|
|
|
## Authentication
|
|
|
|
Use a unary and stream interceptor to attach:
|
|
|
|
```text
|
|
authorization: Bearer <api key>
|
|
```
|
|
|
|
The interceptor should use `metadata.AppendToOutgoingContext` or call options.
|
|
Do not print API keys in errors.
|
|
|
|
## TLS
|
|
|
|
Support:
|
|
|
|
- `credentials/insecure` for local plaintext,
|
|
- `credentials.NewClientTLSFromFile`,
|
|
- custom `tls.Config` for advanced callers.
|
|
|
|
### Trust posture
|
|
|
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
|
PKI). To make that usable, TLS is **lenient by default**: when `Plaintext` is
|
|
`false` and no `CACertFile`/`TLSConfig`/`TransportCredentials` is supplied,
|
|
`buildCredentials` dials with `tls.Config{InsecureSkipVerify: true}` (carrying
|
|
`ServerNameOverride` as the SNI when set), so the gateway's self-signed
|
|
certificate is accepted without verification.
|
|
|
|
To verify the gateway instead:
|
|
|
|
- set `CACertFile` to pin a CA (full verification against that root), or
|
|
- set `RequireCertificateValidation: true` to verify against the OS/system trust
|
|
roots without pinning.
|
|
|
|
Pinning a CA always wins over the lenient default.
|
|
|
|
## Streaming
|
|
|
|
`Events(ctx)` should return a receive channel of:
|
|
|
|
```go
|
|
type EventResult struct {
|
|
Event *pb.MxEvent
|
|
Err error
|
|
}
|
|
```
|
|
|
|
The receive goroutine exits on stream end, context cancellation, or error. The
|
|
channel should be closed exactly once. Do not reorder events.
|
|
|
|
## Error Handling
|
|
|
|
Expose typed errors:
|
|
|
|
```go
|
|
type GatewayError struct { ... }
|
|
type CommandError struct { ... }
|
|
type MxAccessError struct { ... }
|
|
```
|
|
|
|
Use `errors.Is` / `errors.As` support. Preserve raw protobuf replies on command
|
|
errors. `GatewayError` matches the sentinels `ErrUnauthenticated` and
|
|
`ErrPermissionDenied` through `errors.Is` by gRPC status code, so callers treat
|
|
the two authorization failures distinctly (`docs/ClientLibrariesDesign.md`)
|
|
without parsing messages.
|
|
|
|
## Test CLI
|
|
|
|
Binary: `mxgw-go`.
|
|
|
|
Recommended commands:
|
|
|
|
```text
|
|
mxgw-go version
|
|
mxgw-go smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
|
|
mxgw-go write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
|
|
mxgw-go stream-events --session-id <id> --json
|
|
```
|
|
|
|
Recommended CLI library:
|
|
|
|
- standard `flag` for minimalism, or
|
|
- Cobra if subcommand ergonomics matter.
|
|
|
|
## Unit Tests
|
|
|
|
Use `bufconn` for in-memory gRPC tests.
|
|
|
|
Required tests:
|
|
|
|
- auth interceptor on unary calls,
|
|
- auth interceptor on streaming calls,
|
|
- plaintext and TLS dial options,
|
|
- command helper request construction,
|
|
- value conversion,
|
|
- status conversion,
|
|
- typed error wrapping,
|
|
- stream channel closes on cancellation,
|
|
- late stream error propagation,
|
|
- CLI JSON redaction.
|
|
|
|
## Integration Tests
|
|
|
|
Use Go build tags or environment skip:
|
|
|
|
```text
|
|
MXGATEWAY_INTEGRATION=1
|
|
```
|
|
|
|
Integration test should run `OpenSession`, `Register`, `AddItem`, `Advise`,
|
|
bounded `StreamEvents`, and `CloseSession`.
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Go Style Guide](../../docs/style-guides/GoStyleGuide.md)
|