api.go 12.1 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
// Package servercore is the core of the IRMA server library, allowing IRMA verifiers, issuers
2
3
// or attribute-based signature applications to perform IRMA sessions with irmaclient instances
// (i.e. the IRMA app). It exposes a small interface to expose to other programming languages
Sietse Ringers's avatar
Sietse Ringers committed
4
// through cgo. It is used by the irmaserver package but otherwise not meant for use in Go.
5
package servercore
6
7
8
9
10

import (
	"encoding/json"
	"net/http"
	"regexp"
11
	"strconv"
12
	"strings"
13
	"time"
14
15

	"github.com/go-errors/errors"
16
	"github.com/jasonlvhit/gocron"
17
	"github.com/privacybydesign/gabi/revocation"
18
	"github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
19
	"github.com/privacybydesign/irmago/server"
20
	"github.com/sirupsen/logrus"
21
22
)

23
type Server struct {
24
25
26
27
	conf          *server.Configuration
	sessions      sessionStore
	scheduler     *gocron.Scheduler
	stopScheduler chan bool
28
29
30
31
32
33
34
}

func New(conf *server.Configuration) (*Server, error) {
	s := &Server{
		conf:      conf,
		scheduler: gocron.NewScheduler(),
		sessions: &memorySessionStore{
35
36
37
			requestor: make(map[string]*session),
			client:    make(map[string]*session),
			conf:      conf,
38
39
40
41
42
		},
	}
	s.scheduler.Every(10).Seconds().Do(func() {
		s.sessions.deleteExpired()
	})
43
	s.stopScheduler = s.scheduler.Start()
44

45
	return s, s.conf.Check()
46
}
47

48
func (s *Server) Stop() {
49
50
51
	if err := s.conf.IrmaConfiguration.Close(); err != nil {
		_ = server.LogWarning(err)
	}
52
53
54
55
	s.stopScheduler <- true
	s.sessions.stop()
}

56
func (s *Server) validateRequest(request irma.SessionRequest) error {
57
58
59
60
	if _, err := s.conf.IrmaConfiguration.Download(request); err != nil {
		return err
	}
	return request.Disclosure().Disclose.Validate(s.conf.IrmaConfiguration)
61
62
}

63
func (s *Server) StartSession(req interface{}) (*irma.Qr, string, error) {
64
65
	rrequest, err := server.ParseSessionRequest(req)
	if err != nil {
66
		return nil, "", err
67
	}
68
69
70

	request := rrequest.SessionRequest()
	action := request.Action()
71

Leon's avatar
Leon committed
72
73
74
75
	if err := s.validateRequest(request); err != nil {
		return nil, "", err
	}

76
	if action == irma.ActionIssuing {
77
		if err := s.validateIssuanceRequest(request.(*irma.IssuanceRequest)); err != nil {
78
			return nil, "", err
79
80
81
		}
	}

82
83
84
85
	session := s.newSession(action, rrequest)
	s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.token}).Infof("Session started")
	if s.conf.Logger.IsLevelEnabled(logrus.DebugLevel) {
		s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Info("Session request: ", server.ToJson(rrequest))
86
	} else {
87
		s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest)))
88
	}
89
90
	return &irma.Qr{
		Type: action,
91
		URL:  s.conf.URL + "session/" + session.clientToken,
92
93
94
	}, session.token, nil
}

95
96
func (s *Server) GetSessionResult(token string) *server.SessionResult {
	session := s.sessions.get(token)
97
	if session == nil {
98
		s.conf.Logger.Warn("Session result requested of unknown session ", token)
Sietse Ringers's avatar
Sietse Ringers committed
99
100
101
102
103
		return nil
	}
	return session.result
}

104
105
func (s *Server) GetRequest(token string) irma.RequestorRequest {
	session := s.sessions.get(token)
106
	if session == nil {
107
		s.conf.Logger.Warn("Session request requested of unknown session ", token)
108
109
110
111
112
		return nil
	}
	return session.rrequest
}

113
114
func (s *Server) CancelSession(token string) error {
	session := s.sessions.get(token)
115
	if session == nil {
116
		return server.LogError(errors.Errorf("can't cancel unknown session %s", token))
117
118
119
120
121
	}
	session.handleDelete()
	return nil
}

