server.go 9.14 KB
Newer Older
1
2
package cmd

3
4
5
6
7
8
9
10
11
import (
	"os"
	"os/signal"
	"syscall"

	"github.com/go-errors/errors"
	irma "github.com/privacybydesign/irmago"
	"github.com/privacybydesign/irmago/server"
	"github.com/privacybydesign/irmago/server/requestorserver"
12
13
	"github.com/sietseringers/cobra"
	"github.com/sietseringers/viper"
14
15
)

16
17
18
19
var (
	localIP, localIPErr = server.LocalIP()
)

20
21
22
23
var serverCmd = &cobra.Command{
	Use:   "server",
	Short: "IRMA server for verifying and issuing attributes",
	Run: func(command *cobra.Command, args []string) {
24
25
		conf, err := configureServer(command)
		if err != nil {
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
			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
			}
		}
	},
}
60
61

func init() {
62
63
64
65
66
67
68
69
	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 {
70
71
72
73
74
75
76
77
78
	cmd.SetUsageTemplate(headerFlagsTemplate)
	flagHeaders["irma server"] = map[string]string{
		"port":       "Server address and port to listen on",
		"no-auth":    "Requestor authentication and default requestor permissions",
		"jwt-issuer": "JWT configuration",
		"tls-cert":   "TLS configuration (leave empty to disable TLS)",
		"email":      "Email address (see README for more info)",
		"verbose":    "Other options",
	}
79
80
81

	var defaulturl string
	if !production {
82
83
		if localIP != "" {
			defaulturl = "http://" + localIP + ":port"
84
85
86
87
88
		}
	}

	schemespath := irma.DefaultSchemesPath()

89
90
91
	flags := cmd.Flags()
	flags.SortFlags = false

92
93
94
95
96
97
98
99
	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")
100
	flags.String("revocation-db-type", "", "database type for revocation database (supported: mysql, postgres)")
101
	flags.String("revocation-db-str", "", "connection string for revocation database")
102
103
104
105
	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)")
Bram Westerbaan's avatar
Bram Westerbaan committed
106
	flags.StringP("api-prefix", "a", "/", "prefix API endpoints with this string, e.g. POST /session becomes POST {api-prefix}/session")
107
108
109
110
111
112
113
114
115
116
117
118
	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.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)
119
	flags.StringSlice("revoke-perms", nil, "list of credentials that all requestors may revoke")
120
	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")
121
	flags.String("static-sessions", "", "preconfigured static sessions (in JSON)")
122

123
124
	flags.String("revocation-settings", "", "revocation settings (in JSON)")

125
126
127
128
	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")
129
	flags.Bool("allow-unsigned-callbacks", false, "Allow callbackUrl in session requests when no JWT privatekey is installed (potentially unsafe)")
130
	flags.Bool("augment-client-return-url", false, "Augment the client return url with the server session token if present")
131
132
133
134
135
136
137
138
139
140
141
142

	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.StringP("email", "e", "", "Email address of server admin, for incidental notifications such as breaking API changes")
143
	flags.Bool("no-email", !production, "Opt out of providing an email address with --email")
144
145
146
147
148
149
150
151
152

	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")

	return nil
}

153
func configureServer(cmd *cobra.Command) (*requestorserver.Configuration, error) {
154
155
156
157
	if localIPErr != nil {
		logger.Warn("Could not determine local IP address: ", localIPErr.Error())
	}

158
159
160
161
162
163
164
	readConfig(cmd, "irmaserver", "irma server", []string{".", "/etc/irmaserver/", "$HOME/.irmaserver"},
		map[string]interface{}{
			"no-auth":  false,
			"no-email": false,
			"url":      "",
		},
	)
165
166

	// Read configuration from flags and/or environmental variables
167
	conf := &requestorserver.Configuration{
168
		Configuration: configureIRMAServer(),
169
170
171
172
		Permissions: requestorserver.Permissions{
			Disclosing: handlePermission("disclose-perms"),
			Signing:    handlePermission("sign-perms"),
			Issuing:    handlePermission("issue-perms"),
173
			Revoking:   handlePermission("revoke-perms"),
174
		},
175
		SkipPrivateKeysCheck:           viper.GetBool("skip-private-keys-check"),
176
177
		ListenAddress:                  viper.GetString("listen-addr"),
		Port:                           viper.GetInt("port"),
Bram Westerbaan's avatar
Bram Westerbaan committed
178
		ApiPrefix:                      viper.GetString("api-prefix"),
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
		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 == "" {
199
			return nil, 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.")
200
201
		}
		if viper.GetBool("no-email") && conf.Email != "" {
202
			return nil, errors.New("--no-email cannot be combined with --email")
203
204
205
206
		}
	}

	// Handle requestors
207
	var err error
208
	if err = handleMapOrString("requestors", &conf.Requestors); err != nil {
209
		return nil, err
210
	}
211
	if err = handleMapOrString("static-sessions", &conf.StaticSessions); err != nil {
212
		return nil, err
213
	}
214
	var m map[string]*irma.RevocationSetting
215
	if err = handleMapOrString("revocation-settings", &m); err != nil {
216
		return nil, err
217
218
219
220
	}
	for i, s := range m {
		conf.RevocationSettings[irma.NewCredentialTypeIdentifier(i)] = s
	}
221
222
223

	logger.Debug("Done configuring")

224
	return conf, nil
225
}