File
Blob: tun/client/config_test.go
| 1 | package client |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "runtime" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/stretchr/testify/require" |
| 10 | ) |
| 11 | |
| 12 | const v1Cfg = `apex: dev.specter.dev:1234 |
| 13 | tunnels: |
| 14 | - target: tcp://127.0.0.1:1234 |
| 15 | hostname: tcp.dev.specter.dev |
| 16 | ` |
| 17 | |
| 18 | const bare = `version: 2 |
| 19 | apex: dev.specter.dev:1234 |
| 20 | tunnels: |
| 21 | - target: tcp://127.0.0.1:1234 |
| 22 | hostname: tcp.dev.specter.dev |
| 23 | proxyHeaderTimeout: 60s |
| 24 | proxyHeaderHost: blah.com |
| 25 | ` |
| 26 | |
| 27 | const bareHeader = `version: 2 |
| 28 | apex: dev.specter.dev:1234 |
| 29 | tunnels: |
| 30 | - target: tcp://127.0.0.1:1234 |
| 31 | hostname: tcp.dev.specter.dev |
| 32 | headerTimeout: 45s |
| 33 | headerHost: header.com |
| 34 | headerMode: hostname |
| 35 | ` |
| 36 | |
| 37 | const bareMixedHeaders = `version: 2 |
| 38 | apex: dev.specter.dev:1234 |
| 39 | tunnels: |
| 40 | - target: tcp://127.0.0.1:1234 |
| 41 | hostname: tcp.dev.specter.dev |
| 42 | proxyHeaderTimeout: 60s |
| 43 | proxyHeaderHost: old.com |
| 44 | proxyHeaderMode: target |
| 45 | headerTimeout: 30s |
| 46 | headerHost: new.com |
| 47 | headerMode: hostname |
| 48 | ` |
| 49 | |
| 50 | const testPrivateKey = `MC4CAQAwBQYDK2VwBCIEIFXA98L8HvJQxzyqYosZxyaX/G1vfJ4TeSP0E+N0FIfj` |
| 51 | const registered = `version: 2 |
| 52 | apex: dev.specter.dev:1234 |
| 53 | privKey: | |
| 54 | -----BEGIN PRIVATE KEY----- |
| 55 | ` + testPrivateKey + ` |
| 56 | -----END PRIVATE KEY----- |
| 57 | tunnels: |
| 58 | - target: tcp://127.0.0.1:1234 |
| 59 | hostname: tcp.dev.specter.dev |
| 60 | - target: http://127.0.0.1:2813 |
| 61 | hostname: http.dev.specter.dev |
| 62 | ` |
| 63 | |
| 64 | const namedPipe = `version: 2 |
| 65 | apex: dev.specter.dev:1234 |
| 66 | tunnels: |
| 67 | - target: \\.\pipe\something |
| 68 | hostname: pipe |
| 69 | ` |
| 70 | |
| 71 | const unixSocket = `version: 2 |
| 72 | apex: dev.specter.dev:1234 |
| 73 | tunnels: |
| 74 | - target: unix:///tmp/nginx.sock |
| 75 | hostname: unix |
| 76 | ` |
| 77 | |
| 78 | func TestConfig(t *testing.T) { |
| 79 | as := require.New(t) |
| 80 | |
| 81 | bareFile, err := os.CreateTemp("", "client") |
| 82 | as.NoError(err) |
| 83 | defer os.Remove(bareFile.Name()) |
| 84 | |
| 85 | _, err = bareFile.WriteString(bare) |
| 86 | as.NoError(err) |
| 87 | as.NoError(bareFile.Close()) |
| 88 | |
| 89 | bareCfg, err := NewConfig(bareFile.Name()) |
| 90 | as.NoError(err) |
| 91 | as.Equal("dev.specter.dev:1234", bareCfg.Apex) |
| 92 | as.NotEmpty(bareCfg.PrivKey) |
| 93 | bareCfg.buildRouter() |
| 94 | as.Equal(1, bareCfg.router.Len()) |
| 95 | route, ok := bareCfg.router.Load("tcp.dev.specter.dev") |
| 96 | as.True(ok) |
| 97 | as.Equal(time.Second*60, route.proxyHeaderReadTimeout) |
| 98 | as.Equal("blah.com", route.proxyHeaderHost) |
| 99 | |
| 100 | // new header* names only |
| 101 | headerFile, err := os.CreateTemp("", "client") |
| 102 | as.NoError(err) |
| 103 | defer os.Remove(headerFile.Name()) |
| 104 | |
| 105 | _, err = headerFile.WriteString(bareHeader) |
| 106 | as.NoError(err) |
| 107 | as.NoError(headerFile.Close()) |
| 108 | |
| 109 | headerCfg, err := NewConfig(headerFile.Name()) |
| 110 | as.NoError(err) |
| 111 | headerCfg.buildRouter() |
| 112 | as.Equal(1, headerCfg.router.Len()) |
| 113 | route, ok = headerCfg.router.Load("tcp.dev.specter.dev") |
| 114 | as.True(ok) |
| 115 | as.Equal(time.Second*45, route.proxyHeaderReadTimeout) |
| 116 | as.Equal("header.com", route.proxyHeaderHost) |
| 117 | |
| 118 | // mixed legacy and new names: new should win |
| 119 | mixedFile, err := os.CreateTemp("", "client") |
| 120 | as.NoError(err) |
| 121 | defer os.Remove(mixedFile.Name()) |
| 122 | |
| 123 | _, err = mixedFile.WriteString(bareMixedHeaders) |
| 124 | as.NoError(err) |
| 125 | as.NoError(mixedFile.Close()) |
| 126 | |
| 127 | mixedCfg, err := NewConfig(mixedFile.Name()) |
| 128 | as.NoError(err) |
| 129 | mixedCfg.buildRouter() |
| 130 | as.Equal(1, mixedCfg.router.Len()) |
| 131 | route, ok = mixedCfg.router.Load("tcp.dev.specter.dev") |
| 132 | as.True(ok) |
| 133 | // headerTimeout 30s should override proxyHeaderTimeout 60s |
| 134 | as.Equal(time.Second*30, route.proxyHeaderReadTimeout) |
| 135 | // headerHost new.com should override proxyHeaderHost old.com |
| 136 | as.Equal("new.com", route.proxyHeaderHost) |
| 137 | |
| 138 | regFile, err := os.CreateTemp("", "client") |
| 139 | as.NoError(err) |
| 140 | defer os.Remove(regFile.Name()) |
| 141 | |
| 142 | _, err = regFile.WriteString(registered) |
| 143 | as.NoError(err) |
| 144 | as.NoError(regFile.Close()) |
| 145 | |
| 146 | regCfg, err := NewConfig(regFile.Name()) |
| 147 | as.NoError(err) |
| 148 | as.Equal("dev.specter.dev:1234", regCfg.Apex) |
| 149 | as.Contains(regCfg.PrivKey, testPrivateKey) |
| 150 | regCfg.buildRouter() |
| 151 | as.Equal(2, regCfg.router.Len()) |
| 152 | |
| 153 | as.NoError(err) |
| 154 | regCfg.Tunnels = append(regCfg.Tunnels, Tunnel{ |
| 155 | Hostname: "https.dev.specter.dev", |
| 156 | Target: "https://127.0.0.1", |
| 157 | }) |
| 158 | as.NoError(regCfg.writeFile()) |
| 159 | |
| 160 | regCfg, err = NewConfig(regFile.Name()) |
| 161 | as.NoError(err) |
| 162 | regCfg.buildRouter() |
| 163 | as.Equal(3, regCfg.router.Len()) |
| 164 | } |
| 165 | |
| 166 | func TestPipeOrSocket(t *testing.T) { |
| 167 | as := require.New(t) |
| 168 | |
| 169 | pipeFile, err := os.CreateTemp("", "client") |
| 170 | as.NoError(err) |
| 171 | defer os.Remove(pipeFile.Name()) |
| 172 | |
| 173 | _, err = pipeFile.WriteString(namedPipe) |
| 174 | as.NoError(err) |
| 175 | as.NoError(pipeFile.Close()) |
| 176 | |
| 177 | pipeCfg, err := NewConfig(pipeFile.Name()) |
| 178 | if runtime.GOOS == "windows" { |
| 179 | as.NoError(err) |
| 180 | pipeCfg.buildRouter() |
| 181 | as.Equal(1, pipeCfg.router.Len()) |
| 182 | r, ok := pipeCfg.router.Load("pipe") |
| 183 | as.True(ok) |
| 184 | u := r.parsed |
| 185 | as.Equal("winio", u.Scheme) |
| 186 | as.Equal("\\\\.\\pipe\\something", u.Path) |
| 187 | } else { |
| 188 | as.Error(err) |
| 189 | } |
| 190 | |
| 191 | sockFile, err := os.CreateTemp("", "client") |
| 192 | as.NoError(err) |
| 193 | defer os.Remove(sockFile.Name()) |
| 194 | |
| 195 | _, err = sockFile.WriteString(unixSocket) |
| 196 | as.NoError(err) |
| 197 | as.NoError(sockFile.Close()) |
| 198 | |
| 199 | sockCfg, err := NewConfig(sockFile.Name()) |
| 200 | if runtime.GOOS == "windows" { |
| 201 | as.Error(err) |
| 202 | } else { |
| 203 | as.NoError(err) |
| 204 | sockCfg.buildRouter() |
| 205 | as.Equal(1, sockCfg.router.Len()) |
| 206 | r, ok := sockCfg.router.Load("unix") |
| 207 | as.True(ok) |
| 208 | u := r.parsed |
| 209 | as.Equal("unix", u.Scheme) |
| 210 | as.Equal("/tmp/nginx.sock", u.Path) |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | func TestRebuild(t *testing.T) { |
| 215 | as := require.New(t) |
| 216 | |
| 217 | regFile, err := os.CreateTemp("", "client") |
| 218 | as.NoError(err) |
| 219 | defer os.Remove(regFile.Name()) |
| 220 | |
| 221 | _, err = regFile.WriteString(registered) |
| 222 | as.NoError(err) |
| 223 | as.NoError(regFile.Close()) |
| 224 | |
| 225 | regCfg, err := NewConfig(regFile.Name()) |
| 226 | as.NoError(err) |
| 227 | as.NoError(regCfg.validate()) |
| 228 | regCfg.buildRouter() |
| 229 | as.Equal(2, regCfg.router.Len()) |
| 230 | |
| 231 | drop := 1 |
| 232 | dropTunnel := regCfg.Tunnels[drop] |
| 233 | |
| 234 | // drop |
| 235 | _, ok := regCfg.router.Load(dropTunnel.Hostname) |
| 236 | as.True(ok) |
| 237 | regCfg.Tunnels = append(regCfg.Tunnels[:drop], regCfg.Tunnels[drop+1:]...) |
| 238 | as.NoError(regCfg.validate()) |
| 239 | regCfg.buildRouter(dropTunnel) |
| 240 | as.Equal(1, regCfg.router.Len()) |
| 241 | _, ok = regCfg.router.Load(dropTunnel.Hostname) |
| 242 | as.False(ok) |
| 243 | |
| 244 | // update |
| 245 | prev := regCfg.Tunnels[0] |
| 246 | r, ok := regCfg.router.Load(prev.Hostname) |
| 247 | as.True(ok) |
| 248 | as.Equal(prev.Target, r.parsed.String()) |
| 249 | newTarget := "http://127.0.0.1:8080" |
| 250 | regCfg.Tunnels[0].Target = newTarget |
| 251 | as.NoError(regCfg.validate()) |
| 252 | regCfg.buildRouter(prev) |
| 253 | as.Equal(1, regCfg.router.Len()) |
| 254 | r, ok = regCfg.router.Load(prev.Hostname) |
| 255 | as.True(ok) |
| 256 | as.Equal(newTarget, r.parsed.String()) |
| 257 | } |
| 258 | |
| 259 | func TestV1Config(t *testing.T) { |
| 260 | as := require.New(t) |
| 261 | |
| 262 | v1File, err := os.CreateTemp("", "client") |
| 263 | as.NoError(err) |
| 264 | defer os.Remove(v1File.Name()) |
| 265 | |
| 266 | _, err = v1File.WriteString(v1Cfg) |
| 267 | as.NoError(err) |
| 268 | as.NoError(v1File.Close()) |
| 269 | |
| 270 | _, err = NewConfig(v1File.Name()) |
| 271 | as.Error(err) |
| 272 | } |
| 273 | |
| 274 | func TestProxyHeaderModeValidation(t *testing.T) { |
| 275 | as := require.New(t) |
| 276 | |
| 277 | // invalid mode |
| 278 | cfg := &Config{ |
| 279 | Tunnels: []Tunnel{{ |
| 280 | Target: "http://127.0.0.1:8080", |
| 281 | ProxyHeaderMode: "invalid", |
| 282 | }}, |
| 283 | } |
| 284 | err := cfg.validate() |
| 285 | as.Error(err) |
| 286 | |
| 287 | // custom mode without host should fail |
| 288 | cfg = &Config{ |
| 289 | Tunnels: []Tunnel{{ |
| 290 | Target: "http://127.0.0.1:8080", |
| 291 | ProxyHeaderMode: "custom", |
| 292 | }}, |
| 293 | } |
| 294 | err = cfg.validate() |
| 295 | as.Error(err) |
| 296 | |
| 297 | // target mode with pipe/unix targets should fail on all platforms |
| 298 | pipeTarget := "unix:///tmp/nginx.sock" |
| 299 | if runtime.GOOS == "windows" { |
| 300 | pipeTarget = `\\.\\pipe\\something` |
| 301 | } |
| 302 | cfg = &Config{ |
| 303 | Tunnels: []Tunnel{{ |
| 304 | Target: pipeTarget, |
| 305 | ProxyHeaderMode: "target", |
| 306 | }}, |
| 307 | } |
| 308 | err = cfg.validate() |
| 309 | as.Error(err) |
| 310 | } |