Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions internal/usecase/devices/wsman/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ var (
connectionsMu sync.RWMutex
waitForAuthTickTime = 1 * time.Second
queueTickTime = 500 * time.Millisecond
expireAfter = 30 * time.Second // expire the stored connection after 30 seconds
waitForAuth = 3 * time.Second // wait for 3 seconds for the connection to authenticate, prevents multiple api calls trying to auth at the same time
expireAfter = 90 * time.Second // expire the stored connection after 90 seconds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make these configurable rather than hardcoded?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do it in a separate PR ? because these hardcoded values are spread across multiple places and need proper config wiring.

waitForAuth = 30 * time.Second // wait for 30 seconds for the connection to authenticate, prevents multiple api calls trying to auth at the same time
requestQueue = make(chan func(), deviceCallBuffer) // Buffered channel to queue requests
shutdownSignal = make(chan struct{})

Expand Down
4 changes: 2 additions & 2 deletions pkg/httpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
)

const (
_defaultReadTimeout = 15 * time.Second
_defaultWriteTimeout = 15 * time.Second
_defaultReadTimeout = 40 * time.Second
_defaultWriteTimeout = 40 * time.Second
Comment thread
amarnath-ac marked this conversation as resolved.
_defaultAddr = ":80"
_defaultShutdownTimeout = 3 * time.Second

Expand Down
27 changes: 27 additions & 0 deletions pkg/httpserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand All @@ -20,3 +21,29 @@ func TestNew(t *testing.T) { //nolint:paralleltest // httpserver can't be bind t
assert.Equal(t, net.JoinHostPort("localhost", "9090"), s.server.Addr, "expected addr to be set correctly")
assert.Equal(t, _defaultShutdownTimeout, s.shutdownTimeout, "expected shutdown timeout to be set correctly")
}

// The read/write budget must outlast the wsman client's own 30s per-request
// timeout, so a slow AMT device fails there first and the handler can turn it
// into a clean response instead of the server cutting the connection.
func TestDefaultTimeoutsOutlastWsmanClient(t *testing.T) {
t.Parallel()

const wsmanClientTimeout = 30 * time.Second

assert.Greater(t, _defaultReadTimeout, wsmanClientTimeout, "read timeout must outlast the wsman client timeout")
assert.Greater(t, _defaultWriteTimeout, wsmanClientTimeout, "write timeout must outlast the wsman client timeout")
}

func TestServePlainWithInjectedListener(t *testing.T) { //nolint:paralleltest // server lifecycle
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("plain")) })

l := newTestListener(t)
s := New(handler, Listener(l))

defer func() { _ = s.Shutdown() }()

if body := getOK(t, http.DefaultClient, "http://"+l.Addr().String()+"/"); body != "plain" {
t.Fatalf("unexpected body: %q", body)
}
}
181 changes: 181 additions & 0 deletions pkg/httpserver/server_tls_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpserver

import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
Expand Down Expand Up @@ -98,6 +99,69 @@ func writeTempCertPair(t *testing.T) (certPath, keyPath string) { // named resul
return certPath, keyPath
}

// pinTempDir points os.TempDir() at a per-test directory and returns it.
// generateAndServeSelfSignedTLS caches console_selfsigned.crt/.key in the
// system temp dir, so without pinning it, whether the generate branch or the
// reuse branch runs depends on files left behind by an earlier run -- which
// makes the coverage of this package differ between machines and CI runs.
func pinTempDir(t *testing.T) string {
t.Helper()

dir := t.TempDir()

t.Setenv("TMPDIR", dir) // POSIX
t.Setenv("TMP", dir) // Windows
t.Setenv("TEMP", dir) // Windows

return dir
}

// getOK polls url until the server goroutine is listening, then asserts 200.
func getOK(t *testing.T, client *http.Client, url string) string {
t.Helper()

deadline := time.Now().Add(2 * time.Second)

ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

var (
resp *http.Response
err error
)

for time.Now().Before(deadline) {
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if reqErr != nil {
t.Fatalf("create request: %v", reqErr)
}

resp, err = client.Do(req)
if err == nil {
break
}

time.Sleep(50 * time.Millisecond)
}

if err != nil {
t.Fatalf("GET %s failed: %v", url, err)
}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}

