api.go 14.6 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
package server
2
3

import (
4
	"encoding/json"
5
	"fmt"
6
	"io/ioutil"
7
	"net"
8
	"net/http"
9
10
	"os"
	"path/filepath"
11
	"reflect"
12
	"runtime"
13
	"runtime/debug"
14
	"strings"
15
	"time"
16

17
	"github.com/go-errors/errors"
18
	"github.com/privacybydesign/gabi"
19
	"github.com/privacybydesign/irmago"
20
	"github.com/privacybydesign/irmago/internal/fs"
21
	"github.com/sirupsen/logrus"
22
	prefixed "github.com/x-cray/logrus-prefixed-formatter"
23
24
)

25
26
var Logger *logrus.Logger = logrus.StandardLogger()

Sietse Ringers's avatar
Sietse Ringers committed
27
// Configuration contains configuration for the irmaserver library and irmad.
28
type Configuration struct {
29
	// irma_configuration. If not given, this will be popupated using SchemesPath.
30
	IrmaConfiguration *irma.Configuration `json:"-"`
31
32
33
	// Path to IRMA schemes to parse into IrmaConfiguration (only used if IrmaConfiguration == nil).
	// If left empty, default value is taken using DefaultSchemesPath().
	// If an empty folder is specified, default schemes (irma-demo and pbdf) are downloaded into it.
34
35
36
	SchemesPath string `json:"schemes_path" mapstructure:"schemes_path"`
	// If specified, schemes found here are copied into SchemesPath (only used if IrmaConfiguration == nil)
	SchemesAssetsPath string `json:"schemes_assets_path" mapstructure:"schemes_assets_path"`
37
38
39
40
	// Disable scheme updating
	DisableSchemesUpdate bool `json:"disable_schemes_update" mapstructure:"disable_schemes_update"`
	// Update all schemes every x minutes (default value 0 means 60) (use DisableSchemesUpdate to disable)
	SchemesUpdateInterval int `json:"schemes_update" mapstructure:"schemes_update"`
41
	// Path to issuer private keys to parse
42
	IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
43
44
45
46
	// Issuer private keys
	IssuerPrivateKeys map[irma.IssuerIdentifier]*gabi.PrivateKey `json:"-"`
	// URL at which the IRMA app can reach this server during sessions
	URL string `json:"url" mapstructure:"url"`
47
48
49
	// Required to be set to true if URL does not begin with https:// in production mode.
	// In this case, the server would communicate with IRMA apps over plain HTTP. You must otherwise
	// ensure (using eg a reverse proxy with TLS enabled) that the attributes are protected in transit.
50
	DisableTLS bool `json:"no_tls" mapstructure:"no_tls"`
Sietse Ringers's avatar
Sietse Ringers committed
51
52
53
	// (Optional) email address of server admin, for incidental notifications such as breaking API changes
	// See https://github.com/privacybydesign/irmago/tree/master/server#specifying-an-email-address
	// for more information
54
	Email string `json:"email" mapstructure:"email"`
55
	// Enable server sent events for status updates (experimental; tends to hang when a reverse proxy is used)
56
	EnableSSE bool `json:"enable_sse" mapstructure:"enable_sse"`
57
58
59
60
61
62
63
64
65

	// Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
	Verbose int `json:"verbose" mapstructure:"verbose"`
	// Don't log anything at all
	Quiet bool `json:"quiet" mapstructure:"quiet"`
	// Output structured log in JSON format
	LogJSON bool `json:"log_json" mapstructure:"log_json"`
	// Custom logger instance. If specified, Verbose, Quiet and LogJSON are ignored.
	Logger *logrus.Logger `json:"-"`
66
67
68

	// Production mode: enables safer and stricter defaults and config checking
	Production bool `json:"production" mapstructure:"production"`
69
70
}

71
72
73
74
75
type SessionPackage struct {
	SessionPtr *irma.Qr `json:"sessionPtr"`
	Token      string   `json:"token"`
}

