/
server.go
333 lines (297 loc) · 12.7 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package cmd
import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/go-errors/errors"
irma "github.com/markuskreukniet/irmago-measurements"
"github.com/markuskreukniet/irmago-measurements/server"
"github.com/markuskreukniet/irmago-measurements/server/requestorserver"
"github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var conf *requestorserver.Configuration
var serverCmd = &cobra.Command{
Use: "server",
Short: "IRMA server for verifying and issuing attributes",
Run: func(command *cobra.Command, args []string) {
if err := configureServer(command); err != nil {
die("", errors.WrapPrefix(err, "Failed to read configuration", 0))
}
serv, err := requestorserver.New(conf)
if err != nil {
die("", errors.WrapPrefix(err, "Failed to configure server", 0))
}
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
}
}
},
}
func init() {
RootCmd.AddCommand(serverCmd)
if err := setFlags(serverCmd, productionMode()); err != nil {
die("", errors.WrapPrefix(err, "Failed to attach flags to "+serverCmd.Name()+" command", 0))
}
}
func setFlags(cmd *cobra.Command, production bool) error {
flags := cmd.Flags()
flags.SortFlags = false
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"
}
}
schemespath := irma.DefaultSchemesPath()
flags.StringP("config", "c", "", "path to configuration file")
flags.StringP("schemes-path", "s", schemespath, "path to irma_configuration")
flags.String("schemes-assets-path", "", "if specified, copy schemes from here into --schemes-path")
flags.Int("schemes-update", 60, "update IRMA schemes every x minutes (0 to disable)")
flags.StringP("privkeys", "k", "", "path to IRMA private keys")
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")
flags.StringP("url", "u", defaulturl, "external URL to server to which the IRMA client connects, \":port\" being replaced by --port value")
flags.String("revocation-db-type", "", "database type for revocation database (supported: mysql, postgres)")
flags.String("revocation-db-str", "", "connection string for revocation database")
flags.Bool("sse", false, "Enable server sent for status updates (experimental)")
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")
flags.Lookup("port").Header = `Server address and port to listen on`
flags.Bool("no-auth", !production, "whether or not to authenticate requestors (and reject all authenticated requests)")
flags.String("requestors", "", "requestor configuration (in JSON)")
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 *)")
issHelp := "list of attributes that all requestors may issue"
if !production {
issHelp += " (default *)"
}
flags.StringSlice("issue-perms", nil, issHelp)
flags.StringSlice("revoke-perms", nil, "list of credentials that all requestors may revoke")
flags.Bool("skip-private-keys-check", false, "whether or not to skip checking whether the private keys that requestors have permission for using are present in the configuration")
flags.String("static-sessions", "", "preconfigured static sessions (in JSON)")
flags.Lookup("no-auth").Header = `Requestor authentication and default requestor permissions`
flags.String("revocation-settings", "", "revocation settings (in JSON)")
flags.StringP("jwt-issuer", "j", "irmaserver", "JWT issuer")
flags.String("jwt-privkey", "", "JWT private key")
flags.String("jwt-privkey-file", "", "path to JWT private key")
flags.Int("max-request-age", 300, "max age in seconds of a session request JWT")
flags.Lookup("jwt-issuer").Header = `JWT configuration`
flags.String("tls-cert", "", "TLS certificate (chain)")
flags.String("tls-cert-file", "", "path to TLS certificate (chain)")
flags.String("tls-privkey", "", "TLS private key")
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")
flags.String("client-tls-privkey", "", "TLS private key for IRMA app server")
flags.String("client-tls-privkey-file", "", "path to TLS private key for IRMA app server")
flags.Bool("no-tls", false, "Disable TLS")
flags.Lookup("tls-cert").Header = "TLS configuration (leave empty to disable TLS)"
flags.StringP("email", "e", "", "Email address of server admin, for incidental notifications such as breaking API changes")
flags.Bool("no-email", !production, "Opt out of prodiding an email address with --email")
flags.Lookup("email").Header = "Email address (see README for more info)"
flags.CountP("verbose", "v", "verbose (repeatable)")
flags.BoolP("quiet", "q", false, "quiet")
flags.Bool("log-json", false, "Log in JSON format")
flags.Bool("production", false, "Production mode")
flags.Lookup("verbose").Header = `Other options`
return nil
}
func configureServer(cmd *cobra.Command) error {
dashReplacer := strings.NewReplacer("-", "_")
viper.SetEnvKeyReplacer(dashReplacer)
viper.SetFileKeyReplacer(dashReplacer)
viper.SetEnvPrefix("IRMASERVER")
viper.AutomaticEnv()
if err := viper.BindPFlags(cmd.Flags()); err != nil {
return err
}
// 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")
}
err := viper.ReadInConfig() // Hold error checking until we know how much of it to log
// Create our logger instance
logger = server.NewLogger(viper.GetInt("verbose"), viper.GetBool("quiet"), viper.GetBool("log-json"))
// First log output: hello, development or production mode, log level
mode := "development"
if viper.GetBool("production") {
mode = "production"
}
logger.WithFields(logrus.Fields{
"version": irma.Version,
"mode": mode,
"verbosity": server.Verbosity(viper.GetInt("verbose")),
}).Info("irma server running")
// Now we finally examine and log any error from viper.ReadInConfig()
if err != nil {
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))
}
} else {
logger.Info("Config file: ", viper.ConfigFileUsed())
}
// Read configuration from flags and/or environmental variables
conf = &requestorserver.Configuration{
Configuration: &server.Configuration{
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"),
RevocationDBType: viper.GetString("revocation-db-type"),
RevocationDBConnStr: viper.GetString("revocation-db-str"),
RevocationSettings: irma.RevocationSettings{},
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"),
JwtIssuer: viper.GetString("jwt-issuer"),
JwtPrivateKey: viper.GetString("jwt-privkey"),
JwtPrivateKeyFile: viper.GetString("jwt-privkey-file"),
},
Permissions: requestorserver.Permissions{
Disclosing: handlePermission("disclose-perms"),
Signing: handlePermission("sign-perms"),
Issuing: handlePermission("issue-perms"),
Revoking: handlePermission("revoke-perms"),
},
SkipPrivateKeysCheck: viper.GetBool("skip-private-keys-check"),
ListenAddress: viper.GetString("listen-addr"),
Port: viper.GetInt("port"),
ClientListenAddress: viper.GetString("client-listen-addr"),
ClientPort: viper.GetInt("client-port"),
DisableRequestorAuthentication: viper.GetBool("no-auth"),
Requestors: make(map[string]requestorserver.Requestor),
MaxRequestAge: viper.GetInt("max-request-age"),
StaticPath: viper.GetString("static-path"),
StaticPrefix: viper.GetString("static-prefix"),
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"),
}
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")
}
}
// Handle requestors
if err = handleMapOrString("requestors", &conf.Requestors); err != nil {
return err
}
if err = handleMapOrString("static-sessions", &conf.StaticSessions); err != nil {
return err
}
var m map[string]*irma.RevocationSetting
if err = handleMapOrString("revocation-settings", &m); err != nil {
return err
}
for i, s := range m {
conf.RevocationSettings[irma.NewCredentialTypeIdentifier(i)] = s
}
logger.Debug("Done configuring")
return nil
}
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
}
func handlePermission(typ string) []string {
if !viper.IsSet(typ) {
if typ == "revoke-perms" || (viper.GetBool("production") && typ == "issue-perms") {
return []string{}
} else {
return []string{"*"}
}
}
perms := viper.GetStringSlice(typ)
if perms == nil {
return []string{}
}
return perms
}
// 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
}
if checkConfVal(os.Args[i+1]) {
return true
}
}
}
return checkConfVal(os.Getenv("IRMASERVER_PRODUCTION"))
}
func checkConfVal(val string) bool {
lc := strings.ToLower(val)
return lc == "1" || lc == "true" || lc == "yes" || lc == "t"
}