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