• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/SpiNNaker-comms/src/main/java/uk/ac/manchester/spinnaker/alloc/client/ClientSession.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package uk.ac.manchester.spinnaker.alloc.client;
17

18
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
19
import static java.nio.ByteOrder.LITTLE_ENDIAN;
20
import static java.nio.charset.StandardCharsets.UTF_8;
21
import static java.util.Map.entry;
22
import static java.util.Map.ofEntries;
23
import static java.util.Objects.nonNull;
24
import static java.util.Objects.requireNonNull;
25
import static org.apache.commons.io.IOUtils.readLines;
26
import static org.slf4j.LoggerFactory.getLogger;
27
import static uk.ac.manchester.spinnaker.alloc.client.ProxyProtocol.CLOSE;
28
import static uk.ac.manchester.spinnaker.alloc.client.ProxyProtocol.MSG;
29
import static uk.ac.manchester.spinnaker.alloc.client.ProxyProtocol.MSG_TO;
30
import static uk.ac.manchester.spinnaker.alloc.client.ProxyProtocol.OPEN;
31
import static uk.ac.manchester.spinnaker.alloc.client.ProxyProtocol.OPEN_U;
32
import static uk.ac.manchester.spinnaker.alloc.client.SpallocClientFactory.checkForError;
33
import static uk.ac.manchester.spinnaker.alloc.client.SpallocClientFactory.readJson;
34
import static uk.ac.manchester.spinnaker.alloc.client.SpallocClientFactory.writeForm;
35
import static uk.ac.manchester.spinnaker.utils.InetFactory.getByAddressQuietly;
36

37
import java.io.EOFException;
38
import java.io.IOException;
39
import java.net.HttpURLConnection;
40
import java.net.Inet4Address;
41
import java.net.URI;
42
import java.net.URLConnection;
43
import java.nio.ByteBuffer;
44
import java.util.HashMap;
45
import java.util.Map;
46
import java.util.concurrent.BlockingQueue;
47
import java.util.concurrent.CompletableFuture;
48
import java.util.concurrent.ExecutionException;
49
import java.util.regex.Pattern;
50
import java.util.stream.Stream;
51

52
import org.java_websocket.WebSocket;
53
import org.java_websocket.client.WebSocketClient;
54
import org.java_websocket.framing.Framedata;
55
import org.java_websocket.handshake.ServerHandshake;
56
import org.slf4j.Logger;
57

58
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.SpallocException;
59
import uk.ac.manchester.spinnaker.machine.ChipLocation;
60
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
61

62
/**
63
 * Manages the login session. This allows us to avoid the (heavy) cost of the
64
 * password hashing algorithm used, at least most of the time.
65
 *
66
 * @author Donal Fellows
67
 */
