Skip to content

Commit 125fbe5

Browse files
committed
feat: adds tls configuration support and self-signed cert generation
Adds TLS configuration options to the HTTP server, allowing users to enable TLS with specified certificate and key files. Use HTTP_TLS_ENABLED, HTTP_TLS_CERT_FILE, and HTTP_TLS_KEY_FILE environment variables or corresponding config file settings to configure TLS. Setting HTTP_TLS_ENABLED to true and not specifying cert and key files will generate self-signed certificates and place them in the os.TempDir() -- see logs for file paths. Delete the files in the cert temp dir if the self-signed certs need to be regenerated. closes: #663
1 parent 59e217c commit 125fbe5

File tree

12 files changed

+590
-35
lines changed

12 files changed

+590
-35
lines changed

.env.example

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,44 @@
1-
GIN_MODE=release
1+
# Application
2+
APP_NAME=console
3+
APP_REPO=device-management-toolkit/console
4+
APP_ENCRYPTION_KEY=
5+
APP_ALLOW_INSECURE_CIPHERS=false
6+
7+
# HTTP Server
8+
HTTP_HOST=localhost
9+
HTTP_PORT=8181
10+
WS_COMPRESSION=false
11+
HTTP_ALLOWED_ORIGINS=*
12+
HTTP_ALLOWED_HEADERS=*
13+
14+
# TLS
15+
# Enable TLS in release if the app terminates TLS itself. If behind an API gateway or LB that provides TLS, set to false.
16+
HTTP_TLS_ENABLED=true
17+
# If both are empty and HTTP_TLS_ENABLED=true, the server will generate a self-signed certificate at startup.
18+
HTTP_TLS_CERT_FILE=
19+
HTTP_TLS_KEY_FILE=
20+
21+
# Logger
22+
LOG_LEVEL=info
23+
24+
# Database
25+
DB_POOL_MAX=2
26+
DB_URL=
27+
28+
# EA
29+
EA_URL=http://localhost:8000
30+
EA_USERNAME=
31+
EA_PASSWORD=
32+
33+
# Auth
34+
AUTH_DISABLED=false
35+
AUTH_ADMIN_USERNAME=standalone
36+
AUTH_ADMIN_PASSWORD=G@ppm0ym
37+
AUTH_JWT_KEY=your_secret_jwt_key
38+
AUTH_JWT_EXPIRATION=24h
39+
AUTH_REDIRECTION_JWT_EXPIRATION=5m
40+
AUTH_CLIENT_ID=
41+
AUTH_ISSUER=GIN_MODE=release
242
# DB_URL=postgres://postgresadmin:admin123@localhost:5432/rpsdb
343
# OAUTH CONFIGURATION
444
AUTH_CLIENT_ID=

