From 03c6ee5a9f76979ebacb8961b6eaa3f822083ce8 Mon Sep 17 00:00:00 2001 From: Desmond Ang Date: Thu, 4 Jun 2026 02:16:35 +0000 Subject: [PATCH 1/3] HTTP status echo server for auth gateway catchall --- src/go/cmd/status-echo-server/BUILD.bazel | 39 ++++++++++ src/go/cmd/status-echo-server/main.go | 65 ++++++++++++++++ src/go/cmd/status-echo-server/main_test.go | 86 ++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 src/go/cmd/status-echo-server/BUILD.bazel create mode 100644 src/go/cmd/status-echo-server/main.go create mode 100644 src/go/cmd/status-echo-server/main_test.go diff --git a/src/go/cmd/status-echo-server/BUILD.bazel b/src/go/cmd/status-echo-server/BUILD.bazel new file mode 100644 index 00000000..93f1e19d --- /dev/null +++ b/src/go/cmd/status-echo-server/BUILD.bazel @@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@rules_oci//oci:defs.bzl", "oci_image") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/googlecloudrobotics/core/src/go/cmd/status-echo-server", + visibility = ["//visibility:private"], + deps = [ + "@com_github_googlecloudrobotics_ilog//:go_default_library", + ], +) + +go_binary( + name = "status-echo-server-app", + embed = [":go_default_library"], +) + +pkg_tar( + name = "status-echo-server-image-layer", + srcs = [":status-echo-server-app"], + extension = "tar.gz", +) + +oci_image( + name = "status-echo-server-image", + base = "@distroless_base", + entrypoint = ["/status-echo-server-app"], + tars = [":status-echo-server-image-layer"], +) + +go_test( + name = "go_default_test", + srcs = ["main_test.go"], + embed = [":go_default_library"], +) diff --git a/src/go/cmd/status-echo-server/main.go b/src/go/cmd/status-echo-server/main.go new file mode 100644 index 00000000..a016b540 --- /dev/null +++ b/src/go/cmd/status-echo-server/main.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Cloud Robotics Authors +// +// Licensed 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 main runs a multiplexing HTTP relay server. +// +// It exists to make HTTP endpoints on robots accessible without a public +// endpoint. It binds to a public endpoint accessible by both user-client and +// backend and works together with a relay-client that's colocated with the +// backend. +// +// For more details, see README.md. + +package main + +import ( + "flag" + "fmt" + "log/slog" + "net/http" + "os" + + "github.com/googlecloudrobotics/ilog" +) + +var ( + port = flag.Int("port", 80, "Port number to listen on") + httpStatus = flag.Int("http_status", 200, "HTTP Status to echo") + logLevel = flag.Int("log_level", int(slog.LevelInfo), "the log message level required") +) + +func echoHandler(w http.ResponseWriter, req *http.Request) { + slog.Debug("echo", slog.String("path", req.URL.Path)) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(*httpStatus) + w.Write([]byte(http.StatusText(*httpStatus))) +} + +func main() { + flag.Parse() + + // Setup logger + logHandler := ilog.NewLogHandler(slog.Level(*logLevel), os.Stderr) + slog.SetDefault(slog.New(logHandler)) + + http.HandleFunc("/", echoHandler) + + addr := fmt.Sprintf(":%d", *port) + slog.Info("Status echo server running", slog.String("address", addr)) + + if err := http.ListenAndServe(addr, nil); err != nil { + slog.Error("Server failed to start", slog.Any("error", err)) + os.Exit(1) + } +} diff --git a/src/go/cmd/status-echo-server/main_test.go b/src/go/cmd/status-echo-server/main_test.go new file mode 100644 index 00000000..2b7589d4 --- /dev/null +++ b/src/go/cmd/status-echo-server/main_test.go @@ -0,0 +1,86 @@ +// Copyright 2026 The Cloud Robotics Authors +// +// Licensed 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 main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestEchoHandler(t *testing.T) { + tests := []struct { + desc string + path string + httpStatus int + wantStatus int + wantBody string + }{ + { + desc: "OK status on root path", + path: "/", + httpStatus: http.StatusOK, + wantStatus: http.StatusOK, + wantBody: "OK", + }, + { + desc: "Internal Server Error on nested path", + path: "/foo/bar", + httpStatus: http.StatusInternalServerError, + wantStatus: http.StatusInternalServerError, + wantBody: "Internal Server Error", + }, + { + desc: "Teapot status on query parameter path", + path: "/teapot?brew=true", + httpStatus: http.StatusTeapot, + wantStatus: http.StatusTeapot, + wantBody: "I'm a teapot", + }, + { + desc: "OK status on deep path", + path: "/a/b/c/d/e", + httpStatus: http.StatusOK, + wantStatus: http.StatusOK, + wantBody: "OK", + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + *httpStatus = tc.httpStatus + + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + w := httptest.NewRecorder() + + echoHandler(w, req) + + resp := w.Result() + if resp.StatusCode != tc.wantStatus { + t.Errorf("echoHandler returned status code %d, want %d", resp.StatusCode, tc.wantStatus) + } + + contentType := resp.Header.Get("Content-Type") + if contentType != "text/plain" { + t.Errorf("echoHandler returned Content-Type %q, want %q", contentType, "text/plain") + } + + body := w.Body.String() + if body != tc.wantBody { + t.Errorf("echoHandler returned body %q, want %q", body, tc.wantBody) + } + }) + } +} From 6af3a73d1f21af69a6b612b73378dca868399160 Mon Sep 17 00:00:00 2001 From: Desmond Ang Date: Thu, 4 Jun 2026 08:12:49 +0000 Subject: [PATCH 2/3] Update test cases --- src/go/cmd/status-echo-server/main_test.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/go/cmd/status-echo-server/main_test.go b/src/go/cmd/status-echo-server/main_test.go index 2b7589d4..4f8822c9 100644 --- a/src/go/cmd/status-echo-server/main_test.go +++ b/src/go/cmd/status-echo-server/main_test.go @@ -37,18 +37,11 @@ func TestEchoHandler(t *testing.T) { }, { desc: "Internal Server Error on nested path", - path: "/foo/bar", + path: "/foo/bar?key=value", httpStatus: http.StatusInternalServerError, wantStatus: http.StatusInternalServerError, wantBody: "Internal Server Error", }, - { - desc: "Teapot status on query parameter path", - path: "/teapot?brew=true", - httpStatus: http.StatusTeapot, - wantStatus: http.StatusTeapot, - wantBody: "I'm a teapot", - }, { desc: "OK status on deep path", path: "/a/b/c/d/e", @@ -69,17 +62,17 @@ func TestEchoHandler(t *testing.T) { resp := w.Result() if resp.StatusCode != tc.wantStatus { - t.Errorf("echoHandler returned status code %d, want %d", resp.StatusCode, tc.wantStatus) + t.Errorf("got %d, want %d", resp.StatusCode, tc.wantStatus) } contentType := resp.Header.Get("Content-Type") if contentType != "text/plain" { - t.Errorf("echoHandler returned Content-Type %q, want %q", contentType, "text/plain") + t.Errorf("got %q, want %q", contentType, "text/plain") } body := w.Body.String() if body != tc.wantBody { - t.Errorf("echoHandler returned body %q, want %q", body, tc.wantBody) + t.Errorf("got %q, want %q", body, tc.wantBody) } }) } From df7b1c5baffc02615d4e68ab359bc78f01fe138e Mon Sep 17 00:00:00 2001 From: Desmond Ang Date: Fri, 5 Jun 2026 01:11:57 +0000 Subject: [PATCH 3/3] Add default timeouts to http server --- src/go/cmd/status-echo-server/main.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/go/cmd/status-echo-server/main.go b/src/go/cmd/status-echo-server/main.go index a016b540..f65437b7 100644 --- a/src/go/cmd/status-echo-server/main.go +++ b/src/go/cmd/status-echo-server/main.go @@ -29,6 +29,7 @@ import ( "log/slog" "net/http" "os" + "time" "github.com/googlecloudrobotics/ilog" ) @@ -53,12 +54,22 @@ func main() { logHandler := ilog.NewLogHandler(slog.Level(*logLevel), os.Stderr) slog.SetDefault(slog.New(logHandler)) - http.HandleFunc("/", echoHandler) + + mux := http.NewServeMux() + mux.HandleFunc("/", echoHandler) addr := fmt.Sprintf(":%d", *port) - slog.Info("Status echo server running", slog.String("address", addr)) - if err := http.ListenAndServe(addr, nil); err != nil { + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 1 * time.Second, + WriteTimeout: 2 * time.Second, + IdleTimeout: 10 * time.Second, + } + + slog.Info("Status echo server running", slog.String("address", addr)) + if err := srv.ListenAndServe(); err != nil { slog.Error("Server failed to start", slog.Any("error", err)) os.Exit(1) }