root.go 12.2 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
package cmd
2
3

import (
4
	"os"
5
	"os/signal"
6
7
	"path/filepath"
	"strings"
8
	"syscall"
9
10

	"github.com/go-errors/errors"
11
	"github.com/mitchellh/mapstructure"
12
	irma "github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
13
	"github.com/privacybydesign/irmago/server"
Sietse Ringers's avatar
Sietse Ringers committed
14
	"github.com/privacybydesign/irmago/server/requestorserver"
15
	"github.com/sirupsen/logrus"
16
	"github.com/spf13/cast"
17
18
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
19
20
)

21
var logger = server.NewLogger(0, false, false)
Sietse Ringers's avatar
Sietse Ringers committed
22
var conf *requestorserver.Configuration
23

24
25
26
27
28
var RootCommand = &cobra.Command{
	Use:   "irmad",
	Short: "IRMA server for verifying and issuing attributes",
	Run: func(command *cobra.Command, args []string) {
		if err := configure(command); err != nil {
29
30
			die(errors.WrapPrefix(err, "Failed to read configuration", 0))
		}
Sietse Ringers's avatar
Sietse Ringers committed
31
		serv, err := requestorserver.New(conf)
32
		if err != nil {
33
34
			die(errors.WrapPrefix(err, "Failed to configure server", 0))
		}
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

		stopped := make(chan struct{})
		interrupt := make(chan os.Signal, 1)
		signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)

		go func() {
			if err := serv.Start(conf); err != nil {
				die(errors.WrapPrefix(err, "Failed to start server", 0))
			}
			conf.Logger.Debug("Server stopped")
			stopped <- struct{}{}
		}()

		for {
			select {
			case <-interrupt:
				conf.Logger.Debug("Caught interrupt")
				serv.Stop() // causes serv.Start() above to return
				conf.Logger.Debug("Sent stop signal to server")
			case <-stopped:
				conf.Logger.Info("Exiting")
				close(stopped)
				close(interrupt)
				return
			}
60
61
62
63
		}
	},
}

64
func init() {
65
	if err := setFlags(RootCommand, productionMode()); err != nil {
66
67
		die(errors.WrapPrefix(err, "Failed to attach flags to "+RootCommand.Name()+" command", 0))
	}
68
69
70
71
72
}

// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the RootCommand.
func Execute() {
73
	if err := RootCommand.Execute(); err != nil {
74
		die(errors.Wrap(err, 0))
75
	}
76
77
78
}

func die(err *errors.Error) {
79
	msg := err.Error()
80
	if logger.IsLevelEnabled(logrus.DebugLevel) {
81
82
83
		msg += "\nStack trace:\n" + string(err.Stack())
	}
	logger.Fatal(msg)
84
85
}