122
123
124
125
126
func ParsePath(path string) (token, noun string, arg []string, err error) {
	client := regexp.MustCompile("session/(\\w+)/?(|commitments|proofs|status|statusevents)$")
	matches := client.FindStringSubmatch(path)
	if len(matches) == 3 {
		return matches[1], matches[2], nil, nil
127
	}
128
129
130
131
132
133
134
135
136

	rev := regexp.MustCompile("-/revocation/(records)/?(.*)$")
	matches = rev.FindStringSubmatch(path)
	if len(matches) == 3 {
		args := strings.Split(matches[2], "/")
		return "", matches[1], args, nil
	}

	return "", "", nil, server.LogWarning(errors.Errorf("Invalid URL: %s", path))
137
138
}

139
func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error {
140
141
142
143
	if !s.conf.EnableSSE {
		return errors.New("Server sent events disabled")
	}

144
145
146
147
148
149
	var session *session
	if requestor {
		session = s.sessions.get(token)
	} else {
		session = s.sessions.clientGet(token)
	}
150
151
152
153
154
155
156
157
158
	if session == nil {
		return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", token))
	}
	if session.status.Finished() {
		return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", token))
	}

	session.Lock()
	defer session.Unlock()
159
160
161
162
163
164
165
166
167
168
169
170
171

	// The EventSource.onopen Javascript callback is not consistently called across browsers (Chrome yes, Firefox+Safari no).
	// However, when the SSE connection has been opened the webclient needs some signal so that it can early detect SSE failures.
	// So we manually send an "open" event. Unfortunately:
	// - we need to give the webclient that connected just now some time, otherwise it will miss the "open" event
	// - the "open" event also goes to all other webclients currently listening, as we have no way to send this
	//   event to just the webclient currently listening. (Thus the handler of this "open" event must be idempotent.)
	evtSource := session.eventSource()
	go func() {
		time.Sleep(200 * time.Millisecond)
		evtSource.SendEventMessage("", "open", "")
	}()
	evtSource.ServeHTTP(w, r)
172
173
174
	return nil
}

175
func (s *Server) HandleProtocolMessage(
176
177
178
179
	path string,
	method string,
	headers map[string][]string,
	message []byte,
180
181
182
183
) (int, []byte, *server.SessionResult) {
	var start time.Time
	if s.conf.Verbose >= 2 {
		start = time.Now()
184
		server.LogRequest("client", method, path, "", http.Header(headers), message)
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
	}

	status, output, result := s.handleProtocolMessage(path, method, headers, message)

	if s.conf.Verbose >= 2 {
		server.LogResponse(status, time.Now().Sub(start), output)
	}

	return status, output, result
}

func (s *Server) handleProtocolMessage(
	path string,
	method string,
	headers map[string][]string,
	message []byte,
Sietse Ringers's avatar
Sietse Ringers committed
201
) (status int, output []byte, result *server.SessionResult) {
202
203
204
205
206
207
208
209
210
	// Parse path into session and action
	if len(path) > 0 { // Remove any starting and trailing slash
		if path[0] == '/' {
			path = path[1:]
		}
		if path[len(path)-1] == '/' {
			path = path[:len(path)-1]
		}
	}
211

212
	token, noun, args, err := ParsePath(path)
213
214
	if err != nil {
		status, output = server.JsonResponse(nil, server.RemoteError(server.ErrorUnsupported, ""))
215
216
	}

217
218
219
220
221
222
223
224
225
226
227
	if token != "" {
		status, output, result = s.handleClientMessage(token, noun, method, headers, message)
	} else {
		status, output = s.handleRevocationMessage(noun, method, args, headers, message)
	}
	return
}

