api.go 9.35 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
13
	"runtime/debug"

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

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

Sietse Ringers's avatar
Sietse Ringers committed
23
// Configuration contains configuration for the irmaserver library and irmad.
24
type Configuration struct {
25
	// irma_configuration. If not given, this will be popupated using SchemesPath.
26
	IrmaConfiguration *irma.Configuration `json:"-"`
27
28
29
30
31
32
	// Path to IRMA schemes to parse into IrmaConfiguration (only used if IrmaConfiguration == nil)
	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"`
	// Whether or not to download default IRMA schemes if the specified schemes path is empty
	DownloadDefaultSchemes bool `json:"schemes_download_default" mapstructure:"schemes_download_default"`
Sietse Ringers's avatar
Sietse Ringers committed
33
	// Update all schemes every x minutes (0 to disable)
34
	SchemeUpdateInterval int `json:"schemes_update" mapstructure:"schemes_update"`
35
	// Path to issuer private keys to parse
36
	IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
37
38
39
40
41
42
	// 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
43
44
45
46
	// (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
	Email string
47
48
}

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

Sietse Ringers's avatar
Sietse Ringers committed
61
// Status is the status of an IRMA session.
62
63
64
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
65
66
67
68
69
	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
70
)
71

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
}

97
98
99
100
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

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

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

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

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

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

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

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

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")
202
	}
203
204
205
206
207
208
209
}

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

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

251
func DefaultSchemesPath() string {
252
253
	candidates := make([]string, 0, 2)
	if runtime.GOOS != "windows" {
254
		candidates = append(candidates, "/var/tmp/irma/irma_configuration")
255
	}
256
257
	candidates = append(candidates, filepath.Join(os.TempDir(), "irma", "irma_configuration"))
	return firstWritablePath(candidates)
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
}

func firstWritablePath(paths []string) string {
	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
	}
}
280

281
282
283
284
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

285
func log(level logrus.Level, err error) error {
286
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
287
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
288
		_, _ = writer.Write([]byte(e.ErrorStack()))
289
	} else {
290
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
291
292
293
294
	}
	return err
}

295
func LogFatal(err error) error {
296
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
297
	// using log() for this doesn't seem to do anything
298
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.DebugLevel) {
299
		logger.Fatal(e.ErrorStack())
300
	} else {
301
		logger.Fatalf("%s", err.Error())
302
303
	}
	return err
304
305
306
307
308
309
}

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

310
func LogWarning(err error) error {
311
	return log(logrus.WarnLevel, err)
312
313
314
315
316
317
}

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