86
func setFlags(cmd *cobra.Command, production bool) error {
87
88
89
	flags := cmd.Flags()
	flags.SortFlags = false

90
91
92
93
94
95
96
97
98
	var defaulturl string
	var err error
	if !production {
		defaulturl, err = server.LocalIP()
		if err != nil {
			logger.Warn("Could not determine local IP address: ", err.Error())
		} else {
			defaulturl = "http://" + defaulturl + ":port"
		}
99
	}
100

101
102
	schemespath := server.DefaultSchemesPath()

103
	flags.StringP("config", "c", "", "path to configuration file")
Sietse Ringers's avatar
Sietse Ringers committed
104
105
	flags.StringP("schemes-path", "s", schemespath, "path to irma_configuration")
	flags.String("schemes-assets-path", "", "if specified, copy schemes from here into --schemes-path")
106
	flags.Int("schemes-update", 60, "update IRMA schemes every x minutes (0 to disable)")
Sietse Ringers's avatar
Sietse Ringers committed
107
	flags.StringP("privkeys", "k", "", "path to IRMA private keys")
108
109
	flags.String("static-path", "", "Host files under this path as static files (leave empty to disable)")
	flags.String("static-prefix", "/", "Host static files under this URL prefix")
110
	flags.StringP("url", "u", defaulturl, "external URL to server to which the IRMA client connects")
111
	flags.Bool("sse", false, "Enable server sent for status updates (experimental)")
112
113
114
115
116

	flags.IntP("port", "p", 8088, "port at which to listen")
	flags.StringP("listen-addr", "l", "", "address at which to listen (default 0.0.0.0)")
	flags.Int("client-port", 0, "if specified, start a separate server for the IRMA app at this port")
	flags.String("client-listen-addr", "", "address at which server for IRMA app listens")
Sietse Ringers's avatar
Sietse Ringers committed
117
	flags.Lookup("port").Header = `Server address and port to listen on`
118

119
	flags.Bool("no-auth", !production, "whether or not to authenticate requestors (and reject all authenticated requests)")
120
	flags.String("requestors", "", "requestor configuration (in JSON)")
121
122
	flags.StringSlice("disclose-perms", nil, "list of attributes that all requestors may verify (default *)")
	flags.StringSlice("sign-perms", nil, "list of attributes that all requestors may request in signatures (default *)")
123
124
125
126
127
	issHelp := "list of attributes that all requestors may issue"
	if !production {
		issHelp += " (default *)"
	}
	flags.StringSlice("issue-perms", nil, issHelp)
128
	flags.String("static-sessions", "", "preconfigured static sessions (in JSON)")
Sietse Ringers's avatar
Sietse Ringers committed
129
	flags.Lookup("no-auth").Header = `Requestor authentication and default requestor permissions`
130

131
132
	flags.StringP("jwt-issuer", "j", "irmaserver", "JWT issuer")
	flags.String("jwt-privkey", "", "JWT private key")
Sietse Ringers's avatar
Sietse Ringers committed
133
	flags.String("jwt-privkey-file", "", "path to JWT private key")
Sietse Ringers's avatar
Sietse Ringers committed
134
135
	flags.Int("max-request-age", 300, "max age in seconds of a session request JWT")
	flags.Lookup("jwt-issuer").Header = `JWT configuration`
136

137
138
	flags.String("tls-cert", "", "TLS certificate (chain)")
	flags.String("tls-cert-file", "", "path to TLS certificate (chain)")
139
	flags.String("tls-privkey", "", "TLS private key")
140
141
142
	flags.String("tls-privkey-file", "", "path to TLS private key")
	flags.String("client-tls-cert", "", "TLS certificate (chain) for IRMA app server")
	flags.String("client-tls-cert-file", "", "path to TLS certificate (chain) for IRMA app server")
143
	flags.String("client-tls-privkey", "", "TLS private key for IRMA app server")
144
	flags.String("client-tls-privkey-file", "", "path to TLS private key for IRMA app server")
145
	flags.Bool("no-tls", false, "Disable TLS")
Sietse Ringers's avatar
Sietse Ringers committed
146
	flags.Lookup("tls-cert").Header = "TLS configuration (leave empty to disable TLS)"
Sietse Ringers's avatar
Sietse Ringers committed
147

Sietse Ringers's avatar
Sietse Ringers committed
148
	flags.StringP("email", "e", "", "Email address of server admin, for incidental notifications such as breaking API changes")
149
	flags.Bool("no-email", !production, "Opt out of prodiding an email address with --email")
Sietse Ringers's avatar
Sietse Ringers committed
150
	flags.Lookup("email").Header = "Email address (see README for more info)"
Sietse Ringers's avatar
Sietse Ringers committed
151

152
	flags.CountP("verbose", "v", "verbose (repeatable)")
Sietse Ringers's avatar
Sietse Ringers committed
153
	flags.BoolP("quiet", "q", false, "quiet")
154
	flags.Bool("log-json", false, "Log in JSON format")
155
	flags.Bool("production", false, "Production mode")
156
	flags.Lookup("verbose").Header = `Other options`
157

158
159
160
161
	return nil
}