return string(body)
}

func newTestListener(t *testing.T) net.Listener {
t.Helper()

Expand All @@ -114,6 +178,8 @@ func newTestListener(t *testing.T) net.Listener {
}

func TestTLS_SelfSigned_GeneratesAndServes(t *testing.T) { //nolint:paralleltest // binds a port
pinTempDir(t)

handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })

Expand Down Expand Up @@ -317,6 +383,121 @@ func TestTLS_MissingFiles_ReturnsError(t *testing.T) { //nolint:paralleltest //
_ = s.Shutdown()
}

// The generate branch: an empty temp dir has no cached pair, so one is written.
func TestTLS_SelfSigned_WritesPairWhenAbsent(t *testing.T) { //nolint:paralleltest // binds a port
dir := pinTempDir(t)

certPath := filepath.Join(dir, "console_selfsigned.crt")
keyPath := filepath.Join(dir, "console_selfsigned.key")

handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("generated")) })

l := newTestListener(t)
s := New(handler, Listener(l), TLS(true, "", ""), Logger(appLogger.New("info")))

defer func() { _ = s.Shutdown() }()

client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}

if body := getOK(t, client, "https://"+l.Addr().String()+"/"); body != "generated" {
t.Fatalf("unexpected body: %q", body)
}

for _, path := range []string{certPath, keyPath} {
info, err := os.Stat(path)
if err != nil {
t.Fatalf("expected %s to be generated: %v", path, err)
}

if info.Size() == 0 {
t.Fatalf("expected %s to be non-empty", path)
}
}
}

// The reuse branch: a cached pair in the temp dir is served as-is, not rewritten.
func TestTLS_SelfSigned_ReusesCachedPair(t *testing.T) { //nolint:paralleltest // binds a port
dir := pinTempDir(t)

cert, key := writeTempCertPair(t)

certPath := filepath.Join(dir, "console_selfsigned.crt")
keyPath := filepath.Join(dir, "console_selfsigned.key")

certPEM, err := os.ReadFile(cert)
if err != nil {
t.Fatalf("read cert: %v", err)
}

keyPEM, err := os.ReadFile(key)
if err != nil {
t.Fatalf("read key: %v", err)
}

if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
t.Fatalf("seed cert: %v", err)
}

if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
t.Fatalf("seed key: %v", err)
}

handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("reused")) })

l := newTestListener(t)
s := New(handler, Listener(l), TLS(true, "", ""), Logger(appLogger.New("info")))

defer func() { _ = s.Shutdown() }()

// Trusting only the seeded certificate proves the cached pair was served
// rather than a freshly generated one.
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(certPEM); !ok {
t.Fatalf("failed to append cert to pool")
}

client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS13}}}

if body := getOK(t, client, "https://"+l.Addr().String()+"/"); body != "reused" {
t.Fatalf("unexpected body: %q", body)
}

after, err := os.ReadFile(certPath)
if err != nil {
t.Fatalf("read cached cert: %v", err)
}

if !bytes.Equal(after, certPEM) {
t.Error("expected the cached certificate to be left untouched")
}
}

// An unwritable temp dir must surface as an error on Notify, not a panic.
func TestTLS_SelfSigned_UnwritableTempDir_ReturnsError(t *testing.T) {
// A path that does not exist: os.WriteFile fails regardless of the user the
// tests run as, so this stays deterministic on CI and in containers.
t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "absent"))
t.Setenv("TMP", filepath.Join(t.TempDir(), "absent"))
t.Setenv("TEMP", filepath.Join(t.TempDir(), "absent"))

handler := http.NewServeMux()
l := newTestListener(t)
s := New(handler, Listener(l), TLS(true, "", ""), Logger(appLogger.New("info")))

select {
case err := <-s.Notify():
if err == nil {
t.Fatal("expected an error when the certificate cannot be written, got nil")
}
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for server error")
}

_ = s.Shutdown()
}

func TestTLS_Mismatch_ReturnsError(t *testing.T) { //nolint:paralleltest // server lifecycle
handler := http.NewServeMux()
l := newTestListener(t)
Expand Down
Loading