68
@UsedInJavadocOnly(URLConnection.class)
69
final class ClientSession implements Session {
70
        private static final Logger log = getLogger(ClientSession.class);
×
71

72
        private static final String HTTP_UNAUTHORIZED_MESSAGE =
73
                        "Server returned HTTP response code: 401";
74

75
        private static final String COOKIE = "Cookie";
76

77
        private static final String SET_COOKIE = "Set-Cookie";
78

79
        private static final String SESSION_NAME = "JSESSIONID";
80

81
        private static final String CSRF_HEADER_NAME = "X-CSRF-TOKEN";
82

83
        private static final URI LOGIN_FORM = URI.create("system/login.html");
×
84

85
        private static final URI LOGIN_HANDLER = URI.create("system/perform_login");
×
86

87
        private static final URI SPALLOC_ROOT = URI.create("srv/spalloc");
×
88

89
        /**
90
         * RE to find a session handle in a {@code Set-Cookie} header.
91
         * <p>
92
         * Expression: {@code SESSIONID=([A-Z0-9]+);}
93
         */
94
        private static final Pattern SESSION_ID_RE =
×
95
                        Pattern.compile("JSESSIONID=([A-Z0-9]+);");
×
96

97
        /**
98
         * RE to find a CSRF token in an HTML form.
99
         * <p>
100
         * Expression: {@code name="_csrf" value="([-a-z0-9]+)"}
101
         */
102
        private static final Pattern CSRF_ID_RE =
×
103
                        Pattern.compile("name=\"_csrf\" value=\"([-a-z0-9]+)\"");
×
104

105
        private final URI baseUri;
106

107
        private final String username;
108

109
        private final String password;
110

111
        private String session;
112

113
        private String csrfHeader;
114

115
        private String csrf;
116

117
        /**
118
         * Create a session and log it in.
119
         *
120
         * @param baseUri
121
         *            The service base URI. <em>Must</em> be absolute! Must end in a
122
         *            {@code /} character! <em>Must not</em> include a username or
123
         *            password!
124
         * @param username
125
         *            The username to use. Not {@code null}.
126
         * @param password
127
         *            The password to use. Not {@code null}.
128
         * @throws IOException
129
         *             If things go wrong.
130
         */
131
        ClientSession(URI baseUri, String username, String password)
132
                        throws IOException {
×
133
                this.baseUri = baseUri;
×
134
                this.username = username;
×
135
                this.password = password;
×
136
                // This does the actual logging in process
137
                renew(false);
×
138
        }
×
139

140
        /**
141
         * Create a session and log it in.
142
         *
143
         * @param baseUri
144
         *            The service base URI. <em>Must</em> be absolute! <em>Must
145
         *            not</em> include a username or password!
146
         * @param headers
147
         *            The headers to use to authenticate. Not {@code null}.
148
         * @param cookies
149
         *            The cookies to use to authenticate. Not {@code null}.
150
         * @throws IOException
151
         *             If things go wrong.
152
         */
153
        ClientSession(URI baseUri, Map<String, String> headers,
154
                        Map<String, String> cookies) throws IOException {
×
155
                this.baseUri = baseUri;
×
156
                this.username = null;
×
157
                this.password = null;
×
158
                this.session = cookies.get(SESSION_NAME);
×
159
                this.csrf = headers.get(CSRF_HEADER_NAME);
×
160
                if (this.csrf != null) {
×
161
                        this.csrfHeader = CSRF_HEADER_NAME;
×
162
                }
163
                if (session == null) {
×
164
                        // This does the actual logging in process
165
                        renew(false);
×
166
                }
167
        }
×
168

169
        private static HttpURLConnection createConnection(URI url)
170
                        throws IOException {
171
                log.debug("will connect to {}", url);
×
172
                var c = (HttpURLConnection) url.toURL().openConnection();
×
173
                c.setUseCaches(false);
×
174
                return c;
×
175
        }
176

177
        @Override
178
        public HttpURLConnection connection(URI url, boolean forStateChange)
179
                        throws IOException {
180
                var realUrl = baseUri.resolve(url);
×
181
                var c = createConnection(realUrl);
×
182
                authorizeConnection(c, forStateChange);
×
183
                return c;
×
184
        }
185

186
        @Override
187
        public HttpURLConnection connection(URI url, URI url2,
188
                        boolean forStateChange) throws IOException {
189
                var realUrl = baseUri.resolve(url).resolve(url2);
×
190
                var c = createConnection(realUrl);
×
191
                authorizeConnection(c, forStateChange);
×
192
                return c;
×
193
        }
194

195
        @Override
196
        public HttpURLConnection connection(URI url, String url2,
197
                        boolean forStateChange) throws IOException {
198
                var realUrl = baseUri.resolve(url).resolve(url2);
×
199
                var c = createConnection(realUrl);
×
200
                authorizeConnection(c, forStateChange);
×
201
                return c;
×
202
        }
203

204
        /**
205
         * A websocket client that implements the Spalloc-proxied protocol.
206
         */
207
        class ProxyProtocolClientImpl extends WebSocketClient
208
                        implements ProxyProtocolClient {
209
                /** Correlation ID is always second 4-byte word. */
210
                private static final int CORRELATION_ID_POSITION = 4;
211

212
                /** Size of IPv4 address. SpiNNaker always uses IPv4. */
213
                private static final int INET_SIZE = 4;
214

215
                /**
216
                 * Where to put the response messages from a call. Keys are correlation
217
                 * IDs.
218
                 */
219
                private final Map<Integer, CompletableFuture<ByteBuffer>> replyHandlers;
220

221
                /** What channels are we remembering. Keys are channel IDs. */
222
                private final Map<Integer, ChannelBase> channels;
223

224
                private int correlationCounter;
225

226
                private Exception failure;
227

228
                /**
229
                 * @param uri
230
                 *            The address of the websocket.
231
                 */
232
                ProxyProtocolClientImpl(URI uri) {
×
233
                        super(uri);
×
234
                        replyHandlers = new HashMap<>();
×
235
                        channels = new HashMap<>();
×
236
                }
×
237

238
                @Override
239
                public void onWebsocketPong(WebSocket conn, Framedata f) {
240
                        log.debug("pong received");
×
241
                }
×
242

243
                private synchronized int issueCorrelationId() {
244
                        int i = correlationCounter++;
×
245
                        if (i < 1) {
×
246
                                correlationCounter = 1;
×
247
                                i = 0;
×
248
                        }
249
                        return i;
×
250
                }
251

252
                /**
253
                 * Do a synchronous call. Only some proxy operations support this.
254
                 *
255
                 * @param message
256
                 *            The composed call message. The second word of this will be
257
                 *            changed to be the correlation ID; all protocol messages
258
                 *            that have correlated replies have a correlation ID at that
259
                 *            position.
260
                 * @return The payload of the response. The header (protocol message
261
                 *         type ID, correlation ID) will have been stripped from the
262
                 *         message body prior to returning.
263
                 * @throws InterruptedException
264
                 *             If interrupted waiting for a reply.
265
                 * @throws RuntimeException
266
                 *             If an unexpected exception occurs.
267
                 */
268
                ByteBuffer call(ByteBuffer message) throws InterruptedException {
269
                        int correlationId = issueCorrelationId();
×
270
                        var event = new CompletableFuture<ByteBuffer>();
×
271
                        message.putInt(CORRELATION_ID_POSITION, correlationId);
×
272

273
                        // Prepare to handle the reply
274
                        replyHandlers.put(correlationId, event);
×
275

276
                        // Do the send
277
                        send(message);
×
278

279
                        // Wait for the reply
280
                        try {
281
                                return event.get();
×
282
                        } catch (ExecutionException e) {
×
283
                                // Decode the cause
284
                                try {
285
                                        throw requireNonNull(e.getCause(),
×
286
                                                        "cause of execution exception was null");
287
                                } catch (Error | RuntimeException | InterruptedException ex) {
×
288
                                        throw ex;
×
289
                                } catch (Throwable ex) {
×
290
                                        throw new RuntimeException("unexpected exception", ex);
×
291
                                }
292
                        }
293
                }
294

295
                @Override
296
                public void onOpen(ServerHandshake handshakedata) {
297
                        log.info("websocket connection opened");
×
298
                }
×
299

300
                @Override
301
                public void onMessage(String message) {
302
                        log.warn("Unexpected text message on websocket: {}", message);
×
303
                }
×
304

305
                private IOException manufactureException(ByteBuffer message) {
306
                        var bytes = new byte[message.remaining()];
×
307
                        message.get(bytes);
×
308
                        return new IOException(new String(bytes, UTF_8));
×
309
                }
310

311
                @Override
312
                public void onMessage(ByteBuffer message) {
313
                        message.order(LITTLE_ENDIAN);
×
314
                        int code = message.getInt();
×
315
                        switch (ProxyProtocol.values()[code]) {
×
316
                        case OPEN, CLOSE, OPEN_U ->
317
                                // Response to call
318
                                requireNonNull(replyHandlers.remove(message.getInt()),
×
319
                                                "uncorrelated response").complete(message);
×
320
                        case MSG ->
321
                                // Async message from board
322
                                requireNonNull(channels.get(message.getInt()),
×
323
                                                "unrecognised channel").receive(message);
×
324
                        case ERR -> {
325
                                // Error from call
326
                                int correlationId = message.getInt();
×
327
                                var exception = manufactureException(message);
×
328
                                if (correlationId < 0) {
×
329
                                        // General error message
330
                                        log.error("general failure reported by service", exception);
×
331
                                } else {
332
                                        // Response to a particular call
333
                                        requireNonNull(replyHandlers.remove(correlationId),
×
334
                                                        "uncorrelated response")
335
                                                        .completeExceptionally(exception);
×
336
                                }
337
                        }
×
338
                        // case MSG_TO -> // Never sent by service, only by us
339
                        default -> log.error("unexpected message code: {}", code);
×
340
                        }
341
                }
×
342

343
                @Override
344
                public ConnectedChannel openChannel(ChipLocation chip, int port,
345
                                BlockingQueue<ByteBuffer> receiveQueue)
346
                                throws InterruptedException {
347
                        requireNonNull(receiveQueue);
×
348

349
                        var b = OPEN.allocate();
×
350
                        b.putInt(0); // dummy
×
351
                        b.putInt(chip.getX());
×
352
                        b.putInt(chip.getY());
×
353
                        b.putInt(port);
×
354
                        b.flip();
×
355

356
                        int channelId = call(b).getInt();
×
357

358
                        return new ConnectedChannelImpl(channelId, receiveQueue);
×
359
                }
360

361
                @Override
362
                public UnconnectedChannel openUnconnectedChannel(
363
                                BlockingQueue<ByteBuffer> receiveQueue)
364
                                throws InterruptedException {
365
                        requireNonNull(receiveQueue);
×
366

367
                        var b = OPEN_U.allocate();
×
368
                        b.putInt(0); // dummy
×
369
                        b.flip();
×
370

371
                        var msg = call(b);
×
372

373
                        int channelId = msg.getInt();
×
374
                        var addr = new byte[INET_SIZE];
×
375
                        msg.get(addr);
×
376
                        int port = msg.getInt();
×
377
                        return new UnconnectedChannelImpl(channelId,
×
378
                                        getByAddressQuietly(addr), port, receiveQueue);
×
379
                }
380

381
                @Override
382
                public void onClose(int code, String reason, boolean remote) {
383
                        log.info("websocket connection closed: {}", reason);
×
384
                }
×
385

386
                @Override
387
                public void onError(Exception ex) {
388
                        log.error("Failure on websocket", ex);
×
389
                        failure = ex;
×
390
                }
×
391

392
                private void rethrowFailure() throws IOException, InterruptedException {
393
                        try {
394
                                throw failure;
×
395
                        } catch (IOException | InterruptedException | RuntimeException e) {
×
396
                                throw e;
×
397
                        } catch (Exception e) {
×
398
                                throw new IOException("unexpected exception", e);
×
399
                        }
400
                }
401

402
                /** Base class for channels routed via the proxy. */
403
                private abstract class ChannelBase implements AutoCloseable {
404
                        /** Channel ID. Issued by server. */
405
                        final int id;
406

407
                        /** Whether this channel is closed. */
408
                        boolean closed;
409

410
                        /** Where we enqueue the received messages. */
411
                        private final BlockingQueue<ByteBuffer> receiveQueue;
412

413
                        /**
414
                         * @param id
415
                         *            The ID of the channel.
416
                         * @param receiveQueue
417
                         *            Where to enqueue received messages.
418
                         */
419
                        ChannelBase(int id, BlockingQueue<ByteBuffer> receiveQueue) {
×
420
                                this.id = id;
×
421
                                this.receiveQueue = receiveQueue;
×
422
                                channels.put(id, this);
×
423
                        }
×
424

425
                        /**
426
                         * The receive handler. Strips the header and sends the contents to
427
                         * the registered receiver handler.
428
                         *
429
                         * @param msg
430
                         *            The message off the websocket.
431
                         */
432
                        private void receive(ByteBuffer msg) {
433
                                receiveQueue.add(msg.slice().order(LITTLE_ENDIAN));
×
434
                        }
×
435

436
                        /**
437
                         * Close this channel.
438
                         *
439
                         * @throws IOException
440
                         *             If things fail.
441
                         */
442
                        @Override
443
                        public void close() throws IOException {
444
                                if (closed) {
×
445
                                        return;
×
446
                                }
447

448
                                var b = CLOSE.allocate();
×
449
                                b.putInt(0); // dummy
×
450
                                b.putInt(id);
×
451
                                b.flip();
×
452

453
                                try {
454
                                        int reply = call(b).getInt();
×
455
                                        channels.remove(id);
×
456
                                        if (reply != id) {
×
457
                                                log.warn("did not properly close channel");
×
458
                                        }
459
                                        closed = true;
×
460
                                } catch (InterruptedException e) {
×
461
                                        throw new IOException("failed to close channel", e);
×
462
                                }
×
463
                        }
×
464

465
                        /**
466
                         * Forward a (fully prepared) message to the websocket, provided the
467
                         * channel is open.
468
                         *
469
                         * @param fullMessage
470
                         *            The fully prepared message to send, <em>including the
471
                         *            proxy protocol header</em>.
472
                         * @throws EOFException
473
                         *             If the channel is closed.
474
                         */
475
                        final void sendPreparedMessage(ByteBuffer fullMessage)
476
                                        throws EOFException {
477
                                if (closed) {
×
478
                                        throw new EOFException("connection closed");
×
479
                                }
480
                                send(fullMessage);
×
481
                        }
×
482
                }
483

484
                /**
485
                 * A channel that is connected to a particular board.
486
                 */
487
                private class ConnectedChannelImpl extends ChannelBase
488
                                implements ProxyProtocolClient.ConnectedChannel {
489
                        /**
490
                         * @param id
491
                         *            The ID of the channel.
492
                         * @param receiveQueue
493
                         *            Where to enqueue received messages.
494
                         */
495
                        ConnectedChannelImpl(int id,
496
                                        BlockingQueue<ByteBuffer> receiveQueue) {
×
497
                                super(id, receiveQueue);
×
498
                        }
×
499

500
                        @Override
501
                        public void send(ByteBuffer msg) throws IOException {
502
                                var b = MSG.allocate();
×
503
                                b.putInt(id);
×
504
                                b.put(msg);
×
505
                                b.flip();
×
506

507
                                sendPreparedMessage(b);
×
508
                        }
×
509

510
                        @Override
511
                        public String toString() {
512
                                return "Connected Channel " + id;
×
513
                        }
514
                }
515

516
                /**
517
                 * A channel that is not connected to any particular board.
518
                 */
519
                private class UnconnectedChannelImpl extends ChannelBase
520
                                implements ProxyProtocolClient.UnconnectedChannel {
521
                        private final Inet4Address addr;
522

523
                        private final int port;
524

525
                        /**
526
                         * @param id
527
                         *            The ID of the channel.
528
                         * @param addr
529
                         *            The "local" address for this channel (on the server)
530
                         * @param port
531
                         *            The "local" port for this channel (on the server)
532
                         * @param receiveQueue
533
                         *            Where to enqueue received messages.
534
                         * @throws RuntimeException
535
                         *             If the address can't be parsed. Really not expected!
536
                         */
537
                        UnconnectedChannelImpl(int id, Inet4Address addr, int port,
538
                                        BlockingQueue<ByteBuffer> receiveQueue) {
×
539
                                super(id, receiveQueue);
×
540
                                this.addr = addr;
×
541
                                this.port = port;
×
542
                        }
×
543

544
                        @Override
545
                        public Inet4Address getAddress() {
546
                                return addr;
×
547
                        }
548

549
                        @Override
550
                        public int getPort() {
551
                                return port;
×
552
                        }
553

554
                        @Override
555
                        public void send(ChipLocation chip, int port, ByteBuffer msg)
556
                                        throws IOException {
557
                                var b = MSG_TO.allocate();
×
558
                                b.putInt(id);
×
559
                                b.putInt(chip.getX());
×
560
                                b.putInt(chip.getY());
×
561
                                b.putInt(port);
×
562
                                b.put(msg);
×
563
                                b.flip();
×
564

565
                                sendPreparedMessage(b);
×
566
                        }
×
567

568
                        @Override
569
                        public String toString() {
570
                                return "Unconnected channel " + id;
×
571
                        }
572
                }
573
        }
574

575
        /** Time between pings of the websocket. 30s. */
576
        private static final int PING_DELAY = 30;
577

578
        @Override
579
        public ProxyProtocolClient websocket(URI url)
580
                        throws InterruptedException, IOException {
581
                var wsc = new ProxyProtocolClientImpl(url);
×
582
                if (nonNull(session)) {
×
583
                        log.debug("Attaching websocket to session {}", session);
×
584
                        wsc.addHeader(COOKIE, SESSION_NAME + "=" + session);
×
585
                }
586
                if (nonNull(csrfHeader) && nonNull(csrf)) {
×
587
                        log.debug("Marking websocket with token {}={}", csrfHeader, csrf);
×
588
                        wsc.addHeader(csrfHeader, csrf);
×
589
                }
590
                if (!wsc.connectBlocking()) {
×
591
                        if (nonNull(wsc.failure)) {
×
592
                                wsc.rethrowFailure();
×
593
                        }
594
                        // Don't know what went wrong! Log might say
595
                        throw new IOException("undiagnosed connection failure");
×
596
                }
597
                wsc.setConnectionLostTimeout(PING_DELAY);
×
598
                return wsc;
×
599
        }
600

601
        private synchronized void authorizeConnection(HttpURLConnection c,
602
                        boolean forStateChange) {
603
                /*
604
                 * For some really stupid reason, Java doesn't let you set a cookie
605
                 * manager on a per-connection basis, so we need to manage the session
606
                 * cookie ourselves.
607
                 */
608
                if (session != null) {
×
609
                        log.debug("Attaching to session {}", session);
×
610
                        c.setRequestProperty(COOKIE, SESSION_NAME + "=" + session);
×
611
                }
612

613
                if (csrfHeader != null && csrf != null && forStateChange) {
×
614
                        log.debug("Marking session with token {}={}", csrfHeader, csrf);
×
615
                        c.setRequestProperty(csrfHeader, csrf);
×
616
                }
617
                c.setInstanceFollowRedirects(false);
×
618
        }
×
619

620
        /**
621
         * Check for and handle any session cookie changes.
622
         * <p>
623
         * Assumes that the session key is in the {@code JSESSIONID} cookie.
624
         *
625
         * @param conn
626
         *            Connection that's had a transaction processed.
627
         * @return Whether the session cookie was set. Normally uninteresting.
628
         */
629
        @Override
630
        public synchronized boolean trackCookie(HttpURLConnection conn) {
631
                // Careful: spec allows for multiple Set-Cookie fields
632
                boolean found = false;
×
633
                var headerFields = conn.getHeaderFields();
×
634
                var cookiesHeader = headerFields.get(SET_COOKIE);
×
635
                if (cookiesHeader != null) {
×
636
                        for (String setCookie : cookiesHeader) {
×
637
                                log.debug("Cookie header: {}", setCookie);
×
638
                                var m = SESSION_ID_RE.matcher(setCookie);
×
639
                                if (m.find()) {
×
640
                                        session = m.group(1);
×
641
                                        found = true;
×
642
                                }
643
                        }
×
644
                }
645
                return found;
×
646
        }
647

648
        /** Helper for digging CSRF token info out of HTML. */
649
        private Stream<String> getCSRF(String line) {
650
                var m = CSRF_ID_RE.matcher(line);
×
651
                return m.find() ? Stream.of(m.group(1)) : Stream.empty();
×
652
        }
653

654
        /**
655
         * Initialise a new anonymous temporary session.
656
         *
657
         * @return The temporary CSRF token. Allows us to log in.
658
         * @throws IOException
659
         *             If things go wrong.
660
         */
661
        private String makeTemporarySession() throws IOException {
662
                var c = connection(LOGIN_FORM);
×
663
                try (var is = checkForError(c, "couldn't get login form")) {
×
664
                        // There's a session cookie at this point; we need it!
665
                        if (!trackCookie(c)) {
×
666
                                throw new IOException("could not establish session");
×
667
                        }
668
                        // This is nasty; parsing the HTML source
669
                        return readLines(is, UTF_8).stream().flatMap(this::getCSRF)
×
670
                                        .findFirst().orElseThrow(() -> new IOException(
×
671
                                                        "could not parse CSRF token"));
672
                }
673
        }
674

675
        /**
676
         * Upgrade an anonymous session to a logged-in one.
677
         *
678
         * @param tempCsrf
679
         *            The temporary CSRF token.
680
         * @throws IOException
681
         *             If things go wrong.
682
         */
683
        private void logSessionIn(String tempCsrf) throws IOException {
684
                var c = connection(LOGIN_HANDLER, true);
×
685
                c.setRequestMethod("POST");
×
686
                writeForm(c, ofEntries(entry("_csrf", tempCsrf),
×
687
                                entry("submit", "submit"), entry("username", username),
×
688
                                entry("password", password)));
×
689
                try (var ignored = checkForError(c, "login failed")) {
×
690
                        /*
691
                         * The result should be a redirect; the body is irrelevant but the
692
                         * headers matter. In particular, there should be a new session
693
                         * cookie after login.
694
                         */
695
                        if (!trackCookie(c)) {
×
696
                                throw new IOException("could not establish session");
×
697
                        }
698
                }
699
        }
×
700

701
        /**
702
         * Renew the session credentials.
703
         *
704
         * @param postRenew
705
         *            Whether to rediscover the root data after session renewal.
706
         * @throws IOException
707
         *             If things go wrong.
708
         */
709
        private synchronized void renew(boolean postRenew) throws IOException {
710

711
                // Create a temporary session so we can log in
712
                var tempCsrf = makeTemporarySession();
×
713

714
                // If we didn't use a bearer token, we need to log in properly
715
                logSessionIn(tempCsrf);
×
716

717
                if (postRenew) {
×
718
                        discoverRoot();
×
719
                }
720
        }
×
721

722
        @Override
723
        public <T, Exn extends Exception> T withRenewal(Action<T, Exn> action)
724
                        throws Exn, IOException {
725
                try {
726
                        return action.call();
×
727
                } catch (SpallocException e) {
×
728
                        if (e.getResponseCode() == HTTP_UNAUTHORIZED) {
×
729
                                renew(true);
×
730
                                return action.call();
×
731
                        }
732
                        throw e;
×
733
                } catch (IOException e) {
×
734
                        // Need to read the error message, like a barbarian!
735
                        if (e.getMessage().contains(HTTP_UNAUTHORIZED_MESSAGE)) {
×
736
                                renew(true);
×
737
                                return action.call();
×
738
                        }
739
                        throw e;
×
740
                }
741
        }
742

743
        @Override
744
        public synchronized RootInfo discoverRoot() throws IOException {
745
                var conn = connection(SPALLOC_ROOT);
×
746
                try (var is = checkForError(conn, "couldn't read service root")) {
×
747
                        var root = readJson(is, RootInfo.class);
×
748
                        this.csrfHeader = root.csrfHeader;
×
749
                        this.csrf = root.csrfToken;
×
750
                        root.csrfHeader = null;
×
751
                        root.csrfToken = null;
×
752
                        return root;
×
753
                }
754
        }
755
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc