api.go 8.58 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()

23
// Configuration contains configuration for the irmarequestor library and irmaserver.
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:"-"`
43
44
}

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

Sietse Ringers's avatar
Sietse Ringers committed
57
// Status is the status of an IRMA session.
58
59
60
type Status string

const (
Sietse Ringers's avatar
Sietse Ringers committed
61
62
63
64
65
	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
66
)
67

68
69
70
71
func (status Status) Finished() bool {
	return status == StatusDone || status == StatusCancelled || status == StatusTimeout
}

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

94
95
// JsonResponse JSON-marshals the specified object or error
// and returns it along with a suitable HTTP status code
96
97
98
99
100
101
102
103
104
105
106
107
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
	}
108
	Logger.Tracef("HTTP JSON response: %d %s", status, string(b))
109
110
	return status, b
}
111

112
// WriteError writes the specified error and explaining message as JSON to the http.ResponseWriter.
113
func WriteError(w http.ResponseWriter, err Error, msg string) {
114
	WriteResponse(w, nil, RemoteError(err, msg))
115
116
}

117
// WriteJson writes the specified object as JSON to the http.ResponseWriter.
118
func WriteJson(w http.ResponseWriter, object interface{}) {
119
120
121
	WriteResponse(w, object, nil)
}

122
// WriteResponse writes the specified object or error as JSON to the http.ResponseWriter.
123
124
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
	status, bts := JsonResponse(object, rerr)
125
126
127
128
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	w.Write(bts)
}
129

130
// WriteString writes the specified string to the http.ResponseWriter.
131
func WriteString(w http.ResponseWriter, str string) {
132
	Logger.Trace("HTTP text/plain response: ", str)
133
134
135
136
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(str))
}
137

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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")
160
	}
161
162
163
164
165
166
167
168
169
170
171
172
}

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")
173
	}
174
175
176
177
178
179
180
}

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
		}
181
	}
182
	return nil, errors.New("")
183
}
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221

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

222
func DefaultSchemesPath() string {
223
224
	candidates := make([]string, 0, 2)
	if runtime.GOOS != "windows" {
225
		candidates = append(candidates, "/var/tmp/irma/irma_configuration")
226
	}
227
228
	candidates = append(candidates, filepath.Join(os.TempDir(), "irma", "irma_configuration"))
	return firstWritablePath(candidates)
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
}

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
	}
}
251

252
253
254
255
func TypeString(x interface{}) string {
	return reflect.TypeOf(x).String()
}

256
func log(level logrus.Level, err error) error {
257
	writer := Logger.WithFields(logrus.Fields{"err": TypeString(err)}).WriterLevel(level)
258
259
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.TraceLevel) {
		_, _ = writer.Write([]byte(e.ErrorStack()))
260
	} else {
261
		_, _ = writer.Write([]byte(fmt.Sprintf("%s", err.Error())))
262
263
264
265
	}
	return err
}

266
func LogFatal(err error) error {
267
	logger := Logger.WithFields(logrus.Fields{"err": TypeString(err)})
268
269
	// using log() for this doesn't seem to do anything
	if e, ok := err.(*errors.Error); ok && Logger.IsLevelEnabled(logrus.TraceLevel) {
270
		logger.Fatal(e.ErrorStack())
271
	} else {
272
		logger.Fatalf("%s", err.Error())
273
274
	}
	return err
275
276
277
278
279
280
}

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

281
func LogWarning(err error) error {
282
	return log(logrus.WarnLevel, err)
283
284
285
286
287
288
}

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