api.go 12.5 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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
26
// Configuration contains configuration for the irmaserver library and irmad.
27
type Configuration struct {
28
	// irma_configuration. If not given, this will be popupated using SchemesPath.
29
	IrmaConfiguration *irma.Configuration `json:"-"`
30
31
32
	// 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.
33
34
35
	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"`
36
37
38
39
	// 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"`
40
	// Path to issuer private keys to parse
41
	IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
42
43
44
45
	// 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"`
Sietse Ringers's avatar
Sietse Ringers committed
46
47
48
	// (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
49
	Email string `json:"email" mapstructure:"email"`
50
51
	// Enable server sent events for status updates (experimental; tends to hang when a reverse proxy is used)
	EnableSSE bool
52
53
54
55
56
57
58
59
60

	// 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:"-"`
61
62
}

63
64
65
66
67
type SessionPackage struct {
	SessionPtr *irma.Qr `json:"sessionPtr"`
	Token      string   `json:"token"`
}

68
69
// 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.
70
type SessionResult struct {
71
72
73
74
75
76
77
	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"`
78
79
}

Sietse Ringers's avatar
Sietse Ringers committed
80
// Status is the status of an IRMA session.
81
82
83
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
84
85
86
87
88
	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
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
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
}

116
117
118
119
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

120
// RemoteError converts an error and an explaining message to an *irma.RemoteError.
121
func RemoteError(err Error, message string) *irma.RemoteError {
122
123
124
125
126
127
128
	var stack string
	Logger.WithFields(logrus.Fields{
		"status":      err.Status,
		"description": err.Description,
		"error":       err.Type,
		"message":     message,
	}).Warnf("Sending session error")
129
	if Logger.IsLevelEnabled(logrus.DebugLevel) {
130
131
132
		stack = string(debug.Stack())
		Logger.Warn(stack)
	}
133
134
135
136
137
138
139
140
141
	return &irma.RemoteError{
		Status:      err.Status,
		Description: err.Description,
		ErrorName:   string(err.Type),
		Message:     message,
		Stacktrace:  stack,
	}
}

142
143
// JsonResponse JSON-marshals the specified object or error
// and returns it along with a suitable HTTP status code
144
145
146
147
148
149
150
151
152
153
154
155
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
	}
156
	Logger.Tracef("HTTP JSON response: %d %s", status, string(b))
157
158
	return status, b
}
159

160
// WriteError writes the specified error and explaining message as JSON to the http.ResponseWriter.
161
func WriteError(w http.ResponseWriter, err Error, msg string) {
162
	WriteResponse(w, nil, RemoteError(err, msg))
163
164
}

165
// WriteJson writes the specified object as JSON to the http.ResponseWriter.
166
func WriteJson(w http.ResponseWriter, object interface{}) {
167
168
169
	WriteResponse(w, object, nil)
}

170
// WriteResponse writes the specified object or error as JSON to the http.ResponseWriter.
171
172
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
	status, bts := JsonResponse(object, rerr)
173
174
175
176
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	w.Write(bts)
}
177

178
// WriteString writes the specified string to the http.ResponseWriter.
179
func WriteString(w http.ResponseWriter, str string) {
180
	Logger.Trace("HTTP text/plain response: ", str)
181
182
183
184
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(str))
}
185

186
187
188
189
// 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.
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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")
212
	}
213
214
215
216
217
218
219
220
221
222
223
224
}

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")
225
	}
226
227
228
229
230
231
232
}

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
		}
233
	}
234
	return nil, errors.New("")
235
}
236

237
// LocalIP returns the IP address of one of the (non-loopback) network interfaces
238
func LocalIP() (string, error) {
239
	// Based on https://play.golang.org/p/BDt3qEQ_2H from https://stackoverflow.com/a/23558495
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
	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")
}

276
277
278
279
280
281
282
// 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.
283
func DefaultSchemesPath() string {
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
	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"))
301
	}
302
303
304
305
306
307
308
309
310
311
312
313
	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)
314
315
}

316
func firstExistingPath(paths []string) string {
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
	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
	}
}
336

337
338
339
340
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

341
func log(level logrus.Level, err error) error {
342
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
343
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
344
		_, _ = writer.Write([]byte(e.ErrorStack()))
345
	} else {
346
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
347
348
349
350
	}
	return err
}

351
func LogFatal(err error) error {
352
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
353
	// using log() for this doesn't seem to do anything
354
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
355
		logger.Fatal(e.ErrorStack())
356
	} else {
357
		logger.Fatalf("%s", err.Error())
358
359
	}
	return err
360
361
362
363
364
365
}

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

366
func LogWarning(err error) error {
367
	return log(logrus.WarnLevel, err)
368
369
370
371
372
373
}

func ToJson(o interface{}) string {
	bts, _ := json.Marshal(o)
	return string(bts)
}
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394

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
}