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

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

15
	"github.com/go-errors/errors"
16
	"github.com/privacybydesign/gabi"
17
	"github.com/privacybydesign/irmago"
18
	"github.com/privacybydesign/irmago/internal/fs"
19
	"github.com/sirupsen/logrus"
20
21
)

22
23
var Logger *logrus.Logger = logrus.StandardLogger()

Sietse Ringers's avatar
Sietse Ringers committed
24
// Configuration contains configuration for the irmaserver library and irmad.
25
type Configuration struct {
26
	// irma_configuration. If not given, this will be popupated using SchemesPath.
27
	IrmaConfiguration *irma.Configuration `json:"-"`
28
29
30
	// 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.
31
32
33
	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"`
34
35
36
37
	// 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"`
38
	// Path to issuer private keys to parse
39
	IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
40
41
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"`
	// Logging
	Logger *logrus.Logger `json:"-"`
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
}

52
53
// 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.
54
type SessionResult struct {
Sietse Ringers's avatar
Sietse Ringers committed
55
56
	Token       string
	Status      Status
57
	Type        irma.Action
Sietse Ringers's avatar
Sietse Ringers committed
58
59
60
61
	ProofStatus irma.ProofStatus
	Disclosed   []*irma.DisclosedAttribute
	Signature   *irma.SignedMessage
	Err         *irma.RemoteError
62
63
}

Sietse Ringers's avatar
Sietse Ringers committed
64
// Status is the status of an IRMA session.
65
66
67
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
68
69
70
71
72
	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
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
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
}

100
101
102
103
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

104
// RemoteError converts an error and an explaining message to an *irma.RemoteError.
105
func RemoteError(err Error, message string) *irma.RemoteError {
106
107
108
109
110
111
112
	var stack string
	Logger.WithFields(logrus.Fields{
		"status":      err.Status,
		"description": err.Description,
		"error":       err.Type,
		"message":     message,
	}).Warnf("Sending session error")
113
	if Logger.IsLevelEnabled(logrus.DebugLevel) {
114
115
116
		stack = string(debug.Stack())
		Logger.Warn(stack)
	}
117
118
119
120
121
122
123
124
125
	return &irma.RemoteError{
		Status:      err.Status,
		Description: err.Description,
		ErrorName:   string(err.Type),
		Message:     message,
		Stacktrace:  stack,
	}
}

126
127
// JsonResponse JSON-marshals the specified object or error
// and returns it along with a suitable HTTP status code
128
129
130
131
132
133
134
135
136
137
138
139
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
	}
140
	Logger.Tracef("HTTP JSON response: %d %s", status, string(b))
141
142
	return status, b
}
143

144
// WriteError writes the specified error and explaining message as JSON to the http.ResponseWriter.
145
func WriteError(w http.ResponseWriter, err Error, msg string) {
146
	WriteResponse(w, nil, RemoteError(err, msg))
147
148
}

149
// WriteJson writes the specified object as JSON to the http.ResponseWriter.
150
func WriteJson(w http.ResponseWriter, object interface{}) {
151
152
153
	WriteResponse(w, object, nil)
}

154
// WriteResponse writes the specified object or error as JSON to the http.ResponseWriter.
155
156
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
	status, bts := JsonResponse(object, rerr)
157
158
159
160
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	w.Write(bts)
}
161

162
// WriteString writes the specified string to the http.ResponseWriter.
163
func WriteString(w http.ResponseWriter, str string) {
164
	Logger.Trace("HTTP text/plain response: ", str)
165
166
167
168
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(str))
}
169

170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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")
192
	}
193
194
195
196
197
198
199
200
201
202
203
204
}

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")
205
	}
206
207
208
209
210
211
212
}

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
		}
213
	}
214
	return nil, errors.New("")
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

func LocalIP() (string, error) {
	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")
}

254
255
256
257
258
259
260
// 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.
261
func DefaultSchemesPath() string {
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
	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"))
279
	}
280
281
282
283
284
285
286
287
288
289
290
291
	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)
292
293
}

294
func firstExistingPath(paths []string) string {
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
	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
	}
}
314

315
316
317
318
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

319
func log(level logrus.Level, err error) error {
320
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
321
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
322
		_, _ = writer.Write([]byte(e.ErrorStack()))
323
	} else {
324
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
325
326
327
328
	}
	return err
}

329
func LogFatal(err error) error {
330
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
331
	// using log() for this doesn't seem to do anything
332
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
333
		logger.Fatal(e.ErrorStack())
334
	} else {
335
		logger.Fatalf("%s", err.Error())
336
337
	}
	return err
338
339
340
341
342
343
}

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

344
func LogWarning(err error) error {
345
	return log(logrus.WarnLevel, err)
346
347
348
349
350
351
}

func ToJson(o interface{}) string {
	bts, _ := json.Marshal(o)
	return string(bts)
}