session.go 7.44 KB
Newer Older
1
2
3
4
5
package cmd

import (
	"fmt"
	"net/http"
6
	"regexp"
7
	"strconv"
8
	"time"
9
10
11
12

	"github.com/go-errors/errors"
	"github.com/privacybydesign/irmago"
	"github.com/privacybydesign/irmago/server"
Sietse Ringers's avatar
Sietse Ringers committed
13
	"github.com/privacybydesign/irmago/server/irmaserver"
14
	"github.com/sirupsen/logrus"
15
	"github.com/spf13/cobra"
Sietse Ringers's avatar
Sietse Ringers committed
16
	"github.com/x-cray/logrus-prefixed-formatter"
17
18
)

19
20
21
const pollInterval = 1000 * time.Millisecond

var (
Sietse Ringers's avatar
Sietse Ringers committed
22
23
	httpServer *http.Server
	irmaServer *irmaserver.Server
24
	logger     *logrus.Logger
25
	defaulturl string
26
27
)

28
29
30
31
// sessionCmd represents the session command
var sessionCmd = &cobra.Command{
	Use:   "session",
	Short: "Perform an IRMA disclosure, issuance or signature session",
32
33
34
35
36
37
38
39
	Long: `Perform an IRMA disclosure, issuance or signature session on the command line

Using either the builtin IRMA server library, or an external IRMA server (specify its URL
with --server), an IRMA session is started; the QR is printed in the terminal; and the session
result is printed when the session completes or fails.

A session request can either be constructed using the --disclose, --issue, and --sign together
with --message flags, or it can be specified as JSON to the --request flag.`,
40
41
	Example: `irma session --disclose irma-demo.MijnOverheid.root.BSN
irma session --sign irma-demo.MijnOverheid.root.BSN --message message
42
irma session --issue irma-demo.MijnOverheid.ageLower=yes,yes,yes,no --disclose irma-demo.MijnOverheid.root.BSN
43
44
irma session --request '{"type":"disclosing","content":[{"label":"BSN","attributes":["irma-demo.MijnOverheid.root.BSN"]}]}'
irma session --server http://localhost:48680 --authmethod token --key mytoken --disclose irma-demo.MijnOverheid.root.BSN`,
45
	Run: func(cmd *cobra.Command, args []string) {
46
		request, irmaconfig, err := configure(cmd)
47
48
49
		if err != nil {
			die("", err)
		}
50
51
52
		
		// Make sure we always run with latest configuration
		irmaconfig.UpdateSchemes()
53

54
		var result *server.SessionResult
55
		url, _ := cmd.Flags().GetString("url")
56
		serverurl, _ := cmd.Flags().GetString("server")
57
		noqr, _ := cmd.Flags().GetBool("noqr")
58
		flags := cmd.Flags()
59

60
		if url != defaulturl && serverurl != "" {
61
62
63
			die("Failed to read configuration", errors.New("--url can't be combined with --server"))
		}

64
		if serverurl == "" {
65
			port, _ := flags.GetInt("port")
Sietse Ringers's avatar
Sietse Ringers committed
66
			privatekeysPath, _ := flags.GetString("privkeys")
67
68
			verbosity, _ := cmd.Flags().GetCount("verbose")
			result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity)
69
		} else {
70
71
72
73
			authmethod, _ := flags.GetString("authmethod")
			key, _ := flags.GetString("key")
			name, _ := flags.GetString("name")
			result, err = serverRequest(request, serverurl, authmethod, key, name, noqr)
74
75
76
		}
		if err != nil {
			die("Session failed", err)
77
78
79
80
81
		}

		printSessionResult(result)

		// Done!
Sietse Ringers's avatar
Sietse Ringers committed
82
83
		if httpServer != nil {
			_ = httpServer.Close()
84
		}
85
86
87
	},
}

88
func libraryRequest(
89
	request irma.RequestorRequest,
90
	irmaconfig *irma.Configuration,
91
	url string,
92
93
94
	port int,
	privatekeysPath string,
	noqr bool,
95
	verbosity int,
96
) (*server.SessionResult, error) {
97
	if err := configureServer(url, port, privatekeysPath, irmaconfig, verbosity); err != nil {
98
99
100
101
102
103
		return nil, err
	}
	startServer(port)

	// Start the session
	resultchan := make(chan *server.SessionResult)
Sietse Ringers's avatar
Sietse Ringers committed
104
	qr, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) {
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
		resultchan <- r
	})
	if err != nil {
		return nil, errors.WrapPrefix(err, "IRMA session failed", 0)
	}

	// Print QR code
	if err := printQr(qr, noqr); err != nil {
		return nil, errors.WrapPrefix(err, "Failed to print QR", 0)
	}

	// Wait for session to finish and then return session result
	return <-resultchan, nil
}