cmd/app/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ func main() {
4646

4747
if os.Getenv("GIN_MODE") != "debug" {
4848
go func() {
49-
browserError := openBrowser("http://localhost:"+cfg.Port, runtime.GOOS)
49+
scheme := "http"
50+
if cfg.TLS.Enabled {
51+
scheme = "https"
52+
}
53+
54+
browserError := openBrowser(scheme+"://localhost:"+cfg.Port, runtime.GOOS)
5055
if browserError != nil {
5156
panic(browserError)
5257
}

config/config.go

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ type (
4040
AllowedOrigins []string `env-required:"true" yaml:"allowed_origins" env:"HTTP_ALLOWED_ORIGINS"`
4141
AllowedHeaders []string `env-required:"true" yaml:"allowed_headers" env:"HTTP_ALLOWED_HEADERS"`
4242
WSCompression bool `yaml:"ws_compression" env:"WS_COMPRESSION"`
43+
TLS TLS `yaml:"tls"`
44+
}
45+
46+
// TLS -.
47+
TLS struct {
48+
Enabled bool `yaml:"enabled" env:"HTTP_TLS_ENABLED"`
49+
CertFile string `yaml:"certFile" env:"HTTP_TLS_CERT_FILE"`
50+
KeyFile string `yaml:"keyFile" env:"HTTP_TLS_KEY_FILE"`
4351
}
4452

4553
// Log -.
@@ -85,10 +93,9 @@ type (
8593
}
8694
)
8795

88-
// NewConfig returns app config.
89-
func NewConfig() (*Config, error) {
90-
// set defaults
91-
ConsoleConfig = &Config{
96+
// defaultConfig constructs the in-memory default configuration.
97+
func defaultConfig() *Config {
98+
return &Config{
9299
App: App{
93100
Name: "console",
94101
Repo: "device-management-toolkit/console",
@@ -102,6 +109,11 @@ func NewConfig() (*Config, error) {
102109
AllowedOrigins: []string{"*"},
103110
AllowedHeaders: []string{"*"},
104111
WSCompression: true,
112+
TLS: TLS{
113+
Enabled: true,
114+
CertFile: "",
115+
KeyFile: "",
116+
},
105117
},
106118
Log: Log{
107119
Level: "info",
@@ -135,59 +147,86 @@ func NewConfig() (*Config, error) {
135147
},
136148
},
137149
}
150+
}
138151

139-
// Define a command line flag for the config path
140-
var configPathFlag string
141-
if flag.Lookup("config") == nil {
142-
flag.StringVar(&configPathFlag, "config", "", "path to config file")
152+
// resolveConfigPath determines the effective config file path based on a flag value or default location.
153+
func resolveConfigPath(configPathFlag string) (string, error) {
154+
if configPathFlag != "" {
155+
return configPathFlag, nil
143156
}
144157

145-
flag.Parse()
158+
ex, err := os.Executable()
159+
if err != nil {
160+
return "", err
161+
}
146162

147-
// Determine the config path
148-
var configPath string
149-
if configPathFlag != "" {
150-
configPath = configPathFlag
151-
} else {
152-
ex, err := os.Executable()
153-
if err != nil {
154-
panic(err)
155-
}
163+
exPath := filepath.Dir(ex)
156164

157-
exPath := filepath.Dir(ex)
165+
return filepath.Join(exPath, "config", "config.yml"), nil
166+
}
158167

159-
configPath = filepath.Join(exPath, "config", "config.yml")
168+
// readOrInitConfig attempts to read the config file; if it doesn't exist, writes the provided cfg to disk.
169+
func readOrInitConfig(configPath string, cfg *Config) error {
170+
err := cleanenv.ReadConfig(configPath, cfg)
171+
if err == nil {
172+
return nil
160173
}
161174

162-
err := cleanenv.ReadConfig(configPath, ConsoleConfig)
163-
164175
var pathErr *os.PathError
165-
166176
if errors.As(err, &pathErr) {
167177
// Write config file out to disk
168178
configDir := filepath.Dir(configPath)
169-
if err := os.MkdirAll(configDir, os.ModePerm); err != nil {
170-
return nil, err
179+
if mkErr := os.MkdirAll(configDir, os.ModePerm); mkErr != nil {
180+
return mkErr
171181
}
172182

173-
file, err := os.Create(configPath)
174-
if err != nil {
175-
return nil, err
183+
file, cErr := os.Create(configPath)
184+
if cErr != nil {
185+
return cErr
176186
}
177187
defer file.Close()
178188

179189
encoder := yaml.NewEncoder(file)
180190
defer encoder.Close()
181191

182-
if err := encoder.Encode(ConsoleConfig); err != nil {
183-
return nil, err
192+
if encErr := encoder.Encode(cfg); encErr != nil {
193+
return encErr
184194
}
195+
196+
return nil
185197
}
186198

187-
err = cleanenv.ReadEnv(ConsoleConfig)
199+
return err
200+
}
201+
202+
// NewConfig returns app config.
203+
func NewConfig() (*Config, error) {
204+
// set defaults
205+
ConsoleConfig = defaultConfig()
206+
207+
// Define a command line flag for the config path
208+
var configPathFlag string
209+
if flag.Lookup("config") == nil {
210+
flag.StringVar(&configPathFlag, "config", "", "path to config file")
211+
}
212+
213+
if !flag.Parsed() {
214+
flag.Parse()
215+
}
216+
217+
// Determine the config path
218+
configPath, err := resolveConfigPath(configPathFlag)
188219
if err != nil {
189220
return nil, err
190221
}
191222

223+
if err := readOrInitConfig(configPath, ConsoleConfig); err != nil {
224+
return nil, err
225+
}
226+
227+
if err := cleanenv.ReadEnv(ConsoleConfig); err != nil {
228+
return nil, err
229+
}
230+
192231
return ConsoleConfig, nil
193232
}

config/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ http:
99
port: "8181"
1010
# set to true for WAN settings where bandwidth is a concern, false for LAN/low latency/high bandwidth
1111
ws_compression: false
12+
tls:
13+
enabled: true
14+
# If certFile/keyFile are both empty and enabled is true, a self-signed certificate will be generated at runtime.
15+
certFile: ""
16+
keyFile: ""
1217
allowed_origins:
1318
- "*"
1419
allowed_headers:

config/config_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ func TestNewConfig_Defaults(t *testing.T) { //nolint:paralleltest // cannot have
3131
assert.Equal(t, "DEVELOPMENT", cfg.Version)
3232
assert.Equal(t, "test", cfg.EncryptionKey)
3333

34+
assert.Equal(t, "localhost", cfg.Host)
3435
assert.Equal(t, "8181", cfg.Port)
3536
assert.Equal(t, []string{"*"}, cfg.AllowedOrigins)
3637
assert.Equal(t, []string{"*"}, cfg.AllowedHeaders)
38+
assert.Equal(t, true, cfg.TLS.Enabled)
3739

3840
assert.Equal(t, "info", cfg.Level)
3941

@@ -47,6 +49,7 @@ func TestNewConfig_EnvVars(t *testing.T) { //nolint:paralleltest // cannot have
4749
os.Setenv("LOG_LEVEL", "debug")
4850
os.Setenv("DB_POOL_MAX", "10")
4951
os.Setenv("DB_URL", "postgres://user:password@localhost:5432/testdb")
52+
os.Setenv("HTTP_TLS_ENABLED", "false")
5053

5154
defer clearEnv() // Ensure environment variables are cleared after test
5255

@@ -60,6 +63,7 @@ func TestNewConfig_EnvVars(t *testing.T) { //nolint:paralleltest // cannot have
6063
assert.Equal(t, "debug", cfg.Level)
6164
assert.Equal(t, 10, cfg.PoolMax)
6265
assert.Equal(t, "postgres://user:password@localhost:5432/testdb", cfg.DB.URL)
66+
assert.Equal(t, false, cfg.TLS.Enabled)
6367
}
6468

6569
func TestNewConfig_FileAndEnvVars(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying environment variables

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
environment:
2424
APP_ENCRYPTION_KEY: "Jf3Q2nXJ+GZzN1dbVQms0wbB4+i/5PjL" # This is a test encryption key for the app
2525
HTTP_HOST: ""
26+
HTTP_TLS_ENABLED: "false"
2627
GIN_MODE: "debug"
2728
DB_URL: "postgres://postgresadmin:admin123@postgres:5432/rpsdb"
2829
AUTH_DISABLED: true

internal/app/app.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func Run(cfg *config.Config) {
3030
log := logger.New(cfg.Level)
3131
cfg.Version = Version
3232
log.Info("app - Run - version: " + cfg.Version)
33+
// route standard and Gin logs through our JSON logger
34+
logger.SetupStdLog(log)
35+
logger.SetupGin(log)
3336
// Repository
3437
database, err := db.New(cfg.DB.URL, sql.Open, db.MaxPoolSize(cfg.PoolMax), db.EnableForeignKeys(true))
3538
if err != nil {
@@ -73,7 +76,17 @@ func Run(cfg *config.Config) {
7376
}
7477

7578
wsv1.RegisterRoutes(handler, log, usecases.Devices, upgrader)
76-
httpServer := httpserver.New(handler, httpserver.Port(cfg.Host, cfg.Port))
79+
// Configure TLS based on config
80+
tlsEnabled := cfg.TLS.Enabled
81+
certFile := cfg.TLS.CertFile
82+
keyFile := cfg.TLS.KeyFile
83+
84+
httpServer := httpserver.New(
85+
handler,
86+
httpserver.Port(cfg.Host, cfg.Port),
87+
httpserver.TLS(tlsEnabled, certFile, keyFile),
88+
httpserver.Logger(log),
89+
)
7790

7891
// Waiting signal
7992
interrupt := make(chan os.Signal, 1)

internal/controller/http/router.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ func injectConfigToMainJS(l logger.Interface, cfg *config.Config) string {
135135
protocol = "https://"
136136
}
137137

138+
if cfg.TLS.Enabled {
139+
protocol = "https://"
140+
}
141+
138142
// if there is a clientID, we assume oauth will be configured, so inject UI config values from YAML
139143
if cfg.ClientID != "" {
140144
strictDiscoveryReplacement := ",strictDiscoveryDocumentValidation:!1"

pkg/httpserver/options.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package httpserver
33
import (
44
"net"
55
"time"
6+
7+
appLogger "github.com/device-management-toolkit/console/pkg/logger"
68
)
79

810
// Option -.
@@ -15,6 +17,22 @@ func Port(host, port string) Option {
1517
}
1618
}
1719

20+
// TLS enables TLS and optionally sets cert and key file paths.
21+
func TLS(enable bool, certFile, keyFile string) Option {
22+
return func(s *Server) {
23+
s.useTLS = enable
24+
s.certFile = certFile
25+
s.keyFile = keyFile
26+
}
27+
}
28+
29+
// Listener injects a pre-bound listener (useful for tests to avoid binding real ports).
30+
func Listener(l net.Listener) Option {
31+
return func(s *Server) {
32+
s.listener = l
33+
}
34+
}
35+
1836
// ReadTimeout -.
1937
func ReadTimeout(timeout time.Duration) Option {
2038
return func(s *Server) {
@@ -35,3 +53,10 @@ func ShutdownTimeout(timeout time.Duration) Option {
3553
s.shutdownTimeout = timeout
3654
}
3755
}
56+
57+
// Logger injects a logger to be used by the HTTP server internals.
58+
func Logger(l appLogger.Interface) Option {
59+
return func(s *Server) {
60+
s.log = l
61+
}
62+
}

0 commit comments

Comments
 (0)