Files

143 lines
5.5 KiB
Go

package nginxconfig
import (
"regexp"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestRenderAllowlistAndHTTP01(t *testing.T) {
routes := gin.RoutesInfo{
{Method: "GET", Path: "/api/health"},
{Method: "GET", Path: "/api/tunnels/:id/status"},
{Method: "POST", Path: "/api/tunnels/:id/start"},
{Method: "PUT", Path: "/api/tunnels/:id/start"},
}
httpSite, httpsSite, err := Render("test-link.appx365.com", routes)
if err != nil {
t.Fatal(err)
}
for _, fragment := range []string{"server_name test-link.appx365.com;", "location ^~ /.well-known/acme-challenge/", "try_files $uri =404;", "location / { return 444; }"} {
if !strings.Contains(httpSite, fragment) {
t.Errorf("missing HTTP rule %q", fragment)
}
}
for _, fragment := range []string{
"ssl_reject_handshake on;", "location = /api/health",
"if ($host != test-link.appx365.com) { return 444; }",
"location ~ ^/api/tunnels/[^/]+/status$",
"location ~ ^/api/tunnels/[^/]+/start$",
"location = /login", "location = /settings", "location = /identity", "location = /plugins", "location = /nodes", "location = /favicon.svg",
"location / { return 444; }",
} {
if !strings.Contains(httpsSite, fragment) {
t.Errorf("missing HTTPS rule %q", fragment)
}
}
if strings.Count(httpsSite, "location ~ ^/api/tunnels/[^/]+/start$") != 1 {
t.Fatal("duplicate API paths must have one location")
}
pattern := regexp.MustCompile(routePattern("/api/tunnels/:id/status"))
for _, input := range []string{"/api/tunnels/4/status", "/api/tunnels/abc/status"} {
if !pattern.MatchString(input) {
t.Errorf("valid route rejected: %s", input)
}
}
for _, input := range []string{"/api/tunnels/4/status/evil", "/api/tunnels/4/config", "/.well-known/openid-configuration"} {
if pattern.MatchString(input) {
t.Errorf("unknown route accepted: %s", input)
}
}
}
func TestRenderRejectsInjectedDomainAndRoute(t *testing.T) {
if _, _, err := Render("example.com; return 200", nil); err == nil {
t.Fatal("injected domain accepted")
}
_, httpsSite, err := Render("example.com", gin.RoutesInfo{{Path: "/api/health\nreturn 200;"}})
if err != nil {
t.Fatal(err)
}
if strings.Contains(httpsSite, "return 200;") {
t.Fatal("injected route accepted")
}
}
func TestRenderIdentityAndSingleAuthPort(t *testing.T) {
_, site, err := RenderWithAuth("vpn.example.com", gin.RoutesInfo{
{Method: "GET", Path: "/api/health"},
{Method: "PUT", Path: "/api/system/tls/domain"},
{Method: "PUT", Path: "/api/system/identity"},
{Method: "POST", Path: "/api/system/identity/gateway"},
}, 19090)
if err != nil {
t.Fatal(err)
}
for _, route := range []string{"location = /id/.well-known/openid-configuration", "location = /id/feishu/callback", "location = /id/token", "location = /api/system/tls/domain", "location = /api/system/identity", "location = /api/system/identity/gateway", "location ^~ /oauth2/", "proxy_pass http://127.0.0.1:19090;", "location / { return 444; }"} {
if !strings.Contains(site, route) {
t.Errorf("identity route missing: %s", route)
}
}
if strings.Contains(site, "location ^~ /id/") || strings.Contains(site, "location ^~ /.well-known/") {
t.Fatal("identity routes must stay allowlisted")
}
if _, _, err := RenderWithAuth("vpn.example.com", nil, 80); err == nil {
t.Fatal("invalid auth port accepted")
}
_, withoutAuth, err := Render("vpn.example.com", nil)
if err != nil || strings.Contains(withoutAuth, "location ^~ /oauth2/") {
t.Fatal("auth component exposed without a configured port")
}
}
func TestRenderHTTPCustomPort(t *testing.T) {
routes := gin.RoutesInfo{{Method: "GET", Path: "/api/health"}}
httpSite, httpsSite, err := RenderWithOptions(RenderOptions{Domain: "vpn.example.com", Scheme: "http", Port: 8080}, routes, 0)
if err != nil {
t.Fatal(err)
}
if httpsSite != "" {
t.Fatalf("http scheme must not render https site, got %d bytes", len(httpsSite))
}
for _, fragment := range []string{"listen 8080;", "server_name vpn.example.com;", "X-Forwarded-Proto http;", "location = /api/health"} {
if !strings.Contains(httpSite, fragment) {
t.Errorf("missing HTTP rule %q in:\n%s", fragment, httpSite)
}
}
if strings.Contains(httpSite, "acme-challenge") {
t.Fatal("http entry must not include ACME challenge location")
}
}
func TestRenderIPv4HTTPAndHTTPS(t *testing.T) {
opts := RenderOptions{Domain: "121.4.111.208", Scheme: "http", Port: 28080}
site, httpsSite, err := RenderWithOptions(opts, nil, 0)
if err != nil || httpsSite != "" || !strings.Contains(site, "listen 28080;") || !strings.Contains(site, "server_name 121.4.111.208;") {
t.Fatalf("HTTP IPv4 rendering failed: %v, %q, %q", err, site, httpsSite)
}
opts.Scheme = "https"
httpSite, httpsSite, err := RenderWithOptions(opts, nil, 0)
if err != nil || !strings.Contains(httpSite, "server_name 121.4.111.208;") || !strings.Contains(httpsSite, "listen 28080 ssl;") {
t.Fatalf("HTTPS IPv4 rendering failed: %v", err)
}
}
func TestRenderHTTPSCustomPort(t *testing.T) {
routes := gin.RoutesInfo{{Method: "GET", Path: "/api/health"}}
httpSite, httpsSite, err := RenderWithOptions(RenderOptions{Domain: "vpn.example.com", Scheme: "https", Port: 8443}, routes, 0)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(httpSite, "listen 80;") || !strings.Contains(httpSite, "acme-challenge") {
t.Fatalf("https custom port must keep :80 ACME http site:\n%s", httpSite)
}
if !strings.Contains(httpsSite, "listen 8443 ssl;") {
t.Fatalf("https custom port listen missing:\n%s", httpsSite)
}
if !strings.Contains(httpsSite, "X-Forwarded-Proto https;") {
t.Fatal("missing forwarded proto https")
}
}