@@ -3,15 +3,17 @@ package agent
3
3
import (
4
4
"bytes"
5
5
"fmt"
6
- "github.com/hashicorp/consul/sdk/testutil/retry"
7
6
"io"
8
7
"io/ioutil"
9
8
"net/http"
10
9
"net/http/httptest"
11
10
"net/url"
12
11
"path/filepath"
12
+ "sync/atomic"
13
13
"testing"
14
14
15
+ "github.com/hashicorp/consul/sdk/testutil/retry"
16
+
15
17
"github.com/hashicorp/consul/agent/config"
16
18
"github.com/hashicorp/consul/agent/structs"
17
19
"github.com/hashicorp/consul/api"
@@ -1522,3 +1524,172 @@ func TestUIServiceTopology(t *testing.T) {
1522
1524
})
1523
1525
})
1524
1526
}
1527
+
1528
+ func TestUIEndpoint_MetricsProxy (t * testing.T ) {
1529
+ t .Parallel ()
1530
+
1531
+ var lastHeadersSent atomic.Value
1532
+
1533
+ backendH := http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1534
+ lastHeadersSent .Store (r .Header )
1535
+ if r .URL .Path == "/some/prefix/ok" {
1536
+ w .Header ().Set ("X-Random-Header" , "Foo" )
1537
+ w .Write ([]byte ("OK" ))
1538
+ return
1539
+ }
1540
+ if r .URL .Path == "/.passwd" {
1541
+ w .Write ([]byte ("SECRETS!" ))
1542
+ return
1543
+ }
1544
+ http .Error (w , "not found on backend" , http .StatusNotFound )
1545
+ })
1546
+
1547
+ backend := httptest .NewServer (backendH )
1548
+ defer backend .Close ()
1549
+
1550
+ backendURL := backend .URL + "/some/prefix"
1551
+
1552
+ // Share one agent for all these test cases. This has a few nice side-effects:
1553
+ // 1. it's cheaper
1554
+ // 2. it implicitly tests that config reloading works between cases
1555
+ //
1556
+ // Note we can't test the case where UI is disabled though as that's not
1557
+ // reloadable so we'll do that in a separate test below rather than have many
1558
+ // new tests all with a new agent. response headers also aren't reloadable
1559
+ // currently due to the way we wrap API endpoints at startup.
1560
+ a := NewTestAgent (t , `
1561
+ ui_config {
1562
+ enabled = true
1563
+ }
1564
+ http_config {
1565
+ response_headers {
1566
+ "Access-Control-Allow-Origin" = "*"
1567
+ }
1568
+ }
1569
+ ` )
1570
+ defer a .Shutdown ()
1571
+
1572
+ endpointPath := "/v1/internal/ui/metrics-proxy"
1573
+
1574
+ cases := []struct {
1575
+ name string
1576
+ config config.UIMetricsProxy
1577
+ path string
1578
+ wantCode int
1579
+ wantContains string
1580
+ wantHeaders map [string ]string
1581
+ wantHeadersSent map [string ]string
1582
+ }{
1583
+ {
1584
+ name : "disabled" ,
1585
+ config : config.UIMetricsProxy {},
1586
+ path : endpointPath + "/ok" ,
1587
+ wantCode : http .StatusNotFound ,
1588
+ },
1589
+ {
1590
+ name : "basic proxying" ,
1591
+ config : config.UIMetricsProxy {
1592
+ BaseURL : backendURL ,
1593
+ },
1594
+ path : endpointPath + "/ok" ,
1595
+ wantCode : http .StatusOK ,
1596
+ wantContains : "OK" ,
1597
+ wantHeaders : map [string ]string {
1598
+ "X-Random-Header" : "Foo" ,
1599
+ },
1600
+ },
1601
+ {
1602
+ name : "404 on backend" ,
1603
+ config : config.UIMetricsProxy {
1604
+ BaseURL : backendURL ,
1605
+ },
1606
+ path : endpointPath + "/random-path" ,
1607
+ wantCode : http .StatusNotFound ,
1608
+ wantContains : "not found on backend" ,
1609
+ },
1610
+ {
1611
+ // Note that this case actually doesn't exercise our validation logic at
1612
+ // all since the top level API mux resolves this to /v1/internal/.passwd
1613
+ // and it never hits our handler at all. I left it in though as this
1614
+ // wasn't obvious and it's worth knowing if we change something in our mux
1615
+ // that might affect path traversal opportunity here. In fact this makes
1616
+ // our path traversal handling somewhat redundant because any traversal
1617
+ // that goes "back" far enough to traverse up from the BaseURL of the
1618
+ // proxy target will in fact miss our handler entirely. It's still better
1619
+ // to be safe than sorry though.
1620
+ name : "path traversal should fail - api mux" ,
1621
+ config : config.UIMetricsProxy {
1622
+ BaseURL : backendURL ,
1623
+ },
1624
+ path : endpointPath + "/../../.passwd" ,
1625
+ wantCode : http .StatusMovedPermanently ,
1626
+ wantContains : "Moved Permanently" ,
1627
+ },
1628
+ {
1629
+ name : "adding auth header" ,
1630
+ config : config.UIMetricsProxy {
1631
+ BaseURL : backendURL ,
1632
+ AddHeaders : []config.UIMetricsProxyAddHeader {
1633
+ {
1634
+ Name : "Authorization" ,
1635
+ Value : "SECRET_KEY" ,
1636
+ },
1637
+ {
1638
+ Name : "X-Some-Other-Header" ,
1639
+ Value : "foo" ,
1640
+ },
1641
+ },
1642
+ },
1643
+ path : endpointPath + "/ok" ,
1644
+ wantCode : http .StatusOK ,
1645
+ wantContains : "OK" ,
1646
+ wantHeaders : map [string ]string {
1647
+ "X-Random-Header" : "Foo" ,
1648
+ },
1649
+ wantHeadersSent : map [string ]string {
1650
+ "X-Some-Other-Header" : "foo" ,
1651
+ "Authorization" : "SECRET_KEY" ,
1652
+ },
1653
+ },
1654
+ }
1655
+
1656
+ for _ , tc := range cases {
1657
+ tc := tc
1658
+ t .Run (tc .name , func (t * testing.T ) {
1659
+ // Reload the agent config with the desired UI config by making a copy and
1660
+ // using internal reload.
1661
+ cfg := * a .Agent .config
1662
+
1663
+ // Modify the UIConfig part (this is a copy remember and that struct is
1664
+ // not a pointer)
1665
+ cfg .UIConfig .MetricsProxy = tc .config
1666
+
1667
+ require .NoError (t , a .Agent .reloadConfigInternal (& cfg ))
1668
+
1669
+ // Now fetch the API handler to run requests against
1670
+ h := a .srv .handler (true )
1671
+
1672
+ req := httptest .NewRequest ("GET" , tc .path , nil )
1673
+ rec := httptest .NewRecorder ()
1674
+
1675
+ h .ServeHTTP (rec , req )
1676
+
1677
+ require .Equal (t , tc .wantCode , rec .Code ,
1678
+ "Wrong status code. Body = %s" , rec .Body .String ())
1679
+ require .Contains (t , rec .Body .String (), tc .wantContains )
1680
+ for k , v := range tc .wantHeaders {
1681
+ // Headers are a slice of values, just assert that one of the values is
1682
+ // the one we want.
1683
+ require .Contains (t , rec .Result ().Header [k ], v )
1684
+ }
1685
+ if len (tc .wantHeadersSent ) > 0 {
1686
+ headersSent , ok := lastHeadersSent .Load ().(http.Header )
1687
+ require .True (t , ok , "backend not called" )
1688
+ for k , v := range tc .wantHeadersSent {
1689
+ require .Contains (t , headersSent [k ], v ,
1690
+ "header %s doesn't have the right value set" , k )
1691
+ }
1692
+ }
1693
+ })
1694
+ }
1695
+ }
0 commit comments