Skip to content

Commit

Permalink
Move api and api/npipe and monitoring (elastic#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
kvch authored Mar 2, 2022
1 parent ddc9c44 commit 9d07e6f
Show file tree
Hide file tree
Showing 39 changed files with 3,914 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
This repository is the home to the common libraries used by Elastic Agent and Beats.

Provided packages:
* `github.com/elastic/elastic-agent-libs/api` Provides an HTTP API for debugging information.
* `github.com/elastic/elastic-agent-libs/api/npipe` Provides an API for debugging information via named pipes.
* `github.com/elastic/elastic-agent-libs/monitoring` Basic monitoring functionality used by Beats and Agent.
* `github.com/elastic/elastic-agent-libs/atomic` Atomic operations for integer and boolean types.
* `github.com/elastic/elastic-agent-libs/cloudid` is used for parsing `cloud.id` and `cloud.auth` when connecting to the Elastic stack.
* `github.com/elastic/elastic-agent-libs/config` the previous `config.go` file from `github.com/elastic/beats/v7/libbeat/common`. A minimal wrapper around `github.com/elastic/go-ucfg`. It contains helpers for merging and accessing configuration objects and flags.
Expand Down
41 changes: 41 additions & 0 deletions api/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package api

import "os"

// Config is the configuration for the API endpoint.
type Config struct {
Enabled bool `config:"enabled"`
Host string `config:"host"`
Port int `config:"port"`
User string `config:"named_pipe.user"`
SecurityDescriptor string `config:"named_pipe.security_descriptor"`
}

// DefaultConfig is the default configuration used by the API endpoint.
func DefaultConfig() Config {
return Config{
Enabled: false,
Host: "localhost",
Port: 5066,
}
}

// File mode for the socket file, owner of the process can do everything, member of the group can read.
const socketFileMode = os.FileMode(0740)
75 changes: 75 additions & 0 deletions api/make_listener_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build !windows
// +build !windows

package api

import (
"errors"
"fmt"
"net"
"os"

"github.com/elastic/elastic-agent-libs/api/npipe"
)

func makeListener(cfg Config) (net.Listener, error) {
if len(cfg.User) > 0 {
return nil, errors.New("specifying a user is not supported under this platform")
}

if len(cfg.SecurityDescriptor) > 0 {
return nil, errors.New("security_descriptor option for the HTTP endpoint only work on Windows")
}

if npipe.IsNPipe(cfg.Host) {
return nil, fmt.Errorf("cannot use %s as the host, named pipes are only supported on Windows", cfg.Host)
}

network, path, err := parse(cfg.Host, cfg.Port)
if err != nil {
return nil, err
}

if network == unixNetwork {
if _, err := os.Stat(path); !os.IsNotExist(err) {
if err := os.Remove(path); err != nil {
return nil, fmt.Errorf("cannot remove existing unix socket file at location %s: %w", path, err)
}
}
}

l, err := net.Listen(network, path)
if err != nil {
return nil, err
}

// Ensure file mode
if network == unixNetwork {
if err := os.Chmod(path, socketFileMode); err != nil {
return nil, fmt.Errorf("could not set mode %d for unix socket file at location %s: %w",
socketFileMode,
path,
err,
)
}
}

return l, nil
}
64 changes: 64 additions & 0 deletions api/make_listener_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build windows
// +build windows

package api

import (
"errors"
"fmt"
"net"

"github.com/elastic/elastic-agent-libs/api/npipe"
)

func makeListener(cfg Config) (net.Listener, error) {
if len(cfg.User) > 0 && len(cfg.SecurityDescriptor) > 0 {
return nil, errors.New("user and security_descriptor are mutually exclusive, define only one of them")
}

if npipe.IsNPipe(cfg.Host) {
pipe := npipe.TransformString(cfg.Host)
var sd string
var err error
if len(cfg.SecurityDescriptor) == 0 {
sd, err = npipe.DefaultSD(cfg.User)
if err != nil {
return nil, fmt.Errorf("cannot generate security descriptor for the named pipe: %w", err)
}
} else {
sd = cfg.SecurityDescriptor
}
return npipe.NewListener(pipe, sd)
}

network, path, err := parse(cfg.Host, cfg.Port)
if err != nil {
return nil, err
}

if network == unixNetwork {
return nil, fmt.Errorf(
"cannot use %s as the host, unix sockets are not supported on Windows, use npipe instead",
cfg.Host,
)
}

return net.Listen(network, path)
}
109 changes: 109 additions & 0 deletions api/npipe/listener_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build windows
// +build windows

package npipe

import (
"context"
"fmt"
"net"
"os/user"
"strings"

winio "github.com/Microsoft/go-winio"
)

// NewListener creates a new Listener receiving events over a named pipe.
func NewListener(name, sd string) (net.Listener, error) {
c := &winio.PipeConfig{
SecurityDescriptor: sd,
}

l, err := winio.ListenPipe(name, c)
if err != nil {
return nil, fmt.Errorf("failed to listen on the named pipe %s: %w", name, err)
}

return l, nil
}

// TransformString takes an input type name defined as a URI like `npipe:///hello` and transform it into
// `\\.\pipe\hello`
func TransformString(name string) string {
if strings.HasPrefix(name, "npipe:///") {
path := strings.TrimPrefix(name, "npipe:///")
return `\\.\pipe\` + path
}

if strings.HasPrefix(name, `\\.\pipe\`) {
return name
}

return name
}

// DialContext create a Dial to be use with an http.Client to connect to a pipe.
func DialContext(npipe string) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, _, _ string) (net.Conn, error) {
return winio.DialPipeContext(ctx, npipe)
}
}

// Dial create a Dial to be use with an http.Client to connect to a pipe.
func Dial(npipe string) func(string, string) (net.Conn, error) {
return func(_, _ string) (net.Conn, error) {
return winio.DialPipe(npipe, nil)
}
}

// DefaultSD returns a default SecurityDescriptor which is the minimal required permissions to be
// able to write to the named pipe. The security descriptor is returned in SDDL format.
//
// Docs: https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format
func DefaultSD(forUser string) (string, error) {
var u *user.User
var err error
// No user configured we fallback to the current running user.
if len(forUser) == 0 {
u, err = user.Current()
if err != nil {
return "", fmt.Errorf("failed to retrieve the current user: %w", err)
}
} else {
u, err = user.Lookup(forUser)
if err != nil {
return "", fmt.Errorf("failed to retrieve the user %s: %w", forUser, err)
}
}

// Named pipe security and access rights.
// We create the pipe and the specific users should only be able to write to it.
// See docs: https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights
// String definition: https://docs.microsoft.com/en-us/windows/win32/secauthz/ace-strings
// Give generic read/write access to the specified user.
descriptor := "D:P(A;;GA;;;" + u.Uid + ")"
if u.Username == "NT AUTHORITY\\SYSTEM" {
// running as SYSTEM, include Administrators group so Administrators can talk over
// the named pipe to the running Elastic Agent system process
// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
descriptor += "(A;;GA;;;S-1-5-32-544)" // Administrators group
}
return descriptor, nil
}
78 changes: 78 additions & 0 deletions api/npipe/listener_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build windows
// +build windows

package npipe

import (
"fmt"
"io/ioutil"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHTTPOverNamedPipe(t *testing.T) {
sd, err := DefaultSD("")
require.NoError(t, err)
npipe := TransformString("npipe:///hello-world")
l, err := NewListener(npipe, sd)
require.NoError(t, err)
defer l.Close()

mux := http.NewServeMux()
mux.HandleFunc("/echo-hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ehlo!")
})

go func() {
err := http.Serve(l, mux)
require.NoError(t, err)
}()

c := http.Client{
Transport: &http.Transport{
DialContext: DialContext(npipe),
},
}

r, err := c.Get("http://npipe/echo-hello")
require.NoError(t, err)
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()

assert.Equal(t, "ehlo!", string(body))
}

func TestTransformString(t *testing.T) {
t.Run("with npipe:// scheme", func(t *testing.T) {
assert.Equal(t, `\\.\pipe\hello`, TransformString("npipe:///hello"))
})

t.Run("with windows pipe syntax", func(t *testing.T) {
assert.Equal(t, `\\.\pipe\hello`, TransformString(`\\.\pipe\hello`))
})

t.Run("everything else", func(t *testing.T) {
assert.Equal(t, "hello", TransformString("hello"))
})
}
Loading

0 comments on commit 9d07e6f

Please sign in to comment.