chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// addr provides functions to retrieve local IP addresses from device interfaces.
|
||||
package addr
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrIPNotFound no IP address found, and explicit IP not provided.
|
||||
ErrIPNotFound = errors.New("no IP address found, and explicit IP not provided")
|
||||
)
|
||||
|
||||
// IsLocal checks whether an IP belongs to one of the device's interfaces.
|
||||
func IsLocal(addr string) bool {
|
||||
// Extract the host
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
if addr == "localhost" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check against all local ips
|
||||
for _, ip := range IPs() {
|
||||
if addr == ip {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract returns a valid IP address. If the address provided is a valid
|
||||
// address, it will be returned directly. Otherwise, the available interfaces
|
||||
// will be iterated over to find an IP address, preferably private.
|
||||
func Extract(addr string) (string, error) {
|
||||
// if addr is already specified then it's directly returned
|
||||
if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]" && addr != "::") {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
var (
|
||||
addrs []net.Addr
|
||||
loAddrs []net.Addr
|
||||
)
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get interfaces")
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
ifaceAddrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
// ignore error, interface can disappear from system
|
||||
continue
|
||||
}
|
||||
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
loAddrs = append(loAddrs, ifaceAddrs...)
|
||||
continue
|
||||
}
|
||||
|
||||
addrs = append(addrs, ifaceAddrs...)
|
||||
}
|
||||
|
||||
// Add loopback addresses to the end of the list
|
||||
addrs = append(addrs, loAddrs...)
|
||||
|
||||
// Try to find private IP in list, public IP otherwise
|
||||
ip, err := findIP(addrs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ip.String(), nil
|
||||
}
|
||||
|
||||
// IPs returns all available interface IP addresses.
|
||||
func IPs() []string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ipAddrs []string
|
||||
|
||||
for _, i := range ifaces {
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ipAddrs = append(ipAddrs, ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
return ipAddrs
|
||||
}
|
||||
|
||||
// findIP will return the first private IP available in the list.
|
||||
// If no private IP is available it will return the first public IP, if present.
|
||||
// If no public IP is available, it will return the first loopback IP, if present.
|
||||
func findIP(addresses []net.Addr) (net.IP, error) {
|
||||
var publicIP net.IP
|
||||
var localIP net.IP
|
||||
|
||||
for _, rawAddr := range addresses {
|
||||
var ip net.IP
|
||||
switch addr := rawAddr.(type) {
|
||||
case *net.IPAddr:
|
||||
ip = addr.IP
|
||||
case *net.IPNet:
|
||||
ip = addr.IP
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ip.IsLoopback() {
|
||||
if localIP == nil {
|
||||
localIP = ip
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !ip.IsPrivate() {
|
||||
if publicIP == nil {
|
||||
publicIP = ip
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Return private IP if available
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// Return public or virtual IP
|
||||
if len(publicIP) > 0 {
|
||||
return publicIP, nil
|
||||
}
|
||||
|
||||
// Return local IP
|
||||
if len(localIP) > 0 {
|
||||
return localIP, nil
|
||||
}
|
||||
|
||||
return nil, ErrIPNotFound
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package addr
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsLocal(t *testing.T) {
|
||||
testData := []struct {
|
||||
addr string
|
||||
expect bool
|
||||
}{
|
||||
{"localhost", true},
|
||||
{"localhost:8080", true},
|
||||
{"127.0.0.1", true},
|
||||
{"127.0.0.1:1001", true},
|
||||
{"80.1.1.1", false},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
res := IsLocal(d.addr)
|
||||
if res != d.expect {
|
||||
t.Fatalf("expected %t got %t", d.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractor(t *testing.T) {
|
||||
testData := []struct {
|
||||
addr string
|
||||
expect string
|
||||
parse bool
|
||||
}{
|
||||
{"127.0.0.1", "127.0.0.1", false},
|
||||
{"10.0.0.1", "10.0.0.1", false},
|
||||
{"", "", true},
|
||||
{"0.0.0.0", "", true},
|
||||
{"[::]", "", true},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
addr, err := Extract(d.addr)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
if d.parse {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
t.Error("Unexpected nil IP")
|
||||
}
|
||||
} else if addr != d.expect {
|
||||
t.Errorf("Expected %s got %s", d.expect, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindIP(t *testing.T) {
|
||||
localhost, _ := net.ResolveIPAddr("ip", "127.0.0.1")
|
||||
localhostIPv6, _ := net.ResolveIPAddr("ip", "::1")
|
||||
privateIP, _ := net.ResolveIPAddr("ip", "10.0.0.1")
|
||||
publicIP, _ := net.ResolveIPAddr("ip", "100.0.0.1")
|
||||
publicIPv6, _ := net.ResolveIPAddr("ip", "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||
|
||||
testCases := []struct {
|
||||
addrs []net.Addr
|
||||
ip net.IP
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
addrs: []net.Addr{},
|
||||
ip: nil,
|
||||
errMsg: ErrIPNotFound.Error(),
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{localhost},
|
||||
ip: localhost.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{localhost, localhostIPv6},
|
||||
ip: localhost.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{localhostIPv6},
|
||||
ip: localhostIPv6.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{privateIP, localhost},
|
||||
ip: privateIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{privateIP, publicIP, localhost},
|
||||
ip: privateIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{publicIP, privateIP, localhost},
|
||||
ip: privateIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{publicIP, localhost},
|
||||
ip: publicIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{publicIP, localhostIPv6},
|
||||
ip: publicIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{localhostIPv6, publicIP},
|
||||
ip: publicIP.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{localhostIPv6, publicIPv6, publicIP},
|
||||
ip: publicIPv6.IP,
|
||||
},
|
||||
{
|
||||
addrs: []net.Addr{publicIP, publicIPv6},
|
||||
ip: publicIP.IP,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
ip, err := findIP(tc.addrs)
|
||||
if tc.errMsg == "" {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.ip.String(), ip.String())
|
||||
} else {
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, tc.errMsg, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Package backoff provides backoff functionality
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Do is a function x^e multiplied by a factor of 0.1 second.
|
||||
// Result is limited to 2 minute.
|
||||
func Do(attempts int) time.Duration {
|
||||
if attempts > 13 {
|
||||
return 2 * time.Minute
|
||||
}
|
||||
return time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package buf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type buffer struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *buffer) Close() error {
|
||||
b.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(b *bytes.Buffer) *buffer {
|
||||
if b == nil {
|
||||
b = bytes.NewBuffer(nil)
|
||||
}
|
||||
return &buffer{b}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServiceMethod converts a gRPC method to a Go method
|
||||
// Input:
|
||||
// Foo.Bar, /Foo/Bar, /package.Foo/Bar, /a.package.Foo/Bar
|
||||
// Output:
|
||||
// [Foo, Bar].
|
||||
func ServiceMethod(m string) (string, string, error) {
|
||||
if len(m) == 0 {
|
||||
return "", "", fmt.Errorf("malformed method name: %q", m)
|
||||
}
|
||||
|
||||
// grpc method
|
||||
if m[0] == '/' {
|
||||
// [ , Foo, Bar]
|
||||
// [ , package.Foo, Bar]
|
||||
// [ , a.package.Foo, Bar]
|
||||
parts := strings.Split(m, "/")
|
||||
if len(parts) != 3 || len(parts[1]) == 0 || len(parts[2]) == 0 {
|
||||
return "", "", fmt.Errorf("malformed method name: %q", m)
|
||||
}
|
||||
service := strings.Split(parts[1], ".")
|
||||
return service[len(service)-1], parts[2], nil
|
||||
}
|
||||
|
||||
// non grpc method
|
||||
parts := strings.Split(m, ".")
|
||||
|
||||
// expect [Foo, Bar]
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("malformed method name: %q", m)
|
||||
}
|
||||
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
// ServiceFromMethod returns the service
|
||||
// /service.Foo/Bar => service.
|
||||
func ServiceFromMethod(m string) string {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if m[0] != '/' {
|
||||
return m
|
||||
}
|
||||
parts := strings.Split(m, "/")
|
||||
if len(parts) < 3 {
|
||||
return m
|
||||
}
|
||||
parts = strings.Split(parts[1], ".")
|
||||
return strings.Join(parts[:len(parts)-1], ".")
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServiceMethod(t *testing.T) {
|
||||
type testCase struct {
|
||||
input string
|
||||
service string
|
||||
method string
|
||||
err bool
|
||||
}
|
||||
|
||||
methods := []testCase{
|
||||
{"Foo.Bar", "Foo", "Bar", false},
|
||||
{"/Foo/Bar", "Foo", "Bar", false},
|
||||
{"/package.Foo/Bar", "Foo", "Bar", false},
|
||||
{"/a.package.Foo/Bar", "Foo", "Bar", false},
|
||||
{"a.package.Foo/Bar", "", "", true},
|
||||
{"/Foo/Bar/Baz", "", "", true},
|
||||
{"Foo.Bar.Baz", "", "", true},
|
||||
}
|
||||
for _, test := range methods {
|
||||
service, method, err := ServiceMethod(test.input)
|
||||
if err != nil && test.err == true {
|
||||
continue
|
||||
}
|
||||
// unexpected error
|
||||
if err != nil && test.err == false {
|
||||
t.Fatalf("unexpected err %v for %+v", err, test)
|
||||
}
|
||||
// expecter error
|
||||
if test.err == true && err == nil {
|
||||
t.Fatalf("expected error for %+v: got service: %s method: %s", test, service, method)
|
||||
}
|
||||
|
||||
if service != test.service {
|
||||
t.Fatalf("wrong service for %+v: got service: %s method: %s", test, service, method)
|
||||
}
|
||||
|
||||
if method != test.method {
|
||||
t.Fatalf("wrong method for %+v: got service: %s method: %s", test, service, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/selector"
|
||||
)
|
||||
|
||||
// Write sets the status and body on a http ResponseWriter.
|
||||
func Write(w http.ResponseWriter, contentType string, status int, body string) {
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%v", len(body)))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintf(w, `%v`, body)
|
||||
}
|
||||
|
||||
// WriteBadRequestError sets a 400 status code.
|
||||
func WriteBadRequestError(w http.ResponseWriter, err error) {
|
||||
rawBody, err := json.Marshal(map[string]string{
|
||||
"error": err.Error(),
|
||||
})
|
||||
if err != nil {
|
||||
WriteInternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
Write(w, "application/json", 400, string(rawBody))
|
||||
}
|
||||
|
||||
// WriteInternalServerError sets a 500 status code.
|
||||
func WriteInternalServerError(w http.ResponseWriter, err error) {
|
||||
rawBody, err := json.Marshal(map[string]string{
|
||||
"error": err.Error(),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Log(logger.ErrorLevel, err)
|
||||
return
|
||||
}
|
||||
Write(w, "application/json", 500, string(rawBody))
|
||||
}
|
||||
|
||||
func NewRoundTripper(opts ...Option) http.RoundTripper {
|
||||
options := Options{
|
||||
Registry: registry.DefaultRegistry,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &roundTripper{
|
||||
rt: http.DefaultTransport,
|
||||
st: selector.Random,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
// RequestToContext puts the `Authorization` header bearer token into context
|
||||
// so calls to services will be authorized.
|
||||
func RequestToContext(r *http.Request) context.Context {
|
||||
ctx := context.Background()
|
||||
md := make(metadata.Metadata)
|
||||
for k, v := range r.Header {
|
||||
md[k] = strings.Join(v, ",")
|
||||
}
|
||||
return metadata.NewContext(ctx, md)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func TestRoundTripper(t *testing.T) {
|
||||
m := registry.NewMemoryRegistry()
|
||||
|
||||
rt := NewRoundTripper(
|
||||
WithRegistry(m),
|
||||
)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`hello world`))
|
||||
})
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
go http.Serve(l, nil)
|
||||
|
||||
m.Register(®istry.Service{
|
||||
Name: "example.com",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "1",
|
||||
Address: l.Addr().String(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(w.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Body.Close()
|
||||
|
||||
if string(b) != "hello world" {
|
||||
t.Fatal("response is", string(b))
|
||||
}
|
||||
|
||||
// test http request
|
||||
c := &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
|
||||
rsp, err := c.Get("http://example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err = io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsp.Body.Close()
|
||||
|
||||
if string(b) != "hello world" {
|
||||
t.Fatal("response is", string(b))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Registry registry.Registry
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func WithRegistry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v6/selector"
|
||||
)
|
||||
|
||||
type roundTripper struct {
|
||||
rt http.RoundTripper
|
||||
st selector.Strategy
|
||||
opts Options
|
||||
}
|
||||
|
||||
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
s, err := r.opts.Registry.GetService(req.URL.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
next := r.st(s)
|
||||
|
||||
// rudimentary retry 3 times
|
||||
for i := 0; i < 3; i++ {
|
||||
n, err := next()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
req.URL.Host = n.Address
|
||||
w, err := r.rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("failed request")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Package jitter provides a random jitter
|
||||
package jitter
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
// Do returns a random time to jitter with max cap specified.
|
||||
func Do(d time.Duration) time.Duration {
|
||||
v := r.Float64() * float64(d.Nanoseconds())
|
||||
return time.Duration(v)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
@@ -0,0 +1,507 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"go-micro.dev/v6/logger"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
// ServiceEntry is returned after we query for a service.
|
||||
type ServiceEntry struct {
|
||||
Name string
|
||||
Host string
|
||||
Info string
|
||||
AddrV4 net.IP
|
||||
AddrV6 net.IP
|
||||
InfoFields []string
|
||||
|
||||
Addr net.IP // @Deprecated
|
||||
|
||||
Port int
|
||||
TTL int
|
||||
Type uint16
|
||||
|
||||
hasTXT bool
|
||||
sent bool
|
||||
}
|
||||
|
||||
// complete is used to check if we have all the info we need.
|
||||
func (s *ServiceEntry) complete() bool {
|
||||
return (len(s.AddrV4) > 0 || len(s.AddrV6) > 0 || len(s.Addr) > 0) && s.Port != 0 && s.hasTXT
|
||||
}
|
||||
|
||||
// QueryParam is used to customize how a Lookup is performed.
|
||||
type QueryParam struct {
|
||||
Context context.Context // Context
|
||||
Interface *net.Interface // Multicast interface to use
|
||||
Entries chan<- *ServiceEntry // Entries Channel
|
||||
Service string // Service to lookup
|
||||
Domain string // Lookup domain, default "local"
|
||||
Timeout time.Duration // Lookup timeout, default 1 second. Ignored if Context is provided
|
||||
Type uint16 // Lookup type, defaults to dns.TypePTR
|
||||
WantUnicastResponse bool // Unicast response desired, as per 5.4 in RFC
|
||||
}
|
||||
|
||||
// DefaultParams is used to return a default set of QueryParam's.
|
||||
func DefaultParams(service string) *QueryParam {
|
||||
return &QueryParam{
|
||||
Service: service,
|
||||
Domain: "local",
|
||||
Timeout: time.Second,
|
||||
Entries: make(chan *ServiceEntry),
|
||||
WantUnicastResponse: false, // TODO(reddaly): Change this default.
|
||||
}
|
||||
}
|
||||
|
||||
// Query looks up a given service, in a domain, waiting at most
|
||||
// for a timeout before finishing the query. The results are streamed
|
||||
// to a channel. Sends will not block, so clients should make sure to
|
||||
// either read or buffer.
|
||||
func Query(params *QueryParam) error {
|
||||
// Create a new client
|
||||
client, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Set the multicast interface
|
||||
if params.Interface != nil {
|
||||
if err := client.setInterface(params.Interface, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure defaults are set
|
||||
if params.Domain == "" {
|
||||
params.Domain = "local"
|
||||
}
|
||||
|
||||
if params.Context == nil {
|
||||
if params.Timeout == 0 {
|
||||
params.Timeout = time.Second
|
||||
}
|
||||
var cancel context.CancelFunc
|
||||
params.Context, cancel = context.WithTimeout(context.Background(), params.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Run the query
|
||||
return client.query(params)
|
||||
}
|
||||
|
||||
// Listen listens indefinitely for multicast updates.
|
||||
func Listen(entries chan<- *ServiceEntry, exit chan struct{}) error {
|
||||
// Create a new client
|
||||
client, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
_ = client.setInterface(nil, true)
|
||||
|
||||
// Start listening for response packets
|
||||
msgCh := make(chan *dns.Msg, 32)
|
||||
|
||||
go client.recv(client.ipv4UnicastConn, msgCh)
|
||||
go client.recv(client.ipv6UnicastConn, msgCh)
|
||||
go client.recv(client.ipv4MulticastConn, msgCh)
|
||||
go client.recv(client.ipv6MulticastConn, msgCh)
|
||||
|
||||
ip := make(map[string]*ServiceEntry)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-exit:
|
||||
return nil
|
||||
case <-client.closedCh:
|
||||
return nil
|
||||
case m := <-msgCh:
|
||||
e := messageToEntry(m, ip)
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this entry is complete
|
||||
if e.complete() {
|
||||
if e.sent {
|
||||
continue
|
||||
}
|
||||
e.sent = true
|
||||
entries <- e
|
||||
ip = make(map[string]*ServiceEntry)
|
||||
} else {
|
||||
// Fire off a node specific query
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(e.Name, dns.TypePTR)
|
||||
m.RecursionDesired = false
|
||||
if err := client.sendQuery(m); err != nil {
|
||||
logger.Logf(logger.ErrorLevel, "[mdns] failed to query instance %s: %v", e.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Lookup is the same as Query, however it uses all the default parameters.
|
||||
func Lookup(service string, entries chan<- *ServiceEntry) error {
|
||||
params := DefaultParams(service)
|
||||
params.Entries = entries
|
||||
return Query(params)
|
||||
}
|
||||
|
||||
// Client provides a query interface that can be used to
|
||||
// search for service providers using mDNS.
|
||||
type client struct {
|
||||
ipv4UnicastConn *net.UDPConn
|
||||
ipv6UnicastConn *net.UDPConn
|
||||
|
||||
ipv4MulticastConn *net.UDPConn
|
||||
ipv6MulticastConn *net.UDPConn
|
||||
|
||||
closedCh chan struct{} // TODO(reddaly): This doesn't appear to be used.
|
||||
closeLock sync.Mutex
|
||||
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewClient creates a new mdns Client that can be used to query
|
||||
// for records.
|
||||
func newClient() (*client, error) {
|
||||
// TODO(reddaly): At least attempt to bind to the port required in the spec.
|
||||
// Create a IPv4 listener
|
||||
uconn4, err4 := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
uconn6, err6 := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
|
||||
if err4 != nil && err6 != nil {
|
||||
logger.Logf(logger.ErrorLevel, "[mdns] failed to bind to udp port: %v %v", err4, err6)
|
||||
}
|
||||
|
||||
if uconn4 == nil && uconn6 == nil {
|
||||
return nil, fmt.Errorf("failed to bind to any unicast udp port")
|
||||
}
|
||||
|
||||
if uconn4 == nil {
|
||||
uconn4 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
if uconn6 == nil {
|
||||
uconn6 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
mconn4, err4 := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
|
||||
mconn6, err6 := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
|
||||
if err4 != nil && err6 != nil {
|
||||
logger.Logf(logger.ErrorLevel, "[mdns] failed to bind to udp port: %v %v", err4, err6)
|
||||
}
|
||||
|
||||
if mconn4 == nil && mconn6 == nil {
|
||||
return nil, fmt.Errorf("failed to bind to any multicast udp port")
|
||||
}
|
||||
|
||||
if mconn4 == nil {
|
||||
mconn4 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
if mconn6 == nil {
|
||||
mconn6 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
p1 := ipv4.NewPacketConn(mconn4)
|
||||
p2 := ipv6.NewPacketConn(mconn6)
|
||||
_ = p1.SetMulticastLoopback(true)
|
||||
_ = p2.SetMulticastLoopback(true)
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var errCount1, errCount2 int
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
errCount1++
|
||||
}
|
||||
if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
errCount2++
|
||||
}
|
||||
}
|
||||
|
||||
if len(ifaces) == errCount1 && len(ifaces) == errCount2 {
|
||||
return nil, fmt.Errorf("failed to join multicast group on all interfaces")
|
||||
}
|
||||
|
||||
c := &client{
|
||||
ipv4MulticastConn: mconn4,
|
||||
ipv6MulticastConn: mconn6,
|
||||
ipv4UnicastConn: uconn4,
|
||||
ipv6UnicastConn: uconn6,
|
||||
closedCh: make(chan struct{}),
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Close is used to cleanup the client.
|
||||
func (c *client) Close() error {
|
||||
c.closeLock.Lock()
|
||||
defer c.closeLock.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
|
||||
close(c.closedCh)
|
||||
|
||||
if c.ipv4UnicastConn != nil {
|
||||
c.ipv4UnicastConn.Close()
|
||||
}
|
||||
if c.ipv6UnicastConn != nil {
|
||||
c.ipv6UnicastConn.Close()
|
||||
}
|
||||
if c.ipv4MulticastConn != nil {
|
||||
c.ipv4MulticastConn.Close()
|
||||
}
|
||||
if c.ipv6MulticastConn != nil {
|
||||
c.ipv6MulticastConn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setInterface is used to set the query interface, uses system
|
||||
// default if not provided.
|
||||
func (c *client) setInterface(iface *net.Interface, loopback bool) error {
|
||||
p := ipv4.NewPacketConn(c.ipv4UnicastConn)
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return err
|
||||
}
|
||||
p2 := ipv6.NewPacketConn(c.ipv6UnicastConn)
|
||||
if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return err
|
||||
}
|
||||
p = ipv4.NewPacketConn(c.ipv4MulticastConn)
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return err
|
||||
}
|
||||
p2 = ipv6.NewPacketConn(c.ipv6MulticastConn)
|
||||
if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if loopback {
|
||||
_ = p.SetMulticastLoopback(true)
|
||||
_ = p2.SetMulticastLoopback(true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// query is used to perform a lookup and stream results.
|
||||
func (c *client) query(params *QueryParam) error {
|
||||
// Create the service name
|
||||
serviceAddr := fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain))
|
||||
|
||||
// Start listening for response packets
|
||||
msgCh := make(chan *dns.Msg, 32)
|
||||
go c.recv(c.ipv4UnicastConn, msgCh)
|
||||
go c.recv(c.ipv6UnicastConn, msgCh)
|
||||
go c.recv(c.ipv4MulticastConn, msgCh)
|
||||
go c.recv(c.ipv6MulticastConn, msgCh)
|
||||
|
||||
// Send the query
|
||||
m := new(dns.Msg)
|
||||
if params.Type == dns.TypeNone {
|
||||
m.SetQuestion(serviceAddr, dns.TypePTR)
|
||||
} else {
|
||||
m.SetQuestion(serviceAddr, params.Type)
|
||||
}
|
||||
// RFC 6762, section 18.12. Repurposing of Top Bit of qclass in Question
|
||||
// Section
|
||||
//
|
||||
// In the Question Section of a Multicast DNS query, the top bit of the qclass
|
||||
// field is used to indicate that unicast responses are preferred for this
|
||||
// particular question. (See Section 5.4.)
|
||||
if params.WantUnicastResponse {
|
||||
m.Question[0].Qclass |= 1 << 15
|
||||
}
|
||||
m.RecursionDesired = false
|
||||
if err := c.sendQuery(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Map the in-progress responses
|
||||
inprogress := make(map[string]*ServiceEntry)
|
||||
|
||||
for {
|
||||
select {
|
||||
case resp := <-msgCh:
|
||||
inp := messageToEntry(resp, inprogress)
|
||||
|
||||
if inp == nil {
|
||||
continue
|
||||
}
|
||||
if len(resp.Question) == 0 || resp.Question[0].Name != m.Question[0].Name {
|
||||
// discard anything which we've not asked for
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this entry is complete
|
||||
if inp.complete() {
|
||||
if inp.sent {
|
||||
continue
|
||||
}
|
||||
|
||||
inp.sent = true
|
||||
select {
|
||||
case params.Entries <- inp:
|
||||
case <-params.Context.Done():
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Fire off a node specific query
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(inp.Name, inp.Type)
|
||||
m.RecursionDesired = false
|
||||
if err := c.sendQuery(m); err != nil {
|
||||
logger.Logf(logger.ErrorLevel, "[mdns] failed to query instance %s: %v", inp.Name, err)
|
||||
}
|
||||
}
|
||||
case <-params.Context.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendQuery is used to multicast a query out.
|
||||
func (c *client) sendQuery(q *dns.Msg) error {
|
||||
buf, err := q.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.ipv4UnicastConn != nil {
|
||||
_, _ = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr)
|
||||
}
|
||||
if c.ipv6UnicastConn != nil {
|
||||
_, _ = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recv is used to receive until we get a shutdown.
|
||||
func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 65536)
|
||||
for {
|
||||
c.closeLock.Lock()
|
||||
if c.closed {
|
||||
c.closeLock.Unlock()
|
||||
return
|
||||
}
|
||||
c.closeLock.Unlock()
|
||||
n, err := l.Read(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
msg := new(dns.Msg)
|
||||
if err := msg.Unpack(buf[:n]); err != nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case msgCh <- msg:
|
||||
case <-c.closedCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensureName is used to ensure the named node is in progress.
|
||||
func ensureName(inprogress map[string]*ServiceEntry, name string, typ uint16) *ServiceEntry {
|
||||
if inp, ok := inprogress[name]; ok {
|
||||
return inp
|
||||
}
|
||||
inp := &ServiceEntry{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
}
|
||||
inprogress[name] = inp
|
||||
return inp
|
||||
}
|
||||
|
||||
// alias is used to setup an alias between two entries.
|
||||
func alias(inprogress map[string]*ServiceEntry, src, dst string, typ uint16) {
|
||||
srcEntry := ensureName(inprogress, src, typ)
|
||||
inprogress[dst] = srcEntry
|
||||
}
|
||||
|
||||
func messageToEntry(m *dns.Msg, inprogress map[string]*ServiceEntry) *ServiceEntry {
|
||||
var inp *ServiceEntry
|
||||
|
||||
for _, answer := range append(m.Answer, m.Extra...) {
|
||||
// TODO(reddaly): Check that response corresponds to serviceAddr?
|
||||
switch rr := answer.(type) {
|
||||
case *dns.PTR:
|
||||
// Create new entry for this
|
||||
inp = ensureName(inprogress, rr.Ptr, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
case *dns.SRV:
|
||||
// Check for a target mismatch
|
||||
if rr.Target != rr.Hdr.Name {
|
||||
alias(inprogress, rr.Hdr.Name, rr.Target, rr.Hdr.Rrtype)
|
||||
}
|
||||
|
||||
// Get the port
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Host = rr.Target
|
||||
inp.Port = int(rr.Port)
|
||||
case *dns.TXT:
|
||||
// Pull out the txt
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Info = strings.Join(rr.Txt, "|")
|
||||
inp.InfoFields = rr.Txt
|
||||
inp.hasTXT = true
|
||||
case *dns.A:
|
||||
// Pull out the IP
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Addr = rr.A // @Deprecated
|
||||
inp.AddrV4 = rr.A
|
||||
case *dns.AAAA:
|
||||
// Pull out the IP
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Addr = rr.AAAA // @Deprecated
|
||||
inp.AddrV6 = rr.AAAA
|
||||
}
|
||||
|
||||
if inp != nil {
|
||||
inp.TTL = int(answer.Header().Ttl)
|
||||
}
|
||||
}
|
||||
|
||||
return inp
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package mdns
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// DNSSDService is a service that complies with the DNS-SD (RFC 6762) and MDNS
|
||||
// (RFC 6762) specs for local, multicast-DNS-based discovery.
|
||||
//
|
||||
// DNSSDService implements the Zone interface and wraps an MDNSService instance.
|
||||
// To deploy an mDNS service that is compliant with DNS-SD, it's recommended to
|
||||
// register only the wrapped instance with the server.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// service := &mdns.DNSSDService{
|
||||
// MDNSService: &mdns.MDNSService{
|
||||
// Instance: "My Foobar Service",
|
||||
// Service: "_foobar._tcp",
|
||||
// Port: 8000,
|
||||
// }
|
||||
// }
|
||||
// server, err := mdns.NewServer(&mdns.Config{Zone: service})
|
||||
// if err != nil {
|
||||
// log.Fatalf("Error creating server: %v", err)
|
||||
// }
|
||||
// defer server.Shutdown()
|
||||
type DNSSDService struct {
|
||||
MDNSService *MDNSService
|
||||
}
|
||||
|
||||
// Records returns DNS records in response to a DNS question.
|
||||
//
|
||||
// This function returns the DNS response of the underlying MDNSService
|
||||
// instance. It also returns a PTR record for a request for "
|
||||
// _services._dns-sd._udp.<Domain>", as described in section 9 of RFC 6763
|
||||
// ("Service Type Enumeration"), to allow browsing of the underlying MDNSService
|
||||
// instance.
|
||||
func (s *DNSSDService) Records(q dns.Question) []dns.RR {
|
||||
var recs []dns.RR
|
||||
if q.Name == "_services._dns-sd._udp."+s.MDNSService.Domain+"." {
|
||||
recs = s.dnssdMetaQueryRecords(q)
|
||||
}
|
||||
return append(recs, s.MDNSService.Records(q)...)
|
||||
}
|
||||
|
||||
// dnssdMetaQueryRecords returns the DNS records in response to a "meta-query"
|
||||
// issued to browse for DNS-SD services, as per section 9. of RFC6763.
|
||||
//
|
||||
// A meta-query has a name of the form "_services._dns-sd._udp.<Domain>" where
|
||||
// Domain is a fully-qualified domain, such as "local.".
|
||||
func (s *DNSSDService) dnssdMetaQueryRecords(q dns.Question) []dns.RR {
|
||||
// Intended behavior, as described in the RFC:
|
||||
// ...it may be useful for network administrators to find the list of
|
||||
// advertised service types on the network, even if those Service Names
|
||||
// are just opaque identifiers and not particularly informative in
|
||||
// isolation.
|
||||
//
|
||||
// For this purpose, a special meta-query is defined. A DNS query for PTR
|
||||
// records with the name "_services._dns-sd._udp.<Domain>" yields a set of
|
||||
// PTR records, where the rdata of each PTR record is the two-abel
|
||||
// <Service> name, plus the same domain, e.g., "_http._tcp.<Domain>".
|
||||
// Including the domain in the PTR rdata allows for slightly better name
|
||||
// compression in Unicast DNS responses, but only the first two labels are
|
||||
// relevant for the purposes of service type enumeration. These two-label
|
||||
// service types can then be used to construct subsequent Service Instance
|
||||
// Enumeration PTR queries, in this <Domain> or others, to discover
|
||||
// instances of that service type.
|
||||
return []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Ptr: s.MDNSService.serviceAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Announcement returns DNS records that should be broadcast during the initial
|
||||
// availability of the service, as described in section 8.3 of RFC 6762.
|
||||
// TODO(reddaly): Add this when Announcement is added to the mdns.Zone interface.
|
||||
// func (s *DNSSDService) Announcement() []dns.RR {
|
||||
// return s.MDNSService.Announcement()
|
||||
//}
|
||||
@@ -0,0 +1,39 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestDNSSDServiceRecords(t *testing.T) {
|
||||
s := &DNSSDService{
|
||||
MDNSService: &MDNSService{
|
||||
serviceAddr: "_foobar._tcp.local.",
|
||||
Domain: "local",
|
||||
},
|
||||
}
|
||||
q := dns.Question{
|
||||
Name: "_services._dns-sd._udp.local.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if got, want := len(recs), 1; got != want {
|
||||
t.Fatalf("s.Records(%v) returned %v records, want %v", q, got, want)
|
||||
}
|
||||
|
||||
want := dns.RR(&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "_services._dns-sd._udp.local.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Ptr: "_foobar._tcp.local.",
|
||||
})
|
||||
if got := recs[0]; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("s.Records()[0] = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
log "go-micro.dev/v6/logger"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
var (
|
||||
mdnsGroupIPv4 = net.ParseIP("224.0.0.251")
|
||||
mdnsGroupIPv6 = net.ParseIP("ff02::fb")
|
||||
|
||||
// mDNS wildcard addresses.
|
||||
mdnsWildcardAddrIPv4 = &net.UDPAddr{
|
||||
IP: net.ParseIP("224.0.0.0"),
|
||||
Port: 5353,
|
||||
}
|
||||
mdnsWildcardAddrIPv6 = &net.UDPAddr{
|
||||
IP: net.ParseIP("ff02::"),
|
||||
Port: 5353,
|
||||
}
|
||||
|
||||
// mDNS endpoint addresses.
|
||||
ipv4Addr = &net.UDPAddr{
|
||||
IP: mdnsGroupIPv4,
|
||||
Port: 5353,
|
||||
}
|
||||
ipv6Addr = &net.UDPAddr{
|
||||
IP: mdnsGroupIPv6,
|
||||
Port: 5353,
|
||||
}
|
||||
)
|
||||
|
||||
// GetMachineIP is a func which returns the outbound IP of this machine.
|
||||
// Used by the server to determine whether to attempt send the response on a local address.
|
||||
type GetMachineIP func() net.IP
|
||||
|
||||
// Config is used to configure the mDNS server.
|
||||
type Config struct {
|
||||
// Zone must be provided to support responding to queries
|
||||
Zone Zone
|
||||
|
||||
// Iface if provided binds the multicast listener to the given
|
||||
// interface. If not provided, the system default multicase interface
|
||||
// is used.
|
||||
Iface *net.Interface
|
||||
|
||||
// GetMachineIP is a function to return the IP of the local machine
|
||||
GetMachineIP GetMachineIP
|
||||
|
||||
// Port If it is not 0, replace the port 5353 with this port number.
|
||||
Port int
|
||||
|
||||
// LocalhostChecking if enabled asks the server to also send responses to 0.0.0.0 if the target IP
|
||||
// is this host (as defined by GetMachineIP). Useful in case machine is on a VPN which blocks comms on non standard ports
|
||||
LocalhostChecking bool
|
||||
}
|
||||
|
||||
// Server is an mDNS server used to listen for mDNS queries and respond if we
|
||||
// have a matching local record.
|
||||
type Server struct {
|
||||
config *Config
|
||||
|
||||
ipv4List *net.UDPConn
|
||||
ipv6List *net.UDPConn
|
||||
|
||||
shutdownCh chan struct{}
|
||||
|
||||
outboundIP net.IP
|
||||
wg sync.WaitGroup
|
||||
|
||||
shutdownLock sync.Mutex
|
||||
|
||||
shutdown bool
|
||||
}
|
||||
|
||||
// NewServer is used to create a new mDNS server from a config.
|
||||
func NewServer(config *Config) (*Server, error) {
|
||||
setCustomPort(config.Port)
|
||||
|
||||
// Create the listeners
|
||||
// Create wildcard connections (because :5353 can be already taken by other apps)
|
||||
ipv4List, _ := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
|
||||
ipv6List, _ := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
|
||||
if ipv4List == nil && ipv6List == nil {
|
||||
return nil, fmt.Errorf("[ERR] mdns: Failed to bind to any udp port")
|
||||
}
|
||||
|
||||
if ipv4List == nil {
|
||||
ipv4List = &net.UDPConn{}
|
||||
}
|
||||
if ipv6List == nil {
|
||||
ipv6List = &net.UDPConn{}
|
||||
}
|
||||
|
||||
// Join multicast groups to receive announcements
|
||||
p1 := ipv4.NewPacketConn(ipv4List)
|
||||
p2 := ipv6.NewPacketConn(ipv6List)
|
||||
_ = p1.SetMulticastLoopback(true)
|
||||
_ = p2.SetMulticastLoopback(true)
|
||||
|
||||
if config.Iface != nil {
|
||||
if err := p1.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p2.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errCount1, errCount2 := 0, 0
|
||||
for _, iface := range ifaces {
|
||||
if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
errCount1++
|
||||
}
|
||||
if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
errCount2++
|
||||
}
|
||||
}
|
||||
if len(ifaces) == errCount1 && len(ifaces) == errCount2 {
|
||||
return nil, fmt.Errorf("failed to join multicast group on all interfaces")
|
||||
}
|
||||
}
|
||||
|
||||
ipFunc := getOutboundIP
|
||||
if config.GetMachineIP != nil {
|
||||
ipFunc = config.GetMachineIP
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
ipv4List: ipv4List,
|
||||
ipv6List: ipv6List,
|
||||
shutdownCh: make(chan struct{}),
|
||||
outboundIP: ipFunc(),
|
||||
}
|
||||
|
||||
go s.recv(s.ipv4List)
|
||||
go s.recv(s.ipv6List)
|
||||
|
||||
s.wg.Add(1)
|
||||
go s.probe()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Shutdown is used to shutdown the listener.
|
||||
func (s *Server) Shutdown() error {
|
||||
s.shutdownLock.Lock()
|
||||
defer s.shutdownLock.Unlock()
|
||||
|
||||
if s.shutdown {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.shutdown = true
|
||||
close(s.shutdownCh)
|
||||
_ = s.unregister()
|
||||
|
||||
if s.ipv4List != nil {
|
||||
s.ipv4List.Close()
|
||||
}
|
||||
if s.ipv6List != nil {
|
||||
s.ipv6List.Close()
|
||||
}
|
||||
|
||||
s.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// recv is a long running routine to receive packets from an interface.
|
||||
func (s *Server) recv(c *net.UDPConn) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 65536)
|
||||
for {
|
||||
s.shutdownLock.Lock()
|
||||
if s.shutdown {
|
||||
s.shutdownLock.Unlock()
|
||||
return
|
||||
}
|
||||
s.shutdownLock.Unlock()
|
||||
n, from, err := c.ReadFrom(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := s.parsePacket(buf[:n], from); err != nil {
|
||||
log.Errorf("[ERR] mdns: Failed to handle query: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parsePacket is used to parse an incoming packet.
|
||||
func (s *Server) parsePacket(packet []byte, from net.Addr) error {
|
||||
var msg dns.Msg
|
||||
if err := msg.Unpack(packet); err != nil {
|
||||
log.Errorf("[ERR] mdns: Failed to unpack packet: %v", err)
|
||||
return err
|
||||
}
|
||||
// TODO: This is a bit of a hack
|
||||
// We decided to ignore some mDNS answers for the time being
|
||||
// See: https://tools.ietf.org/html/rfc6762#section-7.2
|
||||
msg.Truncated = false
|
||||
return s.handleQuery(&msg, from)
|
||||
}
|
||||
|
||||
// handleQuery is used to handle an incoming query.
|
||||
func (s *Server) handleQuery(query *dns.Msg, from net.Addr) error {
|
||||
if query.Opcode != dns.OpcodeQuery {
|
||||
// "In both multicast query and multicast response messages, the OPCODE MUST
|
||||
// be zero on transmission (only standard queries are currently supported
|
||||
// over multicast). Multicast DNS messages received with an OPCODE other
|
||||
// than zero MUST be silently ignored." Note: OpcodeQuery == 0
|
||||
return fmt.Errorf("mdns: received query with non-zero Opcode %v: %v", query.Opcode, *query)
|
||||
}
|
||||
if query.Rcode != 0 {
|
||||
// "In both multicast query and multicast response messages, the Response
|
||||
// Code MUST be zero on transmission. Multicast DNS messages received with
|
||||
// non-zero Response Codes MUST be silently ignored."
|
||||
return fmt.Errorf("mdns: received query with non-zero Rcode %v: %v", query.Rcode, *query)
|
||||
}
|
||||
|
||||
// TODO(reddaly): Handle "TC (Truncated) Bit":
|
||||
// In query messages, if the TC bit is set, it means that additional
|
||||
// Known-Answer records may be following shortly. A responder SHOULD
|
||||
// record this fact, and wait for those additional Known-Answer records,
|
||||
// before deciding whether to respond. If the TC bit is clear, it means
|
||||
// that the querying host has no additional Known Answers.
|
||||
if query.Truncated {
|
||||
return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", *query)
|
||||
}
|
||||
|
||||
var unicastAnswer, multicastAnswer []dns.RR
|
||||
|
||||
// Handle each question
|
||||
for _, q := range query.Question {
|
||||
mrecs, urecs := s.handleQuestion(q)
|
||||
multicastAnswer = append(multicastAnswer, mrecs...)
|
||||
unicastAnswer = append(unicastAnswer, urecs...)
|
||||
}
|
||||
|
||||
// See section 18 of RFC 6762 for rules about DNS headers.
|
||||
resp := func(unicast bool) *dns.Msg {
|
||||
// 18.1: ID (Query Identifier)
|
||||
// 0 for multicast response, query.Id for unicast response
|
||||
id := uint16(0)
|
||||
if unicast {
|
||||
id = query.Id
|
||||
}
|
||||
|
||||
var answer []dns.RR
|
||||
if unicast {
|
||||
answer = unicastAnswer
|
||||
} else {
|
||||
answer = multicastAnswer
|
||||
}
|
||||
if len(answer) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
|
||||
// 18.2: QR (Query/Response) Bit - must be set to 1 in response.
|
||||
Response: true,
|
||||
|
||||
// 18.3: OPCODE - must be zero in response (OpcodeQuery == 0)
|
||||
Opcode: dns.OpcodeQuery,
|
||||
|
||||
// 18.4: AA (Authoritative Answer) Bit - must be set to 1
|
||||
Authoritative: true,
|
||||
|
||||
// The following fields must all be set to 0:
|
||||
// 18.5: TC (TRUNCATED) Bit
|
||||
// 18.6: RD (Recursion Desired) Bit
|
||||
// 18.7: RA (Recursion Available) Bit
|
||||
// 18.8: Z (Zero) Bit
|
||||
// 18.9: AD (Authentic Data) Bit
|
||||
// 18.10: CD (Checking Disabled) Bit
|
||||
// 18.11: RCODE (Response Code)
|
||||
},
|
||||
// 18.12 pertains to questions (handled by handleQuestion)
|
||||
// 18.13 pertains to resource records (handled by handleQuestion)
|
||||
|
||||
// 18.14: Name Compression - responses should be compressed (though see
|
||||
// caveats in the RFC), so set the Compress bit (part of the dns library
|
||||
// API, not part of the DNS packet) to true.
|
||||
Compress: true,
|
||||
Question: query.Question,
|
||||
Answer: answer,
|
||||
}
|
||||
}
|
||||
|
||||
if mresp := resp(false); mresp != nil {
|
||||
if err := s.sendResponse(mresp, from); err != nil {
|
||||
return fmt.Errorf("mdns: error sending multicast response: %v", err)
|
||||
}
|
||||
}
|
||||
if uresp := resp(true); uresp != nil {
|
||||
if err := s.sendResponse(uresp, from); err != nil {
|
||||
return fmt.Errorf("mdns: error sending unicast response: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleQuestion is used to handle an incoming question
|
||||
//
|
||||
// The response to a question may be transmitted over multicast, unicast, or
|
||||
// both. The return values are DNS records for each transmission type.
|
||||
func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) {
|
||||
records := s.config.Zone.Records(q)
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Handle unicast and multicast responses.
|
||||
// TODO(reddaly): The decision about sending over unicast vs. multicast is not
|
||||
// yet fully compliant with RFC 6762. For example, the unicast bit should be
|
||||
// ignored if the records in question are close to TTL expiration. For now,
|
||||
// we just use the unicast bit to make the decision, as per the spec:
|
||||
// RFC 6762, section 18.12. Repurposing of Top Bit of qclass in Question
|
||||
// Section
|
||||
//
|
||||
// In the Question Section of a Multicast DNS query, the top bit of the
|
||||
// qclass field is used to indicate that unicast responses are preferred
|
||||
// for this particular question. (See Section 5.4.)
|
||||
if q.Qclass&(1<<15) != 0 {
|
||||
return nil, records
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *Server) probe() {
|
||||
defer s.wg.Done()
|
||||
|
||||
sd, ok := s.config.Zone.(*MDNSService)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain))
|
||||
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion(name, dns.TypePTR)
|
||||
q.RecursionDesired = false
|
||||
|
||||
srv := &dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Priority: 0,
|
||||
Weight: 0,
|
||||
Port: uint16(sd.Port),
|
||||
Target: sd.HostName,
|
||||
}
|
||||
txt := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Txt: sd.TXT,
|
||||
}
|
||||
q.Ns = []dns.RR{srv, txt}
|
||||
|
||||
randomizer := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.SendMulticast(q); err != nil {
|
||||
log.Errorf("[ERR] mdns: failed to send probe:", err.Error())
|
||||
}
|
||||
time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond)
|
||||
}
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.Response = true
|
||||
|
||||
// set for query
|
||||
q.SetQuestion(name, dns.TypeANY)
|
||||
|
||||
resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...)
|
||||
|
||||
// reset
|
||||
q.SetQuestion(name, dns.TypePTR)
|
||||
|
||||
// From RFC6762
|
||||
// The Multicast DNS responder MUST send at least two unsolicited
|
||||
// responses, one second apart. To provide increased robustness against
|
||||
// packet loss, a responder MAY send up to eight unsolicited responses,
|
||||
// provided that the interval between unsolicited responses increases by
|
||||
// at least a factor of two with every response sent.
|
||||
timeout := 1 * time.Second
|
||||
timer := time.NewTimer(timeout)
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.SendMulticast(resp); err != nil {
|
||||
log.Errorf("[ERR] mdns: failed to send announcement:", err.Error())
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
timeout *= 2
|
||||
timer.Reset(timeout)
|
||||
case <-s.shutdownCh:
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendMulticast us used to send a multicast response packet.
|
||||
func (s *Server) SendMulticast(msg *dns.Msg) error {
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.ipv4List != nil {
|
||||
_, _ = s.ipv4List.WriteToUDP(buf, ipv4Addr)
|
||||
}
|
||||
if s.ipv6List != nil {
|
||||
_, _ = s.ipv6List.WriteToUDP(buf, ipv6Addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendResponse is used to send a response packet.
|
||||
func (s *Server) sendResponse(resp *dns.Msg, from net.Addr) error {
|
||||
// TODO(reddaly): Respect the unicast argument, and allow sending responses
|
||||
// over multicast.
|
||||
buf, err := resp.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine the socket to send from
|
||||
addr := from.(*net.UDPAddr)
|
||||
conn := s.ipv4List
|
||||
backupTarget := net.IPv4zero
|
||||
|
||||
if addr.IP.To4() == nil {
|
||||
conn = s.ipv6List
|
||||
backupTarget = net.IPv6zero
|
||||
}
|
||||
_, err = conn.WriteToUDP(buf, addr)
|
||||
// If the address we're responding to is this machine then we can also attempt sending on 0.0.0.0
|
||||
// This covers the case where this machine is using a VPN and certain ports are blocked so the response never gets there
|
||||
// Sending two responses is OK
|
||||
if s.config.LocalhostChecking && addr.IP.Equal(s.outboundIP) {
|
||||
// ignore any errors, this is best efforts
|
||||
_, _ = conn.WriteToUDP(buf, &net.UDPAddr{IP: backupTarget, Port: addr.Port})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) unregister() error {
|
||||
sd, ok := s.config.Zone.(*MDNSService)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&sd.TTL, 0)
|
||||
name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain))
|
||||
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion(name, dns.TypeANY)
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.Response = true
|
||||
resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...)
|
||||
|
||||
return s.SendMulticast(resp)
|
||||
}
|
||||
|
||||
func setCustomPort(port int) {
|
||||
if port != 0 {
|
||||
if mdnsWildcardAddrIPv4.Port != port {
|
||||
mdnsWildcardAddrIPv4.Port = port
|
||||
}
|
||||
if mdnsWildcardAddrIPv6.Port != port {
|
||||
mdnsWildcardAddrIPv6.Port = port
|
||||
}
|
||||
if ipv4Addr.Port != port {
|
||||
ipv4Addr.Port = port
|
||||
}
|
||||
if ipv6Addr.Port != port {
|
||||
ipv6Addr.Port = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getOutboundIP() net.IP {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
if err != nil {
|
||||
// no net connectivity maybe so fallback
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
return localAddr.IP
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServer_StartStop(t *testing.T) {
|
||||
s := makeService(t)
|
||||
serv, err := NewServer(&Config{Zone: s, LocalhostChecking: true})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer serv.Shutdown()
|
||||
}
|
||||
|
||||
func TestServer_Lookup(t *testing.T) {
|
||||
serv, err := NewServer(&Config{Zone: makeServiceWithServiceName(t, "_foobar._tcp"), LocalhostChecking: true})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer serv.Shutdown()
|
||||
|
||||
entries := make(chan *ServiceEntry, 1)
|
||||
found := false
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case e := <-entries:
|
||||
if e.Name != "hostname._foobar._tcp.local." {
|
||||
t.Errorf("bad: %v", e)
|
||||
}
|
||||
if e.Port != 80 {
|
||||
t.Errorf("bad: %v", e)
|
||||
}
|
||||
if e.Info != "Local web server" {
|
||||
t.Errorf("bad: %v", e)
|
||||
}
|
||||
found = true
|
||||
|
||||
case <-time.After(80 * time.Millisecond):
|
||||
t.Errorf("timeout")
|
||||
}
|
||||
close(doneCh)
|
||||
}()
|
||||
|
||||
params := &QueryParam{
|
||||
Service: "_foobar._tcp",
|
||||
Domain: "local",
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Entries: entries,
|
||||
}
|
||||
err = Query(params)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
<-doneCh
|
||||
if !found {
|
||||
t.Fatalf("record not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultTTL is the default TTL value in returned DNS records in seconds.
|
||||
defaultTTL = 120
|
||||
)
|
||||
|
||||
// Zone is the interface used to integrate with the server and
|
||||
// to serve records dynamically.
|
||||
type Zone interface {
|
||||
// Records returns DNS records in response to a DNS question.
|
||||
Records(q dns.Question) []dns.RR
|
||||
}
|
||||
|
||||
// MDNSService is used to export a named service by implementing a Zone.
|
||||
type MDNSService struct {
|
||||
Instance string // Instance name (e.g. "hostService name")
|
||||
Service string // Service name (e.g. "_http._tcp.")
|
||||
Domain string // If blank, assumes "local"
|
||||
HostName string // Host machine DNS name (e.g. "mymachine.net.")
|
||||
serviceAddr string // Fully qualified service address
|
||||
instanceAddr string // Fully qualified instance address
|
||||
enumAddr string // _services._dns-sd._udp.<domain>
|
||||
IPs []net.IP // IP addresses for the service's host
|
||||
TXT []string // Service TXT records
|
||||
Port int // Service Port
|
||||
TTL uint32
|
||||
}
|
||||
|
||||
// validateFQDN returns an error if the passed string is not a fully qualified
|
||||
// hdomain name (more specifically, a hostname).
|
||||
func validateFQDN(s string) error {
|
||||
if len(s) == 0 {
|
||||
return fmt.Errorf("FQDN must not be blank")
|
||||
}
|
||||
if s[len(s)-1] != '.' {
|
||||
return fmt.Errorf("FQDN must end in period: %s", s)
|
||||
}
|
||||
// TODO(reddaly): Perform full validation.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMDNSService returns a new instance of MDNSService.
|
||||
//
|
||||
// If domain, hostName, or ips is set to the zero value, then a default value
|
||||
// will be inferred from the operating system.
|
||||
//
|
||||
// TODO(reddaly): This interface may need to change to account for "unique
|
||||
// record" conflict rules of the mDNS protocol. Upon startup, the server should
|
||||
// check to ensure that the instance name does not conflict with other instance
|
||||
// names, and, if required, select a new name. There may also be conflicting
|
||||
// hostName A/AAAA records.
|
||||
func NewMDNSService(instance, service, domain, hostName string, port int, ips []net.IP, txt []string) (*MDNSService, error) {
|
||||
// Sanity check inputs
|
||||
if instance == "" {
|
||||
return nil, fmt.Errorf("missing service instance name")
|
||||
}
|
||||
if service == "" {
|
||||
return nil, fmt.Errorf("missing service name")
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, fmt.Errorf("missing service port")
|
||||
}
|
||||
|
||||
// Set default domain
|
||||
if domain == "" {
|
||||
domain = "local."
|
||||
}
|
||||
if err := validateFQDN(domain); err != nil {
|
||||
return nil, fmt.Errorf("domain %q is not a fully-qualified domain name: %v", domain, err)
|
||||
}
|
||||
|
||||
// Get host information if no host is specified.
|
||||
if hostName == "" {
|
||||
var err error
|
||||
hostName, err = os.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine host: %v", err)
|
||||
}
|
||||
hostName = fmt.Sprintf("%s.", hostName)
|
||||
}
|
||||
if err := validateFQDN(hostName); err != nil {
|
||||
return nil, fmt.Errorf("hostName %q is not a fully-qualified domain name: %v", hostName, err)
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
var err error
|
||||
ips, err = net.LookupIP(trimDot(hostName))
|
||||
if err != nil {
|
||||
// Try appending the host domain suffix and lookup again
|
||||
// (required for Linux-based hosts)
|
||||
tmpHostName := fmt.Sprintf("%s%s", hostName, domain)
|
||||
|
||||
ips, err = net.LookupIP(trimDot(tmpHostName))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine host IP addresses for %s", hostName)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() == nil && ip.To16() == nil {
|
||||
return nil, fmt.Errorf("invalid IP address in IPs list: %v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
return &MDNSService{
|
||||
Instance: instance,
|
||||
Service: service,
|
||||
Domain: domain,
|
||||
HostName: hostName,
|
||||
Port: port,
|
||||
IPs: ips,
|
||||
TXT: txt,
|
||||
TTL: defaultTTL,
|
||||
serviceAddr: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)),
|
||||
instanceAddr: fmt.Sprintf("%s.%s.%s.", instance, trimDot(service), trimDot(domain)),
|
||||
enumAddr: fmt.Sprintf("_services._dns-sd._udp.%s.", trimDot(domain)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// trimDot is used to trim the dots from the start or end of a string.
|
||||
func trimDot(s string) string {
|
||||
return strings.Trim(s, ".")
|
||||
}
|
||||
|
||||
// Records returns DNS records in response to a DNS question.
|
||||
func (m *MDNSService) Records(q dns.Question) []dns.RR {
|
||||
switch q.Name {
|
||||
case m.enumAddr:
|
||||
return m.serviceEnum(q)
|
||||
case m.serviceAddr:
|
||||
return m.serviceRecords(q)
|
||||
case m.instanceAddr:
|
||||
return m.instanceRecords(q)
|
||||
case m.HostName:
|
||||
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA {
|
||||
return m.instanceRecords(q)
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MDNSService) serviceEnum(q dns.Question) []dns.RR {
|
||||
switch q.Qtype {
|
||||
case dns.TypeANY:
|
||||
fallthrough
|
||||
case dns.TypePTR:
|
||||
rr := &dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
Ptr: m.serviceAddr,
|
||||
}
|
||||
return []dns.RR{rr}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// serviceRecords is called when the query matches the service name.
|
||||
func (m *MDNSService) serviceRecords(q dns.Question) []dns.RR {
|
||||
switch q.Qtype {
|
||||
case dns.TypeANY:
|
||||
fallthrough
|
||||
case dns.TypePTR:
|
||||
// Build a PTR response for the service
|
||||
rr := &dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
Ptr: m.instanceAddr,
|
||||
}
|
||||
servRec := []dns.RR{rr}
|
||||
|
||||
// Get the instance records
|
||||
instRecs := m.instanceRecords(dns.Question{
|
||||
Name: m.instanceAddr,
|
||||
Qtype: dns.TypeANY,
|
||||
})
|
||||
|
||||
// Return the service record with the instance records
|
||||
return append(servRec, instRecs...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// serviceRecords is called when the query matches the instance name.
|
||||
func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR {
|
||||
switch q.Qtype {
|
||||
case dns.TypeANY:
|
||||
// Get the SRV, which includes A and AAAA
|
||||
recs := m.instanceRecords(dns.Question{
|
||||
Name: m.instanceAddr,
|
||||
Qtype: dns.TypeSRV,
|
||||
})
|
||||
|
||||
// Add the TXT record
|
||||
recs = append(recs, m.instanceRecords(dns.Question{
|
||||
Name: m.instanceAddr,
|
||||
Qtype: dns.TypeTXT,
|
||||
})...)
|
||||
return recs
|
||||
|
||||
case dns.TypeA:
|
||||
var rr []dns.RR
|
||||
for _, ip := range m.IPs {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
rr = append(rr, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: m.HostName,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
A: ip4,
|
||||
})
|
||||
}
|
||||
}
|
||||
return rr
|
||||
|
||||
case dns.TypeAAAA:
|
||||
var rr []dns.RR
|
||||
for _, ip := range m.IPs {
|
||||
if ip.To4() != nil {
|
||||
// TODO(reddaly): IPv4 addresses could be encoded in IPv6 format and
|
||||
// putinto AAAA records, but the current logic puts ipv4-encodable
|
||||
// addresses into the A records exclusively. Perhaps this should be
|
||||
// configurable?
|
||||
continue
|
||||
}
|
||||
|
||||
if ip16 := ip.To16(); ip16 != nil {
|
||||
rr = append(rr, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: m.HostName,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
AAAA: ip16,
|
||||
})
|
||||
}
|
||||
}
|
||||
return rr
|
||||
|
||||
case dns.TypeSRV:
|
||||
// Create the SRV Record
|
||||
srv := &dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
Priority: 10,
|
||||
Weight: 1,
|
||||
Port: uint16(m.Port),
|
||||
Target: m.HostName,
|
||||
}
|
||||
recs := []dns.RR{srv}
|
||||
|
||||
// Add the A record
|
||||
recs = append(recs, m.instanceRecords(dns.Question{
|
||||
Name: m.instanceAddr,
|
||||
Qtype: dns.TypeA,
|
||||
})...)
|
||||
|
||||
// Add the AAAA record
|
||||
recs = append(recs, m.instanceRecords(dns.Question{
|
||||
Name: m.instanceAddr,
|
||||
Qtype: dns.TypeAAAA,
|
||||
})...)
|
||||
return recs
|
||||
|
||||
case dns.TypeTXT:
|
||||
txt := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
Txt: m.TXT,
|
||||
}
|
||||
return []dns.RR{txt}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func makeService(t *testing.T) *MDNSService {
|
||||
return makeServiceWithServiceName(t, "_http._tcp")
|
||||
}
|
||||
|
||||
func makeServiceWithServiceName(t *testing.T, service string) *MDNSService {
|
||||
m, err := NewMDNSService(
|
||||
"hostname",
|
||||
service,
|
||||
"local.",
|
||||
"testhost.",
|
||||
80, // port
|
||||
[]net.IP{net.IP([]byte{192, 168, 0, 42}), net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc")},
|
||||
[]string{"Local web server"}) // TXT
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func TestNewMDNSService_BadParams(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
testName string
|
||||
hostName string
|
||||
domain string
|
||||
}{
|
||||
{
|
||||
"NewMDNSService should fail when passed hostName that is not a legal fully-qualified domain name",
|
||||
"hostname", // not legal FQDN - should be "hostname." or "hostname.local.", etc.
|
||||
"local.", // legal
|
||||
},
|
||||
{
|
||||
"NewMDNSService should fail when passed domain that is not a legal fully-qualified domain name",
|
||||
"hostname.", // legal
|
||||
"local", // should be "local."
|
||||
},
|
||||
} {
|
||||
_, err := NewMDNSService(
|
||||
"instance name",
|
||||
"_http._tcp",
|
||||
test.domain,
|
||||
test.hostName,
|
||||
80, // port
|
||||
[]net.IP{net.IP([]byte{192, 168, 0, 42})},
|
||||
[]string{"Local web server"}) // TXT
|
||||
if err == nil {
|
||||
t.Fatalf("%s: error expected, but got none", test.testName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_BadAddr(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "random",
|
||||
Qtype: dns.TypeANY,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 0 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_ServiceAddr(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "_http._tcp.local.",
|
||||
Qtype: dns.TypeANY,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if got, want := len(recs), 5; got != want {
|
||||
t.Fatalf("got %d records, want %d: %v", got, want, recs)
|
||||
}
|
||||
|
||||
if ptr, ok := recs[0].(*dns.PTR); !ok {
|
||||
t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs)
|
||||
} else if got, want := ptr.Ptr, "hostname._http._tcp.local."; got != want {
|
||||
t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want)
|
||||
}
|
||||
|
||||
if _, ok := recs[1].(*dns.SRV); !ok {
|
||||
t.Errorf("recs[1] should be SRV record, got: %v, all reccords: %v", recs[1], recs)
|
||||
}
|
||||
if _, ok := recs[2].(*dns.A); !ok {
|
||||
t.Errorf("recs[2] should be A record, got: %v, all records: %v", recs[2], recs)
|
||||
}
|
||||
if _, ok := recs[3].(*dns.AAAA); !ok {
|
||||
t.Errorf("recs[3] should be AAAA record, got: %v, all records: %v", recs[3], recs)
|
||||
}
|
||||
if _, ok := recs[4].(*dns.TXT); !ok {
|
||||
t.Errorf("recs[4] should be TXT record, got: %v, all records: %v", recs[4], recs)
|
||||
}
|
||||
|
||||
q.Qtype = dns.TypePTR
|
||||
if recs2 := s.Records(q); !reflect.DeepEqual(recs, recs2) {
|
||||
t.Fatalf("PTR question should return same result as ANY question: ANY => %v, PTR => %v", recs, recs2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_InstanceAddr_ANY(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "hostname._http._tcp.local.",
|
||||
Qtype: dns.TypeANY,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 4 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
if _, ok := recs[0].(*dns.SRV); !ok {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
if _, ok := recs[1].(*dns.A); !ok {
|
||||
t.Fatalf("bad: %v", recs[1])
|
||||
}
|
||||
if _, ok := recs[2].(*dns.AAAA); !ok {
|
||||
t.Fatalf("bad: %v", recs[2])
|
||||
}
|
||||
if _, ok := recs[3].(*dns.TXT); !ok {
|
||||
t.Fatalf("bad: %v", recs[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_InstanceAddr_SRV(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "hostname._http._tcp.local.",
|
||||
Qtype: dns.TypeSRV,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 3 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
srv, ok := recs[0].(*dns.SRV)
|
||||
if !ok {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
if _, ok := recs[1].(*dns.A); !ok {
|
||||
t.Fatalf("bad: %v", recs[1])
|
||||
}
|
||||
if _, ok := recs[2].(*dns.AAAA); !ok {
|
||||
t.Fatalf("bad: %v", recs[2])
|
||||
}
|
||||
|
||||
if srv.Port != uint16(s.Port) {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_InstanceAddr_A(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "hostname._http._tcp.local.",
|
||||
Qtype: dns.TypeA,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
a, ok := recs[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
if !bytes.Equal(a.A, []byte{192, 168, 0, 42}) {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_InstanceAddr_AAAA(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "hostname._http._tcp.local.",
|
||||
Qtype: dns.TypeAAAA,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
a4, ok := recs[0].(*dns.AAAA)
|
||||
if !ok {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
ip6 := net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc")
|
||||
if got := len(ip6); got != net.IPv6len {
|
||||
t.Fatalf("test IP failed to parse (len = %d, want %d)", got, net.IPv6len)
|
||||
}
|
||||
if !a4.AAAA.Equal(ip6) {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_InstanceAddr_TXT(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "hostname._http._tcp.local.",
|
||||
Qtype: dns.TypeTXT,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
txt, ok := recs[0].(*dns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("bad: %v", recs[0])
|
||||
}
|
||||
if got, want := txt.Txt, s.TXT; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("TXT record mismatch for %v: got %v, want %v", recs[0], got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_HostNameQuery(t *testing.T) {
|
||||
s := makeService(t)
|
||||
for _, test := range []struct {
|
||||
q dns.Question
|
||||
want []dns.RR
|
||||
}{
|
||||
{
|
||||
dns.Question{Name: "testhost.", Qtype: dns.TypeA},
|
||||
[]dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testhost.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 120,
|
||||
},
|
||||
A: net.IP([]byte{192, 168, 0, 42}),
|
||||
}},
|
||||
},
|
||||
{
|
||||
dns.Question{Name: "testhost.", Qtype: dns.TypeAAAA},
|
||||
[]dns.RR{&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testhost.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 120,
|
||||
},
|
||||
AAAA: net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc"),
|
||||
}},
|
||||
},
|
||||
} {
|
||||
if got := s.Records(test.q); !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("hostname query failed: s.Records(%v) = %v, want %v", test.q, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDNSService_serviceEnum_PTR(t *testing.T) {
|
||||
s := makeService(t)
|
||||
q := dns.Question{
|
||||
Name: "_services._dns-sd._udp.local.",
|
||||
Qtype: dns.TypePTR,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("bad: %v", recs)
|
||||
}
|
||||
if ptr, ok := recs[0].(*dns.PTR); !ok {
|
||||
t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs)
|
||||
} else if got, want := ptr.Ptr, "_http._tcp.local."; got != want {
|
||||
t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HostPort format addr and port suitable for dial.
|
||||
func HostPort(addr string, port interface{}) string {
|
||||
host := addr
|
||||
if strings.Count(addr, ":") > 0 {
|
||||
host = fmt.Sprintf("[%s]", addr)
|
||||
}
|
||||
// when port is blank or 0, host is a queue name
|
||||
if v, ok := port.(string); ok && v == "" {
|
||||
return host
|
||||
} else if v, ok := port.(int); ok && v == 0 && net.ParseIP(host) == nil {
|
||||
return host
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%v", host, port)
|
||||
}
|
||||
|
||||
// Listen takes addr:portmin-portmax and binds to the first available port
|
||||
// Example: Listen("localhost:5000-6000", fn).
|
||||
func Listen(addr string, fn func(string) (net.Listener, error)) (net.Listener, error) {
|
||||
if strings.Count(addr, ":") == 1 && strings.Count(addr, "-") == 0 {
|
||||
return fn(addr)
|
||||
}
|
||||
|
||||
// host:port || host:min-max
|
||||
host, ports, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// try to extract port range
|
||||
prange := strings.Split(ports, "-")
|
||||
|
||||
// single port
|
||||
if len(prange) < 2 {
|
||||
return fn(addr)
|
||||
}
|
||||
|
||||
// we have a port range
|
||||
|
||||
// extract min port
|
||||
min, err := strconv.Atoi(prange[0])
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to extract port range")
|
||||
}
|
||||
|
||||
// extract max port
|
||||
max, err := strconv.Atoi(prange[1])
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to extract port range")
|
||||
}
|
||||
|
||||
// range the ports
|
||||
for port := min; port <= max; port++ {
|
||||
// try bind to host:port
|
||||
ln, err := fn(HostPort(host, port))
|
||||
if err == nil {
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
// hit max port
|
||||
if port == max {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// why are we here?
|
||||
return nil, fmt.Errorf("unable to bind to %s", addr)
|
||||
}
|
||||
|
||||
// Proxy returns the proxy and the address if it exits.
|
||||
func Proxy(service string, address []string) (string, []string, bool) {
|
||||
var hasProxy bool
|
||||
|
||||
// get proxy. we parse out address if present
|
||||
if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 {
|
||||
// default name
|
||||
if prx == "service" {
|
||||
prx = "go.micro.proxy"
|
||||
address = nil
|
||||
}
|
||||
|
||||
// check if its an address
|
||||
if v := strings.Split(prx, ":"); len(v) > 1 {
|
||||
address = []string{prx}
|
||||
}
|
||||
|
||||
service = prx
|
||||
hasProxy = true
|
||||
|
||||
return service, address, hasProxy
|
||||
}
|
||||
|
||||
if prx := os.Getenv("MICRO_NETWORK"); len(prx) > 0 {
|
||||
// default name
|
||||
if prx == "service" {
|
||||
prx = "go.micro.network"
|
||||
}
|
||||
service = prx
|
||||
hasProxy = true
|
||||
}
|
||||
|
||||
if prx := os.Getenv("MICRO_NETWORK_ADDRESS"); len(prx) > 0 {
|
||||
address = []string{prx}
|
||||
hasProxy = true
|
||||
}
|
||||
|
||||
return service, address, hasProxy
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListen(t *testing.T) {
|
||||
fn := func(addr string) (net.Listener, error) {
|
||||
return net.Listen("tcp", addr)
|
||||
}
|
||||
|
||||
// try to create a number of listeners
|
||||
for i := 0; i < 10; i++ {
|
||||
l, err := Listen("localhost:10000-11000", fn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
}
|
||||
|
||||
// TODO nats case test
|
||||
// natsAddr := "_INBOX.bID2CMRvlNp0vt4tgNBHWf"
|
||||
// Expect addr DO NOT has extra ":" at the end!
|
||||
}
|
||||
|
||||
// TestProxyEnv checks whether we have proxy/network settings in env.
|
||||
func TestProxyEnv(t *testing.T) {
|
||||
service := "foo"
|
||||
address := []string{"bar"}
|
||||
|
||||
s, a, ok := Proxy(service, address)
|
||||
if ok {
|
||||
t.Fatal("Should not have proxy", s, a, ok)
|
||||
}
|
||||
|
||||
test := func(key, val, expectSrv, expectAddr string) {
|
||||
// set env
|
||||
os.Setenv(key, val)
|
||||
|
||||
s, a, ok := Proxy(service, address)
|
||||
if !ok {
|
||||
t.Fatal("Expected proxy")
|
||||
}
|
||||
if len(expectSrv) > 0 && s != expectSrv {
|
||||
t.Fatal("Expected proxy service", expectSrv, "got", s)
|
||||
}
|
||||
if len(expectAddr) > 0 {
|
||||
if len(a) == 0 || a[0] != expectAddr {
|
||||
t.Fatal("Expected proxy address", expectAddr, "got", a)
|
||||
}
|
||||
}
|
||||
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
|
||||
test("MICRO_PROXY", "service", "go.micro.proxy", "")
|
||||
test("MICRO_NETWORK", "service", "go.micro.network", "")
|
||||
test("MICRO_NETWORK_ADDRESS", "10.0.0.1:8081", "", "10.0.0.1:8081")
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
type pool struct {
|
||||
tr transport.Transport
|
||||
|
||||
closeTimeout time.Duration
|
||||
conns map[string][]*poolConn
|
||||
mu sync.Mutex
|
||||
size int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
type poolConn struct {
|
||||
transport.Client
|
||||
|
||||
closeTimeout time.Duration
|
||||
created time.Time
|
||||
id string
|
||||
}
|
||||
|
||||
func newPool(options Options) *pool {
|
||||
return &pool{
|
||||
size: options.Size,
|
||||
tr: options.Transport,
|
||||
ttl: options.TTL,
|
||||
closeTimeout: options.CloseTimeout,
|
||||
conns: make(map[string][]*poolConn),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pool) Close() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
for k, c := range p.conns {
|
||||
for _, conn := range c {
|
||||
if nerr := conn.close(); nerr != nil {
|
||||
err = nerr
|
||||
}
|
||||
}
|
||||
|
||||
delete(p.conns, k)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NoOp the Close since we manage it.
|
||||
func (p *poolConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *poolConn) Id() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
func (p *poolConn) Created() time.Time {
|
||||
return p.created
|
||||
}
|
||||
|
||||
func (p *pool) Get(addr string, opts ...transport.DialOption) (Conn, error) {
|
||||
p.mu.Lock()
|
||||
conns := p.conns[addr]
|
||||
|
||||
// While we have conns check age and then return one
|
||||
// otherwise we'll create a new conn
|
||||
for len(conns) > 0 {
|
||||
conn := conns[len(conns)-1]
|
||||
conns = conns[:len(conns)-1]
|
||||
p.conns[addr] = conns
|
||||
|
||||
// If conn is old kill it and move on
|
||||
if d := time.Since(conn.Created()); d > p.ttl {
|
||||
if err := conn.close(); err != nil {
|
||||
p.mu.Unlock()
|
||||
c, errConn := p.newConn(addr, opts)
|
||||
if errConn != nil {
|
||||
return nil, errConn
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// We got a good conn, lets unlock and return it
|
||||
p.mu.Unlock()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
p.mu.Unlock()
|
||||
|
||||
return p.newConn(addr, opts)
|
||||
}
|
||||
|
||||
func (p *pool) newConn(addr string, opts []transport.DialOption) (Conn, error) {
|
||||
// create new conn
|
||||
c, err := p.tr.Dial(addr, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &poolConn{
|
||||
Client: c,
|
||||
id: uuid.New().String(),
|
||||
closeTimeout: p.closeTimeout,
|
||||
created: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *pool) Release(conn Conn, err error) error {
|
||||
// don't store the conn if it has errored
|
||||
if err != nil {
|
||||
return conn.(*poolConn).close()
|
||||
}
|
||||
|
||||
// otherwise put it back for reuse
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
conns := p.conns[conn.Remote()]
|
||||
if len(conns) >= p.size {
|
||||
return conn.(*poolConn).close()
|
||||
}
|
||||
|
||||
p.conns[conn.Remote()] = append(conns, conn.(*poolConn))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *poolConn) close() error {
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
ch <- p.Client.Close()
|
||||
}()
|
||||
t := time.NewTimer(p.closeTimeout)
|
||||
var err error
|
||||
select {
|
||||
case <-t.C:
|
||||
err = errors.New("unable to close in time")
|
||||
case err = <-ch:
|
||||
t.Stop()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
func testPool(t *testing.T, size int, ttl time.Duration) {
|
||||
// mock transport
|
||||
tr := transport.NewMemoryTransport()
|
||||
|
||||
options := Options{
|
||||
TTL: ttl,
|
||||
Size: size,
|
||||
Transport: tr,
|
||||
}
|
||||
// zero pool
|
||||
p := newPool(options)
|
||||
|
||||
// listen
|
||||
l, err := tr.Listen(":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
// accept loop
|
||||
go func() {
|
||||
for {
|
||||
if err := l.Accept(func(s transport.Socket) {
|
||||
for {
|
||||
var msg transport.Message
|
||||
if err := s.Recv(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
if err := s.Send(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get a conn
|
||||
c, err := p.Get(l.Addr())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg := &transport.Message{
|
||||
Body: []byte(`hello world`),
|
||||
}
|
||||
|
||||
if err := c.Send(msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rcv transport.Message
|
||||
|
||||
if err := c.Recv(&rcv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(rcv.Body) != string(msg.Body) {
|
||||
t.Fatalf("got %v, expected %v", rcv.Body, msg.Body)
|
||||
}
|
||||
|
||||
// release the conn
|
||||
p.Release(c, nil)
|
||||
|
||||
p.mu.Lock()
|
||||
if i := len(p.conns[l.Addr()]); i > size {
|
||||
p.mu.Unlock()
|
||||
t.Fatalf("pool size %d is greater than expected %d", i, size)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPool(t *testing.T) {
|
||||
testPool(t, 0, time.Minute)
|
||||
testPool(t, 2, time.Minute)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Transport transport.Transport
|
||||
TTL time.Duration
|
||||
CloseTimeout time.Duration
|
||||
Size int
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func Size(i int) Option {
|
||||
return func(o *Options) {
|
||||
o.Size = i
|
||||
}
|
||||
}
|
||||
|
||||
func Transport(t transport.Transport) Option {
|
||||
return func(o *Options) {
|
||||
o.Transport = t
|
||||
}
|
||||
}
|
||||
|
||||
func TTL(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.TTL = t
|
||||
}
|
||||
}
|
||||
|
||||
func CloseTimeout(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CloseTimeout = t
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Package pool is a connection pool
|
||||
package pool
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
// Pool is an interface for connection pooling.
|
||||
type Pool interface {
|
||||
// Close the pool
|
||||
Close() error
|
||||
// Get a connection
|
||||
Get(addr string, opts ...transport.DialOption) (Conn, error)
|
||||
// Release the connection
|
||||
Release(c Conn, status error) error
|
||||
}
|
||||
|
||||
// Conn interface represents a pool connection.
|
||||
type Conn interface {
|
||||
// unique id of connection
|
||||
Id() string
|
||||
// time it was created
|
||||
Created() time.Time
|
||||
// embedded connection
|
||||
transport.Client
|
||||
}
|
||||
|
||||
// NewPool will return a new pool object.
|
||||
func NewPool(opts ...Option) Pool {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return newPool(options)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func addNodes(old, neu []*registry.Node) []*registry.Node {
|
||||
nodes := make([]*registry.Node, len(neu))
|
||||
// add all new nodes
|
||||
for i, n := range neu {
|
||||
node := *n
|
||||
nodes[i] = &node
|
||||
}
|
||||
|
||||
// look at old nodes
|
||||
for _, o := range old {
|
||||
var exists bool
|
||||
|
||||
// check against new nodes
|
||||
for _, n := range nodes {
|
||||
// ids match then skip
|
||||
if o.Id == n.Id {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// keep old node
|
||||
if !exists {
|
||||
node := *o
|
||||
nodes = append(nodes, &node)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func delNodes(old, del []*registry.Node) []*registry.Node {
|
||||
var nodes []*registry.Node
|
||||
for _, o := range old {
|
||||
var rem bool
|
||||
for _, n := range del {
|
||||
if o.Id == n.Id {
|
||||
rem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !rem {
|
||||
nodes = append(nodes, o)
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// CopyService make a copy of service.
|
||||
func CopyService(service *registry.Service) *registry.Service {
|
||||
// copy service
|
||||
s := new(registry.Service)
|
||||
*s = *service
|
||||
|
||||
// copy nodes
|
||||
nodes := make([]*registry.Node, len(service.Nodes))
|
||||
for j, node := range service.Nodes {
|
||||
n := new(registry.Node)
|
||||
*n = *node
|
||||
nodes[j] = n
|
||||
}
|
||||
s.Nodes = nodes
|
||||
|
||||
// copy endpoints
|
||||
eps := make([]*registry.Endpoint, len(service.Endpoints))
|
||||
for j, ep := range service.Endpoints {
|
||||
e := new(registry.Endpoint)
|
||||
*e = *ep
|
||||
eps[j] = e
|
||||
}
|
||||
s.Endpoints = eps
|
||||
return s
|
||||
}
|
||||
|
||||
// Copy makes a copy of services.
|
||||
func Copy(current []*registry.Service) []*registry.Service {
|
||||
services := make([]*registry.Service, len(current))
|
||||
for i, service := range current {
|
||||
services[i] = CopyService(service)
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
// Merge merges two lists of services and returns a new copy.
|
||||
func Merge(olist []*registry.Service, nlist []*registry.Service) []*registry.Service {
|
||||
var srv []*registry.Service
|
||||
|
||||
for _, n := range nlist {
|
||||
var seen bool
|
||||
for _, o := range olist {
|
||||
if o.Version == n.Version {
|
||||
sp := new(registry.Service)
|
||||
// make copy
|
||||
*sp = *o
|
||||
// set nodes
|
||||
sp.Nodes = addNodes(o.Nodes, n.Nodes)
|
||||
|
||||
// mark as seen
|
||||
seen = true
|
||||
srv = append(srv, sp)
|
||||
break
|
||||
} else {
|
||||
sp := new(registry.Service)
|
||||
// make copy
|
||||
*sp = *o
|
||||
srv = append(srv, sp)
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
srv = append(srv, Copy([]*registry.Service{n})...)
|
||||
}
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
// Remove removes services and returns a new copy.
|
||||
func Remove(old, del []*registry.Service) []*registry.Service {
|
||||
var services []*registry.Service
|
||||
|
||||
for _, o := range old {
|
||||
srv := new(registry.Service)
|
||||
*srv = *o
|
||||
|
||||
var rem bool
|
||||
|
||||
for _, s := range del {
|
||||
if srv.Version == s.Version {
|
||||
srv.Nodes = delNodes(srv.Nodes, s.Nodes)
|
||||
|
||||
if len(srv.Nodes) == 0 {
|
||||
rem = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !rem {
|
||||
services = append(services, srv)
|
||||
}
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
services := []*registry.Service{
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-123",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-123",
|
||||
Address: "localhost:6666",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
servs := Remove([]*registry.Service{services[0]}, []*registry.Service{services[1]})
|
||||
if i := len(servs); i > 0 {
|
||||
t.Errorf("Expected 0 nodes, got %d: %+v", i, servs)
|
||||
}
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Logf("Services %+v", servs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveNodes(t *testing.T) {
|
||||
services := []*registry.Service{
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-123",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
{
|
||||
Id: "foo-321",
|
||||
Address: "localhost:6666",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-123",
|
||||
Address: "localhost:6666",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
nodes := delNodes(services[0].Nodes, services[1].Nodes)
|
||||
if i := len(nodes); i != 1 {
|
||||
t.Errorf("Expected only 1 node, got %d: %+v", i, nodes)
|
||||
}
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Logf("Nodes %+v", nodes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Package ring provides a simple ring buffer for storing local data
|
||||
package ring
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Buffer is ring buffer.
|
||||
type Buffer struct {
|
||||
streams map[string]*Stream
|
||||
vals []*Entry
|
||||
size int
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Entry is ring buffer data entry.
|
||||
type Entry struct {
|
||||
Value interface{}
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Stream is used to stream the buffer.
|
||||
type Stream struct {
|
||||
// Buffered entries
|
||||
Entries chan *Entry
|
||||
// Stop channel
|
||||
Stop chan bool
|
||||
// Id of the stream
|
||||
Id string
|
||||
}
|
||||
|
||||
// Put adds a new value to ring buffer.
|
||||
func (b *Buffer) Put(v interface{}) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
// append to values
|
||||
entry := &Entry{
|
||||
Value: v,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
b.vals = append(b.vals, entry)
|
||||
|
||||
// trim if bigger than size required
|
||||
if len(b.vals) > b.size {
|
||||
b.vals = b.vals[1:]
|
||||
}
|
||||
|
||||
// send to every stream
|
||||
for _, stream := range b.streams {
|
||||
select {
|
||||
case <-stream.Stop:
|
||||
delete(b.streams, stream.Id)
|
||||
close(stream.Entries)
|
||||
case stream.Entries <- entry:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the last n entries.
|
||||
func (b *Buffer) Get(n int) []*Entry {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
|
||||
// reset any invalid values
|
||||
if n > len(b.vals) || n < 0 {
|
||||
n = len(b.vals)
|
||||
}
|
||||
|
||||
// create a delta
|
||||
delta := len(b.vals) - n
|
||||
|
||||
// return the delta set
|
||||
return b.vals[delta:]
|
||||
}
|
||||
|
||||
// Return the entries since a specific time.
|
||||
func (b *Buffer) Since(t time.Time) []*Entry {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
|
||||
// return all the values
|
||||
if t.IsZero() {
|
||||
return b.vals
|
||||
}
|
||||
|
||||
// if its in the future return nothing
|
||||
if time.Since(t).Seconds() < 0.0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, v := range b.vals {
|
||||
// find the starting point
|
||||
d := v.Timestamp.Sub(t)
|
||||
|
||||
// return the values
|
||||
if d.Seconds() > 0.0 {
|
||||
return b.vals[i:]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream logs from the buffer
|
||||
// Close the channel when you want to stop.
|
||||
func (b *Buffer) Stream() (<-chan *Entry, chan bool) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
entries := make(chan *Entry, 128)
|
||||
id := uuid.New().String()
|
||||
stop := make(chan bool)
|
||||
|
||||
b.streams[id] = &Stream{
|
||||
Id: id,
|
||||
Entries: entries,
|
||||
Stop: stop,
|
||||
}
|
||||
|
||||
return entries, stop
|
||||
}
|
||||
|
||||
// Size returns the size of the ring buffer.
|
||||
func (b *Buffer) Size() int {
|
||||
return b.size
|
||||
}
|
||||
|
||||
// New returns a new buffer of the given size.
|
||||
func New(i int) *Buffer {
|
||||
return &Buffer{
|
||||
size: i,
|
||||
streams: make(map[string]*Stream),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuffer(t *testing.T) {
|
||||
b := New(10)
|
||||
|
||||
// test one value
|
||||
b.Put("foo")
|
||||
v := b.Get(1)
|
||||
|
||||
if val := v[0].Value.(string); val != "foo" {
|
||||
t.Fatalf("expected foo got %v", val)
|
||||
}
|
||||
|
||||
b = New(10)
|
||||
|
||||
// test 10 values
|
||||
for i := 0; i < 10; i++ {
|
||||
b.Put(i)
|
||||
}
|
||||
|
||||
d := time.Now()
|
||||
v = b.Get(10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
val := v[i].Value.(int)
|
||||
|
||||
if val != i {
|
||||
t.Fatalf("expected %d got %d", i, val)
|
||||
}
|
||||
}
|
||||
|
||||
// test more values
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
v := i * 2
|
||||
b.Put(v)
|
||||
}
|
||||
|
||||
v = b.Get(10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
val := v[i].Value.(int)
|
||||
expect := i * 2
|
||||
if val != expect {
|
||||
t.Fatalf("expected %d got %d", expect, val)
|
||||
}
|
||||
}
|
||||
|
||||
// sleep 100 ms
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// assume we'll get everything
|
||||
v = b.Since(d)
|
||||
|
||||
if len(v) != 10 {
|
||||
t.Fatalf("expected 10 entries but got %d", len(v))
|
||||
}
|
||||
|
||||
// write 1 more entry
|
||||
d = time.Now()
|
||||
b.Put(100)
|
||||
|
||||
// sleep 100 ms
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
v = b.Since(d)
|
||||
if len(v) != 1 {
|
||||
t.Fatalf("expected 1 entries but got %d", len(v))
|
||||
}
|
||||
|
||||
if v[0].Value.(int) != 100 {
|
||||
t.Fatalf("expected value 100 got %v", v[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ShutDownSingals returns all the signals that are being watched for to shut down services.
|
||||
func Shutdown() []os.Signal {
|
||||
return []os.Signal{
|
||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
pool map[string]*Socket
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *Pool) Get(id string) (*Socket, bool) {
|
||||
// attempt to get existing socket
|
||||
p.RLock()
|
||||
socket, ok := p.pool[id]
|
||||
if ok {
|
||||
p.RUnlock()
|
||||
return socket, ok
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
// save socket
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
// double checked locking
|
||||
socket, ok = p.pool[id]
|
||||
if ok {
|
||||
return socket, ok
|
||||
}
|
||||
// create new socket
|
||||
socket = New(id)
|
||||
p.pool[id] = socket
|
||||
|
||||
// return socket
|
||||
return socket, false
|
||||
}
|
||||
|
||||
func (p *Pool) Release(s *Socket) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// close the socket
|
||||
s.Close()
|
||||
delete(p.pool, s.id)
|
||||
}
|
||||
|
||||
// Close the pool and delete all the sockets.
|
||||
func (p *Pool) Close() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
for id, sock := range p.pool {
|
||||
sock.Close()
|
||||
delete(p.pool, id)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPool returns a new socket pool.
|
||||
func NewPool() *Pool {
|
||||
return &Pool{
|
||||
pool: make(map[string]*Socket),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Package socket provides a pseudo socket
|
||||
package socket
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
// Socket is our pseudo socket for transport.Socket.
|
||||
type Socket struct {
|
||||
// closed
|
||||
closed chan bool
|
||||
// send chan
|
||||
send chan *transport.Message
|
||||
// recv chan
|
||||
recv chan *transport.Message
|
||||
id string
|
||||
// remote addr
|
||||
remote string
|
||||
// local addr
|
||||
local string
|
||||
}
|
||||
|
||||
func (s *Socket) SetLocal(l string) {
|
||||
s.local = l
|
||||
}
|
||||
|
||||
func (s *Socket) SetRemote(r string) {
|
||||
s.remote = r
|
||||
}
|
||||
|
||||
// Accept passes a message to the socket which will be processed by the call to Recv.
|
||||
func (s *Socket) Accept(m *transport.Message) error {
|
||||
select {
|
||||
case s.recv <- m:
|
||||
return nil
|
||||
case <-s.closed:
|
||||
return io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
// Process takes the next message off the send queue created by a call to Send.
|
||||
func (s *Socket) Process(m *transport.Message) error {
|
||||
select {
|
||||
case msg := <-s.send:
|
||||
*m = *msg
|
||||
case <-s.closed:
|
||||
// see if we need to drain
|
||||
select {
|
||||
case msg := <-s.send:
|
||||
*m = *msg
|
||||
return nil
|
||||
default:
|
||||
return io.EOF
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Socket) Remote() string {
|
||||
return s.remote
|
||||
}
|
||||
|
||||
func (s *Socket) Local() string {
|
||||
return s.local
|
||||
}
|
||||
|
||||
func (s *Socket) Send(m *transport.Message) error {
|
||||
// send a message
|
||||
select {
|
||||
case s.send <- m:
|
||||
case <-s.closed:
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Socket) Recv(m *transport.Message) error {
|
||||
// receive a message
|
||||
select {
|
||||
case msg := <-s.recv:
|
||||
// set message
|
||||
*m = *msg
|
||||
case <-s.closed:
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the socket.
|
||||
func (s *Socket) Close() error {
|
||||
select {
|
||||
case <-s.closed:
|
||||
// no op
|
||||
default:
|
||||
close(s.closed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New returns a new pseudo socket which can be used in the place of a transport socket.
|
||||
// Messages are sent to the socket via Accept and receives from the socket via Process.
|
||||
// SetLocal/SetRemote should be called before using the socket.
|
||||
func New(id string) *Socket {
|
||||
return &Socket{
|
||||
id: id,
|
||||
closed: make(chan bool),
|
||||
local: "local",
|
||||
remote: "remote",
|
||||
send: make(chan *transport.Message, 128),
|
||||
recv: make(chan *transport.Message, 128),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
// Data is a set of mock registry data.
|
||||
Data = map[string][]*registry.Service{
|
||||
"foo": {
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.0-123",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
{
|
||||
Id: "foo-1.0.0-321",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.1",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.1-321",
|
||||
Address: "localhost:6666",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.3",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.3-345",
|
||||
Address: "localhost:8888",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// EmptyChannel will empty out a error channel by checking if an error is
|
||||
// present, and if so return the error.
|
||||
func EmptyChannel(c chan error) error {
|
||||
select {
|
||||
case err := <-c:
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Package tls provides TLS utilities for go-micro.
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config returns the default TLS config.
|
||||
//
|
||||
// As of v6, certificate verification is ON by default (secure by default).
|
||||
// This is the safe choice now that an agent — not just a human on a
|
||||
// trusted network — can reach an endpoint.
|
||||
//
|
||||
// For development against self-signed certificates, set
|
||||
// MICRO_TLS_INSECURE=true to skip verification, or call InsecureConfig()
|
||||
// directly. (In v5 the default was the reverse: insecure unless
|
||||
// MICRO_TLS_SECURE=true was set. That env var is no longer needed.)
|
||||
func Config() *tls.Config {
|
||||
// Opt out of verification for local/dev against self-signed certs.
|
||||
if os.Getenv("MICRO_TLS_INSECURE") == "true" {
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
// Secure by default.
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
// SecureConfig returns a TLS config with certificate verification enabled.
|
||||
// Use this when you have proper CA-signed certificates.
|
||||
func SecureConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
// InsecureConfig returns a TLS config with certificate verification disabled.
|
||||
// WARNING: Only use for development/testing.
|
||||
func InsecureConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
// Certificate generates a self-signed certificate for the given hosts.
|
||||
// Note: These certs are for development only. For production, use proper
|
||||
// CA-signed certificates or a service mesh.
|
||||
func Certificate(host ...string) (tls.Certificate, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Hour * 24 * 365)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Micro"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
for _, h := range host {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, h)
|
||||
}
|
||||
}
|
||||
|
||||
template.IsCA = true
|
||||
template.KeyUsage |= x509.KeyUsageCertSign
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
// create public key
|
||||
certOut := bytes.NewBuffer(nil)
|
||||
_ = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
|
||||
// create private key
|
||||
keyOut := bytes.NewBuffer(nil)
|
||||
b, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
_ = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
|
||||
|
||||
return tls.X509KeyPair(certOut.Bytes(), keyOut.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envVar string
|
||||
envValue string
|
||||
wantInsecure bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "secure_by_default",
|
||||
envVar: "",
|
||||
envValue: "",
|
||||
wantInsecure: false,
|
||||
description: "v6: certificate verification is on by default",
|
||||
},
|
||||
{
|
||||
name: "insecure_opt_out",
|
||||
envVar: "MICRO_TLS_INSECURE",
|
||||
envValue: "true",
|
||||
wantInsecure: true,
|
||||
description: "MICRO_TLS_INSECURE=true skips verification (dev/self-signed)",
|
||||
},
|
||||
{
|
||||
name: "insecure_disabled_stays_secure",
|
||||
envVar: "MICRO_TLS_INSECURE",
|
||||
envValue: "false",
|
||||
wantInsecure: false,
|
||||
description: "MICRO_TLS_INSECURE=false stays secure",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clean up environment
|
||||
os.Unsetenv("MICRO_TLS_SECURE")
|
||||
os.Unsetenv("MICRO_TLS_INSECURE")
|
||||
// Suppress warning in tests
|
||||
os.Setenv("IN_TRAVIS_CI", "yes")
|
||||
defer os.Unsetenv("IN_TRAVIS_CI")
|
||||
|
||||
// Set environment variable if specified
|
||||
if tt.envVar != "" {
|
||||
os.Setenv(tt.envVar, tt.envValue)
|
||||
defer os.Unsetenv(tt.envVar)
|
||||
}
|
||||
|
||||
config := Config()
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("Config() returned nil")
|
||||
}
|
||||
|
||||
if config.InsecureSkipVerify != tt.wantInsecure {
|
||||
t.Errorf("%s: InsecureSkipVerify = %v, want %v",
|
||||
tt.description, config.InsecureSkipVerify, tt.wantInsecure)
|
||||
}
|
||||
|
||||
// Verify MinVersion is set correctly
|
||||
if config.MinVersion == 0 {
|
||||
t.Error("MinVersion should be set")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureConfig(t *testing.T) {
|
||||
config := SecureConfig()
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("SecureConfig() returned nil")
|
||||
}
|
||||
|
||||
if config.InsecureSkipVerify {
|
||||
t.Error("SecureConfig should have InsecureSkipVerify set to false")
|
||||
}
|
||||
|
||||
if config.MinVersion == 0 {
|
||||
t.Error("MinVersion should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsecureConfig(t *testing.T) {
|
||||
config := InsecureConfig()
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("InsecureConfig() returned nil")
|
||||
}
|
||||
|
||||
if !config.InsecureSkipVerify {
|
||||
t.Error("InsecureConfig should have InsecureSkipVerify set to true")
|
||||
}
|
||||
|
||||
if config.MinVersion == 0 {
|
||||
t.Error("MinVersion should be set")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/debug/stats"
|
||||
"go-micro.dev/v6/debug/trace"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/server"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
)
|
||||
|
||||
type fromServiceWrapper struct {
|
||||
client.Client
|
||||
|
||||
// headers to inject
|
||||
headers metadata.Metadata
|
||||
}
|
||||
|
||||
func (f *fromServiceWrapper) setHeaders(ctx context.Context) context.Context {
|
||||
// don't overwrite keys
|
||||
return metadata.MergeContext(ctx, f.headers, false)
|
||||
}
|
||||
|
||||
func (f *fromServiceWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
ctx = f.setHeaders(ctx)
|
||||
return f.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
|
||||
func (f *fromServiceWrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||
ctx = f.setHeaders(ctx)
|
||||
return f.Client.Stream(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (f *fromServiceWrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
||||
ctx = f.setHeaders(ctx)
|
||||
return f.Client.Publish(ctx, p, opts...)
|
||||
}
|
||||
|
||||
// FromService wraps a client to inject service and auth metadata.
|
||||
func FromService(name string, c client.Client) client.Client {
|
||||
return &fromServiceWrapper{
|
||||
c,
|
||||
metadata.Metadata{
|
||||
headers.Prefix + "From-Service": name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerStats wraps a server handler to generate request/error stats.
|
||||
func HandlerStats(stats stats.Stats) server.HandlerWrapper {
|
||||
// return a handler wrapper
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
// return a function that returns a function
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
// execute the handler
|
||||
err := h(ctx, req, rsp)
|
||||
// record the stats
|
||||
_ = stats.Record(err)
|
||||
// return the error
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type traceWrapper struct {
|
||||
client.Client
|
||||
|
||||
trace trace.Tracer
|
||||
|
||||
name string
|
||||
}
|
||||
|
||||
func (c *traceWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
newCtx, s := c.trace.Start(ctx, req.Service()+"."+req.Endpoint())
|
||||
|
||||
s.Type = trace.SpanTypeRequestOutbound
|
||||
err := c.Client.Call(newCtx, req, rsp, opts...)
|
||||
if err != nil {
|
||||
s.Metadata["error"] = err.Error()
|
||||
}
|
||||
|
||||
// finish the trace
|
||||
_ = c.trace.Finish(s)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TraceCall is a call tracing wrapper.
|
||||
func TraceCall(name string, t trace.Tracer, c client.Client) client.Client {
|
||||
return &traceWrapper{
|
||||
name: name,
|
||||
trace: t,
|
||||
Client: c,
|
||||
}
|
||||
}
|
||||
|
||||
// TraceHandler wraps a server handler to perform tracing.
|
||||
func TraceHandler(t trace.Tracer) server.HandlerWrapper {
|
||||
// return a handler wrapper
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
// return a function that returns a function
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
// don't store traces for debug
|
||||
if strings.HasPrefix(req.Endpoint(), "Debug.") {
|
||||
return h(ctx, req, rsp)
|
||||
}
|
||||
|
||||
// get the span
|
||||
newCtx, s := t.Start(ctx, req.Service()+"."+req.Endpoint())
|
||||
s.Type = trace.SpanTypeRequestInbound
|
||||
|
||||
err := h(newCtx, req, rsp)
|
||||
if err != nil {
|
||||
s.Metadata["error"] = err.Error()
|
||||
}
|
||||
|
||||
// finish
|
||||
_ = t.Finish(s)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AuthCall(a func() auth.Auth, c client.Client) client.Client {
|
||||
return &authWrapper{Client: c, auth: a}
|
||||
}
|
||||
|
||||
type authWrapper struct {
|
||||
client.Client
|
||||
auth func() auth.Auth
|
||||
}
|
||||
|
||||
func (a *authWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
// parse the options
|
||||
var options client.CallOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// check to see if the authorization header has already been set.
|
||||
// We dont't override the header unless the ServiceToken option has
|
||||
// been specified or the header wasn't provided
|
||||
if _, ok := metadata.Get(ctx, "Authorization"); ok && !options.ServiceToken {
|
||||
return a.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// if auth is nil we won't be able to get an access token, so we execute
|
||||
// the request without one.
|
||||
aa := a.auth()
|
||||
if aa == nil {
|
||||
return a.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// set the namespace header if it has not been set (e.g. on a service to service request)
|
||||
if _, ok := metadata.Get(ctx, headers.Namespace); !ok {
|
||||
ctx = metadata.Set(ctx, headers.Namespace, aa.Options().Namespace)
|
||||
}
|
||||
|
||||
// check to see if we have a valid access token
|
||||
aaOpts := aa.Options()
|
||||
if aaOpts.Token != nil && !aaOpts.Token.Expired() {
|
||||
ctx = metadata.Set(ctx, "Authorization", auth.BearerScheme+aaOpts.Token.AccessToken)
|
||||
return a.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// call without an auth token
|
||||
return a.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/metadata"
|
||||
)
|
||||
|
||||
func TestWrapper(t *testing.T) {
|
||||
testData := []struct {
|
||||
existing metadata.Metadata
|
||||
headers metadata.Metadata
|
||||
overwrite bool
|
||||
}{
|
||||
{
|
||||
existing: metadata.Metadata{},
|
||||
headers: metadata.Metadata{
|
||||
"Foo": "bar",
|
||||
},
|
||||
overwrite: true,
|
||||
},
|
||||
{
|
||||
existing: metadata.Metadata{
|
||||
"Foo": "bar",
|
||||
},
|
||||
headers: metadata.Metadata{
|
||||
"Foo": "baz",
|
||||
},
|
||||
overwrite: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
c := &fromServiceWrapper{
|
||||
headers: d.headers,
|
||||
}
|
||||
|
||||
ctx := metadata.NewContext(context.Background(), d.existing)
|
||||
ctx = c.setHeaders(ctx)
|
||||
md, _ := metadata.FromContext(ctx)
|
||||
|
||||
for k, v := range d.headers {
|
||||
if d.overwrite && md[k] != v {
|
||||
t.Fatalf("Expected %s=%s got %s=%s", k, v, k, md[k])
|
||||
}
|
||||
if !d.overwrite && md[k] != d.existing[k] {
|
||||
t.Fatalf("Expected %s=%s got %s=%s", k, d.existing[k], k, md[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user