Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions api/grpc/mpi/v1/command.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/grpc/mpi/v1/command.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/grpc/mpi/v1/command.proto
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ message APIDetails {
string location = 1;
// the API listen directive
string listen = 2;
// the API Ca directive
string Ca = 3;
}

// A set of runtime NGINX App Protect settings
Expand Down
1 change: 1 addition & 0 deletions docs/proto/protos.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ Perform an associated API action on an instance
| ----- | ---- | ----- | ----------- |
| location | [string](#string) | | the API location directive |
| listen | [string](#string) | | the API listen directive |
| Ca | [string](#string) | | the API Ca directive |



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type APIDetails struct {
URL string `mapstructure:"url"`
Listen string `mapstructure:"listen"`
Location string `mapstructure:"location"`
Ca string `mapstructure:"ca"`
}

type AccessLog struct {
Expand All @@ -56,6 +57,7 @@ func CreateDefaultConfig() component.Config {
URL: "http://localhost:80/status",
Listen: "localhost:80",
Location: "status",
Ca: "",
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ package stubstatus

import (
"context"
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -63,6 +66,28 @@ func (s *NginxStubStatusScraper) ID() component.ID {
func (s *NginxStubStatusScraper) Start(_ context.Context, _ component.Host) error {
s.logger.Info("Starting NGINX stub status scraper")
httpClient := http.DefaultClient
caCertLocation := s.cfg.APIDetails.Ca
if caCertLocation != "" {
s.settings.Logger.Debug("Reading from Location for Ca Cert : ", zap.Any(caCertLocation, caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
s.settings.Logger.Error("Error starting NGINX stub scraper. "+
"Failed to read CA certificate : ", zap.Error(err))

return nil
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
},
},
}
}
httpClient.Timeout = s.cfg.ClientConfig.Timeout

if strings.HasPrefix(s.cfg.APIDetails.Listen, "unix:") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.

package stubstatus

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/receiver/receivertest"

"github.com/nginx/agent/v3/internal/collector/nginxossreceiver/internal/config"
)

func TestStubStatusScraperTLS(t *testing.T) {
// Create a test CA certificate and key
ca := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"NGINX Agent Test CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}

caPrivKey, caPrivKeyErr := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, caPrivKeyErr)

caBytes, caBytesErr := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
require.NoError(t, caBytesErr)

// Create a test server certificate signed by the CA
cert := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{
Organization: []string{"NGINX Agent Test"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
DNSNames: []string{"localhost"},
}

certPrivKey, certPrivKeyErr := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, certPrivKeyErr)

certBytes, certBytesErr := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
require.NoError(t, certBytesErr)

// Create a temporary directory for test files
tempDir := t.TempDir()

// Save CA certificate to a file
caFile := filepath.Join(tempDir, "ca.crt")
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caBytes})
writeErr := os.WriteFile(caFile, caPEM, 0o600)
require.NoError(t, writeErr)

// Create a TLS config for the server
serverTLSConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{
{
Certificate: [][]byte{certBytes},
PrivateKey: certPrivKey,
},
},
}

// Create a test server with TLS
server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/status" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`Active connections: 291
server accepts handled requests
16630948 16630946 31070465
Reading: 6 Writing: 179 Waiting: 106
`))

return
}
rw.WriteHeader(http.StatusNotFound)
}))

server.TLS = serverTLSConfig
server.StartTLS()
defer server.Close()

// Test with TLS configuration
t.Run("with TLS CA", func(t *testing.T) {
cfg, ok := config.CreateDefaultConfig().(*config.Config)
require.True(t, ok)

cfg.APIDetails.URL = server.URL + "/status"
cfg.APIDetails.Ca = caFile

scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)

err := scraper.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)

_, err = scraper.Scrape(context.Background())
assert.NoError(t, err)
})
}

func TestStubStatusScraperUnixSocket(t *testing.T) {
// Use a shorter path for the socket to avoid path length issues
socketPath := filepath.Join(os.TempDir(), "test-nginx.sock")
// Clean up the socket file after the test
t.Cleanup(func() { os.Remove(socketPath) })

// Create a Unix domain socket listener
listener, listenErr := net.Listen("unix", socketPath)
require.NoError(t, listenErr)
defer listener.Close()

// Start a simple HTTP server on the Unix socket
server := &http.Server{
Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/status" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`Active connections: 291
server accepts handled requests
16630948 16630946 31070465
Reading: 6 Writing: 179 Waiting: 106
`))

return
}
rw.WriteHeader(http.StatusNotFound)
}),
}

go func() {
_ = server.Serve(listener)
}()
defer server.Close()

// Test with Unix socket
t.Run("with Unix socket", func(t *testing.T) {
cfg, ok := config.CreateDefaultConfig().(*config.Config)
require.True(t, ok)

cfg.APIDetails.URL = "http://unix/status"
cfg.APIDetails.Listen = "unix:" + socketPath

scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)

err := scraper.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)

_, err = scraper.Scrape(context.Background())
assert.NoError(t, err)
})
}
2 changes: 2 additions & 0 deletions internal/collector/nginxplusreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type APIDetails struct {
URL string `mapstructure:"url"`
Listen string `mapstructure:"listen"`
Location string `mapstructure:"location"`
Ca string `mapstructure:"ca"`
}

// Validate checks if the receiver configuration is valid
Expand Down Expand Up @@ -59,6 +60,7 @@ func createDefaultConfig() component.Config {
URL: "http://localhost:80/api",
Listen: "localhost:80",
Location: "/api",
Ca: "",
},
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
}
Expand Down
23 changes: 23 additions & 0 deletions internal/collector/nginxplusreceiver/scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package nginxplusreceiver

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -82,6 +85,26 @@ func (nps *NginxPlusScraper) ID() component.ID {
func (nps *NginxPlusScraper) Start(_ context.Context, _ component.Host) error {
endpoint := strings.TrimPrefix(nps.cfg.APIDetails.URL, "unix:")
httpClient := http.DefaultClient
caCertLocation := nps.cfg.APIDetails.Ca
if caCertLocation != "" {
nps.logger.Debug("Reading from Location for Ca Cert : ", zap.Any(caCertLocation, caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
nps.logger.Error("Unable to start NGINX Plus scraper. Failed to read CA certificate: %v", zap.Error(err))
return err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
},
},
}
}
httpClient.Timeout = nps.cfg.ClientConfig.Timeout

if strings.HasPrefix(nps.cfg.APIDetails.Listen, "unix:") {
Expand Down
1 change: 1 addition & 0 deletions internal/collector/otel_collector_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func (oc *Collector) checkForNewReceivers(ctx context.Context, nginxConfigContex
URL: nginxConfigContext.PlusAPI.URL,
Listen: nginxConfigContext.PlusAPI.Listen,
Location: nginxConfigContext.PlusAPI.Location,
Ca: nginxConfigContext.PlusAPI.Ca,
},
CollectionInterval: defaultCollectionInterval,
},
Expand Down
2 changes: 2 additions & 0 deletions internal/collector/otelcol.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ receivers:
url: "{{- .StubStatus.URL -}}"
listen: "{{- .StubStatus.Listen -}}"
location: "{{- .StubStatus.Location -}}"
ca: "{{- .StubStatus.Ca -}}"
{{- if .CollectionInterval }}
collection_interval: {{ .CollectionInterval }}
{{- end }}
Expand All @@ -98,6 +99,7 @@ receivers:
url: "{{- .PlusAPI.URL -}}"
listen: "{{- .PlusAPI.Listen -}}"
location: "{{- .PlusAPI.Location -}}"
ca: "{{- .PlusAPI.Ca -}}"
{{- if .CollectionInterval }}
collection_interval: {{ .CollectionInterval }}
{{- end }}
Expand Down
Loading