root.go 11.4 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"
Sietse Ringers's avatar
Sietse Ringers committed
12
	"github.com/privacybydesign/irmago/server"
Sietse Ringers's avatar
Sietse Ringers committed
13
	"github.com/privacybydesign/irmago/server/requestorserver"
14
	"github.com/sirupsen/logrus"
15
	"github.com/spf13/cast"
16
17
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
18
19
)

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

23
24
25
26
27
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 {
28
29
			die(errors.WrapPrefix(err, "Failed to read configuration", 0))
		}
Sietse Ringers's avatar
Sietse Ringers committed
30
		serv, err := requestorserver.New(conf)
31
		if err != nil {
32
33
			die(errors.WrapPrefix(err, "Failed to configure server", 0))
		}
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

		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
			}
59
60
61
62
		}
	},
}

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

// 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() {
72
	if err := RootCommand.Execute(); err != nil {
73
		die(errors.Wrap(err, 0))
74
	}
75
76
77
}

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

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

89
90
91
92
93
94
95
96
97
	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"
		}
98
	}
99

100
101
	schemespath := server.DefaultSchemesPath()

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

	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
116
	flags.Lookup("port").Header = `Server address and port to listen on`
117

118
	flags.Bool("no-auth", !production, "whether or not to authenticate requestors")
119
	flags.String("requestors", "", "requestor configuration (in JSON)")
120
121
	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 *)")
122
123
124
125
126
	issHelp := "list of attributes that all requestors may issue"
	if !production {
		issHelp += " (default *)"
	}
	flags.StringSlice("issue-perms", nil, issHelp)
Sietse Ringers's avatar
Sietse Ringers committed
127
	flags.Lookup("no-auth").Header = `Requestor authentication and default requestor permissions`
128

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

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

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

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

156
157
158
159
	return nil
}

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

169
170
171
172
173
174
175
176
177
178
179
180
	// 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
181
	err := viper.ReadInConfig() // Hold error checking until we know how much of it to log
182

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

186
	// First log output: hello, development or production mode, log level
187
188
189
190
191
192
	mode := "development"
	if viper.GetBool("production") {
		mode = "production"
	}
	logger.WithField("mode", mode).WithField("verbosity", server.Verbosity(viper.GetInt("verbose"))).Info("irma server running")

193
	// Now we finally examine and log any error from viper.ReadInConfig()
Sietse Ringers's avatar
Sietse Ringers committed
194
	if err != nil {
195
196
197
198
199
		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))
		}
200
	} else {
Sietse Ringers's avatar
Sietse Ringers committed
201
		logger.Info("Config file: ", viper.ConfigFileUsed())
202
203
204
	}

	// Read configuration from flags and/or environmental variables
Sietse Ringers's avatar
Sietse Ringers committed
205
	conf = &requestorserver.Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
206
		Configuration: &server.Configuration{
207
208
209
210
211
			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"),
212
213
214
215
216
217
218
219
220
			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"),
221
		},
Sietse Ringers's avatar
Sietse Ringers committed
222
		Permissions: requestorserver.Permissions{
223
224
			Disclosing: handlePermission("disclose-perms"),
			Signing:    handlePermission("sign-perms"),
225
			Issuing:    handlePermission("issue-perms"),
226
		},
227
		ListenAddress:                  viper.GetString("listen-addr"),
228
		Port:                           viper.GetInt("port"),
229
230
231
		ClientListenAddress:            viper.GetString("client-listen-addr"),
		ClientPort:                     viper.GetInt("client-port"),
		DisableRequestorAuthentication: viper.GetBool("no-auth"),
Sietse Ringers's avatar
Sietse Ringers committed
232
		Requestors:                     make(map[string]requestorserver.Requestor),
233
234
235
236
		JwtIssuer:                      viper.GetString("jwt-issuer"),
		JwtPrivateKey:                  viper.GetString("jwt-privkey"),
		JwtPrivateKeyFile:              viper.GetString("jwt-privkey-file"),
		MaxRequestAge:                  viper.GetInt("max-request-age"),
237
238
		StaticPath:                     viper.GetString("static-path"),
		StaticPrefix:                   viper.GetString("static-prefix"),
Sietse Ringers's avatar
Sietse Ringers committed
239

240
241
242
243
244
245
246
247
		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"),
248
249
	}

250
251
252
253
254
255
256
	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
257
258
	}

259
	// Handle requestors
260
261
262
263
264
	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)
		}
265
	}
266
	if len(requestors) > 0 {
267
268
		if err := mapstructure.Decode(requestors, &conf.Requestors); err != nil {
			return errors.WrapPrefix(err, "Failed to unmarshal requestors from config file", 0)
269
270
271
		}
	}

Sietse Ringers's avatar
Sietse Ringers committed
272
	logger.Debug("Done configuring")
273
274

	return nil
275
}
276

277
func handlePermission(typ string) []string {
278
	if !viper.IsSet(typ) && (!viper.GetBool("production") || typ != "issue-perms") {
279
		return []string{"*"}
280
	}
281
282
283
	perms := viper.GetStringSlice(typ)
	if perms == nil {
		return []string{}
284
	}
285
	return perms
286
}
287
288
289
290
291
292
293
294
295
296

// 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
			}
297
298
299
			if checkConfVal(os.Args[i+1]) {
				return true
			}
300
301
		}
	}
302
303
304
305
306
307

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