func configure(cmd *cobra.Command) error {
162
163
164
	dashReplacer := strings.NewReplacer("-", "_")
	viper.SetEnvKeyReplacer(dashReplacer)
	viper.SetFileKeyReplacer(dashReplacer)
165
166
	viper.SetEnvPrefix("IRMASERVER")
	viper.AutomaticEnv()
167
168
169
	if err := viper.BindPFlags(cmd.Flags()); err != nil {
		return err
	}
170

171
172
173
174
175
176
177
178
179
180
181
182
	// Locate and read configuration file
	confpath := viper.GetString("config")
	if confpath != "" {
		dir, file := filepath.Dir(confpath), filepath.Base(confpath)
		viper.SetConfigName(strings.TrimSuffix(file, filepath.Ext(file)))
		viper.AddConfigPath(dir)
	} else {
		viper.SetConfigName("irmaserver")
		viper.AddConfigPath(".")
		viper.AddConfigPath("/etc/irmaserver/")
		viper.AddConfigPath("$HOME/.irmaserver")
	}
Sietse Ringers's avatar
Sietse Ringers committed
183
	err := viper.ReadInConfig() // Hold error checking until we know how much of it to log
184

185
186
	// Create our logger instance
	logger = server.NewLogger(viper.GetInt("verbose"), viper.GetBool("quiet"), viper.GetBool("log-json"))
Sietse Ringers's avatar
Sietse Ringers committed
187

188
	// First log output: hello, development or production mode, log level
189
190
191
192
	mode := "development"
	if viper.GetBool("production") {
		mode = "production"
	}
193
194
195
196
197
	logger.WithFields(logrus.Fields{
		"version":   irma.Version,
		"mode":      mode,
		"verbosity": server.Verbosity(viper.GetInt("verbose")),
	}).Info("irma server running")
198

199
	// Now we finally examine and log any error from viper.ReadInConfig()
Sietse Ringers's avatar
Sietse Ringers committed
200
	if err != nil {
201
202
203
204
205
		if _, notfound := err.(viper.ConfigFileNotFoundError); notfound {
			logger.Info("No configuration file found")
		} else {
			die(errors.WrapPrefix(err, "Failed to unmarshal configuration file at "+viper.ConfigFileUsed(), 0))
		}
206
	} else {
Sietse Ringers's avatar
Sietse Ringers committed
207
		logger.Info("Config file: ", viper.ConfigFileUsed())
208
209
210
	}

	// Read configuration from flags and/or environmental variables
Sietse Ringers's avatar
Sietse Ringers committed
211
	conf = &requestorserver.Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
212
		Configuration: &server.Configuration{
213
214
215
216
217
			SchemesPath:           viper.GetString("schemes-path"),
			SchemesAssetsPath:     viper.GetString("schemes-assets-path"),
			SchemesUpdateInterval: viper.GetInt("schemes-update"),
			DisableSchemesUpdate:  viper.GetInt("schemes-update") == 0,
			IssuerPrivateKeysPath: viper.GetString("privkeys"),
218
219
220
221
222
223
224
225
226
			URL:        viper.GetString("url"),
			DisableTLS: viper.GetBool("no-tls"),
			Email:      viper.GetString("email"),
			EnableSSE:  viper.GetBool("sse"),
			Verbose:    viper.GetInt("verbose"),
			Quiet:      viper.GetBool("quiet"),
			LogJSON:    viper.GetBool("log-json"),
			Logger:     logger,
			Production: viper.GetBool("production"),
227
		},
Sietse Ringers's avatar
Sietse Ringers committed
228
		Permissions: requestorserver.Permissions{
229
230
			Disclosing: handlePermission("disclose-perms"),
			Signing:    handlePermission("sign-perms"),
231
			Issuing:    handlePermission("issue-perms"),
232
		},
233
		ListenAddress:                  viper.GetString("listen-addr"),
234
		Port:                           viper.GetInt("port"),
235
236
237
		ClientListenAddress:            viper.GetString("client-listen-addr"),
		ClientPort:                     viper.GetInt("client-port"),
		DisableRequestorAuthentication: viper.GetBool("no-auth"),
Sietse Ringers's avatar
Sietse Ringers committed
238
		Requestors:                     make(map[string]requestorserver.Requestor),
239
240
241
242
		JwtIssuer:                      viper.GetString("jwt-issuer"),
		JwtPrivateKey:                  viper.GetString("jwt-privkey"),
		JwtPrivateKeyFile:              viper.GetString("jwt-privkey-file"),
		MaxRequestAge:                  viper.GetInt("max-request-age"),
243
244
		StaticPath:                     viper.GetString("static-path"),
		StaticPrefix:                   viper.GetString("static-prefix"),
Sietse Ringers's avatar
Sietse Ringers committed
245

246
247
248
249
250
251
252
253
		TlsCertificate:           viper.GetString("tls-cert"),
		TlsCertificateFile:       viper.GetString("tls-cert-file"),
		TlsPrivateKey:            viper.GetString("tls-privkey"),
		TlsPrivateKeyFile:        viper.GetString("tls-privkey-file"),
		ClientTlsCertificate:     viper.GetString("client-tls-cert"),
		ClientTlsCertificateFile: viper.GetString("client-tls-cert-file"),
		ClientTlsPrivateKey:      viper.GetString("client-tls-privkey"),
		ClientTlsPrivateKeyFile:  viper.GetString("client-tls-privkey-file"),
254
255
	}

