@@ -2,14 +2,18 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "crypto/tls"
6
+ "crypto/x509"
5
7
"database/sql"
8
+ "encoding/pem"
6
9
"fmt"
7
10
"io/ioutil"
8
11
"net"
9
12
"net/http"
10
13
"net/url"
11
14
"os"
12
15
"os/signal"
16
+ "strconv"
13
17
"time"
14
18
15
19
"github.com/briandowns/spinner"
@@ -36,23 +40,23 @@ import (
36
40
37
41
func start () * cobra.Command {
38
42
var (
43
+ accessURL string
39
44
address string
45
+ dev bool
40
46
postgresURL string
41
47
provisionerDaemonCount uint8
42
- dev bool
48
+ tlsCertFile string
49
+ tlsClientCAFile string
50
+ tlsClientAuth string
51
+ tlsEnable bool
52
+ tlsKeyFile string
53
+ tlsMinVersion string
43
54
useTunnel bool
44
55
)
45
56
root := & cobra.Command {
46
57
Use : "start" ,
47
58
RunE : func (cmd * cobra.Command , args []string ) error {
48
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), ` ▄█▀ ▀█▄
49
- ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
50
- ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
51
- █▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
52
- ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
53
-
54
- ` )
55
-
59
+ printLogo (cmd )
56
60
if postgresURL == "" {
57
61
// Default to the environment variable!
58
62
postgresURL = os .Getenv ("CODER_PG_CONNECTION_URL" )
@@ -63,6 +67,17 @@ func start() *cobra.Command {
63
67
return xerrors .Errorf ("listen %q: %w" , address , err )
64
68
}
65
69
defer listener .Close ()
70
+
71
+ tlsConfig := & tls.Config {
72
+ MinVersion : tls .VersionTLS12 ,
73
+ }
74
+ if tlsEnable {
75
+ listener , err = configureTLS (tlsConfig , listener , tlsMinVersion , tlsClientAuth , tlsCertFile , tlsKeyFile , tlsClientCAFile )
76
+ if err != nil {
77
+ return xerrors .Errorf ("configure tls: %w" , err )
78
+ }
79
+ }
80
+
66
81
tcpAddr , valid := listener .Addr ().(* net.TCPAddr )
67
82
if ! valid {
68
83
return xerrors .New ("must be listening on tcp" )
@@ -76,7 +91,12 @@ func start() *cobra.Command {
76
91
Scheme : "http" ,
77
92
Host : tcpAddr .String (),
78
93
}
79
- accessURL := localURL
94
+ if tlsEnable {
95
+ localURL .Scheme = "https"
96
+ }
97
+ if accessURL == "" {
98
+ accessURL = localURL .String ()
99
+ }
80
100
var tunnelErr <- chan error
81
101
// If we're attempting to tunnel in dev-mode, the access URL
82
102
// needs to be changed to use the tunnel.
@@ -88,27 +108,25 @@ func start() *cobra.Command {
88
108
IsConfirm : true ,
89
109
})
90
110
if err == nil {
91
- var accessURLRaw string
92
- accessURLRaw , tunnelErr , err = tunnel .New (cmd .Context (), localURL .String ())
111
+ accessURL , tunnelErr , err = tunnel .New (cmd .Context (), localURL .String ())
93
112
if err != nil {
94
113
return xerrors .Errorf ("create tunnel: %w" , err )
95
114
}
96
- accessURL , err = url .Parse (accessURLRaw )
97
- if err != nil {
98
- return xerrors .Errorf ("parse: %w" , err )
99
- }
100
-
101
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), cliui .Styles .Paragraph .Render (cliui .Styles .Wrap .Render (cliui .Styles .Prompt .String ()+ `Tunnel started. Your deployment is accessible at:` ))+ "\n " + cliui .Styles .Field .Render (accessURL .String ()))
115
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), cliui .Styles .Paragraph .Render (cliui .Styles .Wrap .Render (cliui .Styles .Prompt .String ()+ `Tunnel started. Your deployment is accessible at:` ))+ "\n " + cliui .Styles .Field .Render (accessURL ))
102
116
}
103
117
}
104
118
validator , err := idtoken .NewValidator (cmd .Context (), option .WithoutAuthentication ())
105
119
if err != nil {
106
120
return err
107
121
}
108
122
123
+ accessURLParsed , err := url .Parse (accessURL )
124
+ if err != nil {
125
+ return xerrors .Errorf ("parse access url %q: %w" , accessURL , err )
126
+ }
109
127
logger := slog .Make (sloghuman .Sink (os .Stderr ))
110
128
options := & coderd.Options {
111
- AccessURL : accessURL ,
129
+ AccessURL : accessURLParsed ,
112
130
Logger : logger .Named ("coderd" ),
113
131
Database : databasefake .New (),
114
132
Pubsub : database .NewPubsubInMemory (),
@@ -137,6 +155,13 @@ func start() *cobra.Command {
137
155
138
156
handler , closeCoderd := coderd .New (options )
139
157
client := codersdk .New (localURL )
158
+ if tlsEnable {
159
+ // Use the TLS config here. This client is used for creating the
160
+ // default user, among other things.
161
+ client .HTTPClient .Transport = & http.Transport {
162
+ TLSClientConfig : tlsConfig ,
163
+ }
164
+ }
140
165
141
166
provisionerDaemons := make ([]* provisionerd.Server , 0 )
142
167
for i := uint8 (0 ); i < provisionerDaemonCount ; i ++ {
@@ -152,10 +177,18 @@ func start() *cobra.Command {
152
177
}
153
178
}()
154
179
155
- errCh := make (chan error )
180
+ errCh := make (chan error , 1 )
181
+ shutdownConnsCtx , shutdownConns := context .WithCancel (cmd .Context ())
182
+ defer shutdownConns ()
156
183
go func () {
157
184
defer close (errCh )
158
- errCh <- http .Serve (listener , handler )
185
+ server := http.Server {
186
+ Handler : handler ,
187
+ BaseContext : func (_ net.Listener ) context.Context {
188
+ return shutdownConnsCtx
189
+ },
190
+ }
191
+ errCh <- server .Serve (listener )
159
192
}()
160
193
161
194
config := createConfig (cmd )
@@ -271,6 +304,7 @@ func start() *cobra.Command {
271
304
}
272
305
273
306
_ , _ = fmt .Fprintf (cmd .OutOrStdout (), cliui .Styles .Prompt .String ()+ "Waiting for WebSocket connections to close...\n " )
307
+ shutdownConns ()
274
308
closeCoderd ()
275
309
return nil
276
310
},
@@ -279,11 +313,42 @@ func start() *cobra.Command {
279
313
if defaultAddress == "" {
280
314
defaultAddress = "127.0.0.1:3000"
281
315
}
282
- root .Flags ().StringVarP (& address , "address" , "a" , defaultAddress , "The address to serve the API and dashboard." )
283
- root .Flags ().BoolVarP (& dev , "dev" , "" , false , "Serve Coder in dev mode for tinkering." )
284
- root .Flags ().StringVarP (& postgresURL , "postgres-url" , "" , "" , "URL of a PostgreSQL database to connect to (defaults to $CODER_PG_CONNECTION_URL)." )
316
+ root .Flags ().StringVarP (& accessURL , "access-url" , "" , os .Getenv ("CODER_ACCESS_URL" ), "Specifies the external URL to access Coder (uses $CODER_ACCESS_URL)." )
317
+ root .Flags ().StringVarP (& address , "address" , "a" , defaultAddress , "The address to serve the API and dashboard (uses $CODER_ADDRESS)." )
318
+ defaultDev , _ := strconv .ParseBool (os .Getenv ("CODER_DEV_MODE" ))
319
+ root .Flags ().BoolVarP (& dev , "dev" , "" , defaultDev , "Serve Coder in dev mode for tinkering (uses $CODER_DEV_MODE)." )
320
+ root .Flags ().StringVarP (& postgresURL , "postgres-url" , "" , "" ,
321
+ "URL of a PostgreSQL database to connect to (defaults to $CODER_PG_CONNECTION_URL)." )
285
322
root .Flags ().Uint8VarP (& provisionerDaemonCount , "provisioner-daemons" , "" , 1 , "The amount of provisioner daemons to create on start." )
286
- root .Flags ().BoolVarP (& useTunnel , "tunnel" , "" , true , "Serve dev mode through a Cloudflare Tunnel for easy setup." )
323
+ defaultTLSEnable , _ := strconv .ParseBool (os .Getenv ("CODER_TLS_ENABLE" ))
324
+ root .Flags ().BoolVarP (& tlsEnable , "tls-enable" , "" , defaultTLSEnable , "Specifies if TLS will be enabled (uses $CODER_TLS_ENABLE)." )
325
+ root .Flags ().StringVarP (& tlsCertFile , "tls-cert-file" , "" , os .Getenv ("CODER_TLS_CERT_FILE" ),
326
+ "Specifies the path to the certificate for TLS. It requires a PEM-encoded file. " +
327
+ "To configure the listener to use a CA certificate, concatenate the primary certificate " +
328
+ "and the CA certificate together. The primary certificate should appear first in the combined file (uses $CODER_TLS_CERT_FILE)." )
329
+ root .Flags ().StringVarP (& tlsClientCAFile , "tls-client-ca-file" , "" , os .Getenv ("CODER_TLS_CLIENT_CA_FILE" ),
330
+ "PEM-encoded Certificate Authority file used for checking the authenticity of client (uses $CODER_TLS_CLIENT_CA_FILE)." )
331
+ defaultTLSClientAuth := os .Getenv ("CODER_TLS_CLIENT_AUTH" )
332
+ if defaultTLSClientAuth == "" {
333
+ defaultTLSClientAuth = "request"
334
+ }
335
+ root .Flags ().StringVarP (& tlsClientAuth , "tls-client-auth" , "" , defaultTLSClientAuth ,
336
+ `Specifies the policy the server will follow for TLS Client Authentication. ` +
337
+ `Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify" (uses $CODER_TLS_CLIENT_AUTH).` )
338
+ root .Flags ().StringVarP (& tlsKeyFile , "tls-key-file" , "" , os .Getenv ("CODER_TLS_KEY_FILE" ),
339
+ "Specifies the path to the private key for the certificate. It requires a PEM-encoded file (uses $CODER_TLS_KEY_FILE)." )
340
+ defaultTLSMinVersion := os .Getenv ("CODER_TLS_MIN_VERSION" )
341
+ if defaultTLSMinVersion == "" {
342
+ defaultTLSMinVersion = "tls12"
343
+ }
344
+ root .Flags ().StringVarP (& tlsMinVersion , "tls-min-version" , "" , defaultTLSMinVersion ,
345
+ `Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13" (uses $CODER_TLS_MIN_VERSION).` )
346
+ defaultTunnelRaw := os .Getenv ("CODER_DEV_TUNNEL" )
347
+ if defaultTunnelRaw == "" {
348
+ defaultTunnelRaw = "true"
349
+ }
350
+ defaultTunnel , _ := strconv .ParseBool (defaultTunnelRaw )
351
+ root .Flags ().BoolVarP (& useTunnel , "tunnel" , "" , defaultTunnel , "Serve dev mode through a Cloudflare Tunnel for easy setup (uses $CODER_DEV_TUNNEL)." )
287
352
_ = root .Flags ().MarkHidden ("tunnel" )
288
353
289
354
return root
@@ -346,3 +411,88 @@ func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger s
346
411
WorkDirectory : tempDir ,
347
412
}), nil
348
413
}
414
+
415
+ func printLogo (cmd * cobra.Command ) {
416
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), ` ▄█▀ ▀█▄
417
+ ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
418
+ ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
419
+ █▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
420
+ ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
421
+
422
+ ` )
423
+ }
424
+
425
+ func configureTLS (tlsConfig * tls.Config , listener net.Listener , tlsMinVersion , tlsClientAuth , tlsCertFile , tlsKeyFile , tlsClientCAFile string ) (net.Listener , error ) {
426
+ switch tlsMinVersion {
427
+ case "tls10" :
428
+ tlsConfig .MinVersion = tls .VersionTLS10
429
+ case "tls11" :
430
+ tlsConfig .MinVersion = tls .VersionTLS11
431
+ case "tls12" :
432
+ tlsConfig .MinVersion = tls .VersionTLS12
433
+ case "tls13" :
434
+ tlsConfig .MinVersion = tls .VersionTLS13
435
+ default :
436
+ return nil , xerrors .Errorf ("unrecognized tls version: %q" , tlsMinVersion )
437
+ }
438
+
439
+ switch tlsClientAuth {
440
+ case "none" :
441
+ tlsConfig .ClientAuth = tls .NoClientCert
442
+ case "request" :
443
+ tlsConfig .ClientAuth = tls .RequestClientCert
444
+ case "require-any" :
445
+ tlsConfig .ClientAuth = tls .RequireAnyClientCert
446
+ case "verify-if-given" :
447
+ tlsConfig .ClientAuth = tls .VerifyClientCertIfGiven
448
+ case "require-and-verify" :
449
+ tlsConfig .ClientAuth = tls .RequireAndVerifyClientCert
450
+ default :
451
+ return nil , xerrors .Errorf ("unrecognized tls client auth: %q" , tlsClientAuth )
452
+ }
453
+
454
+ if tlsCertFile == "" {
455
+ return nil , xerrors .New ("tls-cert-file is required when tls is enabled" )
456
+ }
457
+ if tlsKeyFile == "" {
458
+ return nil , xerrors .New ("tls-key-file is required when tls is enabled" )
459
+ }
460
+
461
+ certPEMBlock , err := os .ReadFile (tlsCertFile )
462
+ if err != nil {
463
+ return nil , xerrors .Errorf ("read file %q: %w" , tlsCertFile , err )
464
+ }
465
+ keyPEMBlock , err := os .ReadFile (tlsKeyFile )
466
+ if err != nil {
467
+ return nil , xerrors .Errorf ("read file %q: %w" , tlsKeyFile , err )
468
+ }
469
+ keyBlock , _ := pem .Decode (keyPEMBlock )
470
+ if keyBlock == nil {
471
+ return nil , xerrors .New ("decoded pem is blank" )
472
+ }
473
+ cert , err := tls .X509KeyPair (certPEMBlock , keyPEMBlock )
474
+ if err != nil {
475
+ return nil , xerrors .Errorf ("create key pair: %w" , err )
476
+ }
477
+ tlsConfig .GetCertificate = func (chi * tls.ClientHelloInfo ) (* tls.Certificate , error ) {
478
+ return & cert , nil
479
+ }
480
+
481
+ certPool := x509 .NewCertPool ()
482
+ certPool .AppendCertsFromPEM (certPEMBlock )
483
+ tlsConfig .RootCAs = certPool
484
+
485
+ if tlsClientCAFile != "" {
486
+ caPool := x509 .NewCertPool ()
487
+ data , err := ioutil .ReadFile (tlsClientCAFile )
488
+ if err != nil {
489
+ return nil , xerrors .Errorf ("read %q: %w" , tlsClientCAFile , err )
490
+ }
491
+ if ! caPool .AppendCertsFromPEM (data ) {
492
+ return nil , xerrors .Errorf ("failed to parse CA certificate in tls-client-ca-file" )
493
+ }
494
+ tlsConfig .ClientCAs = caPool
495
+ }
496
+
497
+ return tls .NewListener (listener , tlsConfig ), nil
498
+ }
0 commit comments