func serverRequest(
121
	request irma.RequestorRequest,
122
	serverurl, authmethod, key, name string,
123
124
125
126
127
	noqr bool,
) (*server.SessionResult, error) {
	logger.Debug("Server URL: ", serverurl)

	// Start session at server
128
129
	qr, transport, err := postRequest(serverurl, request, name, authmethod, key)
	if err != nil {
130
131
132
133
134
135
136
137
138
139
140
		return nil, err
	}

	// Print session QR
	logger.Debug("QR: ", prettyprint(qr))
	if err := printQr(qr, noqr); err != nil {
		return nil, errors.WrapPrefix(err, "Failed to print QR", 0)
	}

	statuschan := make(chan server.Status)

141
	// Wait until client connects
142
	go poll(server.StatusInitialized, transport, statuschan)
143
144
145
146
147
	status := <-statuschan
	if status != server.StatusConnected {
		return nil, errors.Errorf("Unexpected status: %s", status)
	}

148
	// Wait until client finishes
149
	go poll(server.StatusConnected, transport, statuschan)
150
151
152
153
154
155
156
	status = <-statuschan
	if status != server.StatusDone {
		return nil, errors.Errorf("Unexpected status: %s", status)
	}

	// Retrieve session result
	result := &server.SessionResult{}
157
	if err := transport.Get("result", result); err != nil {
158
159
160
161
162
		return nil, errors.WrapPrefix(err, "Failed to get session result", 0)
	}
	return result, nil
}

163
func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) (*irma.Qr, *irma.HTTPTransport, error) {
164
165
	var (
		err       error
166
		pkg       = &server.SessionPackage{}
167
168
169
170
171
		transport = irma.NewHTTPTransport(serverurl)
	)

	switch authmethod {
	case "none":
172
		err = transport.Post("session", pkg, request)
173
	case "token":
174
		transport.SetHeader("Authorization", key)
175
		err = transport.Post("session", pkg, request)
176
	case "hmac", "rsa":
Sietse Ringers's avatar
Sietse Ringers committed
177
178
		var jwtstr string
		jwtstr, err = signRequest(request, name, authmethod, key)
Sietse Ringers's avatar
Sietse Ringers committed
179
		if err != nil {
180
181
182
			return nil, nil, err
		}
		logger.Debug("Session request JWT: ", jwtstr)
183
		err = transport.Post("session", pkg, jwtstr)
184
	default:
185
		return nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)")
186
187
	}

188
	token := pkg.Token
189
	transport.Server += fmt.Sprintf("session/%s/", token)
190
	return pkg.SessionPtr, transport, err
191
192
}

193
194
// Configuration functions

195
func configureServer(url string, port int, privatekeysPath string, irmaconfig *irma.Configuration, verbosity int) error {
196
197
198
199
	// Replace "port" in url with actual port
	replace := "$1:" + strconv.Itoa(port)
	url = string(regexp.MustCompile("(https?://[^/]*):port").ReplaceAll([]byte(url), []byte(replace)))

200
	config := &server.Configuration{
201
202
		IrmaConfiguration:    irmaconfig,
		Logger:               logger,
203
		URL:                  url,
204
		DisableSchemesUpdate: true,
205
		Verbose:              verbosity,
206
207
208
209
210
	}
	if privatekeysPath != "" {
		config.IssuerPrivateKeysPath = privatekeysPath
	}

211
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
212
213
	irmaServer, err = irmaserver.New(config)
	return err
214
215
}

216
func configure(cmd *cobra.Command) (irma.RequestorRequest, *irma.Configuration, error) {
217
218
219
	verbosity, _ := cmd.Flags().GetCount("verbose")
	logger = logrus.New()
	logger.Level = server.Verbosity(verbosity)
Sietse Ringers's avatar
Sietse Ringers committed
220
	logger.Formatter = &prefixed.TextFormatter{FullTimestamp: true}
221
	irma.Logger = logger
222

Sietse Ringers's avatar
Sietse Ringers committed
223
	return configureRequest(cmd)
224
}
225
226
227
228

func init() {
	RootCmd.AddCommand(sessionCmd)

229
230
	var err error
	defaulturl, err = server.LocalIP()
231
232
233
234
235
236
	if err != nil {
		logger.Warn("Could not determine local IP address: ", err.Error())
	} else {
		defaulturl = "http://" + defaulturl + ":port"
	}

237
238
	flags := sessionCmd.Flags()
	flags.SortFlags = false
239
240
241
	flags.String("server", "", "External IRMA server to post request to (leave blank to use builtin library)")
	flags.StringP("url", "u", defaulturl, "external URL to which IRMA app connects (when not using --server)")
	flags.IntP("port", "p", 48680, "port to listen at (when not using --server)")
242
	flags.Bool("noqr", false, "Print JSON instead of draw QR")
Sietse Ringers's avatar
Sietse Ringers committed
243
244
	flags.StringP("request", "r", "", "JSON session request")
	flags.StringP("privkeys", "k", "", "path to private keys")
245

Sietse Ringers's avatar
Sietse Ringers committed
246
	addRequestFlags(flags)
247

Sietse Ringers's avatar
Sietse Ringers committed
248
	flags.CountP("verbose", "v", "verbose (repeatable)")
249
}