256
257
258
259
260
261
262
	if conf.Production {
		if !viper.GetBool("no-email") && conf.Email == "" {
			return errors.New("In production mode it is required to specify either an email address with the --email flag, or explicitly opting out with --no-email. See help or README for more info.")
		}
		if viper.GetBool("no-email") && conf.Email != "" {
			return errors.New("--no-email cannot be combined with --email")
		}
Sietse Ringers's avatar
Sietse Ringers committed
263
264
	}

265
	// Handle requestors
266
267
268
269
270
	var requestors map[string]interface{}
	if val, flagOrEnv := viper.Get("requestors").(string); !flagOrEnv || val != "" {
		if requestors, err = cast.ToStringMapE(viper.Get("requestors")); err != nil {
			return errors.WrapPrefix(err, "Failed to unmarshal requestors from flag or env var", 0)
		}
271
	}
272
	if len(requestors) > 0 {
273
274
		if err := mapstructure.Decode(requestors, &conf.Requestors); err != nil {
			return errors.WrapPrefix(err, "Failed to unmarshal requestors from config file", 0)
275
276
277
		}
	}

278
279
280
281
	if err = handleMapOrString("static-sessions", &conf.StaticSessions); err != nil {
		return err
	}

Sietse Ringers's avatar
Sietse Ringers committed
282
	logger.Debug("Done configuring")
283
284

	return nil
285
}
286

287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
func handleMapOrString(key string, dest interface{}) error {
	var m map[string]interface{}
	var err error
	if val, flagOrEnv := viper.Get(key).(string); !flagOrEnv || val != "" {
		if m, err = cast.ToStringMapE(viper.Get(key)); err != nil {
			return errors.WrapPrefix(err, "Failed to unmarshal "+key+" from flag or env var", 0)
		}
	}
	if len(m) == 0 {
		return nil
	}
	if err := mapstructure.Decode(m, dest); err != nil {
		return errors.WrapPrefix(err, "Failed to unmarshal "+key+" from config file", 0)
	}
	return nil
}

304
func handlePermission(typ string) []string {
305
	if !viper.IsSet(typ) && (!viper.GetBool("production") || typ != "issue-perms") {
306
		return []string{"*"}
307
	}
308
309
310
	perms := viper.GetStringSlice(typ)
	if perms == nil {
		return []string{}
311
	}
312
	return perms
313
}
314
315
316
317
318
319
320
321
322
323

// productionMode examines the arguments passed to the executably to see if --production is enabled.
// (This should really be done using viper, but when the help message is printed, viper is not yet
// initialized.)
func productionMode() bool {
	for i, arg := range os.Args {
		if arg == "--production" {
			if len(os.Args) == i+1 || strings.HasPrefix(os.Args[i+1], "--") {
				return true
			}
324
325
326
			if checkConfVal(os.Args[i+1]) {
				return true
			}
327
328
		}
	}
329
330
331
332
333
334

	return checkConfVal(os.Getenv("IRMASERVER_PRODUCTION"))
}
func checkConfVal(val string) bool {
	lc := strings.ToLower(val)
	return lc == "1" || lc == "true" || lc == "yes" || lc == "t"
335
}