]>
Commit | Line | Data |
---|---|---|
1d09f67e TL |
1 | /* |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to You under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | ||
18 | package org.apache.arrow.flight.client; | |
19 | ||
20 | import java.io.IOException; | |
21 | ||
22 | import org.apache.arrow.flight.CallHeaders; | |
23 | import org.apache.arrow.flight.CallInfo; | |
24 | import org.apache.arrow.flight.CallStatus; | |
25 | import org.apache.arrow.flight.Criteria; | |
26 | import org.apache.arrow.flight.ErrorFlightMetadata; | |
27 | import org.apache.arrow.flight.FlightClient; | |
28 | import org.apache.arrow.flight.FlightInfo; | |
29 | import org.apache.arrow.flight.FlightMethod; | |
30 | import org.apache.arrow.flight.FlightProducer; | |
31 | import org.apache.arrow.flight.FlightServer; | |
32 | import org.apache.arrow.flight.FlightServerMiddleware; | |
33 | import org.apache.arrow.flight.FlightTestUtil; | |
34 | import org.apache.arrow.flight.NoOpFlightProducer; | |
35 | import org.apache.arrow.flight.RequestContext; | |
36 | import org.apache.arrow.memory.BufferAllocator; | |
37 | import org.apache.arrow.memory.RootAllocator; | |
38 | import org.apache.arrow.util.AutoCloseables; | |
39 | import org.junit.After; | |
40 | import org.junit.Assert; | |
41 | import org.junit.Before; | |
42 | import org.junit.Ignore; | |
43 | import org.junit.Test; | |
44 | ||
45 | /** | |
46 | * Tests for correct handling of cookies from the FlightClient using {@link ClientCookieMiddleware}. | |
47 | */ | |
48 | public class TestCookieHandling { | |
49 | private static final String SET_COOKIE_HEADER = "Set-Cookie"; | |
50 | private static final String COOKIE_HEADER = "Cookie"; | |
51 | private BufferAllocator allocator; | |
52 | private FlightServer server; | |
53 | private FlightClient client; | |
54 | ||
55 | private ClientCookieMiddlewareTestFactory testFactory = new ClientCookieMiddlewareTestFactory(); | |
56 | private ClientCookieMiddleware cookieMiddleware = new ClientCookieMiddleware(testFactory); | |
57 | ||
58 | @Before | |
59 | public void setup() throws Exception { | |
60 | allocator = new RootAllocator(Long.MAX_VALUE); | |
61 | startServerAndClient(); | |
62 | } | |
63 | ||
64 | @After | |
65 | public void cleanup() throws Exception { | |
66 | testFactory = new ClientCookieMiddlewareTestFactory(); | |
67 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
68 | AutoCloseables.close(client, server, allocator); | |
69 | client = null; | |
70 | server = null; | |
71 | allocator = null; | |
72 | } | |
73 | ||
74 | @Test | |
75 | public void basicCookie() { | |
76 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
77 | headersToSend.insert(SET_COOKIE_HEADER, "k=v"); | |
78 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
79 | cookieMiddleware.onHeadersReceived(headersToSend); | |
80 | Assert.assertEquals("k=v", cookieMiddleware.getValidCookiesAsString()); | |
81 | } | |
82 | ||
83 | @Test | |
84 | public void cookieStaysAfterMultipleRequests() { | |
85 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
86 | headersToSend.insert(SET_COOKIE_HEADER, "k=v"); | |
87 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
88 | cookieMiddleware.onHeadersReceived(headersToSend); | |
89 | Assert.assertEquals("k=v", cookieMiddleware.getValidCookiesAsString()); | |
90 | ||
91 | headersToSend = new ErrorFlightMetadata(); | |
92 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
93 | cookieMiddleware.onHeadersReceived(headersToSend); | |
94 | Assert.assertEquals("k=v", cookieMiddleware.getValidCookiesAsString()); | |
95 | ||
96 | headersToSend = new ErrorFlightMetadata(); | |
97 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
98 | cookieMiddleware.onHeadersReceived(headersToSend); | |
99 | Assert.assertEquals("k=v", cookieMiddleware.getValidCookiesAsString()); | |
100 | } | |
101 | ||
102 | @Ignore | |
103 | @Test | |
104 | public void cookieAutoExpires() { | |
105 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
106 | headersToSend.insert(SET_COOKIE_HEADER, "k=v; Max-Age=2"); | |
107 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
108 | cookieMiddleware.onHeadersReceived(headersToSend); | |
109 | // Note: using max-age changes cookie version from 0->1, which quotes values. | |
110 | Assert.assertEquals("k=\"v\"", cookieMiddleware.getValidCookiesAsString()); | |
111 | ||
112 | headersToSend = new ErrorFlightMetadata(); | |
113 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
114 | cookieMiddleware.onHeadersReceived(headersToSend); | |
115 | Assert.assertEquals("k=\"v\"", cookieMiddleware.getValidCookiesAsString()); | |
116 | ||
117 | try { | |
118 | Thread.sleep(5000); | |
119 | } catch (InterruptedException ignored) { | |
120 | } | |
121 | ||
122 | // Verify that the k cookie was discarded because it expired. | |
123 | Assert.assertTrue(cookieMiddleware.getValidCookiesAsString().isEmpty()); | |
124 | } | |
125 | ||
126 | @Test | |
127 | public void cookieExplicitlyExpires() { | |
128 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
129 | headersToSend.insert(SET_COOKIE_HEADER, "k=v; Max-Age=2"); | |
130 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
131 | cookieMiddleware.onHeadersReceived(headersToSend); | |
132 | // Note: using max-age changes cookie version from 0->1, which quotes values. | |
133 | Assert.assertEquals("k=\"v\"", cookieMiddleware.getValidCookiesAsString()); | |
134 | ||
135 | // Note: The JDK treats Max-Age < 0 as not expired and treats 0 as expired. | |
136 | // This violates the RFC, which states that less than zero and zero should both be expired. | |
137 | headersToSend = new ErrorFlightMetadata(); | |
138 | headersToSend.insert(SET_COOKIE_HEADER, "k=v; Max-Age=0"); | |
139 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
140 | cookieMiddleware.onHeadersReceived(headersToSend); | |
141 | ||
142 | // Verify that the k cookie was discarded because the server told the client it is expired. | |
143 | Assert.assertTrue(cookieMiddleware.getValidCookiesAsString().isEmpty()); | |
144 | } | |
145 | ||
146 | @Ignore | |
147 | @Test | |
148 | public void cookieExplicitlyExpiresWithMaxAgeMinusOne() { | |
149 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
150 | headersToSend.insert(SET_COOKIE_HEADER, "k=v; Max-Age=2"); | |
151 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
152 | cookieMiddleware.onHeadersReceived(headersToSend); | |
153 | // Note: using max-age changes cookie version from 0->1, which quotes values. | |
154 | Assert.assertEquals("k=\"v\"", cookieMiddleware.getValidCookiesAsString()); | |
155 | ||
156 | headersToSend = new ErrorFlightMetadata(); | |
157 | ||
158 | // The Java HttpCookie class has a bug where it uses a -1 maxAge to indicate | |
159 | // a persistent cookie, when the RFC spec says this should mean the cookie expires immediately. | |
160 | headersToSend.insert(SET_COOKIE_HEADER, "k=v; Max-Age=-1"); | |
161 | cookieMiddleware = testFactory.onCallStarted(new CallInfo(FlightMethod.DO_ACTION)); | |
162 | cookieMiddleware.onHeadersReceived(headersToSend); | |
163 | ||
164 | // Verify that the k cookie was discarded because the server told the client it is expired. | |
165 | Assert.assertTrue(cookieMiddleware.getValidCookiesAsString().isEmpty()); | |
166 | } | |
167 | ||
168 | @Test | |
169 | public void changeCookieValue() { | |
170 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
171 | headersToSend.insert(SET_COOKIE_HEADER, "k=v"); | |
172 | cookieMiddleware.onHeadersReceived(headersToSend); | |
173 | Assert.assertEquals("k=v", cookieMiddleware.getValidCookiesAsString()); | |
174 | ||
175 | headersToSend = new ErrorFlightMetadata(); | |
176 | headersToSend.insert(SET_COOKIE_HEADER, "k=v2"); | |
177 | cookieMiddleware.onHeadersReceived(headersToSend); | |
178 | Assert.assertEquals("k=v2", cookieMiddleware.getValidCookiesAsString()); | |
179 | } | |
180 | ||
181 | @Test | |
182 | public void multipleCookiesWithSetCookie() { | |
183 | CallHeaders headersToSend = new ErrorFlightMetadata(); | |
184 | headersToSend.insert(SET_COOKIE_HEADER, "firstKey=firstVal"); | |
185 | headersToSend.insert(SET_COOKIE_HEADER, "secondKey=secondVal"); | |
186 | cookieMiddleware.onHeadersReceived(headersToSend); | |
187 | Assert.assertEquals("firstKey=firstVal; secondKey=secondVal", cookieMiddleware.getValidCookiesAsString()); | |
188 | } | |
189 | ||
190 | @Test | |
191 | public void cookieStaysAfterMultipleRequestsEndToEnd() { | |
192 | client.handshake(); | |
193 | Assert.assertEquals("k=v", testFactory.clientCookieMiddleware.getValidCookiesAsString()); | |
194 | client.handshake(); | |
195 | Assert.assertEquals("k=v", testFactory.clientCookieMiddleware.getValidCookiesAsString()); | |
196 | client.listFlights(Criteria.ALL); | |
197 | Assert.assertEquals("k=v", testFactory.clientCookieMiddleware.getValidCookiesAsString()); | |
198 | } | |
199 | ||
200 | /** | |
201 | * A server middleware component that injects SET_COOKIE_HEADER into the outgoing headers. | |
202 | */ | |
203 | static class SetCookieHeaderInjector implements FlightServerMiddleware { | |
204 | private final Factory factory; | |
205 | ||
206 | public SetCookieHeaderInjector(Factory factory) { | |
207 | this.factory = factory; | |
208 | } | |
209 | ||
210 | @Override | |
211 | public void onBeforeSendingHeaders(CallHeaders outgoingHeaders) { | |
212 | if (!factory.receivedCookieHeader) { | |
213 | outgoingHeaders.insert(SET_COOKIE_HEADER, "k=v"); | |
214 | } | |
215 | } | |
216 | ||
217 | @Override | |
218 | public void onCallCompleted(CallStatus status) { | |
219 | ||
220 | } | |
221 | ||
222 | @Override | |
223 | public void onCallErrored(Throwable err) { | |
224 | ||
225 | } | |
226 | ||
227 | static class Factory implements FlightServerMiddleware.Factory<SetCookieHeaderInjector> { | |
228 | private boolean receivedCookieHeader = false; | |
229 | ||
230 | @Override | |
231 | public SetCookieHeaderInjector onCallStarted(CallInfo info, CallHeaders incomingHeaders, | |
232 | RequestContext context) { | |
233 | receivedCookieHeader = null != incomingHeaders.get(COOKIE_HEADER); | |
234 | return new SetCookieHeaderInjector(this); | |
235 | } | |
236 | } | |
237 | } | |
238 | ||
239 | public static class ClientCookieMiddlewareTestFactory extends ClientCookieMiddleware.Factory { | |
240 | ||
241 | private ClientCookieMiddleware clientCookieMiddleware; | |
242 | ||
243 | @Override | |
244 | public ClientCookieMiddleware onCallStarted(CallInfo info) { | |
245 | this.clientCookieMiddleware = new ClientCookieMiddleware(this); | |
246 | return this.clientCookieMiddleware; | |
247 | } | |
248 | } | |
249 | ||
250 | private void startServerAndClient() throws IOException { | |
251 | final FlightProducer flightProducer = new NoOpFlightProducer() { | |
252 | public void listFlights(CallContext context, Criteria criteria, | |
253 | StreamListener<FlightInfo> listener) { | |
254 | listener.onCompleted(); | |
255 | } | |
256 | }; | |
257 | ||
258 | this.server = FlightTestUtil.getStartedServer((location) -> FlightServer | |
259 | .builder(allocator, location, flightProducer) | |
260 | .middleware(FlightServerMiddleware.Key.of("test"), new SetCookieHeaderInjector.Factory()) | |
261 | .build()); | |
262 | ||
263 | this.client = FlightClient.builder(allocator, server.getLocation()) | |
264 | .intercept(testFactory) | |
265 | .build(); | |
266 | } | |
267 | } |