Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+165
View File
@@ -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
}
+16
View File
@@ -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
}
+21
View File
@@ -0,0 +1,21 @@
package buf
import (
"bytes"
)
type buffer struct {
*bytes.Buffer
}
func (b *buffer) Close() error {
b.Buffer.Reset()
return nil
}
func New(b *bytes.Buffer) *buffer {
if b == nil {
b = bytes.NewBuffer(nil)
}
return &buffer{b}
}
+660
View File
@@ -0,0 +1,660 @@
// Package cmd is an interface for parsing the command line
package cmd
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v4/auth"
"go-micro.dev/v4/broker"
"go-micro.dev/v4/cache"
"go-micro.dev/v4/client"
"go-micro.dev/v4/config"
"go-micro.dev/v4/debug/profile"
"go-micro.dev/v4/debug/profile/http"
"go-micro.dev/v4/debug/profile/pprof"
"go-micro.dev/v4/debug/trace"
"go-micro.dev/v4/logger"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/runtime"
"go-micro.dev/v4/selector"
"go-micro.dev/v4/server"
"go-micro.dev/v4/store"
"go-micro.dev/v4/transport"
)
type Cmd interface {
// The cli app within this cmd
App() *cli.App
// Adds options, parses flags and initialize
// exits on error
Init(opts ...Option) error
// Options set within this command
Options() Options
}
type cmd struct {
opts Options
app *cli.App
}
type Option func(o *Options)
var (
DefaultCmd = newCmd()
DefaultFlags = []cli.Flag{
&cli.StringFlag{
Name: "client",
EnvVars: []string{"MICRO_CLIENT"},
Usage: "Client for go-micro; rpc",
},
&cli.StringFlag{
Name: "client_request_timeout",
EnvVars: []string{"MICRO_CLIENT_REQUEST_TIMEOUT"},
Usage: "Sets the client request timeout. e.g 500ms, 5s, 1m. Default: 5s",
},
&cli.IntFlag{
Name: "client_retries",
EnvVars: []string{"MICRO_CLIENT_RETRIES"},
Value: client.DefaultRetries,
Usage: "Sets the client retries. Default: 1",
},
&cli.IntFlag{
Name: "client_pool_size",
EnvVars: []string{"MICRO_CLIENT_POOL_SIZE"},
Usage: "Sets the client connection pool size. Default: 1",
},
&cli.StringFlag{
Name: "client_pool_ttl",
EnvVars: []string{"MICRO_CLIENT_POOL_TTL"},
Usage: "Sets the client connection pool ttl. e.g 500ms, 5s, 1m. Default: 1m",
},
&cli.IntFlag{
Name: "register_ttl",
EnvVars: []string{"MICRO_REGISTER_TTL"},
Value: 60,
Usage: "Register TTL in seconds",
},
&cli.IntFlag{
Name: "register_interval",
EnvVars: []string{"MICRO_REGISTER_INTERVAL"},
Value: 30,
Usage: "Register interval in seconds",
},
&cli.StringFlag{
Name: "server",
EnvVars: []string{"MICRO_SERVER"},
Usage: "Server for go-micro; rpc",
},
&cli.StringFlag{
Name: "server_name",
EnvVars: []string{"MICRO_SERVER_NAME"},
Usage: "Name of the server. go.micro.srv.example",
},
&cli.StringFlag{
Name: "server_version",
EnvVars: []string{"MICRO_SERVER_VERSION"},
Usage: "Version of the server. 1.1.0",
},
&cli.StringFlag{
Name: "server_id",
EnvVars: []string{"MICRO_SERVER_ID"},
Usage: "Id of the server. Auto-generated if not specified",
},
&cli.StringFlag{
Name: "server_address",
EnvVars: []string{"MICRO_SERVER_ADDRESS"},
Usage: "Bind address for the server. 127.0.0.1:8080",
},
&cli.StringFlag{
Name: "server_advertise",
EnvVars: []string{"MICRO_SERVER_ADVERTISE"},
Usage: "Used instead of the server_address when registering with discovery. 127.0.0.1:8080",
},
&cli.StringSliceFlag{
Name: "server_metadata",
EnvVars: []string{"MICRO_SERVER_METADATA"},
Value: &cli.StringSlice{},
Usage: "A list of key-value pairs defining metadata. version=1.0.0",
},
&cli.StringFlag{
Name: "broker",
EnvVars: []string{"MICRO_BROKER"},
Usage: "Broker for pub/sub. http, nats, rabbitmq",
},
&cli.StringFlag{
Name: "broker_address",
EnvVars: []string{"MICRO_BROKER_ADDRESS"},
Usage: "Comma-separated list of broker addresses",
},
&cli.StringFlag{
Name: "profile",
Usage: "Debug profiler for cpu and memory stats",
EnvVars: []string{"MICRO_DEBUG_PROFILE"},
},
&cli.StringFlag{
Name: "registry",
EnvVars: []string{"MICRO_REGISTRY"},
Usage: "Registry for discovery. etcd, mdns",
},
&cli.StringFlag{
Name: "registry_address",
EnvVars: []string{"MICRO_REGISTRY_ADDRESS"},
Usage: "Comma-separated list of registry addresses",
},
&cli.StringFlag{
Name: "runtime",
Usage: "Runtime for building and running services e.g local, kubernetes",
EnvVars: []string{"MICRO_RUNTIME"},
},
&cli.StringFlag{
Name: "runtime_source",
Usage: "Runtime source for building and running services e.g github.com/micro/service",
EnvVars: []string{"MICRO_RUNTIME_SOURCE"},
Value: "github.com/micro/services",
},
&cli.StringFlag{
Name: "selector",
EnvVars: []string{"MICRO_SELECTOR"},
Usage: "Selector used to pick nodes for querying",
},
&cli.StringFlag{
Name: "store",
EnvVars: []string{"MICRO_STORE"},
Usage: "Store used for key-value storage",
},
&cli.StringFlag{
Name: "store_address",
EnvVars: []string{"MICRO_STORE_ADDRESS"},
Usage: "Comma-separated list of store addresses",
},
&cli.StringFlag{
Name: "store_database",
EnvVars: []string{"MICRO_STORE_DATABASE"},
Usage: "Database option for the underlying store",
},
&cli.StringFlag{
Name: "store_table",
EnvVars: []string{"MICRO_STORE_TABLE"},
Usage: "Table option for the underlying store",
},
&cli.StringFlag{
Name: "transport",
EnvVars: []string{"MICRO_TRANSPORT"},
Usage: "Transport mechanism used; http",
},
&cli.StringFlag{
Name: "transport_address",
EnvVars: []string{"MICRO_TRANSPORT_ADDRESS"},
Usage: "Comma-separated list of transport addresses",
},
&cli.StringFlag{
Name: "tracer",
EnvVars: []string{"MICRO_TRACER"},
Usage: "Tracer for distributed tracing, e.g. memory, jaeger",
},
&cli.StringFlag{
Name: "tracer_address",
EnvVars: []string{"MICRO_TRACER_ADDRESS"},
Usage: "Comma-separated list of tracer addresses",
},
&cli.StringFlag{
Name: "auth",
EnvVars: []string{"MICRO_AUTH"},
Usage: "Auth for role based access control, e.g. service",
},
&cli.StringFlag{
Name: "auth_id",
EnvVars: []string{"MICRO_AUTH_ID"},
Usage: "Account ID used for client authentication",
},
&cli.StringFlag{
Name: "auth_secret",
EnvVars: []string{"MICRO_AUTH_SECRET"},
Usage: "Account secret used for client authentication",
},
&cli.StringFlag{
Name: "auth_namespace",
EnvVars: []string{"MICRO_AUTH_NAMESPACE"},
Usage: "Namespace for the services auth account",
Value: "go.micro",
},
&cli.StringFlag{
Name: "auth_public_key",
EnvVars: []string{"MICRO_AUTH_PUBLIC_KEY"},
Usage: "Public key for JWT auth (base64 encoded PEM)",
},
&cli.StringFlag{
Name: "auth_private_key",
EnvVars: []string{"MICRO_AUTH_PRIVATE_KEY"},
Usage: "Private key for JWT auth (base64 encoded PEM)",
},
&cli.StringFlag{
Name: "config",
EnvVars: []string{"MICRO_CONFIG"},
Usage: "The source of the config to be used to get configuration",
},
}
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{}
DefaultClients = map[string]func(...client.Option) client.Client{}
DefaultRegistries = map[string]func(...registry.Option) registry.Registry{}
DefaultSelectors = map[string]func(...selector.Option) selector.Selector{}
DefaultServers = map[string]func(...server.Option) server.Server{}
DefaultTransports = map[string]func(...transport.Option) transport.Transport{}
DefaultRuntimes = map[string]func(...runtime.Option) runtime.Runtime{}
DefaultStores = map[string]func(...store.Option) store.Store{}
DefaultTracers = map[string]func(...trace.Option) trace.Tracer{}
DefaultAuths = map[string]func(...auth.Option) auth.Auth{}
DefaultProfiles = map[string]func(...profile.Option) profile.Profile{
"http": http.NewProfile,
"pprof": pprof.NewProfile,
}
DefaultConfigs = map[string]func(...config.Option) (config.Config, error){}
DefaultCaches = map[string]func(...cache.Option) cache.Cache{}
)
func init() {
rand.Seed(time.Now().Unix())
}
func newCmd(opts ...Option) Cmd {
options := Options{
Auth: &auth.DefaultAuth,
Broker: &broker.DefaultBroker,
Client: &client.DefaultClient,
Registry: &registry.DefaultRegistry,
Server: &server.DefaultServer,
Selector: &selector.DefaultSelector,
Transport: &transport.DefaultTransport,
Runtime: &runtime.DefaultRuntime,
Store: &store.DefaultStore,
Tracer: &trace.DefaultTracer,
Profile: &profile.DefaultProfile,
Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,
Brokers: DefaultBrokers,
Clients: DefaultClients,
Registries: DefaultRegistries,
Selectors: DefaultSelectors,
Servers: DefaultServers,
Transports: DefaultTransports,
Runtimes: DefaultRuntimes,
Stores: DefaultStores,
Tracers: DefaultTracers,
Auths: DefaultAuths,
Profiles: DefaultProfiles,
Configs: DefaultConfigs,
Caches: DefaultCaches,
}
for _, o := range opts {
o(&options)
}
if len(options.Description) == 0 {
options.Description = "a go-micro service"
}
cmd := new(cmd)
cmd.opts = options
cmd.app = cli.NewApp()
cmd.app.Name = cmd.opts.Name
cmd.app.Version = cmd.opts.Version
cmd.app.Usage = cmd.opts.Description
cmd.app.Before = cmd.Before
cmd.app.Flags = DefaultFlags
cmd.app.Action = func(c *cli.Context) error {
return nil
}
if len(options.Version) == 0 {
cmd.app.HideVersion = true
}
return cmd
}
func (c *cmd) App() *cli.App {
return c.app
}
func (c *cmd) Options() Options {
return c.opts
}
func (c *cmd) Before(ctx *cli.Context) error {
// If flags are set then use them otherwise do nothing
var serverOpts []server.Option
var clientOpts []client.Option
// Set the client
if name := ctx.String("client"); len(name) > 0 {
// only change if we have the client and type differs
if cl, ok := c.opts.Clients[name]; ok && (*c.opts.Client).String() != name {
*c.opts.Client = cl()
}
}
// Set the server
if name := ctx.String("server"); len(name) > 0 {
// only change if we have the server and type differs
if s, ok := c.opts.Servers[name]; ok && (*c.opts.Server).String() != name {
*c.opts.Server = s()
}
}
// Set the store
if name := ctx.String("store"); len(name) > 0 {
s, ok := c.opts.Stores[name]
if !ok {
return fmt.Errorf("Unsupported store: %s", name)
}
*c.opts.Store = s(store.WithClient(*c.opts.Client))
}
// Set the runtime
if name := ctx.String("runtime"); len(name) > 0 {
r, ok := c.opts.Runtimes[name]
if !ok {
return fmt.Errorf("Unsupported runtime: %s", name)
}
*c.opts.Runtime = r(runtime.WithClient(*c.opts.Client))
}
// Set the tracer
if name := ctx.String("tracer"); len(name) > 0 {
r, ok := c.opts.Tracers[name]
if !ok {
return fmt.Errorf("Unsupported tracer: %s", name)
}
*c.opts.Tracer = r()
}
// Setup auth
authOpts := []auth.Option{}
if len(ctx.String("auth_id")) > 0 || len(ctx.String("auth_secret")) > 0 {
authOpts = append(authOpts, auth.Credentials(
ctx.String("auth_id"), ctx.String("auth_secret"),
))
}
if len(ctx.String("auth_public_key")) > 0 {
authOpts = append(authOpts, auth.PublicKey(ctx.String("auth_public_key")))
}
if len(ctx.String("auth_private_key")) > 0 {
authOpts = append(authOpts, auth.PrivateKey(ctx.String("auth_private_key")))
}
if len(ctx.String("auth_namespace")) > 0 {
authOpts = append(authOpts, auth.Namespace(ctx.String("auth_namespace")))
}
if name := ctx.String("auth"); len(name) > 0 {
r, ok := c.opts.Auths[name]
if !ok {
return fmt.Errorf("Unsupported auth: %s", name)
}
*c.opts.Auth = r(authOpts...)
}
// Set the registry
if name := ctx.String("registry"); len(name) > 0 && (*c.opts.Registry).String() != name {
r, ok := c.opts.Registries[name]
if !ok {
return fmt.Errorf("Registry %s not found", name)
}
*c.opts.Registry = r()
serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
if err := (*c.opts.Selector).Init(selector.Registry(*c.opts.Registry)); err != nil {
logger.Fatalf("Error configuring registry: %v", err)
}
clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))
if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil {
logger.Fatalf("Error configuring broker: %v", err)
}
}
// Set the profile
if name := ctx.String("profile"); len(name) > 0 {
p, ok := c.opts.Profiles[name]
if !ok {
return fmt.Errorf("Unsupported profile: %s", name)
}
*c.opts.Profile = p()
}
// Set the broker
if name := ctx.String("broker"); len(name) > 0 && (*c.opts.Broker).String() != name {
b, ok := c.opts.Brokers[name]
if !ok {
return fmt.Errorf("Broker %s not found", name)
}
*c.opts.Broker = b()
serverOpts = append(serverOpts, server.Broker(*c.opts.Broker))
clientOpts = append(clientOpts, client.Broker(*c.opts.Broker))
}
// Set the selector
if name := ctx.String("selector"); len(name) > 0 && (*c.opts.Selector).String() != name {
s, ok := c.opts.Selectors[name]
if !ok {
return fmt.Errorf("Selector %s not found", name)
}
*c.opts.Selector = s(selector.Registry(*c.opts.Registry))
// No server option here. Should there be?
clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))
}
// Set the transport
if name := ctx.String("transport"); len(name) > 0 && (*c.opts.Transport).String() != name {
t, ok := c.opts.Transports[name]
if !ok {
return fmt.Errorf("Transport %s not found", name)
}
*c.opts.Transport = t()
serverOpts = append(serverOpts, server.Transport(*c.opts.Transport))
clientOpts = append(clientOpts, client.Transport(*c.opts.Transport))
}
// Parse the server options
metadata := make(map[string]string)
for _, d := range ctx.StringSlice("server_metadata") {
var key, val string
parts := strings.Split(d, "=")
key = parts[0]
if len(parts) > 1 {
val = strings.Join(parts[1:], "=")
}
metadata[key] = val
}
if len(metadata) > 0 {
serverOpts = append(serverOpts, server.Metadata(metadata))
}
if len(ctx.String("broker_address")) > 0 {
if err := (*c.opts.Broker).Init(broker.Addrs(strings.Split(ctx.String("broker_address"), ",")...)); err != nil {
logger.Fatalf("Error configuring broker: %v", err)
}
}
if len(ctx.String("registry_address")) > 0 {
if err := (*c.opts.Registry).Init(registry.Addrs(strings.Split(ctx.String("registry_address"), ",")...)); err != nil {
logger.Fatalf("Error configuring registry: %v", err)
}
}
if len(ctx.String("transport_address")) > 0 {
if err := (*c.opts.Transport).Init(transport.Addrs(strings.Split(ctx.String("transport_address"), ",")...)); err != nil {
logger.Fatalf("Error configuring transport: %v", err)
}
}
if len(ctx.String("store_address")) > 0 {
if err := (*c.opts.Store).Init(store.Nodes(strings.Split(ctx.String("store_address"), ",")...)); err != nil {
logger.Fatalf("Error configuring store: %v", err)
}
}
if len(ctx.String("store_database")) > 0 {
if err := (*c.opts.Store).Init(store.Database(ctx.String("store_database"))); err != nil {
logger.Fatalf("Error configuring store database option: %v", err)
}
}
if len(ctx.String("store_table")) > 0 {
if err := (*c.opts.Store).Init(store.Table(ctx.String("store_table"))); err != nil {
logger.Fatalf("Error configuring store table option: %v", err)
}
}
if len(ctx.String("server_name")) > 0 {
serverOpts = append(serverOpts, server.Name(ctx.String("server_name")))
}
if len(ctx.String("server_version")) > 0 {
serverOpts = append(serverOpts, server.Version(ctx.String("server_version")))
}
if len(ctx.String("server_id")) > 0 {
serverOpts = append(serverOpts, server.Id(ctx.String("server_id")))
}
if len(ctx.String("server_address")) > 0 {
serverOpts = append(serverOpts, server.Address(ctx.String("server_address")))
}
if len(ctx.String("server_advertise")) > 0 {
serverOpts = append(serverOpts, server.Advertise(ctx.String("server_advertise")))
}
if ttl := time.Duration(ctx.Int("register_ttl")); ttl >= 0 {
serverOpts = append(serverOpts, server.RegisterTTL(ttl*time.Second))
}
if val := time.Duration(ctx.Int("register_interval")); val >= 0 {
serverOpts = append(serverOpts, server.RegisterInterval(val*time.Second))
}
if len(ctx.String("runtime_source")) > 0 {
if err := (*c.opts.Runtime).Init(runtime.WithSource(ctx.String("runtime_source"))); err != nil {
logger.Fatalf("Error configuring runtime: %v", err)
}
}
// client opts
if r := ctx.Int("client_retries"); r >= 0 {
clientOpts = append(clientOpts, client.Retries(r))
}
if t := ctx.String("client_request_timeout"); len(t) > 0 {
d, err := time.ParseDuration(t)
if err != nil {
return fmt.Errorf("failed to parse client_request_timeout: %v", t)
}
clientOpts = append(clientOpts, client.RequestTimeout(d))
}
if r := ctx.Int("client_pool_size"); r > 0 {
clientOpts = append(clientOpts, client.PoolSize(r))
}
if t := ctx.String("client_pool_ttl"); len(t) > 0 {
d, err := time.ParseDuration(t)
if err != nil {
return fmt.Errorf("failed to parse client_pool_ttl: %v", t)
}
clientOpts = append(clientOpts, client.PoolTTL(d))
}
// We have some command line opts for the server.
// Lets set it up
if len(serverOpts) > 0 {
if err := (*c.opts.Server).Init(serverOpts...); err != nil {
logger.Fatalf("Error configuring server: %v", err)
}
}
// Use an init option?
if len(clientOpts) > 0 {
if err := (*c.opts.Client).Init(clientOpts...); err != nil {
logger.Fatalf("Error configuring client: %v", err)
}
}
// config
if name := ctx.String("config"); len(name) > 0 {
// only change if we have the server and type differs
if r, ok := c.opts.Configs[name]; ok {
rc, err := r()
if err != nil {
logger.Fatalf("Error configuring config: %v", err)
}
*c.opts.Config = rc
}
}
return nil
}
func (c *cmd) Init(opts ...Option) error {
for _, o := range opts {
o(&c.opts)
}
if len(c.opts.Name) > 0 {
c.app.Name = c.opts.Name
}
if len(c.opts.Version) > 0 {
c.app.Version = c.opts.Version
}
c.app.HideVersion = len(c.opts.Version) == 0
c.app.Usage = c.opts.Description
c.app.RunAndExitOnError()
return nil
}
func DefaultOptions() Options {
return DefaultCmd.Options()
}
func App() *cli.App {
return DefaultCmd.App()
}
func Init(opts ...Option) error {
return DefaultCmd.Init(opts...)
}
func NewCmd(opts ...Option) Cmd {
return newCmd(opts...)
}
+244
View File
@@ -0,0 +1,244 @@
package cmd
import (
"context"
"go-micro.dev/v4/auth"
"go-micro.dev/v4/broker"
"go-micro.dev/v4/cache"
"go-micro.dev/v4/client"
"go-micro.dev/v4/config"
"go-micro.dev/v4/debug/profile"
"go-micro.dev/v4/debug/trace"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/runtime"
"go-micro.dev/v4/selector"
"go-micro.dev/v4/server"
"go-micro.dev/v4/store"
"go-micro.dev/v4/transport"
)
type Options struct {
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
Auth *auth.Auth
Selector *selector.Selector
Profile *profile.Profile
Registry *registry.Registry
Brokers map[string]func(...broker.Option) broker.Broker
Transport *transport.Transport
Cache *cache.Cache
Config *config.Config
Client *client.Client
Server *server.Server
Runtime *runtime.Runtime
Caches map[string]func(...cache.Option) cache.Cache
Tracer *trace.Tracer
Profiles map[string]func(...profile.Option) profile.Profile
// We need pointers to things so we can swap them out if needed.
Broker *broker.Broker
Auths map[string]func(...auth.Option) auth.Auth
Store *store.Store
Configs map[string]func(...config.Option) (config.Config, error)
Clients map[string]func(...client.Option) client.Client
Registries map[string]func(...registry.Option) registry.Registry
Selectors map[string]func(...selector.Option) selector.Selector
Servers map[string]func(...server.Option) server.Server
Transports map[string]func(...transport.Option) transport.Transport
Runtimes map[string]func(...runtime.Option) runtime.Runtime
Stores map[string]func(...store.Option) store.Store
Tracers map[string]func(...trace.Option) trace.Tracer
Version string
// For the Command Line itself
Name string
Description string
}
// Command line Name.
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// Command line Description.
func Description(d string) Option {
return func(o *Options) {
o.Description = d
}
}
// Command line Version.
func Version(v string) Option {
return func(o *Options) {
o.Version = v
}
}
func Broker(b *broker.Broker) Option {
return func(o *Options) {
o.Broker = b
}
}
func Cache(c *cache.Cache) Option {
return func(o *Options) {
o.Cache = c
}
}
func Config(c *config.Config) Option {
return func(o *Options) {
o.Config = c
}
}
func Selector(s *selector.Selector) Option {
return func(o *Options) {
o.Selector = s
}
}
func Registry(r *registry.Registry) Option {
return func(o *Options) {
o.Registry = r
}
}
func Runtime(r *runtime.Runtime) Option {
return func(o *Options) {
o.Runtime = r
}
}
func Transport(t *transport.Transport) Option {
return func(o *Options) {
o.Transport = t
}
}
func Client(c *client.Client) Option {
return func(o *Options) {
o.Client = c
}
}
func Server(s *server.Server) Option {
return func(o *Options) {
o.Server = s
}
}
func Store(s *store.Store) Option {
return func(o *Options) {
o.Store = s
}
}
func Tracer(t *trace.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
func Auth(a *auth.Auth) Option {
return func(o *Options) {
o.Auth = a
}
}
func Profile(p *profile.Profile) Option {
return func(o *Options) {
o.Profile = p
}
}
// New broker func.
func NewBroker(name string, b func(...broker.Option) broker.Broker) Option {
return func(o *Options) {
o.Brokers[name] = b
}
}
// New cache func.
func NewCache(name string, c func(...cache.Option) cache.Cache) Option {
return func(o *Options) {
o.Caches[name] = c
}
}
// New client func.
func NewClient(name string, b func(...client.Option) client.Client) Option {
return func(o *Options) {
o.Clients[name] = b
}
}
// New registry func.
func NewRegistry(name string, r func(...registry.Option) registry.Registry) Option {
return func(o *Options) {
o.Registries[name] = r
}
}
// New selector func.
func NewSelector(name string, s func(...selector.Option) selector.Selector) Option {
return func(o *Options) {
o.Selectors[name] = s
}
}
// New server func.
func NewServer(name string, s func(...server.Option) server.Server) Option {
return func(o *Options) {
o.Servers[name] = s
}
}
// New transport func.
func NewTransport(name string, t func(...transport.Option) transport.Transport) Option {
return func(o *Options) {
o.Transports[name] = t
}
}
// New runtime func.
func NewRuntime(name string, r func(...runtime.Option) runtime.Runtime) Option {
return func(o *Options) {
o.Runtimes[name] = r
}
}
// New tracer func.
func NewTracer(name string, t func(...trace.Option) trace.Tracer) Option {
return func(o *Options) {
o.Tracers[name] = t
}
}
// New auth func.
func NewAuth(name string, t func(...auth.Option) auth.Auth) Option {
return func(o *Options) {
o.Auths[name] = t
}
}
// New config func.
func NewConfig(name string, t func(...config.Option) (config.Config, error)) Option {
return func(o *Options) {
o.Configs[name] = t
}
}
// New profile func.
func NewProfile(name string, t func(...profile.Option) profile.Profile) Option {
return func(o *Options) {
o.Profiles[name] = t
}
}
+18
View File
@@ -0,0 +1,18 @@
package ctx
import (
"context"
"net/http"
"strings"
"go-micro.dev/v4/metadata"
)
func FromRequest(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)
}
+57
View File
@@ -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,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
+509
View File
@@ -0,0 +1,509 @@
package mdns
import (
"context"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
"go-micro.dev/v4/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
}
params.Context, _ = context.WithTimeout(context.Background(), params.Timeout)
if err != nil {
return err
}
}
// 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)
}
}
}
}
return nil
}
// 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
}
+85
View File
@@ -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()
//}
+518
View File
@@ -0,0 +1,518 @@
package mdns
import (
"fmt"
"math/rand"
"net"
"sync"
"sync/atomic"
"time"
"github.com/miekg/dns"
log "go-micro.dev/v4/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.MsgHdr.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.MsgHdr.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
}
+309
View File
@@ -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
}
+119
View File
@@ -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
}
+129
View File
@@ -0,0 +1,129 @@
package pool
import (
"sync"
"time"
"github.com/google/uuid"
"go-micro.dev/v4/transport"
)
type pool struct {
tr transport.Transport
conns map[string][]*poolConn
size int
ttl time.Duration
sync.Mutex
}
type poolConn struct {
created time.Time
transport.Client
id string
}
func newPool(options Options) *pool {
return &pool{
size: options.Size,
tr: options.Transport,
ttl: options.TTL,
conns: make(map[string][]*poolConn),
}
}
func (p *pool) Close() error {
p.Lock()
defer p.Unlock()
var err error
for k, c := range p.conns {
for _, conn := range c {
if nerr := conn.Client.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.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.Client.Close(); err != nil {
p.Unlock()
return nil, err
}
continue
}
// We got a good conn, lets unlock and return it
p.Unlock()
return conn, nil
}
p.Unlock()
// create new conn
c, err := p.tr.Dial(addr, opts...)
if err != nil {
return nil, err
}
return &poolConn{
Client: c,
id: uuid.New().String(),
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).Client.Close()
}
// otherwise put it back for reuse
p.Lock()
defer p.Unlock()
conns := p.conns[conn.Remote()]
if len(conns) >= p.size {
return conn.(*poolConn).Client.Close()
}
p.conns[conn.Remote()] = append(conns, conn.(*poolConn))
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package pool
import (
"time"
"go-micro.dev/v4/transport"
)
type Options struct {
Transport transport.Transport
TTL 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
}
}
+38
View File
@@ -0,0 +1,38 @@
// Package pool is a connection pool
package pool
import (
"time"
"go-micro.dev/v4/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)
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Jon Calhoun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+55
View File
@@ -0,0 +1,55 @@
# qson
This is copy from https://github.com/joncalhoun/qson
As author says he is not acrivelly maintains the repo and not plan to do that.
## Usage
You can either turn a URL query param into a JSON byte array, or unmarshal that directly into a Go object.
Transforming the URL query param into a JSON byte array:
```go
import "github.com/joncalhoun/qson"
func main() {
b, err := qson.ToJSON("bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112")
if err != nil {
panic(err)
}
fmt.Println(string(b))
// Should output: {"bar":{"one":{"red":112,"two":2}}}
}
```
Or unmarshalling directly into a Go object using JSON struct tags:
```go
import "github.com/joncalhoun/qson"
type unmarshalT struct {
A string `json:"a"`
B unmarshalB `json:"b"`
}
type unmarshalB struct {
C int `json:"c"`
}
func main() {
var out unmarshalT
query := "a=xyz&b[c]=456"
err := Unmarshal(&out, query)
if err != nil {
t.Error(err)
}
// out should equal
// unmarshalT{
// A: "xyz",
// B: unmarshalB{
// C: 456,
// },
// }
}
```
To get a query string like in the two previous examples you can use the `RawQuery` field on the [net/url.URL](https://golang.org/pkg/net/url/#URL) type.
+35
View File
@@ -0,0 +1,35 @@
package qson
// merge merges a with b if they are either both slices
// or map[string]interface{} types. Otherwise it returns b.
func merge(a interface{}, b interface{}) interface{} {
switch aT := a.(type) {
case map[string]interface{}:
return mergeMap(aT, b.(map[string]interface{}))
case []interface{}:
return mergeSlice(aT, b.([]interface{}))
default:
return b
}
}
// mergeMap merges a with b, attempting to merge any nested
// values in nested maps but eventually overwriting anything
// in a that can't be merged with whatever is in b.
func mergeMap(a map[string]interface{}, b map[string]interface{}) map[string]interface{} {
for bK, bV := range b {
if _, ok := a[bK]; ok {
a[bK] = merge(a[bK], bV)
} else {
a[bK] = bV
}
}
return a
}
// mergeSlice merges a with b and returns the result.
func mergeSlice(a []interface{}, b []interface{}) []interface{} {
a = append(a, b...)
return a
}
+170
View File
@@ -0,0 +1,170 @@
// Package qson implmenets decoding of URL query params
// into JSON and Go values (using JSON struct tags).
//
// See https://golang.org/pkg/encoding/json/ for more
// details on JSON struct tags.
package qson
import (
"encoding/json"
"errors"
"net/url"
"regexp"
"strings"
)
var (
// ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function.
// Specifically, this will be returned when there is no equals sign present in the URL query parameter.
ErrInvalidParam = errors.New("qson: invalid url query param provided")
bracketSplitter *regexp.Regexp
)
func init() {
bracketSplitter = regexp.MustCompile(`\[|\]`)
}
// Unmarshal will take a dest along with URL
// query params and attempt to first turn the query params
// into JSON and then unmarshal those into the dest variable
//
// BUG(joncalhoun): If a URL query param value is something
// like 123 but is expected to be parsed into a string this
// will currently result in an error because the JSON
// transformation will assume this is intended to be an int.
// This should only affect the Unmarshal function and
// could likely be fixed, but someone will need to submit a
// PR if they want that fixed.
func Unmarshal(dst interface{}, query string) error {
b, err := ToJSON(query)
if err != nil {
return err
}
return json.Unmarshal(b, dst)
}
// ToJSON will turn a query string like:
//
// cat=1&bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112
//
// Into a JSON object with all the data merged as nicely as
// possible. Eg the example above would output:
//
// {"bar":{"one":{"two":2,"red":112}}}
func ToJSON(query string) ([]byte, error) {
var (
builder interface{} = make(map[string]interface{})
)
params := strings.Split(query, "&")
for _, part := range params {
tempMap, err := queryToMap(part)
if err != nil {
return nil, err
}
builder = merge(builder, tempMap)
}
return json.Marshal(builder)
}
// queryToMap turns something like a[b][c]=4 into
//
// map[string]interface{}{
// "a": map[string]interface{}{
// "b": map[string]interface{}{
// "c": 4,
// },
// },
// }
func queryToMap(param string) (map[string]interface{}, error) {
rawKey, rawValue, err := splitKeyAndValue(param)
if err != nil {
return nil, err
}
rawValue, err = url.QueryUnescape(rawValue)
if err != nil {
return nil, err
}
rawKey, err = url.QueryUnescape(rawKey)
if err != nil {
return nil, err
}
pieces := bracketSplitter.Split(rawKey, -1)
key := pieces[0]
// If len==1 then rawKey has no [] chars and we can just
// decode this as key=value into {key: value}
if len(pieces) == 1 {
var value interface{}
// First we try parsing it as an int, bool, null, etc
err = json.Unmarshal([]byte(rawValue), &value)
if err != nil {
// If we got an error we try wrapping the value in
// quotes and processing it as a string
err = json.Unmarshal([]byte("\""+rawValue+"\""), &value)
if err != nil {
// If we can't decode as a string we return the err
return nil, err
}
}
return map[string]interface{}{
key: value,
}, nil
}
// If len > 1 then we have something like a[b][c]=2
// so we need to turn this into {"a": {"b": {"c": 2}}}
// To do this we break our key into two pieces:
// a and b[c]
// and then we set {"a": queryToMap("b[c]", value)}
ret := make(map[string]interface{}, 0)
ret[key], err = queryToMap(buildNewKey(rawKey) + "=" + rawValue)
if err != nil {
return nil, err
}
// When URL params have a set of empty brackets (eg a[]=1)
// it is assumed to be an array. This will get us the
// correct value for the array item and return it as an
// []interface{} so that it can be merged properly.
if pieces[1] == "" {
temp := ret[key].(map[string]interface{})
ret[key] = []interface{}{temp[""]}
}
return ret, nil
}
// buildNewKey will take something like:
// origKey = "bar[one][two]"
// pieces = [bar one two ]
// and return "one[two]".
func buildNewKey(origKey string) string {
pieces := bracketSplitter.Split(origKey, -1)
ret := origKey[len(pieces[0])+1:]
ret = ret[:len(pieces[1])] + ret[len(pieces[1])+1:]
return ret
}
// splitKeyAndValue splits a URL param at the last equal
// sign and returns the two strings. If no equal sign is
// found, the ErrInvalidParam error is returned.
func splitKeyAndValue(param string) (string, string, error) {
li := strings.LastIndex(param, "=")
if li == -1 {
return "", "", ErrInvalidParam
}
return param[:li], param[li+1:], nil
}
+148
View File
@@ -0,0 +1,148 @@
package registry
import (
"go-micro.dev/v4/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
}
+139
View File
@@ -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),
}
}
+13
View File
@@ -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,
}
}
+62
View File
@@ -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),
}
}
+117
View File
@@ -0,0 +1,117 @@
// Package socket provides a pseudo socket
package socket
import (
"io"
"go-micro.dev/v4/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),
}
}
+74
View File
@@ -0,0 +1,74 @@
package tls
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"time"
)
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{"Acme Co"},
},
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())
}