76
77
// SessionResult contains session information such as the session status, type, possible errors,
// and disclosed attributes or attribute-based signature if appropriate to the session type.
78
type SessionResult struct {
79
80
81
82
83
84
85
	Token       string                       `json:"token"`
	Status      Status                       `json:"status"`
	Type        irma.Action                  `json:"type"'`
	ProofStatus irma.ProofStatus             `json:"proofStatus,omitempty"`
	Disclosed   [][]*irma.DisclosedAttribute `json:"disclosed,omitempty"`
	Signature   *irma.SignedMessage          `json:"signature,omitempty"`
	Err         *irma.RemoteError            `json:"error,omitempty"`
86

87
	LegacySession bool `json:"-"` // true if request was started with legacy (i.e. pre-condiscon) session request
88
89
}

Sietse Ringers's avatar
Sietse Ringers committed
90
// Status is the status of an IRMA session.
91
92
93
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
94
95
96
97
98
	StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client
	StatusConnected   Status = "CONNECTED"   // The client has retrieved the session request, we wait for its response
	StatusCancelled   Status = "CANCELLED"   // The session is cancelled, possibly due to an error
	StatusDone        Status = "DONE"        // The session has completed successfully
	StatusTimeout     Status = "TIMEOUT"     // Session timed out
99
)
100

101
// Remove this when dropping support for legacy pre-condiscon session requests
102
103
104
105
106
107
108
109
110
type LegacySessionResult struct {
	Token       string                     `json:"token"`
	Status      Status                     `json:"status"`
	Type        irma.Action                `json:"type"`
	ProofStatus irma.ProofStatus           `json:"proofStatus,omitempty"`
	Disclosed   []*irma.DisclosedAttribute `json:"disclosed,omitempty"`
	Signature   *irma.SignedMessage        `json:"signature,omitempty"`
	Err         *irma.RemoteError          `json:"error,omitempty"`
}
111

112
113
// Remove this when dropping support for legacy pre-condiscon session requests
func (r *SessionResult) Legacy() *LegacySessionResult {
114
115
116
117
	var disclosed []*irma.DisclosedAttribute
	for _, l := range r.Disclosed {
		disclosed = append(disclosed, l[0])
	}
118
	return &LegacySessionResult{r.Token, r.Status, r.Type, r.ProofStatus, disclosed, r.Signature, r.Err}
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
func (conf *Configuration) PrivateKey(id irma.IssuerIdentifier) (sk *gabi.PrivateKey, err error) {
	sk = conf.IssuerPrivateKeys[id]
	if sk == nil {
		if sk, err = conf.IrmaConfiguration.PrivateKey(id); err != nil {
			return nil, err
		}
	}
	return sk, nil
}

func (conf *Configuration) HavePrivateKeys() (bool, error) {
	var err error
	var sk *gabi.PrivateKey
	for id := range conf.IrmaConfiguration.Issuers {
		sk, err = conf.PrivateKey(id)
		if err != nil {
			return false, err
		}
		if sk != nil {
			return true, nil
		}
	}
	return false, nil
}

146
147
148
149
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

150
// RemoteError converts an error and an explaining message to an *irma.RemoteError.
151
func RemoteError(err Error, message string) *irma.RemoteError {
152
153
154
155
156
157
158
	var stack string
	Logger.WithFields(logrus.Fields{
		"status":      err.Status,
		"description": err.Description,
		"error":       err.Type,
		"message":     message,
	}).Warnf("Sending session error")
159
	if Logger.IsLevelEnabled(logrus.DebugLevel) {
160
161
162
		stack = string(debug.Stack())
		Logger.Warn(stack)
	}
163
164
165
166
167
168
169
170
171
	return &irma.RemoteError{
		Status:      err.Status,
		Description: err.Description,
		ErrorName:   string(err.Type),
		Message:     message,
		Stacktrace:  stack,
	}
}

172
173
// JsonResponse JSON-marshals the specified object or error
// and returns it along with a suitable HTTP status code
174
175
176
177
178
179
180
181
182
183
184
185
186
187
func JsonResponse(v interface{}, err *irma.RemoteError) (int, []byte) {
	msg := v
	status := http.StatusOK
	if err != nil {
		msg = err
		status = err.Status
	}
	b, e := json.Marshal(msg)
	if e != nil {
		Logger.Error("Failed to serialize response:", e.Error())
		return http.StatusInternalServerError, nil
	}
	return status, b
}
188

189
// WriteError writes the specified error and explaining message as JSON to the http.ResponseWriter.
190
func WriteError(w http.ResponseWriter, err Error, msg string) {
191
	WriteResponse(w, nil, RemoteError(err, msg))
192
193
}

194
// WriteJson writes the specified object as JSON to the http.ResponseWriter.
195
func WriteJson(w http.ResponseWriter, object interface{}) {
196
197
198
	WriteResponse(w, object, nil)
}

199
// WriteResponse writes the specified object or error as JSON to the http.ResponseWriter.
200
201
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
	status, bts := JsonResponse(object, rerr)
202
203
204
205
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	w.Write(bts)
}
206

