1 /*
   2  * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.IOException;
  25 import java.io.InputStream;
  26 import java.io.OutputStream;
  27 import java.io.UncheckedIOException;
  28 import java.net.ServerSocket;
  29 import java.net.Socket;
  30 import java.net.URI;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.nio.file.Paths;
  34 import java.nio.ByteBuffer;
  35 import java.util.ArrayList;
  36 import java.util.List;
  37 import java.util.concurrent.CompletableFuture;
  38 import java.util.concurrent.ExecutionException;
  39 import java.util.concurrent.Flow;
  40 import java.util.concurrent.TimeoutException;
  41 import java.util.concurrent.TimeUnit;
  42 import java.util.function.Supplier;
  43 import jdk.incubator.http.HttpClient;
  44 import jdk.incubator.http.HttpResponse;
  45 import jdk.incubator.http.HttpRequest;
  46 import jdk.incubator.http.HttpTimeoutException;
  47 
  48 import static java.lang.System.err;
  49 import static java.nio.charset.StandardCharsets.US_ASCII;
  50 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
  51 import static java.nio.charset.StandardCharsets.UTF_8;
  52 
  53 /**
  54  * @test
  55  * @bug 8151441
  56  * @summary Request body of incorrect (larger or smaller) sizes than that
  57  *          reported by the body publisher
  58  * @run main/othervm ShortRequestBody
  59  */
  60 
  61 public class ShortRequestBody {
  62 
  63     static final Path testSrc = Paths.get(System.getProperty("test.src", "."));
  64 
  65     // Some body types ( sources ) for testing.
  66     static final String STRING_BODY = "Hello world";
  67     static final byte[] BYTE_ARRAY_BODY = new byte[] {
  68         (byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE };
  69     static final Path FILE_BODY = testSrc.resolve("docs").resolve("files").resolve("foo.txt");
  70 
  71     // Body lengths and offsets ( amount to be wrong by ), to make coordination
  72     // between client and server easier.
  73     static final int[] BODY_LENGTHS = new int[] { STRING_BODY.length(),
  74                                                   BYTE_ARRAY_BODY.length,
  75                                                   fileSize(FILE_BODY) };
  76     static final int[] BODY_OFFSETS = new int[] { 0, +1, -1, +2, -2, +3, -3 };
  77 
  78     // A delegating Body Publisher. Subtypes will have a concrete body type.
  79     static abstract class AbstractDelegateRequestBody
  80             implements HttpRequest.BodyPublisher {
  81 
  82         final HttpRequest.BodyPublisher delegate;
  83         final long contentLength;
  84 
  85         AbstractDelegateRequestBody(HttpRequest.BodyPublisher delegate,
  86                                     long contentLength) {
  87             this.delegate = delegate;
  88             this.contentLength = contentLength;
  89         }
  90 
  91         @Override
  92         public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
  93             delegate.subscribe(subscriber);
  94         }
  95 
  96         @Override
  97         public long contentLength() { return contentLength; /* may be wrong! */ }
  98     }
  99 
 100     // Request body Publishers that may generate a different number of actual
 101     // bytes to that of what is reported through their {@code contentLength}.
 102 
 103     static class StringRequestBody extends AbstractDelegateRequestBody {
 104         StringRequestBody(String body, int additionalLength) {
 105             super(HttpRequest.BodyPublisher.fromString(body),
 106                   body.getBytes(UTF_8).length + additionalLength);
 107         }
 108     }
 109 
 110     static class ByteArrayRequestBody extends AbstractDelegateRequestBody {
 111         ByteArrayRequestBody(byte[] body, int additionalLength) {
 112             super(HttpRequest.BodyPublisher.fromByteArray(body),
 113                   body.length + additionalLength);
 114         }
 115     }
 116 
 117     static class FileRequestBody extends AbstractDelegateRequestBody {
 118         FileRequestBody(Path path, int additionalLength) throws IOException {
 119             super(HttpRequest.BodyPublisher.fromFile(path),
 120                   Files.size(path) + additionalLength);
 121         }
 122     }
 123 
 124     // ---
 125 
 126     public static void main(String[] args) throws Exception {
 127         HttpClient sharedClient = HttpClient.newHttpClient();
 128         List<Supplier<HttpClient>> clientSuppliers = new ArrayList<>();
 129         clientSuppliers.add(() -> HttpClient.newHttpClient());
 130         clientSuppliers.add(() -> sharedClient);
 131 
 132         try (Server server = new Server()) {
 133             for (Supplier<HttpClient> cs : clientSuppliers) {
 134                 err.println("\n---- next supplier ----\n");
 135                 URI uri = new URI("http://127.0.0.1:" + server.getPort() + "/");
 136 
 137                 // sanity ( 6 requests to keep client and server offsets easy to workout )
 138                 success(cs, uri, new StringRequestBody(STRING_BODY, 0));
 139                 success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
 140                 success(cs, uri, new FileRequestBody(FILE_BODY, 0));
 141                 success(cs, uri, new StringRequestBody(STRING_BODY, 0));
 142                 success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
 143                 success(cs, uri, new FileRequestBody(FILE_BODY, 0));
 144 
 145                 for (int i = 1; i < BODY_OFFSETS.length; i++) {
 146                     failureBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
 147                     failureBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
 148                     failureBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
 149 
 150                     failureNonBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
 151                     failureNonBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
 152                     failureNonBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
 153                 }
 154             }
 155         }
 156     }
 157 
 158     static void success(Supplier<HttpClient> clientSupplier,
 159                         URI uri,
 160                         HttpRequest.BodyPublisher publisher)
 161         throws Exception
 162     {
 163         CompletableFuture<HttpResponse<Void>> cf;
 164         HttpRequest request = HttpRequest.newBuilder(uri)
 165                                          .POST(publisher)
 166                                          .build();
 167         cf = clientSupplier.get().sendAsync(request, discard(null));
 168 
 169         HttpResponse<Void> resp = cf.get(30, TimeUnit.SECONDS);
 170         err.println("Response code: " + resp.statusCode());
 171         check(resp.statusCode() == 200, "Expected 200, got ", resp.statusCode());
 172     }
 173 
 174     static void failureNonBlocking(Supplier<HttpClient> clientSupplier,
 175                                    URI uri,
 176                                    HttpRequest.BodyPublisher publisher)
 177         throws Exception
 178     {
 179         CompletableFuture<HttpResponse<Void>> cf;
 180         HttpRequest request = HttpRequest.newBuilder(uri)
 181                                          .POST(publisher)
 182                                          .build();
 183         cf = clientSupplier.get().sendAsync(request, discard(null));
 184 
 185         try {
 186             HttpResponse<Void> r = cf.get(30, TimeUnit.SECONDS);
 187             throw new RuntimeException("Unexpected response: " + r.statusCode());
 188         } catch (TimeoutException x) {
 189             throw new RuntimeException("Unexpected timeout", x);
 190         } catch (ExecutionException expected) {
 191             err.println("Caught expected: " + expected);
 192             Throwable t = expected.getCause();
 193             check(t instanceof IOException,
 194                   "Expected cause IOException, but got: ", expected.getCause());
 195             String msg = t.getMessage();
 196             check(msg.contains("Too many") || msg.contains("Too few"),
 197                     "Expected Too many|Too few, got: ", t);
 198         }
 199     }
 200 
 201     static void failureBlocking(Supplier<HttpClient> clientSupplier,
 202                                 URI uri,
 203                                 HttpRequest.BodyPublisher publisher)
 204         throws Exception
 205     {
 206         HttpRequest request = HttpRequest.newBuilder(uri)
 207                                          .POST(publisher)
 208                                          .build();
 209         try {
 210             HttpResponse<Void> r = clientSupplier.get().send(request, discard(null));
 211             throw new RuntimeException("Unexpected response: " + r.statusCode());
 212         } catch (HttpTimeoutException x) {
 213             throw new RuntimeException("Unexpected timeout", x);
 214         } catch (IOException expected) {
 215             err.println("Caught expected: " + expected);
 216             String msg = expected.getMessage();
 217             check(msg.contains("Too many") || msg.contains("Too few"),
 218                     "Expected Too many|Too few, got: ", expected);
 219         }
 220     }
 221 
 222     static class Server extends Thread implements AutoCloseable {
 223 
 224         static String RESPONSE = "HTTP/1.1 200 OK\r\n" +
 225                                  "Connection: close\r\n"+
 226                                  "Content-length: 0\r\n\r\n";
 227 
 228         private final ServerSocket ss;
 229         private volatile boolean closed;
 230 
 231         Server() throws IOException {
 232             super("Test-Server");
 233             ss = new ServerSocket(0); this.start();
 234         }
 235 
 236         int getPort() { return ss.getLocalPort(); }
 237 
 238         @Override
 239         public void run() {
 240             int count = 0;
 241             int offset = 0;
 242 
 243             while (!closed) {
 244                 try (Socket s = ss.accept()) {
 245                     err.println("Server: got connection");
 246                     InputStream is = s.getInputStream();
 247                     readRequestHeaders(is);
 248                     byte[] ba = new byte[1024];
 249 
 250                     int length = BODY_LENGTHS[count % 3];
 251                     length += BODY_OFFSETS[offset];
 252                     err.println("Server: count=" + count + ", offset=" + offset);
 253                     err.println("Server: expecting " +length+ " bytes");
 254                     int read = is.readNBytes(ba, 0, length);
 255                     err.println("Server: actually read " + read + " bytes");
 256 
 257                     // Update the counts before replying, to prevent the
 258                     // client-side racing reset with this thread.
 259                     count++;
 260                     if (count % 6 == 0) // 6 is the number of failure requests per offset
 261                         offset++;
 262                     if (count % 42 == 0) {
 263                         count = 0;  // reset, for second iteration
 264                         offset = 0;
 265                     }
 266 
 267                     if (read < length) {
 268                         // no need to reply, client has already closed
 269                         // ensure closed
 270                         if (is.read() != -1)
 271                             new AssertionError("Unexpected read");
 272                     } else {
 273                         OutputStream os = s.getOutputStream();
 274                         err.println("Server: writing "
 275                                 + RESPONSE.getBytes(US_ASCII).length + " bytes");
 276                         os.write(RESPONSE.getBytes(US_ASCII));
 277                     }
 278 
 279                 } catch (IOException e) {
 280                     if (!closed)
 281                         System.out.println("Unexpected" + e);
 282                 }
 283             }
 284         }
 285 
 286         @Override
 287         public void close() {
 288             if (closed)
 289                 return;
 290             closed = true;
 291             try {
 292                 ss.close();
 293             } catch (IOException e) {
 294                 throw new UncheckedIOException("Unexpected", e);
 295             }
 296         }
 297     }
 298 
 299     static final byte[] requestEnd = new byte[] {'\r', '\n', '\r', '\n' };
 300 
 301     // Read until the end of a HTTP request headers
 302     static void readRequestHeaders(InputStream is) throws IOException {
 303         int requestEndCount = 0, r;
 304         while ((r = is.read()) != -1) {
 305             if (r == requestEnd[requestEndCount]) {
 306                 requestEndCount++;
 307                 if (requestEndCount == 4) {
 308                     break;
 309                 }
 310             } else {
 311                 requestEndCount = 0;
 312             }
 313         }
 314     }
 315 
 316     static int fileSize(Path p) {
 317         try { return (int) Files.size(p); }
 318         catch (IOException x) { throw new UncheckedIOException(x); }
 319     }
 320 
 321     static boolean check(boolean cond, Object... failedArgs) {
 322         if (cond)
 323             return true;
 324         // We are going to fail...
 325         StringBuilder sb = new StringBuilder();
 326         for (Object o : failedArgs)
 327                 sb.append(o);
 328         throw new RuntimeException(sb.toString());
 329     }
 330 }