@@ -85,6 +85,25 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
85
85
if err != nil {
86
86
return xerrors .Errorf ("getting deployment config: %w" , err )
87
87
}
88
+
89
+ // Validate bind addresses.
90
+ if cfg .Address .Value != "" {
91
+ cmd .PrintErr (cliui .Styles .Warn .Render ("WARN:" ) + " --address and -a are deprecated, please use --http-address and --tls-address instead" )
92
+ if cfg .TLS .Enable .Value {
93
+ cfg .HTTPAddress .Value = ""
94
+ cfg .TLS .Address .Value = cfg .Address .Value
95
+ } else {
96
+ cfg .HTTPAddress .Value = cfg .Address .Value
97
+ cfg .TLS .Address .Value = ""
98
+ }
99
+ }
100
+ if cfg .TLS .Enable .Value && cfg .TLS .Address .Value == "" {
101
+ return xerrors .Errorf ("TLS address must be set if TLS is enabled" )
102
+ }
103
+ if ! cfg .TLS .Enable .Value && cfg .HTTPAddress .Value == "" {
104
+ return xerrors .Errorf ("either HTTP or TLS must be enabled" )
105
+ }
106
+
88
107
printLogo (cmd )
89
108
logger := slog .Make (sloghuman .Sink (cmd .ErrOrStderr ()))
90
109
if ok , _ := cmd .Flags ().GetBool (varVerbose ); ok {
@@ -186,14 +205,41 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
186
205
}()
187
206
}
188
207
189
- listener , err := net .Listen ("tcp" , cfg .Address .Value )
190
- if err != nil {
191
- return xerrors .Errorf ("listen %q: %w" , cfg .Address .Value , err )
208
+ var (
209
+ httpListener net.Listener
210
+ httpURL * url.URL
211
+ )
212
+ if cfg .HTTPAddress .Value != "" {
213
+ httpListener , err = net .Listen ("tcp" , cfg .HTTPAddress .Value )
214
+ if err != nil {
215
+ return xerrors .Errorf ("listen %q: %w" , cfg .HTTPAddress .Value , err )
216
+ }
217
+ defer httpListener .Close ()
218
+
219
+ tcpAddr , tcpAddrValid := httpListener .Addr ().(* net.TCPAddr )
220
+ if ! tcpAddrValid {
221
+ return xerrors .Errorf ("invalid TCP address type %T" , httpListener .Addr ())
222
+ }
223
+ if tcpAddr .IP .IsUnspecified () {
224
+ tcpAddr .IP = net .IPv4 (127 , 0 , 0 , 1 )
225
+ }
226
+ httpURL = & url.URL {
227
+ Scheme : "http" ,
228
+ Host : tcpAddr .String (),
229
+ }
230
+ cmd .Println ("Started HTTP listener at " + httpURL .String ())
192
231
}
193
- defer listener .Close ()
194
232
195
- var tlsConfig * tls.Config
233
+ var (
234
+ tlsConfig * tls.Config
235
+ httpsListener net.Listener
236
+ httpsURL * url.URL
237
+ )
196
238
if cfg .TLS .Enable .Value {
239
+ if cfg .TLS .Address .Value == "" {
240
+ return xerrors .New ("tls address must be set if tls is enabled" )
241
+ }
242
+
197
243
tlsConfig , err = configureTLS (
198
244
cfg .TLS .MinVersion .Value ,
199
245
cfg .TLS .ClientAuth .Value ,
@@ -204,7 +250,38 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
204
250
if err != nil {
205
251
return xerrors .Errorf ("configure tls: %w" , err )
206
252
}
207
- listener = tls .NewListener (listener , tlsConfig )
253
+ httpsListenerInner , err := net .Listen ("tcp" , cfg .TLS .Address .Value )
254
+ if err != nil {
255
+ return xerrors .Errorf ("listen %q: %w" , cfg .TLS .Address .Value , err )
256
+ }
257
+ defer httpsListenerInner .Close ()
258
+
259
+ httpsListener = tls .NewListener (httpsListenerInner , tlsConfig )
260
+ defer httpsListener .Close ()
261
+
262
+ tcpAddr , tcpAddrValid := httpsListener .Addr ().(* net.TCPAddr )
263
+ if ! tcpAddrValid {
264
+ return xerrors .Errorf ("invalid TCP address type %T" , httpsListener .Addr ())
265
+ }
266
+ if tcpAddr .IP .IsUnspecified () {
267
+ tcpAddr .IP = net .IPv4 (127 , 0 , 0 , 1 )
268
+ }
269
+ httpsURL = & url.URL {
270
+ Scheme : "https" ,
271
+ Host : tcpAddr .String (),
272
+ }
273
+ cmd .Println ("Started TLS/HTTPS listener at " + httpsURL .String ())
274
+ }
275
+
276
+ // Sanity check that at least one listener was started.
277
+ if httpListener == nil && httpsListener == nil {
278
+ return xerrors .New ("must listen on at least one address" )
279
+ }
280
+
281
+ // Prefer HTTP because it's less prone to TLS errors over localhost.
282
+ localURL := httpsURL
283
+ if httpURL != nil {
284
+ localURL = httpURL
208
285
}
209
286
210
287
ctx , httpClient , err := configureHTTPClient (
@@ -217,24 +294,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
217
294
return xerrors .Errorf ("configure http client: %w" , err )
218
295
}
219
296
220
- tcpAddr , valid := listener .Addr ().(* net.TCPAddr )
221
- if ! valid {
222
- return xerrors .New ("must be listening on tcp" )
223
- }
224
- // If just a port is specified, assume localhost.
225
- if tcpAddr .IP .IsUnspecified () {
226
- tcpAddr .IP = net .IPv4 (127 , 0 , 0 , 1 )
227
- }
228
- // If no access URL is specified, fallback to the
229
- // bounds URL.
230
- localURL := & url.URL {
231
- Scheme : "http" ,
232
- Host : tcpAddr .String (),
233
- }
234
- if cfg .TLS .Enable .Value {
235
- localURL .Scheme = "https"
236
- }
237
-
238
297
var (
239
298
ctxTunnel , closeTunnel = context .WithCancel (ctx )
240
299
tunnel * devtunnel.Tunnel
@@ -289,6 +348,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
289
348
cmd .Printf ("%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n " , cliui .Styles .Warn .Render ("Warning:" ), cliui .Styles .Field .Render (accessURLParsed .String ()), reason )
290
349
}
291
350
351
+ // Redirect from the HTTP listener to the access URL if:
352
+ // 1. The redirect flag is enabled.
353
+ // 2. HTTP listening is enabled (obviously).
354
+ // 3. TLS is enabled (otherwise they're likely using a reverse proxy
355
+ // which can do this instead).
356
+ // 4. The access URL has been set manually (not a tunnel).
357
+ // 5. The access URL is HTTPS.
358
+ shouldRedirectHTTPToAccessURL := cfg .TLS .RedirectHTTP .Value && cfg .HTTPAddress .Value != "" && cfg .TLS .Enable .Value && tunnel == nil && accessURLParsed .Scheme == "https"
359
+
292
360
// A newline is added before for visibility in terminal output.
293
361
cmd .Printf ("\n View the Web UI: %s\n " , accessURLParsed .String ())
294
362
@@ -630,6 +698,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
630
698
defer client .HTTPClient .CloseIdleConnections ()
631
699
}
632
700
701
+ // This is helpful for tests, but can be silently ignored.
702
+ // Coder may be ran as users that don't have permission to write in the homedir,
703
+ // such as via the systemd service.
704
+ _ = config .URL ().Write (client .URL .String ())
705
+
633
706
// Since errCh only has one buffered slot, all routines
634
707
// sending on it must be wrapped in a select/default to
635
708
// avoid leaving dangling goroutines waiting for the
@@ -657,40 +730,65 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
657
730
shutdownConnsCtx , shutdownConns := context .WithCancel (ctx )
658
731
defer shutdownConns ()
659
732
660
- // ReadHeaderTimeout is purposefully not enabled. It caused some issues with
661
- // websockets over the dev tunnel.
733
+ // Wrap the server in middleware that redirects to the access URL if
734
+ // the request is not to a local IP.
735
+ var handler http.Handler = coderAPI .RootHandler
736
+ if shouldRedirectHTTPToAccessURL {
737
+ handler = redirectHTTPToAccessURL (handler , accessURLParsed )
738
+ }
739
+
740
+ // ReadHeaderTimeout is purposefully not enabled. It caused some
741
+ // issues with websockets over the dev tunnel.
662
742
// See: https://github.com/coder/coder/pull/3730
663
743
//nolint:gosec
664
- server := & http.Server {
665
- // These errors are typically noise like "TLS: EOF". Vault does similar:
744
+ httpServer := & http.Server {
745
+ // These errors are typically noise like "TLS: EOF". Vault does
746
+ // similar:
666
747
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
667
748
ErrorLog : log .New (io .Discard , "" , 0 ),
668
- Handler : coderAPI . RootHandler ,
749
+ Handler : handler ,
669
750
BaseContext : func (_ net.Listener ) context.Context {
670
751
return shutdownConnsCtx
671
752
},
672
753
}
673
754
defer func () {
674
- _ = shutdownWithTimeout (server .Shutdown , 5 * time .Second )
755
+ _ = shutdownWithTimeout (httpServer .Shutdown , 5 * time .Second )
675
756
}()
676
757
677
- eg := errgroup.Group {}
678
- eg .Go (func () error {
679
- // Make sure to close the tunnel listener if we exit so the
680
- // errgroup doesn't wait forever!
758
+ // We call this in the routine so we can kill the other listeners if
759
+ // one of them fails.
760
+ closeListenersNow := func () {
761
+ if httpListener != nil {
762
+ _ = httpListener .Close ()
763
+ }
764
+ if httpsListener != nil {
765
+ _ = httpsListener .Close ()
766
+ }
681
767
if tunnel != nil {
682
- defer tunnel .Listener .Close ()
768
+ _ = tunnel .Listener .Close ()
683
769
}
770
+ }
684
771
685
- return server .Serve (listener )
686
- })
772
+ eg := errgroup.Group {}
773
+ if httpListener != nil {
774
+ eg .Go (func () error {
775
+ defer closeListenersNow ()
776
+ return httpServer .Serve (httpListener )
777
+ })
778
+ }
779
+ if httpsListener != nil {
780
+ eg .Go (func () error {
781
+ defer closeListenersNow ()
782
+ return httpServer .Serve (httpsListener )
783
+ })
784
+ }
687
785
if tunnel != nil {
688
786
eg .Go (func () error {
689
- defer listener .Close ()
690
-
691
- return server .Serve (tunnel .Listener )
787
+ defer closeListenersNow ()
788
+ return httpServer .Serve (tunnel .Listener )
692
789
})
693
790
}
791
+
694
792
go func () {
695
793
select {
696
794
case errCh <- eg .Wait ():
@@ -718,11 +816,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
718
816
autobuildExecutor := executor .New (ctx , options .Database , logger , autobuildPoller .C )
719
817
autobuildExecutor .Run ()
720
818
721
- // This is helpful for tests, but can be silently ignored.
722
- // Coder may be ran as users that don't have permission to write in the homedir,
723
- // such as via the systemd service.
724
- _ = config .URL ().Write (client .URL .String ())
725
-
726
819
// Currently there is no way to ask the server to shut
727
820
// itself down, so any exit signal will result in a non-zero
728
821
// exit of the server.
@@ -759,7 +852,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
759
852
// in-flight requests, give in-flight requests 5 seconds to
760
853
// complete.
761
854
cmd .Println ("Shutting down API server..." )
762
- err = shutdownWithTimeout (server .Shutdown , 3 * time .Second )
855
+ err = shutdownWithTimeout (httpServer .Shutdown , 3 * time .Second )
763
856
if err != nil {
764
857
cmd .Printf ("API server shutdown took longer than 3s: %s\n " , err )
765
858
} else {
@@ -1357,3 +1450,14 @@ func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile stri
1357
1450
}
1358
1451
return ctx , & http.Client {}, nil
1359
1452
}
1453
+
1454
+ func redirectHTTPToAccessURL (handler http.Handler , accessURL * url.URL ) http.Handler {
1455
+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1456
+ if r .TLS == nil {
1457
+ http .Redirect (w , r , accessURL .String (), http .StatusTemporaryRedirect )
1458
+ return
1459
+ }
1460
+
1461
+ handler .ServeHTTP (w , r )
1462
+ })
1463
+ }
0 commit comments