func (s *Server) handleClientMessage(
	token, noun, method string, headers map[string][]string, message []byte,
) (status int, output []byte, result *server.SessionResult) {
Sietse Ringers's avatar
Sietse Ringers committed
228
	// Fetch the session
229
	session := s.sessions.clientGet(token)
230
	if session == nil {
231
		s.conf.Logger.WithField("clientToken", token).Warn("Session not found")
Sietse Ringers's avatar
Sietse Ringers committed
232
		status, output = server.JsonResponse(nil, server.RemoteError(server.ErrorSessionUnknown, ""))
233
		return
234
	}
235
236
	session.Lock()
	defer session.Unlock()
237

238
239
	// However we return, if the session status has been updated
	// then we should inform the user by returning a SessionResult
240
	defer func() {
241
242
		if session.status != session.prevStatus {
			session.prevStatus = session.status
243
244
245
246
			result = session.result
		}
	}()

247
	// Route to handler
248
	switch len(noun) {
249
	case 0:
250
		if method == http.MethodDelete {
251
252
253
			session.handleDelete()
			status = http.StatusOK
			return
254
		}
255
		if method == http.MethodGet {
256
257
258
259
			status, output = session.checkCache(message, server.StatusConnected)
			if len(output) != 0 {
				return
			}
260
261
262
263
			h := http.Header(headers)
			min := &irma.ProtocolVersion{}
			max := &irma.ProtocolVersion{}
			if err := json.Unmarshal([]byte(h.Get(irma.MinVersionHeader)), min); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
264
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
265
				return
266
267
			}
			if err := json.Unmarshal([]byte(h.Get(irma.MaxVersionHeader)), max); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
268
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
269
				return
270
			}
Sietse Ringers's avatar
Sietse Ringers committed
271
			status, output = server.JsonResponse(session.handleGetRequest(min, max))
272
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusConnected}
273
			return
274
		}
Sietse Ringers's avatar
Sietse Ringers committed
275
		status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
276
		return
277

278
	default:
279
280
281
282
283
284
		if noun == "statusevents" {
			err := server.RemoteError(server.ErrorInvalidRequest, "server sent events not supported by this server")
			status, output = server.JsonResponse(nil, err)
			return
		}

285
286
		if method == http.MethodGet && noun == "status" {
			status, output = server.JsonResponse(session.handleGetStatus())
Sietse Ringers's avatar
Sietse Ringers committed
287
			return
288
289
290
		}

		// Below are only POST enpoints
291
		if method != http.MethodPost {
Sietse Ringers's avatar
Sietse Ringers committed
292
			status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
Sietse Ringers's avatar
Sietse Ringers committed
293
294
295
			return
		}

296
		if noun == "commitments" && session.action == irma.ActionIssuing {
297
298
299
300
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
Sietse Ringers's avatar
Sietse Ringers committed
301
			commitments := &irma.IssueCommitmentMessage{}
302
303
			if err = irma.UnmarshalValidate(message, commitments); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
304
				return
305
			}
Sietse Ringers's avatar
Sietse Ringers committed
306
			status, output = server.JsonResponse(session.handlePostCommitments(commitments))
307
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
Sietse Ringers's avatar
Sietse Ringers committed
308
309
			return
		}
310

311
		if noun == "proofs" && session.action == irma.ActionDisclosing {
312
313
314
315
316
317
318
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
			disclosure := &irma.Disclosure{}
			if err = irma.UnmarshalValidate(message, disclosure); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
319
				return
320
			}
Sietse Ringers's avatar
Sietse Ringers committed
321
			status, output = server.JsonResponse(session.handlePostDisclosure(disclosure))
322
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
Sietse Ringers's avatar
Sietse Ringers committed
323
324
			return
		}
325

326
		if noun == "proofs" && session.action == irma.ActionSigning {
327
328
329
330
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
Sietse Ringers's avatar
Sietse Ringers committed
331
			signature := &irma.SignedMessage{}
332
333
			if err = irma.UnmarshalValidate(message, signature); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
334
				return
335
			}
Sietse Ringers's avatar
Sietse Ringers committed
336
			status, output = server.JsonResponse(session.handlePostSignature(signature))
337
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
338
			return
339
		}
Sietse Ringers's avatar
Sietse Ringers committed
340

Sietse Ringers's avatar
Sietse Ringers committed
341
		status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
342
		return
343
344
	}
}
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373

func (s *Server) handleRevocationMessage(
	noun, method string, args []string, headers map[string][]string, message []byte,
) (int, []byte) {
	if noun == "records" && method == http.MethodGet {
		if len(args) != 2 {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "GET records expects 2 url arguments"))
		}
		index, err := strconv.Atoi(args[1])
		if err != nil {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
		}
		cred := irma.NewCredentialTypeIdentifier(args[0])
		return server.JsonResponse(s.handleGetRevocationRecords(cred, index))
	}
	if noun == "records" && method == http.MethodPost {
		if len(args) != 1 {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "POST records expects 1 url arguments"))
		}
		cred := irma.NewCredentialTypeIdentifier(args[0])
		var records []*revocation.Record
		if err := json.Unmarshal(message, &records); err != nil {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
		}
		return server.JsonResponse(s.handlePostRevocationRecords(cred, records))
	}

	return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, ""))
}