207
// WriteString writes the specified string to the http.ResponseWriter.
208
209
210
211
212
func WriteString(w http.ResponseWriter, str string) {
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(str))
}
213

214
215
216
217
// ParseSessionRequest attempts to parse the input as an irma.RequestorRequest instance, accepting (skipping "irma.")
//  - RequestorRequest instances directly (ServiceProviderRequest, SignatureRequestorRequest, IdentityProviderRequest)
//  - SessionRequest instances (DisclosureRequest, SignatureRequest, IssuanceRequest)
//  - JSON representations ([]byte or string) of any of the above.
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
func ParseSessionRequest(request interface{}) (irma.RequestorRequest, error) {
	switch r := request.(type) {
	case irma.RequestorRequest:
		return r, nil
	case irma.SessionRequest:
		return wrapSessionRequest(r)
	case string:
		return ParseSessionRequest([]byte(r))
	case []byte:
		var attempts = []irma.Validator{&irma.ServiceProviderRequest{}, &irma.SignatureRequestorRequest{}, &irma.IdentityProviderRequest{}}
		t, err := tryUnmarshalJson(r, attempts)
		if err == nil {
			return t.(irma.RequestorRequest), nil
		}
		attempts = []irma.Validator{&irma.DisclosureRequest{}, &irma.SignatureRequest{}, &irma.IssuanceRequest{}}
		t, err = tryUnmarshalJson(r, attempts)
		if err == nil {
			return wrapSessionRequest(t.(irma.SessionRequest))
		}
		return nil, errors.New("Failed to JSON unmarshal request bytes")
	default:
		return nil, errors.New("Invalid request type")
240
	}
241
242
243
244
245
246
247
248
249
250
251
252
}

func wrapSessionRequest(request irma.SessionRequest) (irma.RequestorRequest, error) {
	switch r := request.(type) {
	case *irma.DisclosureRequest:
		return &irma.ServiceProviderRequest{Request: r}, nil
	case *irma.SignatureRequest:
		return &irma.SignatureRequestorRequest{Request: r}, nil
	case *irma.IssuanceRequest:
		return &irma.IdentityProviderRequest{Request: r}, nil
	default:
		return nil, errors.New("Invalid session type")
253
	}
254
255
256
257
258
259
260
}

func tryUnmarshalJson(bts []byte, attempts []irma.Validator) (irma.Validator, error) {
	for _, a := range attempts {
		if err := irma.UnmarshalValidate(bts, a); err == nil {
			return a, nil
		}
261
	}
262
	return nil, errors.New("")
263
}
264

265
// LocalIP returns the IP address of one of the (non-loopback) network interfaces
266
func LocalIP() (string, error) {
267
	// Based on https://play.golang.org/p/BDt3qEQ_2H from https://stackoverflow.com/a/23558495
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
	ifaces, err := net.Interfaces()
	if err != nil {
		return "", err
	}
	for _, iface := range ifaces {
		if iface.Flags&net.FlagUp == 0 {
			continue // interface down
		}
		if iface.Flags&net.FlagLoopback != 0 {
			continue // loopback interface
		}
		addrs, err := iface.Addrs()
		if err != nil {
			return "", err
		}
		for _, addr := range addrs {
			var ip net.IP
			switch v := addr.(type) {
			case *net.IPNet:
				ip = v.IP
			case *net.IPAddr:
				ip = v.IP
			}
			if ip == nil || ip.IsLoopback() {
				continue
			}
			ip = ip.To4()
			if ip == nil {
				continue // not an ipv4 address
			}
			return ip.String(), nil
		}
	}
	return "", errors.New("No IP found")
}

