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

import (
4
5
	"bytes"
	"encoding/gob"
6
	"encoding/json"
7
	"fmt"
8
	"io/ioutil"
9
	"net"
10
	"net/http"
11
	"reflect"
12
	"runtime"
13
	"runtime/debug"
14
	"time"
15

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

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

24
25
26
27
28
type SessionPackage struct {
	SessionPtr *irma.Qr `json:"sessionPtr"`
	Token      string   `json:"token"`
}

29
30
// 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.
31
type SessionResult struct {
32
33
34
35
36
37
38
	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"`
39

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

Sietse Ringers's avatar
Sietse Ringers committed
43
// Status is the status of an IRMA session.
44
45
46
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
47
48
49
50
51
	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
52
)
53

54
// Remove this when dropping support for legacy pre-condiscon session requests
55
56
57
58
59
60
61
62
63
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"`
}
64

65
66
// Remove this when dropping support for legacy pre-condiscon session requests
func (r *SessionResult) Legacy() *LegacySessionResult {
67
68
69
70
	var disclosed []*irma.DisclosedAttribute
	for _, l := range r.Disclosed {
		disclosed = append(disclosed, l[0])
	}
71
	return &LegacySessionResult{r.Token, r.Status, r.Type, r.ProofStatus, disclosed, r.Signature, r.Err}
72
73
}

74
75
76
77
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

78
// RemoteError converts an error and an explaining message to an *irma.RemoteError.
79
func RemoteError(err Error, message string) *irma.RemoteError {
80
81
82
83
84
85
86
	var stack string
	Logger.WithFields(logrus.Fields{
		"status":      err.Status,
		"description": err.Description,
		"error":       err.Type,
		"message":     message,
	}).Warnf("Sending session error")
87
	if Logger.IsLevelEnabled(logrus.DebugLevel) {
88
89
90
		stack = string(debug.Stack())
		Logger.Warn(stack)
	}
91
92
93
94
95
96
97
98
99
	return &irma.RemoteError{
		Status:      err.Status,
		Description: err.Description,
		ErrorName:   string(err.Type),
		Message:     message,
		Stacktrace:  stack,
	}
}

100
101
// JsonResponse JSON-marshals the specified object or error
// and returns it along with a suitable HTTP status code
102
func JsonResponse(v interface{}, err *irma.RemoteError) (int, []byte) {
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
	return encodeValOrError(v, err, json.Marshal)
}

func GobResponse(v interface{}, err *irma.RemoteError) (int, []byte) {
	return encodeValOrError(v, err, gobMarshal)
}

func gobMarshal(v interface{}) ([]byte, error) {
	var b bytes.Buffer
	if err := gob.NewEncoder(&b).Encode(v); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

func encodeValOrError(v interface{}, err *irma.RemoteError, encoder func(interface{}) ([]byte, error)) (int, []byte) {
119
120
121
122
123
124
	msg := v
	status := http.StatusOK
	if err != nil {
		msg = err
		status = err.Status
	}
125
	b, e := encoder(msg)
126
127
128
129
130
131
	if e != nil {
		Logger.Error("Failed to serialize response:", e.Error())
		return http.StatusInternalServerError, nil
	}
	return status, b
}
132

133
// WriteError writes the specified error and explaining message as JSON to the http.ResponseWriter.
134
func WriteError(w http.ResponseWriter, err Error, msg string) {
135
	WriteResponse(w, nil, RemoteError(err, msg))
136
137
}

138
// WriteJson writes the specified object as JSON to the http.ResponseWriter.
139
func WriteJson(w http.ResponseWriter, object interface{}) {
140
141
142
	WriteResponse(w, object, nil)
}

143
// WriteResponse writes the specified object or error as JSON to the http.ResponseWriter.
144
145
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
	status, bts := JsonResponse(object, rerr)
146
147
148
149
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	w.Write(bts)
}
150

151
// WriteString writes the specified string to the http.ResponseWriter.
152
153
154
155
156
func WriteString(w http.ResponseWriter, str string) {
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(str))
}
157

158
159
160
161
// 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.
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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")
184
	}
185
186
187
188
189
190
191
192
193
194
195
196
}

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")
197
	}
198
199
200
201
202
203
204
}

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
		}
205
	}
206
	return nil, errors.New("")
207
}
208

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

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

259
260
261
262
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

263
func log(level logrus.Level, err error) error {
264
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
265
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
266
		_, _ = writer.Write([]byte(e.ErrorStack()))
267
	} else {
268
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
269
270
271
272
	}
	return err
}

273
func LogFatal(err error) error {
274
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
275
	// using log() for this doesn't seem to do anything
276
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
277
		logger.Fatal(e.ErrorStack())
278
	} else {
279
		logger.Fatalf("%s", err.Error())
280
281
	}
	return err
282
283
284
285
286
287
}

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

288
func LogWarning(err error) error {
289
	return log(logrus.WarnLevel, err)
290
291
}

292
func LogRequest(typ, method, url, from string, headers http.Header, message []byte) {
293
	fields := logrus.Fields{
294
295
296
297
298
299
		"type":   typ,
		"method": method,
		"url":    url,
	}
	if len(headers) > 0 {
		fields["headers"] = headers
300
	}
301
302
303
304
305
	if len(message) > 0 {
		fields["message"] = string(message)
	}
	if from != "" {
		fields["from"] = from
306
307
308
309
310
	}
	Logger.WithFields(fields).Tracef("=> request")
}

func LogResponse(status int, duration time.Duration, response []byte) {
311
	fields := logrus.Fields{
312
313
		"status":   status,
		"duration": duration.String(),
314
315
316
317
318
319
320
321
322
323
	}
	if len(response) > 0 {
		fields["response"] = string(response)
	}
	l := Logger.WithFields(fields)
	if status < 400 {
		l.Trace("<= response")
	} else {
		l.Warn("<= response")
	}
324
325
}

326
327
328
329
func ToJson(o interface{}) string {
	bts, _ := json.Marshal(o)
	return string(bts)
}
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350

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
}