diff --git a/src/java.base/share/classes/java/io/Console.java b/src/java.base/share/classes/java/io/Console.java index f94aebc8e539..7c6398b13bc9 100644 --- a/src/java.base/share/classes/java/io/Console.java +++ b/src/java.base/share/classes/java/io/Console.java @@ -383,7 +383,9 @@ private char[] readPassword0(boolean noNewLine, String fmt, Object ... args) { ioe.addSuppressed(x); } if (ioe != null) { - Arrays.fill(passwd, ' '); + if (passwd != null) { + Arrays.fill(passwd, ' '); + } try { if (reader instanceof LineReader lr) { lr.zeroOut(); diff --git a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java index 0f92a366e242..bd9d39dcd710 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -2005,10 +2005,7 @@ private InputStream getInputStream0() throws IOException { pi.finishTracking(); pi = null; } - http.finished(); - http = null; - inputStream = new EmptyInputStream(); - connected = false; + noResponseBody(); } if (respCode == 200 || respCode == 203 || respCode == 206 || @@ -2090,6 +2087,24 @@ private InputStream getInputStream0() throws IOException { } } + /** + * This method is called when a response with no response + * body is received, and arrange for the http client to + * be returned to the pool (or released) immediately when + * possible. + * @apiNote Used by {@link sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection} + * to preserve the TLS information after receiving an empty body. + * @implSpec + * Subclasses that override this method should call the super class + * implementation. + */ + protected void noResponseBody() { + http.finished(); + http = null; + inputStream = new EmptyInputStream(); + connected = false; + } + /* * Creates a chained exception that has the same type as * original exception and with the same message. Right now, diff --git a/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java index ff54e474b8e6..727c054e0ee0 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2001, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,6 +51,7 @@ public abstract class AbstractDelegateHttpsURLConnection extends HttpURLConnection { + private SSLSession savedSession = null; protected AbstractDelegateHttpsURLConnection(URL url, sun.net.www.protocol.http.Handler handler) throws IOException { this(url, null, handler); @@ -92,6 +93,7 @@ public void setNewClient (URL url) public void setNewClient (URL url, boolean useCache) throws IOException { int readTimeout = getReadTimeout(); + savedSession = null; http = HttpsClient.New (getSSLSocketFactory(), url, getHostnameVerifier(), @@ -184,6 +186,7 @@ public void connect() throws IOException { if (!http.isCachedConnection() && http.needsTunneling()) { doTunneling(); } + savedSession = null; ((HttpsClient)http).afterConnect(); } @@ -204,6 +207,19 @@ protected HttpClient getNewHttpClient(URL url, Proxy p, int connectTimeout, useCache, connectTimeout, this); } + @Override + protected void noResponseBody() { + savedSession = ((HttpsClient)http).getSSLSession(); + super.noResponseBody(); + } + + private SSLSession session() { + if (http instanceof HttpsClient https) { + return https.getSSLSession(); + } + return savedSession; + } + /** * Returns the cipher suite in use on this connection. */ @@ -211,11 +227,12 @@ public String getCipherSuite () { if (cachedResponse != null) { return ((SecureCacheResponse)cachedResponse).getCipherSuite(); } - if (http == null) { + + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); - } else { - return ((HttpsClient)http).getCipherSuite (); } + return session.getCipherSuite(); } /** @@ -231,11 +248,12 @@ public java.security.cert.Certificate[] getLocalCertificates() { return l.toArray(new java.security.cert.Certificate[0]); } } - if (http == null) { + + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); - } else { - return (((HttpsClient)http).getLocalCertificates ()); } + return session.getLocalCertificates(); } /** @@ -256,11 +274,11 @@ public java.security.cert.Certificate[] getServerCertificates() } } - if (http == null) { + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); - } else { - return (((HttpsClient)http).getServerCertificates ()); } + return session.getPeerCertificates(); } /** @@ -274,11 +292,11 @@ Principal getPeerPrincipal() return ((SecureCacheResponse)cachedResponse).getPeerPrincipal(); } - if (http == null) { + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); - } else { - return (((HttpsClient)http).getPeerPrincipal()); } + return getPeerPrincipal(session); } /** @@ -291,11 +309,11 @@ Principal getLocalPrincipal() return ((SecureCacheResponse)cachedResponse).getLocalPrincipal(); } - if (http == null) { + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); - } else { - return (((HttpsClient)http).getLocalPrincipal()); } + return getLocalPrincipal(session); } SSLSession getSSLSession() { @@ -307,11 +325,12 @@ SSLSession getSSLSession() { } } - if (http == null) { + var session = session(); + if (session == null) { throw new IllegalStateException("connection not yet open"); } - return ((HttpsClient)http).getSSLSession(); + return session; } /* @@ -354,7 +373,7 @@ protected HttpCallerInfo getHttpCallerInfo(URL url, String proxy, int port, } HttpsClient https = (HttpsClient)http; try { - Certificate[] certs = https.getServerCertificates(); + Certificate[] certs = https.getSSLSession().getPeerCertificates(); if (certs[0] instanceof X509Certificate x509Cert) { return new HttpCallerInfo(url, proxy, port, x509Cert, authenticator); } @@ -372,7 +391,7 @@ protected HttpCallerInfo getHttpCallerInfo(URL url, Authenticator authenticator) } HttpsClient https = (HttpsClient)http; try { - Certificate[] certs = https.getServerCertificates(); + Certificate[] certs = https.getSSLSession().getPeerCertificates(); if (certs[0] instanceof X509Certificate x509Cert) { return new HttpCallerInfo(url, x509Cert, authenticator); } @@ -381,4 +400,58 @@ protected HttpCallerInfo getHttpCallerInfo(URL url, Authenticator authenticator) } return super.getHttpCallerInfo(url, authenticator); } + + @Override + public void disconnect() { + super.disconnect(); + savedSession = null; + } + + /** + * Returns the principal with which the server authenticated + * itself, or throw a SSLPeerUnverifiedException if the + * server did not authenticate. + * @param session The {@linkplain #getSSLSession() SSL session} + */ + private static Principal getPeerPrincipal(SSLSession session) + throws SSLPeerUnverifiedException + { + Principal principal; + try { + principal = session.getPeerPrincipal(); + } catch (AbstractMethodError e) { + // if the provider does not support it, fallback to peer certs. + // return the X500Principal of the end-entity cert. + java.security.cert.Certificate[] certs = + session.getPeerCertificates(); + principal = ((X509Certificate)certs[0]).getSubjectX500Principal(); + } + return principal; + } + + /** + * Returns the principal the client sent to the + * server, or null if the client did not authenticate. + * @param session The {@linkplain #getSSLSession() SSL session} + */ + private static Principal getLocalPrincipal(SSLSession session) + { + Principal principal; + try { + principal = session.getLocalPrincipal(); + } catch (AbstractMethodError e) { + principal = null; + // if the provider does not support it, fallback to local certs. + // return the X500Principal of the end-entity cert. + java.security.cert.Certificate[] certs = + session.getLocalCertificates(); + if (certs != null) { + principal = ((X509Certificate)certs[0]).getSubjectX500Principal(); + } + } + return principal; + } + + + } diff --git a/src/java.base/share/classes/sun/net/www/protocol/https/HttpsClient.java b/src/java.base/share/classes/sun/net/www/protocol/https/HttpsClient.java index 7b3d871c2f25..4ce10df98d0a 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/https/HttpsClient.java +++ b/src/java.base/share/classes/sun/net/www/protocol/https/HttpsClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -698,75 +698,6 @@ public void closeIdleConnection() { } } - /** - * Returns the cipher suite in use on this connection. - */ - String getCipherSuite() { - return session.getCipherSuite(); - } - - /** - * Returns the certificate chain the client sent to the - * server, or null if the client did not authenticate. - */ - public java.security.cert.Certificate [] getLocalCertificates() { - return session.getLocalCertificates(); - } - - /** - * Returns the certificate chain with which the server - * authenticated itself, or throw a SSLPeerUnverifiedException - * if the server did not authenticate. - */ - java.security.cert.Certificate [] getServerCertificates() - throws SSLPeerUnverifiedException - { - return session.getPeerCertificates(); - } - - /** - * Returns the principal with which the server authenticated - * itself, or throw a SSLPeerUnverifiedException if the - * server did not authenticate. - */ - Principal getPeerPrincipal() - throws SSLPeerUnverifiedException - { - Principal principal; - try { - principal = session.getPeerPrincipal(); - } catch (AbstractMethodError e) { - // if the provider does not support it, fallback to peer certs. - // return the X500Principal of the end-entity cert. - java.security.cert.Certificate[] certs = - session.getPeerCertificates(); - principal = ((X509Certificate)certs[0]).getSubjectX500Principal(); - } - return principal; - } - - /** - * Returns the principal the client sent to the - * server, or null if the client did not authenticate. - */ - Principal getLocalPrincipal() - { - Principal principal; - try { - principal = session.getLocalPrincipal(); - } catch (AbstractMethodError e) { - principal = null; - // if the provider does not support it, fallback to local certs. - // return the X500Principal of the end-entity cert. - java.security.cert.Certificate[] certs = - session.getLocalCertificates(); - if (certs != null) { - principal = ((X509Certificate)certs[0]).getSubjectX500Principal(); - } - } - return principal; - } - /** * Returns the {@code SSLSession} in use on this connection. */ diff --git a/src/java.base/share/classes/sun/security/util/Password.java b/src/java.base/share/classes/sun/security/util/Password.java index c26c76892cad..e3c59da0ff75 100644 --- a/src/java.base/share/classes/sun/security/util/Password.java +++ b/src/java.base/share/classes/sun/security/util/Password.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,9 +30,11 @@ import java.nio.charset.*; import java.util.Arrays; +import jdk.internal.access.SharedSecrets; +import jdk.internal.misc.VM; + /** * A utility class for reading passwords - * */ public class Password { /** Reads user password from given input stream. */ @@ -49,29 +51,39 @@ public static char[] readPassword(InputStream in, boolean isEchoOn) char[] consoleEntered = null; byte[] consoleBytes = null; + char[] buf = null; try { // Use the new java.io.Console class - Console con = null; - if (!isEchoOn && in == System.in && ((con = System.console()) != null)) { - consoleEntered = con.readPassword(); - // readPassword returns "" if you just print ENTER, - // to be compatible with old Password class, change to null - if (consoleEntered != null && consoleEntered.length == 0) { - return null; + if (!isEchoOn) { + if (in == System.in + && ConsoleHolder.consoleIsAvailable()) { + consoleEntered = ConsoleHolder.readPassword(); + // readPassword might return null. Stop now. + if (consoleEntered == null) { + return null; + } + consoleBytes = ConsoleHolder.convertToBytes(consoleEntered); + in = new ByteArrayInputStream(consoleBytes); + } else if (in == System.in && VM.isBooted() + && System.in.available() == 0) { + // Warn if reading password from System.in but it's empty. + // This may be running in an IDE Run Window or in JShell, + // which acts like an interactive console and echoes the + // entered password. In this case, print a warning that + // the password might be echoed. If available() is not zero, + // it's more likely the input comes from a pipe, such as + // "echo password |" or "cat password_file |" where input + // will be silently consumed without echoing to the screen. + // Warn only if VM is booted and ResourcesMgr is available. + System.err.print(ResourcesMgr.getString + ("warning.input.may.be.visible.on.screen")); } - consoleBytes = convertToBytes(consoleEntered); - in = new ByteArrayInputStream(consoleBytes); } // Rest of the lines still necessary for KeyStoreLoginModule // and when there is no console. - - char[] lineBuffer; - char[] buf; - int i; - - buf = lineBuffer = new char[128]; + buf = new char[128]; int room = buf.length; int offset = 0; @@ -99,11 +111,11 @@ public static char[] readPassword(InputStream in, boolean isEchoOn) /* fall through */ default: if (--room < 0) { + char[] oldBuf = buf; buf = new char[offset + 128]; room = buf.length - offset - 1; - System.arraycopy(lineBuffer, 0, buf, 0, offset); - Arrays.fill(lineBuffer, ' '); - lineBuffer = buf; + System.arraycopy(oldBuf, 0, buf, 0, offset); + Arrays.fill(oldBuf, ' '); } buf[offset++] = (char) c; break; @@ -116,8 +128,6 @@ public static char[] readPassword(InputStream in, boolean isEchoOn) char[] ret = new char[offset]; System.arraycopy(buf, 0, ret, 0, offset); - Arrays.fill(buf, ' '); - return ret; } finally { if (consoleEntered != null) { @@ -126,35 +136,74 @@ public static char[] readPassword(InputStream in, boolean isEchoOn) if (consoleBytes != null) { Arrays.fill(consoleBytes, (byte)0); } + if (buf != null) { + Arrays.fill(buf, ' '); + } } } - /** - * Change a password read from Console.readPassword() into - * its original bytes. - * - * @param pass a char[] - * @return its byte[] format, similar to new String(pass).getBytes() - */ - private static byte[] convertToBytes(char[] pass) { - if (enc == null) { - synchronized (Password.class) { - enc = System.console() - .charset() - .newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); + // Everything on Console is inside this class. + private static class ConsoleHolder { + + // primary console; may be null + private static final Console c1; + // secondary console (when stdout is redirected); may be null + private static final Console c2; + // encoder for c1 or c2 + private static final CharsetEncoder enc; + + static { + c1 = System.console(); + Charset charset; + if (c1 != null) { + c2 = null; + charset = c1.charset(); + } else { + c2 = SharedSecrets.getJavaIOAccess().passwordConsole() + .orElse(null); + charset = (c2 != null) ? c2.charset() : null; } + enc = charset == null ? null : charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); } - byte[] ba = new byte[(int)(enc.maxBytesPerChar() * pass.length)]; - ByteBuffer bb = ByteBuffer.wrap(ba); - synchronized (enc) { - enc.reset().encode(CharBuffer.wrap(pass), bb, true); + + public static boolean consoleIsAvailable() { + return c1 != null || c2 != null; + } + + public static char[] readPassword() { + assert consoleIsAvailable(); + if (c1 != null) { + return c1.readPassword(); + } else { + try { + return SharedSecrets.getJavaIOAccess() + .readPasswordNoNewLine(c2); + } finally { + System.err.println(); + } + } } - if (bb.position() < ba.length) { - ba[bb.position()] = '\n'; + + /** + * Convert a password read from console into its original bytes. + * + * @param pass a char[] + * @return its byte[] format, equivalent to new String(pass).getBytes() + * but String is immutable and cannot be cleaned up. + */ + public static byte[] convertToBytes(char[] pass) { + assert consoleIsAvailable(); + byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)]; + ByteBuffer bb = ByteBuffer.wrap(ba); + synchronized (enc) { + enc.reset().encode(CharBuffer.wrap(pass), bb, true); + } + if (bb.remaining() > 0) { + bb.put((byte)'\n'); // will be recognized as a stop sign + } + return ba; } - return ba; } - private static volatile CharsetEncoder enc; } diff --git a/src/java.base/share/classes/sun/security/util/Resources.java b/src/java.base/share/classes/sun/security/util/Resources.java index 15bf07baa31e..533e3eb02175 100644 --- a/src/java.base/share/classes/sun/security/util/Resources.java +++ b/src/java.base/share/classes/sun/security/util/Resources.java @@ -138,6 +138,10 @@ public class Resources extends java.util.ListResourceBundle { // sun.security.pkcs11.SunPKCS11 {"PKCS11.Token.providerName.Password.", "PKCS11 Token [{0}] Password: "}, + + // sun.security.util.Password + {"warning.input.may.be.visible.on.screen", + "[WARNING: Input may be visible on screen]\u0020"}, }; diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java index 9b7e0328dd5f..5fa531dd4734 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java @@ -26,12 +26,8 @@ package sun.net.httpserver; import java.io.*; -import java.net.*; import java.util.Objects; -import com.sun.net.httpserver.*; -import com.sun.net.httpserver.spi.*; - /** * a class which allows the caller to write an arbitrary * number of bytes to an underlying stream. @@ -153,7 +149,7 @@ public void close () throws IOException { closed = true; } - WriteFinishedEvent e = new WriteFinishedEvent (t); + Event e = new Event.WriteFinished(t); t.getHttpContext().getServerImpl().addEvent (e); } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java index 6577d5fbcdd3..18c2535492b3 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,14 +25,35 @@ package sun.net.httpserver; -import com.sun.net.httpserver.*; -import com.sun.net.httpserver.spi.*; +import java.util.Objects; -class Event { +abstract sealed class Event { - ExchangeImpl exchange; + final ExchangeImpl exchange; - protected Event (ExchangeImpl t) { + protected Event(ExchangeImpl t) { this.exchange = t; } + + /** + * Stopping event for the http server. + * The event applies to the whole server and is not tied to any particular + * exchange. + */ + static final class StopRequested extends Event { + StopRequested() { + super(null); + } + } + + /** + * Event indicating that writing is finished for a given exchange. + */ + static final class WriteFinished extends Event { + WriteFinished(ExchangeImpl t) { + super(Objects.requireNonNull(t)); + assert !t.writefinished; + t.writefinished = true; + } + } } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java index 277e6bb42287..f800bdaba181 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,12 +26,8 @@ package sun.net.httpserver; import java.io.*; -import java.net.*; import java.util.Objects; -import com.sun.net.httpserver.*; -import com.sun.net.httpserver.spi.*; - /** * a class which allows the caller to write up to a defined * number of bytes to an underlying stream. The caller *must* @@ -98,7 +94,7 @@ public void close () throws IOException { is.close(); } catch (IOException e) {} } - WriteFinishedEvent e = new WriteFinishedEvent (t); + Event e = new Event.WriteFinished(t); t.getHttpContext().getServerImpl().addEvent (e); } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java index 4043fdd880d8..44357aa7eccd 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -62,7 +62,9 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; /** * Provides implementation for both HTTP and HTTPS @@ -92,7 +94,7 @@ class ServerImpl { private final Set rspConnections; private List events; private final Object lolock = new Object(); - private volatile boolean finished = false; + private final CountDownLatch finishedLatch = new CountDownLatch(1); private volatile boolean terminating = false; private boolean bound = false; private boolean started = false; @@ -178,7 +180,7 @@ public void bind (InetSocketAddress addr, int backlog) throws IOException { } public void start () { - if (!bound || started || finished) { + if (!bound || started || finished()) { throw new IllegalStateException ("server in wrong state"); } if (executor == null) { @@ -221,45 +223,75 @@ public HttpsConfigurator getHttpsConfigurator () { return httpsConfig; } + private final boolean finished(){ + // if the latch is 0, the server is finished + return finishedLatch.getCount() == 0; + } + public final boolean isFinishing() { - return finished; + return finished(); } + /** + * This method stops the server by adding a stop request event and + * waiting for the server until the event is triggered or until the maximum delay is triggered. + *

+ * This ensures that the server is stopped immediately after all exchanges are complete. HttpConnections will be forcefully closed if active exchanges do not + * complete within the imparted delay. + * + * @param delay maximum delay to wait for exchanges completion, in seconds + */ public void stop (int delay) { if (delay < 0) { throw new IllegalArgumentException ("negative delay parameter"); } + + logger.log(Level.TRACE, "stopping"); + // posting a stop event, which will flip finished flag if it finishes + // before the timeout in this method terminating = true; + + addEvent(new Event.StopRequested()); + try { schan.close(); } catch (IOException e) {} selector.wakeup(); - long latest = System.currentTimeMillis() + delay * 1000; - while (System.currentTimeMillis() < latest) { - delay(); - if (finished) { - break; + + try { + // waiting for the duration of the delay, unless released before + finishedLatch.await(delay, TimeUnit.SECONDS); + + } catch (InterruptedException e) { + logger.log(Level.TRACE, "Error in awaiting the delay"); + + } finally { + + logger.log(Level.TRACE, "closing connections"); + finishedLatch.countDown(); + selector.wakeup(); + synchronized (allConnections) { + for (HttpConnection c : allConnections) { + c.close(); + } } - } - finished = true; - selector.wakeup(); - synchronized (allConnections) { - for (HttpConnection c : allConnections) { - c.close(); + allConnections.clear(); + idleConnections.clear(); + newlyAcceptedConnections.clear(); + timer.cancel(); + if (reqRspTimeoutEnabled) { + timer1.cancel(); } - } - allConnections.clear(); - idleConnections.clear(); - newlyAcceptedConnections.clear(); - timer.cancel(); - if (reqRspTimeoutEnabled) { - timer1.cancel(); - } - if (dispatcherThread != null && dispatcherThread != Thread.currentThread()) { - try { - dispatcherThread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.log (Level.TRACE, "ServerImpl.stop: ", e); + logger.log(Level.TRACE, "connections closed"); + + if (dispatcherThread != null && dispatcherThread != Thread.currentThread()) { + logger.log(Level.TRACE, "waiting for dispatcher thread"); + try { + dispatcherThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.log(Level.TRACE, "ServerImpl.stop: ", e); + } } + logger.log(Level.TRACE, "server stopped"); } } @@ -389,15 +421,34 @@ void addEvent (Event r) { class Dispatcher implements Runnable { private void handleEvent (Event r) { + + // Stopping marking the state as finished if stop is requested, + // termination is in progress and exchange count is 0 + if (r instanceof Event.StopRequested) { + logger.log(Level.TRACE, "Handling Stop Requested Event"); + + // checking if terminating is set to true + final boolean terminatingCopy = terminating; + assert terminatingCopy; + + if (getExchangeCount() == 0 && reqConnections.isEmpty()) { + finishedLatch.countDown(); + } else { + logger.log(Level.TRACE, "Some requests are still pending"); + } + return; + } + ExchangeImpl t = r.exchange; HttpConnection c = t.getConnection(); + try { - if (r instanceof WriteFinishedEvent) { + if (r instanceof Event.WriteFinished) { logger.log(Level.TRACE, "Write Finished"); int exchanges = endExchange(); - if (terminating && exchanges == 0) { - finished = true; + if (terminating && exchanges == 0 && reqConnections.isEmpty()) { + finishedLatch.countDown(); } LeftOverInputStream is = t.getOriginalInputStream(); if (!is.isEOF()) { @@ -447,11 +498,12 @@ void reRegister (HttpConnection c) { } public void run() { - while (!finished) { + // finished() will be true when there are no active exchange after terminating + while (!finished()) { try { List list = null; synchronized (lolock) { - if (events.size() > 0) { + if (!events.isEmpty()) { list = events; events = new ArrayList<>(); } @@ -595,18 +647,18 @@ private void closeConnection(HttpConnection conn) { conn.close(); allConnections.remove(conn); switch (conn.getState()) { - case REQUEST: - reqConnections.remove(conn); - break; - case RESPONSE: - rspConnections.remove(conn); - break; - case IDLE: - idleConnections.remove(conn); - break; - case NEWLY_ACCEPTED: - newlyAcceptedConnections.remove(conn); - break; + case REQUEST: + reqConnections.remove(conn); + break; + case RESPONSE: + rspConnections.remove(conn); + break; + case IDLE: + idleConnections.remove(conn); + break; + case NEWLY_ACCEPTED: + newlyAcceptedConnections.remove(conn); + break; } assert !reqConnections.remove(conn); assert !rspConnections.remove(conn); @@ -636,6 +688,18 @@ class Exchange implements Runnable { public void run () { /* context will be null for new connections */ logger.log(Level.TRACE, "exchange started"); + + if (dispatcherThread == Thread.currentThread()) { + try { + // call selector to process cancelled keys + selector.selectNow(); + } catch (IOException ioe) { + logger.log(Level.DEBUG, "processing of cancelled keys failed: closing"); + closeConnection(connection); + return; + } + } + context = connection.getHttpContext(); boolean newconnection; SSLEngine engine = null; @@ -917,19 +981,16 @@ void logReply (int code, String requestStr, String text) { logger.log (Level.DEBUG, message); } - void delay () { - Thread.yield(); - try { - Thread.sleep (200); - } catch (InterruptedException e) {} - } - private int exchangeCount = 0; synchronized void startExchange () { exchangeCount ++; } + synchronized int getExchangeCount() { + return exchangeCount; + } + synchronized int endExchange () { exchangeCount --; assert exchangeCount >= 0; diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java index 76a1be6ec5f3..7bfc39c84a13 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,12 +26,8 @@ package sun.net.httpserver; import java.io.*; -import java.net.*; import java.util.Objects; -import com.sun.net.httpserver.*; -import com.sun.net.httpserver.spi.*; - /** * a class which allows the caller to write an indefinite * number of bytes to an underlying stream , but without using @@ -79,7 +75,7 @@ public void close () throws IOException { is.close(); } catch (IOException e) {} } - WriteFinishedEvent e = new WriteFinishedEvent (t); + Event e = new Event.WriteFinished(t); t.getHttpContext().getServerImpl().addEvent (e); } diff --git a/test/jdk/com/sun/net/httpserver/ServerStopTerminationTest.java b/test/jdk/com/sun/net/httpserver/ServerStopTerminationTest.java new file mode 100644 index 000000000000..887c3dafe6a1 --- /dev/null +++ b/test/jdk/com/sun/net/httpserver/ServerStopTerminationTest.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import com.sun.net.httpserver.HttpServer; +import jdk.test.lib.Utils; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @bug 8304065 + * @summary HttpServer.stop() should terminate immediately if no exchanges are in progress, + * else terminate after a timeout + * @library /test/lib + * @run junit/othervm -Djdk.internal.httpclient.debug=err ServerStopTerminationTest + */ + +public class ServerStopTerminationTest { + + // enabling logging for the http server + private final static Logger logger = Logger.getLogger("com.sun.net.httpserver"); + + static { + logger.setLevel(Level.FINEST); + Logger.getLogger("").getHandlers()[0].setLevel(Level.FINEST); + } + + // The server instance used to test shutdown timing + private HttpServer server; + // Client for initiating exchanges + private HttpClient client; + // Allows test to await the start of the exchange + private final CountDownLatch start = new CountDownLatch(1); + // Allows test to signal when exchange should complete + private final CountDownLatch complete = new CountDownLatch(1); + + @BeforeEach + public void setup() throws IOException { + + // Create an HttpServer binding to the loopback address with an ephemeral port + server = HttpServer.create(); + final InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); + server.bind( + new InetSocketAddress(loopbackAddress, 0), + 0); + + // A handler with completion timing controlled by tests + server.createContext("/", exchange -> { + // Let the test know the exchange is started + start.countDown(); + try { + // Wait for test to signal that we can complete the exchange + complete.await(); + exchange.sendResponseHeaders(200, 0); + exchange.close(); + } catch (final InterruptedException e) { + throw new IOException(e); + } + }); + + // Start server and client + server.start(); + client = HttpClient.newBuilder().build(); + } + + /** + * Clean up resources used by this test + */ + @AfterEach + public void cleanup() { + // client.shutdown(); + } + + /** + * Verify that a stop operation with a 1-second exchange and a 2-second delay + * completes when the exchange completes. + * + * @throws InterruptedException if an unexpected interruption occurs + */ + @Test + public void shouldAwaitActiveExchange() throws InterruptedException { + // Initiate an exchange + startExchange(); + // Wait for the server to receive the exchange + start.await(); + log("Exchange started"); + + // Complete the exchange one second into the future + final Duration exchangeDuration = Duration.ofSeconds(1); + completeExchange(exchangeDuration); + log("Complete Exchange triggered"); + + // Time the shutdown sequence + final Duration delayDuration = Duration.ofSeconds(Utils.adjustTimeout(5)); + log("Shutdown triggered with the delay of " + delayDuration.getSeconds()); + final long elapsed = timeShutdown(delayDuration); + log("Shutdown complete"); + + // The shutdown should take at least as long as the exchange duration + if (elapsed < exchangeDuration.toNanos()) { + fail("HttpServer.stop terminated before exchange completed"); + } + + // The delay should not have expired + if (elapsed >= delayDuration.toNanos()) { + fail("HttpServer.stop terminated after delay expired"); + } + } + + /** + * Verify that a stop operation with a 1-second delay and a 10-second exchange + * completes after the delay expires. + * + * @throws InterruptedException if an unexpected interruption occurs + */ + @Test + public void shouldCompeteAfterDelay() throws InterruptedException { + // Initiate an exchange + startExchange(); + // Wait for the server to receive the exchange + start.await(); + log("Exchange started"); + + // Complete the exchange 10 second into the future. + // Runs in parallel, so won't block the server stop + final Duration exchangeDuration = Duration.ofSeconds(Utils.adjustTimeout(10)); + completeExchange(exchangeDuration); + log("Complete Exchange triggered"); + + + // Time the shutdown sequence + final Duration delayDuration = Duration.ofSeconds(1); + log("Shutdown triggered with the delay of " + delayDuration.getSeconds()); + final long elapsed = timeShutdown(delayDuration); + log("Shutdown complete"); + + + // The shutdown should not await the exchange to complete + if (elapsed >= exchangeDuration.toNanos()) { + fail("HttpServer.stop terminated too late"); + } + + // The shutdown delay should have expired + if (elapsed < delayDuration.toNanos()) { + fail("HttpServer.stop terminated before delay"); + } + } + + /** + * Verify that a stop operation with a 1-second delay and a 20-second executor + * completes after the delay expires. + * + * @throws InterruptedException if an unexpected interruption occurs + */ + @Test + public void shouldCompeteAfterDelayCustomHandler() throws IOException, InterruptedException { + + // Custom server setup to include the executor + log("Changing the server to the server with a custom executor"); + // Create an HttpServer binding + final InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); + server = HttpServer.create(new InetSocketAddress(loopbackAddress, 0), 0); + server.createContext("/", exchange -> { + exchange.sendResponseHeaders(200, 0); + exchange.close(); + }); + final Duration executorSleepTime = Duration.ofSeconds(Utils.adjustTimeout(20)); + server.setExecutor(command -> new Thread(() -> { + try { + // Let the test know the executor was triggered + start.countDown(); + log("Custom executor started, sleeping"); + // waiting in the executor stage before running the exchange + Thread.sleep(executorSleepTime.toMillis()); + log("Custom executor sleep complete"); + command.run();// start the exchange + + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + }).start()); + // Start server and client + server.start(); + log("Custom setup complete"); + + // Initiate an exchange + startExchange(); + // Wait for the server to start the executor + start.await(); + log("Exchange (Executor) started"); + + // Time the shutdown sequence + final Duration delayDuration = Duration.ofSeconds(1); + log("Shutdown triggered with the delay of " + delayDuration.getSeconds()); + final long elapsed = timeShutdown(delayDuration); + log("Shutdown complete"); + + // The shutdown should not await the exchange to complete + if (elapsed >= executorSleepTime.toNanos()) { + fail("HttpServer.stop terminated too late"); + } + + // The shutdown delay should have expired + if (elapsed < delayDuration.toNanos()) { + fail("HttpServer.stop terminated before delay"); + } + } + + /** + * Verify that an HttpServer with no active exchanges terminates + * before the delay timeout occurs. + */ + @Test + public void noActiveExchanges() { + // With no active exchanges, shutdown should complete immediately + final Duration delayDuration = Duration.ofSeconds(Utils.adjustTimeout(5)); + final long elapsed = timeShutdown(delayDuration); + log("Shutting down the server with no exchanges"); + if (elapsed >= delayDuration.toNanos()) { + fail("Expected HttpServer.stop to terminate immediately with no active exchanges"); + } + } + + /** + * Verify that an already stopped HttpServer can be stopped + */ + @Test + public void shouldAllowRepeatedStop() { + final Duration delayDuration = Duration.ofSeconds(1); + log("Shutting down the server the first time"); + timeShutdown(delayDuration); + log("Shutting down the server the second time"); + timeShutdown(delayDuration); + } + + /** + * Run HttpServer::stop with the given delay, returning the + * elapsed time in nanoseconds for the shutdown to complete + */ + private long timeShutdown(Duration delayDuration) { + final long startTime = System.nanoTime(); + + server.stop((int) delayDuration.toSeconds()); + return System.nanoTime() - startTime; + } + + /** + * Initiate an exchange asynchronously + */ + private void startExchange() { + try { + final HttpRequest request = HttpRequest.newBuilder() + .uri(URIBuilder.newBuilder() + .scheme("http") + .loopback() + .port(server.getAddress().getPort()) + .build()) + // We need to use POST to prevent retries + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .whenCompleteAsync((r, t) -> { + System.out.println("request completed (" + r + ", " + t + ")"); + // count the latch down to allow the handler to complete + // and the server's dispatcher thread to proceed; The handler + // is called within the dispatcher thread since we haven't + // set any executor on the server side + complete.countDown(); + }); + } catch (final URISyntaxException e) { + throw new RuntimeException(e); + } + } + + /** + * At the specified time into the future, signal to the + * handler that the exchange can complete. + * + * @param exchangeDuration the duration to wait before signaling completion + */ + private void completeExchange(Duration exchangeDuration) { + new Thread(() -> { + try { + Thread.sleep(exchangeDuration.toMillis()); + complete.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); + } + + /** + * This logging method will log the name of the method which called the log + * for easier debug + * + * @param message message to include in the log + */ + private void log(final String message) { + + String filename = ""; + try { + filename = Thread.currentThread() + .getStackTrace()[2].getMethodName(); + } finally { + System.out.printf("{%s}: %s%n", filename, message); + } + } +} diff --git a/test/jdk/sun/net/www/protocol/http/NTLMHeadTest.java b/test/jdk/sun/net/www/protocol/http/NTLMHeadTest.java index c68c9a399f62..08cab59c3be4 100644 --- a/test/jdk/sun/net/www/protocol/http/NTLMHeadTest.java +++ b/test/jdk/sun/net/www/protocol/http/NTLMHeadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,8 +22,9 @@ */ /* - * @test + * @test id=default * @bug 8270290 + * @requires os.family != "windows" * @modules java.base/sun.net.www * @library /test/lib * @run main/othervm NTLMHeadTest SERVER @@ -49,6 +50,18 @@ * include the body. */ +/* + * @test id=windows + * @bug 8270290 + * @comment Only run on specific Windows OS versions because NTLMv1 is no longer supported starting Windows 11 and Windows Server 2025 + * @requires os.family == "windows" & (os.name == "Windows 10" | os.name == "Windows Server 2016" | os.name == "Windows Server 2019" | os.name == "Windows Server 2022") + * @modules java.base/sun.net.www + * @library /test/lib + * @run main/othervm NTLMHeadTest SERVER + * @run main/othervm NTLMHeadTest PROXY + * @run main/othervm NTLMHeadTest TUNNEL + */ + import java.net.*; import java.io.*; import java.util.*; diff --git a/test/jdk/sun/net/www/protocol/http/TestTransparentNTLM.java b/test/jdk/sun/net/www/protocol/http/TestTransparentNTLM.java index 0df834765c8e..16b5d4194013 100644 --- a/test/jdk/sun/net/www/protocol/http/TestTransparentNTLM.java +++ b/test/jdk/sun/net/www/protocol/http/TestTransparentNTLM.java @@ -26,7 +26,8 @@ * @bug 8225425 * @summary Verifies that transparent NTLM (on Windows) is not used by default, * and is used only when the relevant property is set. - * @requires os.family == "windows" + * @comment Only run on specific Windows OS versions because NTLMv1 is no longer supported starting Windows 11 and Windows Server 2025 + * @requires os.family == "windows" & (os.name == "Windows 10" | os.name == "Windows Server 2016" | os.name == "Windows Server 2019" | os.name == "Windows Server 2022") * @library /test/lib * @run testng/othervm * -Dtest.auth.succeed=false diff --git a/test/jdk/sun/net/www/protocol/https/HttpsURLConnection/GetServerCertificates.java b/test/jdk/sun/net/www/protocol/https/HttpsURLConnection/GetServerCertificates.java new file mode 100644 index 000000000000..135c2375f222 --- /dev/null +++ b/test/jdk/sun/net/www/protocol/https/HttpsURLConnection/GetServerCertificates.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/* + * @test + * @bug 8376031 + * @modules jdk.httpserver + * @library /test/lib + * @summary Ensure HttpsURLConnection::getServerCertificates does not + * throw after calling getResponseCode() if the response doesn't have + * a body. + * @run main/othervm ${test.main.class} + * @run main/othervm -Djava.net.preferIPv6Addresses=true ${test.main.class} + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; + +// Use of the static import (RSPBODY_EMPTY) depends on JDK-8331195, +// which requires a CSR approval before backporting. +// import static com.sun.net.httpserver.HttpExchange.RSPBODY_EMPTY; + + +public class GetServerCertificates { + + static final String URI_PATH = "/GetServerCertificates/"; + static final String BODY = "Go raibh maith agat"; + // Using constant (RSPBODY_EMPTY) instead of a static import that depends + // on JDK-8331195, which requires a CSR approval before backporting. + static final long RSPBODY_EMPTY = -1l; + enum TESTS { + HEAD("head", 200, "HEAD"), + NOBODY("nobody", 200, "GET", "POST"), + S204("204", 204, "GET", "POST"), + S304("304", 304, "GET", "POST"), + S200("200", 200, "GET", "POST"); + final String test; + final int code; + final List methods; + private TESTS(String test, int code, String... methods) { + this.test = test; + this.code = code; + this.methods = List.of(methods); + } + boolean isFor(String path) { + return path != null && path.endsWith("/" + test); + } + + String test() { return test; } + int code() { return code; } + List methods() { return methods; } + static Optional fromPath(String path) { + return Stream.of(values()) + .filter(test -> test.isFor(path)) + .findFirst(); + } + } + + void test(String[] args) throws Exception { + SSLContext.setDefault(SimpleSSLContext.findSSLContext()); + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + HttpServer server = startHttpServer(); + try { + InetSocketAddress address = server.getAddress(); + URI uri = URIBuilder.newBuilder() + .scheme("https") + .host(address.getAddress()) + .port(address.getPort()) + .path(URI_PATH) + .build(); + for (var test : TESTS.values()) { + for (String method : test.methods()) { + doClient(method, uri, test); + } + } + + } finally { + server.stop(1000); + } + } + + void doClient(String method, URI baseUri, TESTS test) throws Exception { + assert baseUri.getRawQuery() == null; + assert baseUri.getRawFragment() == null; + assert test.methods().contains(method); + + String uriStr = baseUri.toString(); + if (!uriStr.endsWith("/")) uriStr = uriStr + "/"; + + URI uri = new URI(uriStr + test.test()); + assert uri.toString().endsWith("/" + test.test()); + int code = test.code(); + System.out.println("doClient(%s, %s, %s)" + .formatted(method, test.test(), test.code)); + + // first request - should create a TCP connection + HttpsURLConnection uc = (HttpsURLConnection) + uri.toURL().openConnection(Proxy.NO_PROXY); + if (!"GET".equals(method)) { + uc.setRequestMethod(method); + } + try { + uc.getServerCertificates(); + throw new AssertionError("Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + System.out.println("Got expected ISE: " + ise); + } + int resp = uc.getResponseCode(); + check(resp == code, "Unexpected response code. Expected %s, got %s" + .formatted(code, resp)); + + check(uc.getServerCertificates()); + if (test == TESTS.S200) { + byte[] bytes = uc.getInputStream().readAllBytes(); + String body = new String(bytes, StandardCharsets.UTF_8); + System.out.println("body: " + body); + check(BODY.equals(body), "Unexpected response body. Expected \"%s\", got \"%s\"" + .formatted(BODY, body)); + } + + // second request - should go on the same TCP connection. + // We don't have a reliable way to test that, and it could + // go on a new TCP connection if the previous connection + // was already closed. It is not an issue either way. + uc = (HttpsURLConnection) + uri.toURL().openConnection(Proxy.NO_PROXY); + if (!"GET".equals(method)) { + uc.setRequestMethod(method); + } + try { + uc.getServerCertificates(); + throw new AssertionError("Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + System.out.println("Got expected ISE: " + ise); + } + resp = uc.getResponseCode(); + check(resp == code, "Unexpected response code. Expected %s, got %s" + .formatted(code, resp)); + + check(uc.getServerCertificates()); + if (test == TESTS.S200) { + byte[] bytes = uc.getInputStream().readAllBytes(); + String body = new String(bytes, StandardCharsets.UTF_8); + System.out.println("body: " + body); + check(BODY.equals(body), "Unexpected response body. Expected \"%s\", got \"%s\"" + .formatted(BODY, body)); + } + + uc.disconnect(); + try { + uc.getServerCertificates(); + throw new AssertionError("Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + System.out.println("Got expected ISE: " + ise); + } + + // third request - forces the connection to close + // after use so that we don't find any connection in the pool + // for the next test case, assuming there was only + // one connection in the first place. + // Again there's no easy way to verify that the pool + // is empty (and it's not really necessary to bother) + uc = (HttpsURLConnection) + uri.toURL().openConnection(Proxy.NO_PROXY); + if (!"GET".equals(method)) { + uc.setRequestMethod(method); + } + uc.setRequestProperty("Connection", "close"); + try { + uc.getServerCertificates(); + throw new AssertionError("Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + System.out.println("Got expected ISE: " + ise); + } + resp = uc.getResponseCode(); + check(resp == code, "Unexpected response code. Expected %s, got %s" + .formatted(code, resp)); + check(uc.getServerCertificates()); + uc.disconnect(); + try { + uc.getServerCertificates(); + throw new AssertionError("Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + System.out.println("Got expected ISE: " + ise); + } + } + + // HTTP Server + HttpServer startHttpServer() throws IOException { + InetAddress localhost = InetAddress.getLoopbackAddress(); + HttpsServer httpServer = HttpsServer + .create(new InetSocketAddress(localhost, 0), 0); + var configurator = new HttpsConfigurator(SimpleSSLContext.findSSLContext()); + httpServer.setHttpsConfigurator(configurator); + httpServer.createContext(URI_PATH, new SimpleHandler()); + httpServer.start(); + return httpServer; + } + + static class SimpleHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + try { + String path = t.getRequestURI().getRawPath(); + var test = TESTS.fromPath(path); + if (!path.startsWith(URI_PATH) || !test.isPresent()) { + t.getRequestBody().close(); + t.getResponseHeaders().add("Connection", "close"); + t.sendResponseHeaders(421, RSPBODY_EMPTY); + t.close(); + return; + } + try (var is = t.getRequestBody()) { + is.readAllBytes(); + } + switch (test.get()) { + case S204, S304, NOBODY -> + t.sendResponseHeaders(test.get().code(), RSPBODY_EMPTY); + case S200 -> { + byte[] bytes = BODY.getBytes(StandardCharsets.UTF_8); + t.sendResponseHeaders(test.get().code(), bytes.length); + try (var os = t.getResponseBody()) { + os.write(bytes); + } + } + case HEAD -> { + assert t.getRequestMethod().equals("HEAD"); + byte[] bytes = BODY.getBytes(StandardCharsets.UTF_8); + t.sendResponseHeaders(test.get().code(), bytes.length); + } + } + t.close(); + } catch (Throwable error) { + error.printStackTrace(); + throw error; + } + } + } + + volatile int passed = 0, failed = 0; + boolean debug = false; + void pass() {passed++;} + void fail() {failed++;} + void fail(String msg) {System.err.println(msg); fail();} + void unexpected(Throwable t) {failed++; t.printStackTrace();} + void debug(String message) { if (debug) System.out.println(message); } + void check(boolean cond, String failMessage) {if (cond) pass(); else fail(failMessage);} + void check(java.security.cert.Certificate[] certs) { + // Use List.of to check that certs is not null and does not + // contain null. NullPointerException will be thrown here + // if that happens, which will make the test fail. + check(!List.of(certs).isEmpty(), "no certificates returned"); + } + public static void main(String[] args) throws Throwable { + Class k = new Object(){}.getClass().getEnclosingClass(); + try {k.getMethod("instanceMain",String[].class) + .invoke( k.newInstance(), (Object) args);} + catch (Throwable e) {throw e.getCause();}} + public void instanceMain(String[] args) throws Throwable { + try {test(args);} catch (Throwable t) {unexpected(t);} + System.out.printf("%nPassed = %d, failed = %d%n%n", passed, failed); + if (failed > 0) throw new AssertionError("Some tests failed");} +} diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java new file mode 100644 index 000000000000..7622ea9e8eac --- /dev/null +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8354469 + * @summary keytool password does not echo in multiple cases + * @library /java/awt/regtesthelpers + * @modules java.base/jdk.internal.util + * @build PassFailJFrame + * @compile -encoding UTF-8 EchoPassword.java + * @run main/manual/othervm EchoPassword + */ + +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.event.HyperlinkEvent; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.io.File; +import java.nio.file.Path; + +public class EchoPassword { + + static JLabel label; + + public static void main(String[] args) throws Exception { + + var ks1 = "\"" + Path.of("8354469.ks1").toAbsolutePath() + "\""; + var ks2 = "\"" + Path.of("8354469.ks2").toAbsolutePath() + "\""; + var ks3 = "\"" + Path.of("8354469.ks3").toAbsolutePath() + "\""; + + final String keytool = "\"" + System.getProperty("java.home") + + File.separator + "bin" + File.separator + "keytool\""; + final String nonASCII = "äöäöäöäö"; + + final String[][] commands = { + // Input password from real Console + {"First command", keytool + " -keystore " + ks1 + + " -genkeypair -keyalg ec -dname cn=a -alias first"}, + // Input password from limited Console (when stdout is redirected) + {"Second command", keytool + " -keystore " + ks2 + + " -genkeypair -keyalg ec -dname cn=b -alias second | sort"}, + // Input password from System.in stream + {"Third command", "echo changeit| " + keytool + " -keystore " + ks1 + + " -genkeypair -keyalg ec -dname cn=c -alias third"}, + // Ensure limited Console does not write a newline to System.out + {"Fourth command", keytool + " -keystore " + ks1 + + " -exportcert -alias first | " + + keytool + " -printcert -rfc"}, + {"The password", nonASCII} + }; + + final String message = String.format(""" + Open a terminal or Windows Command Prompt window, perform + the following steps, and record the final result. Each time you + click a link to copy something, make sure the status line at the + bottom shows the link has been successfully clicked. +

Part I: Password Echoing Tests

+
    +
  1. Click Copy First Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. When prompted + again, enter "changeit" again and press Enter. Verify that the + two password prompts show up on different lines, both + passwords are hidden, and a key pair is generated successfully. + +

  2. Click Copy Second Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. When prompted + again, enter "changeit" again and press Enter. Verify that the + two password prompts show up on different lines, both + passwords are hidden, and a key pair is generated successfully. + +

  3. Click Copy Third Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + You will see a prompt but you don't need to enter anything. + Verify that the password "changeit" is not shown in the command + output and a key pair is generated successfully. + +

  4. Click Copy Fourth Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. Verify that the + password is hidden and a PEM certificate is correctly shown. +

+ Press "pass" if the behavior matches expectations; + otherwise, press "fail". + """, commands[0][1], commands[1][1], commands[2][1], commands[3][1], + commands[4][1]); + + PassFailJFrame.builder() + .instructions(message) + .rows(40).columns(100) + .hyperlinkListener(e -> { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + int pos = Integer.parseInt(e.getDescription().substring(1)); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new StringSelection(commands[pos][1]), null); + label.setText(commands[pos][0] + " copied"); + if (e.getSource() instanceof JEditorPane ep) { + ep.getCaret().setVisible(false); + } + } + }) + .splitUIBottom(() -> { + label = new JLabel("Status"); + return label; + }) + .build() + .awaitAndCheck(); + } +} diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/WriteFinishedEvent.java b/test/jdk/sun/security/tools/keytool/SetInPassword.java similarity index 53% rename from src/jdk.httpserver/share/classes/sun/net/httpserver/WriteFinishedEvent.java rename to test/jdk/sun/security/tools/keytool/SetInPassword.java index 6a04129d6794..ba6f06f898f7 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/WriteFinishedEvent.java +++ b/test/jdk/sun/security/tools/keytool/SetInPassword.java @@ -1,12 +1,10 @@ /* - * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. + * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -23,12 +21,24 @@ * questions. */ -package sun.net.httpserver; +/* + * @test + * @bug 8354469 + * @summary ensure password can be read from user's System.in + * @library /test/lib + * @modules java.base/sun.security.tools.keytool + */ + +import jdk.test.lib.SecurityTools; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; -class WriteFinishedEvent extends Event { - WriteFinishedEvent (ExchangeImpl t) { - super (t); - assert !t.writefinished; - t.writefinished = true; +public class SetInPassword { + public static void main(String[] args) throws Exception { + SecurityTools.keytool("-keystore ks -storepass changeit -genkeypair -alias a -dname CN=A -keyalg EC") + .shouldHaveExitValue(0); + System.setIn(new ByteArrayInputStream("changeit".getBytes(StandardCharsets.UTF_8))); + sun.security.tools.keytool.Main.main("-keystore ks -alias a -certreq".split(" ")); } } diff --git a/test/jdk/sun/security/util/Password/EmptyIn.java b/test/jdk/sun/security/util/Password/EmptyIn.java new file mode 100644 index 000000000000..b5692b9e7f91 --- /dev/null +++ b/test/jdk/sun/security/util/Password/EmptyIn.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.test.lib.Asserts; +import sun.security.util.Password; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/* + * @test + * @bug 8374555 + * @summary only print warning when reading from System.in + * @modules java.base/sun.security.util + * @library /test/lib + */ +public class EmptyIn { + public static void main(String[] args) throws Exception { + testSystemIn(); + testNotSystemIn(); + } + + static void testSystemIn() throws Exception { + var in = new ByteArrayInputStream(new byte[0]); + var err = new ByteArrayOutputStream(); + var oldErr = System.err; + var oldIn = System.in; + try { + System.setIn(in); + System.setErr(new PrintStream(err)); + Password.readPassword(System.in); + } finally { + System.setIn(oldIn); + System.setErr(oldErr); + } + // Read from System.in. Should warn. + Asserts.assertNotEquals(0, err.size()); + } + + static void testNotSystemIn() throws Exception { + var in = new ByteArrayInputStream(new byte[0]); + var err = new ByteArrayOutputStream(); + var oldErr = System.err; + try { + System.setErr(new PrintStream(err)); + Password.readPassword(in); + } finally { + System.setErr(oldErr); + } + // Not read from System.in. Should not warn. + Asserts.assertEQ(0, err.size()); + } +} diff --git a/test/jdk/sun/security/util/Resources/Usages.java b/test/jdk/sun/security/util/Resources/Usages.java index cd36f6794a2e..e35df2429e76 100644 --- a/test/jdk/sun/security/util/Resources/Usages.java +++ b/test/jdk/sun/security/util/Resources/Usages.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8215937 + * @bug 8215937 8354469 * @modules java.base/sun.security.util * java.base/sun.security.tools.keytool * jdk.jartool/sun.security.tools.jarsigner @@ -132,6 +132,8 @@ public class Usages { List.of(LOC_GETNONLOC, NEW_LOC)), new Pair("java.base/share/classes/sun/security/provider/PolicyFile.java", List.of(MGR_GETSTRING, LOC_GETNONLOC, LOC_GETNONLOC_POLICY)), + new Pair("java.base/share/classes/sun/security/util/Password.java", + List.of(MGR_GETSTRING)), new Pair("java.base/share/classes/javax/security/auth/", List.of(MGR_GETSTRING))) );