304
305
306
307
308
309
310
// DefaultSchemesPath returns the default path for IRMA schemes, using XDG Base Directory Specification
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html:
//  - %LOCALAPPDATA% (i.e. C:\Users\$user\AppData\Local) if on Windows,
//  - $XDG_DATA_HOME if set, otherwise $HOME/.local/share
//  - $XDG_DATA_DIRS if set, otherwise /usr/local/share/ and /usr/share/
//  - then the OSes temp dir (os.TempDir()),
// returning the first of these that exists or can be created.
311
func DefaultSchemesPath() string {
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
	candidates := make([]string, 0, 8)
	home := os.Getenv("HOME")
	xdgDataHome := os.Getenv("XDG_DATA_HOME")
	xdgDataDirs := os.Getenv("XDG_DATA_DIRS")

	if runtime.GOOS == "windows" {
		appdata := os.Getenv("LOCALAPPDATA") // C:\Users\$user\AppData\Local
		if appdata != "" {
			candidates = append(candidates, appdata)
		}
	}

	if xdgDataHome != "" {
		candidates = append(candidates, xdgDataHome)
	}
	if xdgDataHome == "" && home != "" {
		candidates = append(candidates, filepath.Join(home, ".local", "share"))
329
	}
330
331
332
333
334
335
336
337
338
339
340
341
	if xdgDataDirs != "" {
		candidates = append(candidates, strings.Split(xdgDataDirs, ":")...)
	} else {
		candidates = append(candidates, "/usr/local/share", "/usr/share")
	}
	candidates = append(candidates, filepath.Join(os.TempDir()))

	for i := range candidates {
		candidates[i] = filepath.Join(candidates[i], "irma", "irma_configuration")
	}

	return firstExistingPath(candidates)
342
343
}

344
func firstExistingPath(paths []string) string {
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
	for _, path := range paths {
		if err := fs.EnsureDirectoryExists(path); err != nil {
			continue
		}
		return path
	}
	return ""
}

func Verbosity(level int) logrus.Level {
	switch {
	case level == 1:
		return logrus.DebugLevel
	case level > 1:
		return logrus.TraceLevel
	default:
		return logrus.InfoLevel
	}
}
364

365
366
367
368
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

369
func log(level logrus.Level, err error) error {
370
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
371
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
372
		_, _ = writer.Write([]byte(e.ErrorStack()))
373
	} else {
374
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
375
376
377
378
	}
	return err
}

379
func LogFatal(err error) error {
380
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
381
	// using log() for this doesn't seem to do anything
382
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
383
		logger.Fatal(e.ErrorStack())
384
	} else {
385
		logger.Fatalf("%s", err.Error())
386
387
	}
	return err
388
389
390
391
392
393
}

func LogError(err error) error {
	return log(logrus.ErrorLevel, err)
}

394
func LogWarning(err error) error {
395
	return log(logrus.WarnLevel, err)
396
397
}

398
func LogRequest(typ, method, url, from string, headers http.Header, message []byte) {
399
	fields := logrus.Fields{
400
401
402
403
404
405
		"type":   typ,
		"method": method,
		"url":    url,
	}
	if len(headers) > 0 {
		fields["headers"] = headers
406
	}
407
408
409
410
411
	if len(message) > 0 {
		fields["message"] = string(message)
	}
	if from != "" {
		fields["from"] = from
412
413
414
415
416
	}
	Logger.WithFields(fields).Tracef("=> request")
}

func LogResponse(status int, duration time.Duration, response []byte) {
417
	fields := logrus.Fields{
418
419
		"status":   status,
		"duration": duration.String(),
420
421
422
423
424
425
426
427
428
429
	}
	if len(response) > 0 {
		fields["response"] = string(response)
	}
	l := Logger.WithFields(fields)
	if status < 400 {
		l.Trace("<= response")
	} else {
		l.Warn("<= response")
	}
430
431
}

432
433
434
435
func ToJson(o interface{}) string {
	bts, _ := json.Marshal(o)
	return string(bts)
}
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456

func NewLogger(verbosity int, quiet bool, json bool) *logrus.Logger {
	logger := logrus.New()

	if quiet {
		logger.Out = ioutil.Discard
		return logger
	}

	logger.Level = Verbosity(verbosity)
	if json {
		logger.SetFormatter(&logrus.JSONFormatter{})
	} else {
		logger.SetFormatter(&prefixed.TextFormatter{
			FullTimestamp: true,
			DisableColors: runtime.GOOS == "windows",
		})
	}

	return logger
}