+ {
+ /**
+ * No name, id and data in event ( only comment )
+ *
+ * @return true, if the event has no name, id and data. false, otherwise
+ */
+ boolean isEmpty();
+
+ /**
+ * an SSE event name
+ *
+ * @return the event name
+ */
+ String name();
+
+ /**
+ * an SSE event id. this can be used to request events starting from specified
+ *
+ * @return the event id
+ * @see HTML Standard: 9.2.4 The `Last-Event-ID` header
+ */
+ String id();
+
+ /**
+ * an SSE "data" line.
+ *
+ * @return the event data
+ */
+ D data();
+
+
+ /**
+ * an SSE comment concatenated
+ *
+ * @return the event comment
+ */
+ String comment();
+
+ }
+
+}
diff --git a/src/one/nio/http/EventSourceResponse.java b/src/one/nio/http/EventSourceResponse.java
new file mode 100644
index 0000000..1778ca0
--- /dev/null
+++ b/src/one/nio/http/EventSourceResponse.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.http;
+
+import java.io.IOException;
+
+/**
+ * A Response, which can poll for server emitted events.
+ * Unlike regular {@link Response} this object must be close'd to
+ * prevent resource leak.
+ *
+ * The usage flow is as follows:
+ *
+ * Call {@link HttpClient#openEvents(Request, int)}
+ * Inspect the result code, if it is not OK process the error
+ * Inspect the content-type, it must be text/event-stream; if it is not - process the response body - there will be no events
+ * while ( ( event = poll() ) != null ) process( event )
+ * call {@link #close()}
+ * call {@link HttpClient#reopenEvents(Request, String, int)} with last processed {@link Event#id()} and go to p.2
+ *
+ *
+ * @see HTML Standard: 9.2 Server-sent events
+ * @see HttpClient#openEvents(Request, int)
+ */
+public class EventSourceResponse extends Response implements EventSource
+{
+ private EventSource eventSource;
+
+ public EventSourceResponse( String resultCode )
+ {
+ super( resultCode );
+ }
+
+ @Override
+ public Event poll() throws IOException, HttpException
+ {
+ return eventSource == null ? null : eventSource.poll();
+ }
+
+ void setEventSource( EventSource es ) {
+ this.eventSource = es;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if ( eventSource != null ) {
+ eventSource.close();
+ eventSource = null;
+ }
+ }
+}
diff --git a/src/one/nio/http/HttpClient.java b/src/one/nio/http/HttpClient.java
index 1cddd08..422cb07 100755
--- a/src/one/nio/http/HttpClient.java
+++ b/src/one/nio/http/HttpClient.java
@@ -16,11 +16,11 @@
package one.nio.http;
+import one.nio.net.SslClientContextFactory;
import one.nio.net.ConnectionString;
import one.nio.net.HttpProxy;
import one.nio.net.Socket;
import one.nio.net.SocketClosedException;
-import one.nio.net.SslContext;
import one.nio.pool.PoolException;
import one.nio.pool.SocketPool;
import one.nio.util.Utf8;
@@ -28,10 +28,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Closeable;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Iterator;
import java.util.List;
public class HttpClient extends SocketPool {
@@ -55,7 +57,7 @@ public HttpClient(ConnectionString conn, String... permanentHeaders) {
protected void setProperties(ConnectionString conn) {
boolean https = "https".equals(conn.getProtocol());
if (https) {
- sslContext = SslContext.getDefault();
+ sslContext = SslClientContextFactory.create();
}
if (port == 0) {
port = https ? 443 : 80;
@@ -119,6 +121,11 @@ public Response get(String uri, String... headers)
return invoke(createRequest(Request.METHOD_GET, uri, headers));
}
+ public EventSourceResponse openEvents(String uri, String... headers)
+ throws InterruptedException, PoolException, IOException, HttpException {
+ return openEvents( createRequest( Request.METHOD_GET, uri, headers ), readTimeout );
+ }
+
public Response delete(String uri, String... headers)
throws InterruptedException, PoolException, IOException, HttpException {
return invoke(createRequest(Request.METHOD_DELETE, uri, headers));
@@ -189,6 +196,51 @@ public Response connect(String uri, String... headers)
return invoke(createRequest(Request.METHOD_CONNECT, uri, headers));
}
+ @SuppressWarnings ( "resource")
+ public EventSourceResponse openEvents( Request request, int timeout ) throws InterruptedException, PoolException, IOException, HttpException
+ {
+ request.addHeader( "Accept: text/event-stream" );
+
+ int method = request.getMethod();
+ byte[] rawRequest = request.toBytes();
+ ServerSentEventsReader sseReader;
+
+ Socket socket = borrowObject();
+ boolean open = false;
+
+ try {
+ try {
+ socket.setTimeout( timeout == 0 ? readTimeout : timeout );
+ socket.writeFully( rawRequest, 0, rawRequest.length );
+ sseReader = new ServerSentEventsReader( socket, bufferSize );
+ } catch (SocketTimeoutException e) {
+ throw e;
+ } catch (IOException e) {
+ // Stale connection? Retry on a fresh socket
+ destroyObject(socket);
+ socket = createObject();
+ socket.setTimeout( timeout == 0 ? readTimeout : timeout );
+ socket.writeFully(rawRequest, 0, rawRequest.length);
+ sseReader = new ServerSentEventsReader( socket, bufferSize );
+ }
+
+ EventSourceResponse response = sseReader.readResponse(method);
+ open = true;
+ return response;
+ } finally {
+ if (!open) {
+ invalidateObject(socket);
+ }
+ }
+ }
+
+ public EventSourceResponse reopenEvents( Request request, String lastId, int timeout ) throws InterruptedException, PoolException, IOException, HttpException
+ {
+ request.addHeader( "Last-Event-ID: " + lastId );
+
+ return openEvents( request, timeout );
+ }
+
public Request createRequest(int method, String uri, String... headers) {
Request request = new Request(method, uri, true);
for (String header : permanentHeaders) {
@@ -213,16 +265,30 @@ static class ResponseReader {
}
Response readResponse(int method) throws IOException, HttpException {
+ Response response = new Response( readResultCode() );
+ readResponseHeaders( response );
+ readResponseBody( method, response );
+ return response;
+ }
+
+ String readResultCode() throws IOException, HttpException
+ {
String responseHeader = readLine();
if (responseHeader.length() <= 9) {
throw new HttpException("Invalid response header: " + responseHeader);
}
+ return responseHeader.substring(9);
+ }
- Response response = new Response(responseHeader.substring(9));
+ void readResponseHeaders(Response response) throws IOException, HttpException
+ {
for (String header; !(header = readLine()).isEmpty(); ) {
response.addHeader(header);
}
+ }
+ void readResponseBody( int method, Response response ) throws IOException, HttpException
+ {
if (method != Request.METHOD_HEAD && mayHaveBody(response.getStatus())) {
if ("chunked".equalsIgnoreCase(response.getHeader("Transfer-Encoding:"))) {
response.setBody(readChunkedBody());
@@ -238,8 +304,6 @@ Response readResponse(int method) throws IOException, HttpException {
}
}
}
-
- return response;
}
String readLine() throws IOException, HttpException {
@@ -345,4 +409,343 @@ private static boolean mayHaveBody(int status) {
return status >= 200 && status != 204 && status != 304;
}
}
+
+ class ChunkedLineReader extends ResponseReader implements Iterator, Closeable {
+
+ private byte[] ch;
+ private int chPos, chLen;
+
+ private boolean hasNext;
+
+
+ ChunkedLineReader( Socket socket, int bufferSize ) throws IOException
+ {
+ super( socket, bufferSize );
+ this.ch = buf;
+ this.chPos = 0;
+ this.chLen = 0;
+ this.hasNext = true;
+ }
+
+ private boolean nextChunk() throws IOException, HttpException {
+
+ // the very first chunk header is written without empty line at the start, like:
+ // 999\n\r
+ // all subsequent chunk headers start with empty line, like:
+ // \n\r999\n\r
+ String l = readLine();
+ int chunkSize = Integer.parseInt( l.isEmpty() ? readLine() : l, 16 );
+ if (chunkSize == 0) {
+ readLine();
+ this.chPos = 0;
+ this.chLen = 0;
+ this.hasNext = false;
+ return false;
+ }
+
+ if ( chunkSize > ch.length ) {
+ // initially ch points to buf and reallocates to separate only if chunk size is greater than buf
+ ch = new byte[ chunkSizeFor( chunkSize ) ];
+ }
+
+ int contentBytes = length - pos;
+ if (contentBytes < chunkSize) {
+ System.arraycopy(buf, pos, ch, 0, contentBytes);
+ socket.readFully(ch, contentBytes, chunkSize - contentBytes);
+ pos = 0;
+ length = 0;
+ chPos = 0;
+ } else {
+ if ( ch != buf ) {
+ System.arraycopy(buf, pos, ch, 0, chunkSize);
+ chPos = 0;
+ } else {
+ chPos = pos;
+ }
+ pos += chunkSize;
+ }
+ chLen = chunkSize;
+
+ return true;
+
+ }
+
+ private int chunkSizeFor( int cap )
+ {
+ int n = -1 >>> Integer.numberOfLeadingZeros( cap - 1 );
+ return n + 1;
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return hasNext;
+ }
+
+ @Override
+ public String next()
+ {
+ try {
+ return readChunkedLine();
+ } catch ( IOException | HttpException e ) {
+ log.debug("Event stream is closed by server");
+ close();
+ }
+
+ return null;
+ }
+
+ private String readChunkedLine() throws IOException, HttpException {
+ // whole line is found within current chunk
+ int end = findLineEnd( ch, chPos, chLen );
+ if ( end >= 0 ) {
+ int lineLen = end - chPos;
+ String line = Utf8.read( ch, chPos, lineLen );
+ lineLen++; // skip over \n
+ chLen -= lineLen;
+ chPos += lineLen;
+ return line;
+ }
+
+ ArrayList chunks = new ArrayList<>();
+ int lineLen = 0;
+
+ do {
+ chunks.add(Arrays.copyOfRange(ch, this.chPos, this.chPos + this.chLen));
+ lineLen += this.chLen;
+ this.ch = this.buf;
+ this.chPos = 0;
+ this.chLen = 0;
+
+ if ( !nextChunk() ) {
+ // end of stream detected
+ end = 0;
+ break;
+ }
+
+ end = findLineEnd( ch, chPos, chLen );
+ } while ( end < 0 );
+
+ lineLen += Math.max( end - chPos, 0 );
+ if ( lineLen == 0 )
+ return "";
+
+ byte[] lineBytes = new byte[ lineLen ];
+ int linePos = 0;
+ for ( byte[] b : chunks ) {
+ System.arraycopy( b, 0, lineBytes, linePos, b.length );
+ linePos += b.length;
+ }
+
+ // ch has last piece of line, if end > 0 || lineLen > linePos
+ if ( end > 0 ) {
+ System.arraycopy( ch, this.chPos, lineBytes, linePos, end - this.chPos );
+ linePos += end - this.chPos;
+ chLen -= end - chPos + 1;
+ chPos = end + 1; // skip over \n
+ }
+
+ assert linePos == lineLen;
+
+ String line = Utf8.read( lineBytes, 0, lineBytes.length );
+ return line;
+ }
+
+ private int findLineEnd( byte[] b, int start, int len ) {
+ int end = start + len;
+ for ( ; start < end && b[start] != '\n'; start++ ) ;
+
+ return start >= end ? -1 : start ;
+ }
+
+ @Override
+ public void close()
+ {
+ if ( socket == null )
+ return;
+
+ invalidateObject(socket);
+ this.hasNext = false;
+ this.socket = null;
+ }
+
+ }
+
+ class ServerSentEventsReader extends ChunkedLineReader implements EventSource {
+
+ private boolean keepAlive;
+
+ ServerSentEventsReader( Socket socket, int bufferSize ) throws IOException
+ {
+ super( socket, bufferSize );
+ }
+
+ EventSourceResponse readResponse(int method) throws IOException, HttpException {
+ EventSourceResponse response = new EventSourceResponse( readResultCode() );
+ readResponseHeaders( response );
+
+ if ( response.getHeader( "Content-Type: text/event-stream" ) == null ) {
+ try {
+ readResponseBody( method, response );
+ keepAlive = !"close".equalsIgnoreCase(response.getHeader("Connection:"));
+ return response;
+ } finally {
+ close();
+ }
+ }
+
+ if ( !"chunked".equalsIgnoreCase( response.getHeader( "Transfer-Encoding:" ) ) ) {
+ throw new UnsupportedOperationException( "Only chunked transfer encoding is supported for text/event-stream" );
+ }
+
+ response.setEventSource( this );
+
+ return response;
+ }
+
+ @Override
+ public Event poll( )
+ {
+ if ( !hasNext() )
+ return null;
+
+ String line = next();
+ return line == null || line.isEmpty() ? null : readEvent( line );
+
+ }
+
+ private EventImpl readEvent( String line )
+ {
+ EventImpl eimpl = new EventImpl();
+
+ StringBuilder databuf = new StringBuilder( line.length() );
+ String field=":"; // impossible value
+ try {
+ do {
+ int cpos = line.indexOf( ':' );
+ String f;
+ if ( cpos == 0 ) {
+ // comment. sometimes used alone as keep alive messages
+ f="";
+ cpos++;
+ } else if ( cpos < 0 ) {
+ // no colon - whole line is field name as per spec
+ f = line;
+ cpos = line.length();
+ } else {
+ // field name separated from data by colon with optional
+ // single space char after colon, like field-name: data
+ f = line.substring( 0, cpos );
+ cpos++;
+ if ( cpos < line.length() && line.charAt( cpos )==' ')
+ cpos++;
+ }
+
+ if ( !field.equals( f ) ) {
+
+ eimpl.with( field, databuf );
+
+ field = f;
+ databuf.setLength( 0 );
+ } else {
+ // multiple lines of the same field name concatenate data with newline
+ // a:b
+ // a:c
+ // a="b\nc"
+ databuf.append('\n');
+ }
+
+ databuf.append( line, cpos, line.length() );
+
+ line = next();
+ if (line == null) {
+ // EOF
+ return null;
+ }
+ } while ( !line.isEmpty() );
+
+ if ( databuf.length() > 0 )
+ eimpl.with( field, databuf );
+
+ } catch ( RuntimeException e ) {
+ log.error( "Cannot parse line: {}", line, e );
+ throw e;
+ }
+
+ log.debug( "Read event from stream: {}", eimpl );
+
+ return eimpl;
+ }
+
+ @Override
+ public void close()
+ {
+ if ( socket != null && keepAlive) {
+ returnObject(socket);
+ socket = null;
+ } else {
+ super.close();
+ }
+ }
+
+ }
+
+ static class EventImpl implements EventSource.Event {
+
+ private String id, name, data, comment;
+
+ @Override
+ public String name()
+ {
+ return name;
+ }
+
+ @Override
+ public String id()
+ {
+ return id;
+ }
+
+ @Override
+ public String data()
+ {
+ return data;
+ }
+
+ @Override
+ public String comment()
+ {
+ return comment;
+ }
+
+ boolean with( String field, StringBuilder databuf ) {
+ switch ( field ) {
+ case "id":
+ id = databuf.toString();
+ break;
+ case "event":
+ name = databuf.toString();
+ break;
+ case "data":
+ data = databuf.toString();
+ break;
+ case "":
+ comment = databuf.toString();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ public boolean isEmpty() {
+ return id == null && name == null && data == null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return isEmpty() ? "empty" : name + ":" + id;
+ }
+ }
}
diff --git a/src/one/nio/http/HttpSession.java b/src/one/nio/http/HttpSession.java
index 425b926..697a11c 100755
--- a/src/one/nio/http/HttpSession.java
+++ b/src/one/nio/http/HttpSession.java
@@ -19,6 +19,7 @@
import one.nio.net.Session;
import one.nio.net.Socket;
import one.nio.net.SocketClosedException;
+import one.nio.net.SslOption;
import one.nio.util.Utf8;
import java.io.IOException;
@@ -93,8 +94,10 @@ protected void processRead(byte[] buffer) throws IOException {
}
protected void handleSocketClosed() {
- // Unsubscribe from read events
- listen(queueHead == null ? 0 : WRITEABLE);
+ if (selector != null) {
+ // Unsubscribe from read events
+ listen(queueHead == null ? 0 : WRITEABLE);
+ }
if (handling == null) {
scheduleClose();
@@ -176,6 +179,12 @@ protected int processHttpBuffer(byte[] buffer, int length) throws IOException, H
if (parsing == null) {
parsing = parseRequest(buffer, lineStart, lineLength);
+ if (isSsl()) {
+ boolean earlyDataAccepted = socket.getSslOption(SslOption.SESSION_EARLYDATA_ACCEPTED);
+ boolean handshakeDone = socket.getSslOption(SslOption.SESSION_HANDSHAKE_DONE);
+ parsing.setEarlyData(earlyDataAccepted && !handshakeDone);
+ }
+
} else if (lineLength > 0) {
if (parsing.getHeaderCount() < MAX_HEADERS) {
parsing.addHeader(Utf8.read(buffer, lineStart, lineLength));
diff --git a/src/one/nio/http/Request.java b/src/one/nio/http/Request.java
index 5120095..c946018 100755
--- a/src/one/nio/http/Request.java
+++ b/src/one/nio/http/Request.java
@@ -113,6 +113,16 @@ public boolean isHttp11() {
return http11;
}
+ void setEarlyData(boolean earlyData) {
+ if (earlyData) {
+ addHeader("Early-Data: 1");
+ }
+ }
+
+ public boolean isEarlyData() {
+ return "1".equals(getHeader("Early-Data:"));
+ }
+
public String getPath() {
return params >= 0 ? uri.substring(0, params) : uri;
}
@@ -262,6 +272,43 @@ public void consumeHeaders(String prefix, Consumer suffixConsumer) {
}
}
+ /**
+ * Returns trimmed header value after ':' delimiter
+ *
+ * @param key header name without ':'
+ * @return trimmed value after key:
+ */
+ public String getHeaderValue(String key) {
+ int keyLength = key.length();
+ for (int i = 0; i < headerCount; i++) {
+ String header = headers[i];
+ if (header.length() > keyLength
+ && header.charAt(keyLength) == ':'
+ && header.regionMatches(true, 0, key, 0, keyLength)) {
+ return trim(header, keyLength + 1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Consume trimmed header value after ':' delimiter
+
+ * @param key header name without ':'
+ * @param suffixConsumer a function for processing the header value
+ */
+ public void consumeHeaderValues(String key, Consumer suffixConsumer) {
+ int keyLength = key.length();
+ for (int i = 0; i < headerCount; i++) {
+ String header = headers[i];
+ if (header.length() > keyLength
+ && header.charAt(keyLength) == ':'
+ && header.regionMatches(true, 0, key, 0, keyLength)) {
+ suffixConsumer.accept(trim(header, keyLength + 1));
+ }
+ }
+ }
+
public String getHeader(String key, String defaultValue) {
String value = getHeader(key);
return value != null ? value : defaultValue;
diff --git a/src/one/nio/lz4/LZ4.java b/src/one/nio/lz4/LZ4.java
index c349aad..d4eb93e 100644
--- a/src/one/nio/lz4/LZ4.java
+++ b/src/one/nio/lz4/LZ4.java
@@ -133,7 +133,7 @@ public static int decompress(byte[] src, int srcOffset, byte[] dst, int dstOffse
}
if (result < 0) {
- throw new IllegalArgumentException("Malformed input");
+ throw new IllegalArgumentException("Malformed input or destination buffer overflow");
}
return result;
}
@@ -147,7 +147,7 @@ public static int decompress(ByteBuffer src, ByteBuffer dst) {
}
if (result < 0) {
- throw new IllegalArgumentException("Malformed input");
+ throw new IllegalArgumentException("Malformed input or destination buffer overflow");
}
src.position(src.limit());
@@ -541,13 +541,15 @@ private static int decompress(final Object src, final long srcOffset,
s = unsafe.getByte(src, ip++) & 0xff;
length += s;
} while (ip < srcEnd - RUN_MASK && s == 255);
- if (length < 0) return -1; // Error: overflow
+ if (length < 0)
+ return -1; // Error: overflow
}
// Copy literals
long cpy = op + length;
if (cpy > dstEnd - MFLIMIT || ip + length > srcEnd - (2 + 1 + LASTLITERALS)) {
- if (ip + length != srcEnd || cpy > dstEnd) return -1; // Error: input must be consumed
+ if (ip + length != srcEnd || cpy > dstEnd)
+ return -1; // Error: input must be consumed
unsafe.copyMemory(src, ip, dst, op, length);
op += length;
return (int) (op - dstOffset);
@@ -559,18 +561,21 @@ private static int decompress(final Object src, final long srcOffset,
// Get offset
long match = cpy - (unsafe.getShort(src, ip) & 0xffff);
ip += 2;
- if (match < dstOffset) return -1; // Error: offset outside destination buffer
+ if (match < dstOffset)
+ return -1; // Error: offset outside destination buffer
// Get matchlength
length = token & ML_MASK;
if (length == ML_MASK) {
int s;
do {
- if (ip > srcEnd - LASTLITERALS) return -1;
+ if (ip > srcEnd - LASTLITERALS)
+ return -1;
s = unsafe.getByte(src, ip++) & 0xff;
length += s;
} while (s == 255);
- if (length < 0) return -1; // Error: overflow
+ if (length < 0)
+ return -1; // Error: overflow
}
length += MINMATCH;
@@ -593,7 +598,8 @@ private static int decompress(final Object src, final long srcOffset,
}
if (cpy > dstEnd - 12) {
- if (cpy > dstEnd - LASTLITERALS) return -1; // Error: last LASTLITERALS bytes must be literals
+ if (cpy > dstEnd - LASTLITERALS)
+ return -1; // Error: last LASTLITERALS bytes must be literals
if (op < dstEnd - 8) {
wildCopy(dst, match, dst, op, dstEnd - 8);
match += (dstEnd - 8) - op;
diff --git a/src/one/nio/mem/LongHashSet.java b/src/one/nio/mem/LongHashSet.java
index fd17fe9..1e8e839 100755
--- a/src/one/nio/mem/LongHashSet.java
+++ b/src/one/nio/mem/LongHashSet.java
@@ -116,8 +116,18 @@ public final void setKeyAt(int index, long value) {
unsafe.putOrderedLong(null, keys + (long) index * 8, value);
}
+ /**
+ * This method is not atomic and must not be invoked concurrently with other modification methods (e.g., {@link LongHashSet#putKey} or {@link LongHashSet#removeKey})
+ */
public void clear() {
+ int sizeBefore = size;
unsafe.setMemory(keys, (long) capacity * 8, (byte) 0);
+ for (;;) {
+ int current = size;
+ if (unsafe.compareAndSwapInt(this, sizeOffset, current, Math.max(0, current - sizeBefore))) {
+ return;
+ }
+ }
}
protected void incrementSize() {
diff --git a/src/one/nio/mem/MallocMT.java b/src/one/nio/mem/MallocMT.java
index 1d76848..ab71373 100755
--- a/src/one/nio/mem/MallocMT.java
+++ b/src/one/nio/mem/MallocMT.java
@@ -69,6 +69,9 @@ public Malloc segment(int index) {
/**
* Deterministically get one of the segments by some {@code long} value
+ *
+ * @param n an index of the segment to return
+ * @return the {@link Malloc} instance for the specified segment
*/
public Malloc segmentFor(long n) {
return segments[(int) n & (segments.length - 1)];
diff --git a/src/one/nio/mem/OffheapBitSet.java b/src/one/nio/mem/OffheapBitSet.java
index 12d037c..f2bdf1c 100644
--- a/src/one/nio/mem/OffheapBitSet.java
+++ b/src/one/nio/mem/OffheapBitSet.java
@@ -47,7 +47,8 @@ public OffheapBitSet(long address, long sizeBytes) {
}
/**
- * returns the number of 64 bit words it would take to hold numBits
+ * @param numBits a number of bits to hold
+ * @return the number of 64 bit words it would take to hold numBits
*/
public static long bits2words(long numBits) {
return (((numBits - 1) >>> 6) + 1);
@@ -69,7 +70,9 @@ public long capacity() {
* Returns true or false for the specified bit index. The index should be
* less than the capacity.
*
- * @throws IndexOutOfBoundsException
+ * @param index the bit index
+ * @return the value of the bit with the specified index
+ * @throws IndexOutOfBoundsException if the index is out of range
*/
public boolean get(long index) {
return unsafeGet(checkBounds(index));
@@ -80,6 +83,9 @@ public boolean get(long index) {
* bounds. This allows to make it few ticks faster in exchange to seg fault
* possibility. Use when going out of capacity is ensured by other means
* outside of this method
+ *
+ * @param index a bit index
+ * @return the value of the bit with the specified index
*/
public boolean unsafeGet(long index) {
long word = index >> 6; // div 8 and round to long word
@@ -91,6 +97,9 @@ public boolean unsafeGet(long index) {
/**
* Sets the bit at the specified index. The index should be less than the
* capacity.
+ *
+ * @param index a bit index
+ * @throws IndexOutOfBoundsException if the index is out of range
*/
public void set(long index) {
unsafeSet(checkBounds(index));
@@ -106,6 +115,8 @@ public void unsafeSet(long index) {
/**
* clears the bit. The index should be less than the capacity.
+ *
+ * @param index a bit index
*/
public void clear(long index) {
unsafeClear(checkBounds(index));
diff --git a/src/one/nio/net/JavaServerSocket.java b/src/one/nio/net/JavaServerSocket.java
index b6b32ce..f6704dc 100755
--- a/src/one/nio/net/JavaServerSocket.java
+++ b/src/one/nio/net/JavaServerSocket.java
@@ -18,15 +18,22 @@
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
+import java.net.SocketOption;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+import one.nio.util.JavaInternals;
final class JavaServerSocket extends SelectableJavaSocket {
+ private static final SocketOption SO_REUSEPORT_COMPAT = findReusePortOption();
+
final ServerSocketChannel ch;
JavaServerSocket() throws IOException {
@@ -49,7 +56,8 @@ public final void close() {
@Override
public final JavaSocket accept() throws IOException {
- return new JavaSocket(ch.accept());
+ SocketChannel accepted = ch.accept();
+ return accepted != null ? new JavaSocket(accepted) : null;
}
@Override
@@ -208,7 +216,9 @@ public boolean getDeferAccept() {
public final void setReuseAddr(boolean reuseAddr, boolean reusePort) {
try {
ch.setOption(StandardSocketOptions.SO_REUSEADDR, reuseAddr);
- // todo: java 9+ SO_REUSEPORT
+ if (SO_REUSEPORT_COMPAT != null && ch.supportedOptions().contains(SO_REUSEPORT_COMPAT)) {
+ ch.setOption(SO_REUSEPORT_COMPAT, reusePort);
+ }
} catch (IOException e) {
// Ignore
}
@@ -225,7 +235,13 @@ public boolean getReuseAddr() {
@Override
public boolean getReusePort() {
- return false;
+ try {
+ return SO_REUSEPORT_COMPAT != null && ch.supportedOptions().contains(SO_REUSEPORT_COMPAT)
+ ? ch.getOption(SO_REUSEPORT_COMPAT)
+ : false;
+ } catch (IOException e) {
+ return false;
+ }
}
@Override
@@ -266,11 +282,12 @@ public int getTos() {
@Override
public final void setSendBuffer(int sendBuf) {
- // Ignore
+ // See sun.nio.ch.ServerSocketChannelImpl.supportedOptions
}
@Override
public int getSendBuffer() {
+ // See sun.nio.ch.ServerSocketChannelImpl.supportedOptions
return 0;
}
@@ -322,4 +339,15 @@ public T getSslOption(SslOption option) {
public SelectableChannel getSelectableChannel() {
return ch;
}
+
+ private static SocketOption findReusePortOption() {
+ try {
+ Field reusePortField = JavaInternals.findField(StandardSocketOptions.class, "SO_REUSEPORT");
+ if (reusePortField != null) {
+ return (SocketOption) reusePortField.get(null);
+ }
+ } catch (Throwable ignored) {
+ }
+ return null;
+ }
}
diff --git a/src/one/nio/net/JavaSslClientContext.java b/src/one/nio/net/JavaSslClientContext.java
new file mode 100644
index 0000000..ede9167
--- /dev/null
+++ b/src/one/nio/net/JavaSslClientContext.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.net;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+
+// Should be used only for development purposes only on local machine
+public class JavaSslClientContext extends SslContext {
+ private final SSLParameters parameters;
+ private final SSLContext sslContext;
+
+ public JavaSslClientContext() throws NoSuchAlgorithmException, IOException {
+ sslContext = SSLContext.getDefault();
+ parameters = sslContext.getDefaultSSLParameters();
+ }
+
+ public JavaSslClientContext(SSLContext sslContext) {
+ this.sslContext = sslContext;
+ this.parameters = sslContext.getDefaultSSLParameters();
+ }
+
+ @Override
+ public void setDebug(boolean debug) {
+ // Ignore
+ }
+
+ @Override
+ public boolean getDebug() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setRdrand(boolean rdrand) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setProtocols(String protocols) throws SSLException {
+ parameters.setProtocols(protocols.split("\\+"));
+ }
+
+ @Override
+ public void setCiphers(String ciphers) throws SSLException {
+ parameters.setCipherSuites(ciphers.split(":"));
+ }
+
+ @Override
+ public void setCurve(String curve) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setCertificate(String certFile) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setPrivateKey(String privateKeyFile) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setPassphrase(byte[] passphrase) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setCA(String caFile) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setVerify(int verifyMode) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setTicketKeys(byte[] keys) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setSessionCache(String mode, int size) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setTimeout(long timeout) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setSessionId(byte[] sessionId) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setApplicationProtocols(String[] protocols) throws SSLException {
+ parameters.setApplicationProtocols(protocols);
+ }
+
+ @Override
+ public void setOCSP(byte[] response) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setSNI(SslConfig[] sni) throws IOException {
+ // Ignore
+ }
+
+ @Override
+ public void setMaxEarlyData(int size) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setKernelTlsEnabled(boolean kernelTlsEnabled) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setCompressionAlgorithms(String[] algorithms) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setAntiReplayEnabled(boolean antiReplayEnabled) throws SSLException {
+ // Ignore
+ }
+
+ @Override
+ public void setKeylog(boolean keylog) {
+ // Ignore
+ }
+
+ public SSLSocket createSocket() throws IOException {
+ SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
+ socket.setSSLParameters(parameters);
+ return socket;
+ }
+}
diff --git a/src/one/nio/net/JavaSslClientSocket.java b/src/one/nio/net/JavaSslClientSocket.java
new file mode 100644
index 0000000..3f7e269
--- /dev/null
+++ b/src/one/nio/net/JavaSslClientSocket.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+
+import javax.net.ssl.SSLSocket;
+
+import one.nio.mem.DirectMemory;
+
+public final class JavaSslClientSocket extends Socket {
+ private final SSLSocket socket;
+ private final JavaSslClientContext sslContext;
+ private volatile WritableByteChannel outCh;
+ private volatile ReadableByteChannel inCh;
+ private volatile OutputStream outputStream;
+ private volatile InputStream inputStream;
+
+ public JavaSslClientSocket(JavaSslClientContext sslContext) {
+ try {
+ this.sslContext = sslContext;
+ this.socket = this.sslContext.createSocket();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public boolean isOpen() {
+ return !socket.isClosed();
+ }
+
+ @Override
+ public void close() {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public Socket accept() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void connect(InetAddress address, int port) throws IOException {
+ this.socket.connect(new InetSocketAddress(address, port));
+ this.outputStream = socket.getOutputStream();
+ this.outCh = Channels.newChannel(outputStream);
+ this.inputStream = socket.getInputStream();
+ this.inCh = Channels.newChannel(inputStream);
+ }
+
+ @Override
+ public void bind(InetAddress address, int port, int backlog) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int writeRaw(long buf, int count, int flags) throws IOException {
+ return outCh.write(DirectMemory.wrap(buf, count));
+ }
+
+ @Override
+ public int write(byte[] data, int offset, int count, int flags) throws IOException {
+ return outCh.write(ByteBuffer.wrap(data, offset, count));
+ }
+
+ @Override
+ public void writeFully(byte[] data, int offset, int count) throws IOException {
+ outputStream.write(data, offset, count);
+ }
+
+ @Override
+ public int send(ByteBuffer src, int flags, InetAddress address, int port) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int readRaw(long buf, int count, int flags) throws IOException {
+ return inCh.read(DirectMemory.wrap(buf, count));
+ }
+
+ @Override
+ public int read(byte[] data, int offset, int count, int flags) throws IOException {
+ return inCh.read(ByteBuffer.wrap(data, offset, count));
+ }
+
+ @Override
+ public void readFully(byte[] data, int offset, int count) throws IOException {
+ while (count > 0) {
+ int bytes = inputStream.read(data, offset, count);
+ if (bytes < 0) {
+ throw new SocketClosedException();
+ }
+ offset += bytes;
+ count -= bytes;
+ }
+ }
+
+ @Override
+ public long sendFile(RandomAccessFile file, long offset, long count) throws IOException {
+ return file.getChannel().transferTo(offset, count, outCh);
+ }
+
+ @Override
+ public InetSocketAddress recv(ByteBuffer dst, int flags) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int sendMsg(Msg msg, int flags) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int recvMsg(Msg msg, int flags) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setBlocking(boolean blocking) {
+ // Ignore
+ }
+
+ @Override
+ public boolean isBlocking() {
+ return true;
+ }
+
+ @Override
+ public void setTimeout(int timeout) {
+ try {
+ socket.setSoTimeout(timeout);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public int getTimeout() {
+ try {
+ return socket.getSoTimeout();
+ } catch (SocketException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public void setKeepAlive(boolean keepAlive) {
+ try {
+ socket.setKeepAlive(keepAlive);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public boolean getKeepAlive() {
+ try {
+ return socket.getKeepAlive();
+ } catch (SocketException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void setNoDelay(boolean noDelay) {
+ try {
+ socket.setTcpNoDelay(noDelay);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public boolean getNoDelay() {
+ try {
+ return socket.getTcpNoDelay();
+ } catch (SocketException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void setTcpFastOpen(boolean tcpFastOpen) {
+ // Ignore
+ }
+
+ @Override
+ public boolean getTcpFastOpen() {
+ return false;
+ }
+
+ @Override
+ public void setDeferAccept(boolean deferAccept) {
+ // Ignore
+ }
+
+ @Override
+ public boolean getDeferAccept() {
+ return false;
+ }
+
+ @Override
+ public void setReuseAddr(boolean reuseAddr, boolean reusePort) {
+ try {
+ socket.setReuseAddress(reuseAddr);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public boolean getReuseAddr() {
+ try {
+ return socket.getReuseAddress();
+ } catch (SocketException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean getReusePort() {
+ return false;
+ }
+
+ @Override
+ public void setRecvBuffer(int recvBuf) {
+ try {
+ socket.setReceiveBufferSize(recvBuf);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public int getRecvBuffer() {
+ try {
+ return socket.getReceiveBufferSize();
+ } catch (SocketException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public void setSendBuffer(int sendBuf) {
+ try {
+ socket.setSendBufferSize(sendBuf);
+ } catch (SocketException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public int getSendBuffer() {
+ try {
+ return socket.getSendBufferSize();
+ } catch (SocketException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public void setTos(int tos) {
+ // Ignore
+ }
+
+ @Override
+ public int getTos() {
+ return 0;
+ }
+
+ @Override
+ public byte[] getOption(int level, int option) {
+ return new byte[0];
+ }
+
+ @Override
+ public boolean setOption(int level, int option, byte[] value) {
+ return false;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress() {
+ return new InetSocketAddress(socket.getLocalAddress(), socket.getPort());
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress() {
+ return (InetSocketAddress) socket.getRemoteSocketAddress();
+ }
+
+ @Override
+ public Socket sslWrap(SslContext context) {
+ return this;
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException {
+ return inCh.read(dst);
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ return outCh.write(src);
+ }
+
+ @Override
+ public Socket sslUnwrap() {
+ return this;
+ }
+
+ @Override
+ public SslContext getSslContext() {
+ return sslContext;
+ }
+
+ @Override
+ public T getSslOption(SslOption option) {
+ return null;
+ }
+
+ @Override
+ public void listen(int backlog) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/one/nio/net/KeylogHolder.java b/src/one/nio/net/KeylogHolder.java
new file mode 100644
index 0000000..1d6a2d6
--- /dev/null
+++ b/src/one/nio/net/KeylogHolder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.net;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KeylogHolder {
+ private static final Logger log = LoggerFactory.getLogger(KeylogHolder.class);
+
+ public static final BiConsumer DEFAULT_CONSUMER = (keyLine, addr) -> log.info(keyLine);
+ public static final BiConsumer NOP_CONSUMER = (s, bytes) -> {};
+
+ private static volatile BiConsumer CONSUMER = DEFAULT_CONSUMER;
+
+ public static void setConsumer(BiConsumer consumer) {
+ CONSUMER = Objects.requireNonNull(consumer);
+ }
+
+ public static void log(String keyLine, InetSocketAddress addr) {
+ try {
+ CONSUMER.accept(keyLine, addr);
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/net/NativeSocket.java b/src/one/nio/net/NativeSocket.java
index 80a6573..e358c9a 100755
--- a/src/one/nio/net/NativeSocket.java
+++ b/src/one/nio/net/NativeSocket.java
@@ -50,12 +50,14 @@ public final boolean isOpen() {
@Override
public NativeSocket accept() throws IOException {
- return new NativeSocket(accept0(false));
+ int fd = accept0(false);
+ return fd >= 0 ? new NativeSocket(fd) : null;
}
@Override
public NativeSocket acceptNonBlocking() throws IOException {
- return new NativeSocket(accept0(true));
+ int fd = accept0(true);
+ return fd >= 0 ? new NativeSocket(fd) : null;
}
@Override
@@ -298,6 +300,18 @@ public int recvMsg(Msg msg, int flags) throws IOException {
@Override
public final native int getTos();
+ @Override
+ public final native void setNotsentLowat(int lowat);
+
+ @Override
+ public final native int getNotsentLowat();
+
+ @Override
+ public final native void setThinLinearTimeouts(boolean thinLto);
+
+ @Override
+ public final native boolean getThinLinearTimeouts();
+
@Override
public native byte[] getOption(int level, int option);
@@ -315,7 +329,7 @@ static Object toNativeAddr(String host, int port) throws UnknownHostException {
final native void connect0(Object address, int port) throws IOException;
final native void bind0(Object address, int port) throws IOException;
final native int accept0(boolean nonblock) throws IOException;
- final native long sendFile0(int sourceFD, long offset, long count) throws IOException;
+ native long sendFile0(int sourceFD, long offset, long count) throws IOException;
final native int sendTo0(byte[] data, int offset, int size, int flags, Object address, int port) throws IOException;
final native int sendTo1(long buf, int size, int flags, Object address, int port) throws IOException;
final native int recvFrom0(byte[] data, int offset, int maxSize, int flags, AddressHolder holder) throws IOException;
diff --git a/src/one/nio/net/NativeSslContext.java b/src/one/nio/net/NativeSslContext.java
index 74d7ae1..a8a15f3 100755
--- a/src/one/nio/net/NativeSslContext.java
+++ b/src/one/nio/net/NativeSslContext.java
@@ -16,23 +16,59 @@
package one.nio.net;
-import one.nio.mgt.Management;
-import one.nio.util.ByteArrayBuilder;
-import one.nio.util.Utf8;
-
-import javax.net.ssl.SSLException;
import java.io.IOException;
import java.util.ServiceConfigurationError;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLException;
+
+import one.nio.mgt.Management;
+import one.nio.util.ByteArrayBuilder;
+import one.nio.util.Utf8;
+
class NativeSslContext extends SslContext {
private static final AtomicInteger counter = new AtomicInteger();
+
+ private static class CompressionAlgorithms {
+ // Possible compression values from RFC8879 (Refer to openssl/tls1.h)
+ public static int ZLIB = 1;
+ public static int BROTLI = 2;
+ public static int ZSTD = 3;
+ }
+
+ private static class SslOption {
+ public static long ENABLE_KTLS = 1L << 3;
+ public static long NO_COMPRESSION = 1L << 17;
+ public static long NO_SSLv2 = 0; // as of OpenSSL 1.0.2g the SSL_OP_NO_SSLv2 option is set by default.
+ public static long NO_ANTI_REPLAY = 1L << 24;
+ public static long NO_SSLv3 = 1L << 25;
+ public static long NO_TLSv1 = 1L << 26;
+ public static long NO_TLSv1_2 = 1L << 27;
+ public static long NO_TLSv1_1 = 1L << 28;
+ public static long NO_TLSv1_3 = 1L << 29;
+ public static long NO_TX_CERT_COMPRESSION = 1L << 32;
+ }
+
+ private static class CacheMode {
+ public static int NONE = 0;
+ public static int INTERNAL = 1;
+ public static int EXTERNAL = 2;
+ }
+
+ private static final long ALL_DISABLED = SslOption.NO_COMPRESSION
+ | SslOption.NO_SSLv2
+ | SslOption.NO_SSLv3
+ | SslOption.NO_TLSv1
+ | SslOption.NO_TLSv1_1
+ | SslOption.NO_TLSv1_2
+ | SslOption.NO_TLSv1_3;
final int id;
long ctx;
NativeSslContext[] subcontexts;
+
NativeSslContext() throws SSLException {
this.id = counter.incrementAndGet();
this.ctx = ctxNew();
@@ -93,32 +129,68 @@ public void setProtocols(String protocols) {
String protocol = st.nextToken();
switch (protocol) {
case "compression":
- enabled |= 0x00020000;
+ enabled |= SslOption.NO_COMPRESSION;
break;
case "sslv2":
- enabled |= 0x01000000;
+ enabled |= SslOption.NO_SSLv2;
break;
case "sslv3":
- enabled |= 0x02000000;
+ enabled |= SslOption.NO_SSLv3;
break;
case "tlsv1":
- enabled |= 0x04000000;
+ enabled |= SslOption.NO_TLSv1;
break;
case "tlsv1.1":
- enabled |= 0x10000000;
+ enabled |= SslOption.NO_TLSv1_1;
break;
case "tlsv1.2":
- enabled |= 0x08000000;
+ enabled |= SslOption.NO_TLSv1_2;
break;
case "tlsv1.3":
- enabled |= 0x20000000;
+ enabled |= SslOption.NO_TLSv1_3;
break;
}
}
- int all = 0x00020000 + 0x01000000 + 0x02000000 + 0x04000000 + 0x08000000 + 0x10000000 + 0x20000000;
clearOptions(enabled);
- setOptions(all - enabled);
+ setOptions(ALL_DISABLED & ~enabled);
+ }
+
+ @Override
+ public void setKernelTlsEnabled(boolean kernelTlsEnabled) throws SSLException {
+ if (kernelTlsEnabled) {
+ setOptions(SslOption.ENABLE_KTLS);
+ } else {
+ clearOptions(SslOption.ENABLE_KTLS);
+ }
+ }
+
+ @Override
+ public void setAntiReplayEnabled(boolean antiReplayEnabled) throws SSLException {
+ if (antiReplayEnabled) {
+ clearOptions(SslOption.NO_ANTI_REPLAY);
+ } else {
+ setOptions(SslOption.NO_ANTI_REPLAY);
+ }
+ }
+
+ @Override
+ public void setSessionCache(String mode, int size) throws SSLException {
+ switch (mode) {
+ case "none":
+ setCacheMode(CacheMode.NONE);
+ break;
+ case "internal":
+ setCacheMode(CacheMode.INTERNAL);
+ setInternalCacheSize(size);
+ break;
+ case "external":
+ setCacheMode(CacheMode.EXTERNAL);
+ SslSessionCache.Singleton.setCapacity(size);
+ break;
+ default:
+ throw new SSLException("Unsupported session cache mode: " + mode);
+ }
}
@Override
@@ -127,6 +199,15 @@ public void setProtocols(String protocols) {
@Override
public native void setCiphers(String ciphers) throws SSLException;
+ /**
+ * Sets the curve used for ECDH temporary keys used during key exchange.
+ * Use openssl ecparam -list_curves
to get list of supported curves.
+ * @param curve short name of the curve, if null - all curves built into the OpenSSL library will be allowed
+ * @throws SSLException
+ */
+ @Override
+ public native void setCurve(String curve) throws SSLException;
+
@Override
public native void setCertificate(String certFile) throws SSLException;
@@ -145,9 +226,6 @@ public void setProtocols(String protocols) {
@Override
public native void setTicketKeys(byte[] keys) throws SSLException;
- @Override
- public native void setCacheSize(int size) throws SSLException;
-
@Override
public native void setTimeout(long timeout) throws SSLException;
@@ -157,6 +235,9 @@ public void setProtocols(String protocols) {
@Override
public native void setOCSP(byte[] response) throws SSLException;
+ @Override
+ public native void setMaxEarlyData(int size) throws SSLException;
+
@Override
public void setApplicationProtocols(String[] protocols) throws SSLException {
ByteArrayBuilder builder = new ByteArrayBuilder();
@@ -194,14 +275,50 @@ public void setSNI(SslConfig[] sni) throws IOException {
setSNI0(names.toBytes(), contexts);
}
+ @Override
+ public void setCompressionAlgorithms(String[] compressionAlgorithms) throws SSLException {
+ if (compressionAlgorithms == null || compressionAlgorithms.length == 0) {
+ setOptions(SslOption.NO_TX_CERT_COMPRESSION);
+ return;
+ }
+
+ int[] algorithms = new int[compressionAlgorithms.length];
+ for (int i = 0; i < compressionAlgorithms.length; i++) {
+ String algorithm = compressionAlgorithms[i];
+ switch (algorithm) {
+ case "zlib":
+ algorithms[i] = CompressionAlgorithms.ZLIB;
+ break;
+ case "brotli":
+ algorithms[i] = CompressionAlgorithms.BROTLI;
+ break;
+ case "zstd":
+ algorithms[i] = CompressionAlgorithms.ZSTD;
+ break;
+ default:
+ throw new SSLException("Unsupported cert compression algorithm: " + algorithm);
+ }
+ }
+ clearOptions(SslOption.NO_TX_CERT_COMPRESSION);
+ setCompressionAlgorithms0(algorithms);
+ }
+
+ private native void setCompressionAlgorithms0(int[] algorithms) throws SSLException;
+
private native void setSNI0(byte[] names, long[] contexts) throws SSLException;
- private native void setOptions(int options);
- private native void clearOptions(int options);
+ @Override
+ public native void setKeylog(boolean keylog);
+
+ private native void setOptions(long options);
+ private native void clearOptions(long options);
private native long getSessionCounter(int key);
private native long[] getSessionCounters(int keysBitmap);
+ private native void setInternalCacheSize(int size) throws SSLException;
+ private native void setCacheMode(int mode) throws SSLException;
+
private static native void init();
private static native long ctxNew() throws SSLException;
private static native void ctxFree(long ctx);
diff --git a/src/one/nio/net/NativeSslSocket.java b/src/one/nio/net/NativeSslSocket.java
index c1da41e..c460e01 100755
--- a/src/one/nio/net/NativeSslSocket.java
+++ b/src/one/nio/net/NativeSslSocket.java
@@ -23,6 +23,9 @@ class NativeSslSocket extends NativeSocket {
NativeSslContext context;
long ssl;
+ private volatile boolean isEarlyDataAccepted = false;
+ private volatile boolean isHandshakeDone = false;
+
NativeSslSocket(int fd, NativeSslContext context, boolean serverMode) throws IOException {
super(fd);
context.refresh();
@@ -41,12 +44,14 @@ public synchronized void close() {
@Override
public NativeSocket accept() throws IOException {
- return new NativeSslSocket(accept0(false), context, true);
+ int fd = accept0(false);
+ return fd >= 0 ? new NativeSslSocket(fd, context, true) : null;
}
@Override
public NativeSocket acceptNonBlocking() throws IOException {
- return new NativeSslSocket(accept0(true), context, true);
+ int fd = accept0(true);
+ return fd >= 0 ? new NativeSslSocket(fd, context, true) : null;
}
@Override
@@ -63,31 +68,29 @@ public SslContext getSslContext() {
@SuppressWarnings("unchecked")
public Object getSslOption(SslOption option) {
switch (option.id) {
- case 1:
+ case SslOption.PEER_CERTIFICATE_ID:
return sslPeerCertificate();
- case 2:
+ case SslOption.PEER_CERTIFICATE_CHAIN_ID:
return sslPeerCertificateChain();
- case 3:
+ case SslOption.PEER_SUBJECT_ID:
return sslCertName(0);
- case 4:
+ case SslOption.PEER_ISSUER_ID:
return sslCertName(1);
- case 5:
+ case SslOption.VERIFY_RESULT_ID:
return sslVerifyResult();
- case 6:
+ case SslOption.SESSION_REUSED_ID:
return sslSessionReused();
- case 7:
+ case SslOption.SESSION_TICKET_ID:
return sslSessionTicket();
- case 8:
+ case SslOption.CURRENT_CIPHER_ID:
return sslCurrentCipher();
+ case SslOption.SESSION_EARLYDATA_ACCEPTED_ID:
+ return sslSessionEarlyDataAccepted();
+ case SslOption.SESSION_HANDSHAKE_DONE_ID:
+ return sslHandshakeDone();
}
return null;
}
-
- @Override
- public long sendFile(RandomAccessFile file, long offset, long count) throws IOException {
- throw new IOException("Cannot use sendFile with SSL");
- }
-
@Override
public synchronized native void handshake() throws IOException;
@@ -109,6 +112,19 @@ public long sendFile(RandomAccessFile file, long offset, long count) throws IOEx
@Override
public synchronized native void readFully(byte[] data, int offset, int count) throws IOException;
+ @Override
+ synchronized native long sendFile0(int sourceFD, long offset, long count) throws IOException;
+
+ private boolean sslSessionEarlyDataAccepted() {
+ // the value is updated by native code during IO operations
+ return isEarlyDataAccepted;
+ }
+
+ private boolean sslHandshakeDone() {
+ // the value is updated by native code during IO operations
+ return isHandshakeDone;
+ }
+
private synchronized native byte[] sslPeerCertificate();
private synchronized native Object[] sslPeerCertificateChain();
private synchronized native String sslCertName(int which);
diff --git a/src/one/nio/net/Socket.java b/src/one/nio/net/Socket.java
index df5b6ca..30621cf 100755
--- a/src/one/nio/net/Socket.java
+++ b/src/one/nio/net/Socket.java
@@ -141,6 +141,10 @@ public abstract class Socket implements ByteChannel {
public abstract int getSendBuffer();
public abstract void setTos(int tos);
public abstract int getTos();
+ public void setNotsentLowat(int lowat) {}
+ public int getNotsentLowat() {return 0;}
+ public void setThinLinearTimeouts(boolean thinLto) {}
+ public boolean getThinLinearTimeouts(){return false;}
public abstract byte[] getOption(int level, int option);
public abstract boolean setOption(int level, int option, byte[] value);
public abstract InetSocketAddress getLocalAddress();
@@ -152,7 +156,9 @@ public abstract class Socket implements ByteChannel {
public Socket acceptNonBlocking() throws IOException {
Socket s = accept();
- s.setBlocking(false);
+ if (s != null) {
+ s.setBlocking(false);
+ }
return s;
}
@@ -180,8 +186,23 @@ public int read(byte[] data, int offset, int count) throws IOException {
return read(data, offset, count, 0);
}
+ @Deprecated
public static Socket create() throws IOException {
- return NativeLibrary.IS_SUPPORTED ? new NativeSocket(0, SOCK_STREAM) : new JavaSocket();
+ return createClientSocket(null);
+ }
+
+ public static Socket createClientSocket() throws IOException {
+ return createClientSocket(null);
+ }
+
+ public static Socket createClientSocket(SslContext sslContext) throws IOException {
+ Socket socket;
+ if (NativeLibrary.IS_SUPPORTED) {
+ socket = new NativeSocket(0, SOCK_STREAM);
+ } else {
+ socket = sslContext == null ? new JavaSocket() : new JavaSslClientSocket((JavaSslClientContext) sslContext);
+ }
+ return socket;
}
public static Socket createServerSocket() throws IOException {
@@ -201,7 +222,7 @@ public static Socket createUnixSocket(int type) throws IOException {
}
public static Socket connectInet(InetAddress address, int port) throws IOException {
- Socket sock = create();
+ Socket sock = createClientSocket();
sock.connect(address, port);
return sock;
}
diff --git a/src/one/nio/net/SslClientContextFactory.java b/src/one/nio/net/SslClientContextFactory.java
new file mode 100644
index 0000000..837a86c
--- /dev/null
+++ b/src/one/nio/net/SslClientContextFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.net;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+import one.nio.os.NativeLibrary;
+
+public class SslClientContextFactory {
+
+ public static SslContext create() {
+ if (NativeLibrary.IS_SUPPORTED) {
+ return SslContext.getDefault();
+ } else {
+ try {
+ return new JavaSslClientContext();
+ } catch (NoSuchAlgorithmException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/src/one/nio/net/SslConfig.java b/src/one/nio/net/SslConfig.java
index 4226319..7056621 100644
--- a/src/one/nio/net/SslConfig.java
+++ b/src/one/nio/net/SslConfig.java
@@ -25,6 +25,7 @@
public class SslConfig {
// Conservative ciphersuite according to https://wiki.mozilla.org/Security/Server_Side_TLS
static final String DEFAULT_CIPHERS = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA";
+ static final String DEFAULT_CACHE_MODE = "internal";
static final int DEFAULT_CACHE_SIZE = 262144;
static final long DEFAULT_TIMEOUT_SEC = 300;
static final long DEFAULT_REFRESH_INTERVAL = 300_000;
@@ -33,6 +34,7 @@ public class SslConfig {
public boolean rdrand;
public String protocols;
public String ciphers;
+ public String curve;
public String[] certFile;
public String[] privateKeyFile;
public String passphrase;
@@ -40,7 +42,8 @@ public class SslConfig {
public String ticketKeyFile;
public String ticketDir;
public int verifyMode;
- public int cacheSize;
+ public String cacheMode = DEFAULT_CACHE_MODE; // "none", "internal", "external"
+ public int cacheSize = DEFAULT_CACHE_SIZE;
@Converter(method = "longTime")
public long timeout;
@Converter(method = "longTime")
@@ -48,6 +51,11 @@ public class SslConfig {
public String sessionId;
public String[] applicationProtocols;
public String ocspFile;
+ public String[] compressionAlgorithms;
+ public int maxEarlyDataSize = 0; // zero value disables 0-RTT feature
+ public boolean kernelTlsEnabled = false;
+ public boolean antiReplayEnabled = true; // flag is relevant only if early-data used
+ public boolean keylog;
// The following fields should not be updated by SslContext.inherit()
String hostName;
@@ -57,6 +65,7 @@ public static SslConfig from(Properties props) {
SslConfig config = new SslConfig();
config.protocols = props.getProperty("one.nio.ssl.protocols");
config.ciphers = props.getProperty("one.nio.ssl.ciphers");
+ config.curve = props.getProperty("one.nio.ssl.curve");
config.certFile = toArray(props.getProperty("one.nio.ssl.certFile"));
config.privateKeyFile = toArray(props.getProperty("one.nio.ssl.privateKeyFile"));
config.passphrase = props.getProperty("one.nio.ssl.passphrase");
diff --git a/src/one/nio/net/SslContext.java b/src/one/nio/net/SslContext.java
index 775bf26..f50e6c7 100755
--- a/src/one/nio/net/SslContext.java
+++ b/src/one/nio/net/SslContext.java
@@ -16,14 +16,6 @@
package one.nio.net;
-import one.nio.os.NativeLibrary;
-import one.nio.util.ByteArrayBuilder;
-import one.nio.util.Utf8;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ssl.SSLException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
@@ -34,6 +26,15 @@
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
+import javax.net.ssl.SSLException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import one.nio.os.NativeLibrary;
+import one.nio.util.ByteArrayBuilder;
+import one.nio.util.Utf8;
+
public abstract class SslContext {
private static final Logger log = LoggerFactory.getLogger(SslContext.class);
@@ -84,6 +85,9 @@ public synchronized SslContext configure(SslConfig config) throws IOException {
setCiphers(config.ciphers != null ? config.ciphers : SslConfig.DEFAULT_CIPHERS);
+ // with null the curve will be auto-selected by openssl
+ setCurve(config.curve);
+
if (changed(config.passphrase, currentConfig.passphrase)) {
setPassphrase(Utf8.toBytes(getPassphrase(config.passphrase)));
}
@@ -116,9 +120,19 @@ public synchronized SslContext configure(SslConfig config) throws IOException {
}
setVerify(config.verifyMode);
- setCacheSize(config.cacheSize != 0 ? config.cacheSize : SslConfig.DEFAULT_CACHE_SIZE);
+
+ setSessionCache(config.cacheMode, config.cacheSize != 0 ? config.cacheSize : SslConfig.DEFAULT_CACHE_SIZE);
+
setTimeout(config.timeout != 0 ? config.timeout / 1000 : SslConfig.DEFAULT_TIMEOUT_SEC);
+ setMaxEarlyData(config.maxEarlyDataSize);
+
+ if (config.maxEarlyDataSize > 0) {
+ setAntiReplayEnabled(config.antiReplayEnabled);
+ }
+
+ setKernelTlsEnabled(config.kernelTlsEnabled);
+
if (changed(config.sessionId, currentConfig.sessionId)) {
setSessionId(Utf8.toBytes(config.sessionId));
}
@@ -138,6 +152,14 @@ public synchronized SslContext configure(SslConfig config) throws IOException {
setSNI(config.sni);
}
+ if (config.compressionAlgorithms != currentConfig.compressionAlgorithms) {
+ setCompressionAlgorithms(config.compressionAlgorithms);
+ }
+
+ if (config.keylog != currentConfig.keylog) {
+ setKeylog(config.keylog);
+ }
+
this.currentConfig = config;
return this;
}
@@ -285,16 +307,22 @@ void refresh() {
public abstract void setRdrand(boolean rdrand) throws SSLException;
public abstract void setProtocols(String protocols) throws SSLException;
public abstract void setCiphers(String ciphers) throws SSLException;
+ public abstract void setCurve(String curve) throws SSLException;
public abstract void setCertificate(String certFile) throws SSLException;
public abstract void setPrivateKey(String privateKeyFile) throws SSLException;
public abstract void setPassphrase(byte[] passphrase) throws SSLException;
public abstract void setCA(String caFile) throws SSLException;
public abstract void setVerify(int verifyMode) throws SSLException;
public abstract void setTicketKeys(byte[] keys) throws SSLException;
- public abstract void setCacheSize(int size) throws SSLException;
+ public abstract void setSessionCache(String mode, int size) throws SSLException;
public abstract void setTimeout(long timeout) throws SSLException;
public abstract void setSessionId(byte[] sessionId) throws SSLException;
public abstract void setApplicationProtocols(String[] protocols) throws SSLException;
public abstract void setOCSP(byte[] response) throws SSLException;
public abstract void setSNI(SslConfig[] sni) throws IOException;
+ public abstract void setMaxEarlyData(int size) throws SSLException;
+ public abstract void setKernelTlsEnabled(boolean kernelTlsEnabled) throws SSLException;
+ public abstract void setCompressionAlgorithms(String[] algorithms) throws SSLException;
+ public abstract void setAntiReplayEnabled(boolean antiReplayEnabled) throws SSLException;
+ public abstract void setKeylog(boolean keylog);
}
diff --git a/src/one/nio/net/SslOption.java b/src/one/nio/net/SslOption.java
index e374637..7ad8c0e 100644
--- a/src/one/nio/net/SslOption.java
+++ b/src/one/nio/net/SslOption.java
@@ -17,16 +17,29 @@
package one.nio.net;
public class SslOption {
- public static final SslOption PEER_CERTIFICATE = new SslOption<>(1, byte[].class);
- public static final SslOption PEER_CERTIFICATE_CHAIN = new SslOption<>(2, Object[].class);
- public static final SslOption PEER_SUBJECT = new SslOption<>(3, String.class);
- public static final SslOption PEER_ISSUER = new SslOption<>(4, String.class);
- public static final SslOption VERIFY_RESULT = new SslOption<>(5, String.class);
-
- public static final SslOption SESSION_REUSED = new SslOption<>(6, Boolean.class);
- public static final SslOption SESSION_TICKET = new SslOption<>(7, Integer.class);
-
- public static final SslOption CURRENT_CIPHER = new SslOption<>(8, String.class);
+ static final int PEER_CERTIFICATE_ID = 1;
+ static final int PEER_CERTIFICATE_CHAIN_ID = 2;
+ static final int PEER_SUBJECT_ID = 3;
+ static final int PEER_ISSUER_ID = 4;
+ static final int VERIFY_RESULT_ID = 5;
+ static final int SESSION_REUSED_ID = 6;
+ static final int SESSION_TICKET_ID = 7;
+ static final int CURRENT_CIPHER_ID = 8;
+ static final int SESSION_EARLYDATA_ACCEPTED_ID = 9;
+ static final int SESSION_HANDSHAKE_DONE_ID = 10;
+
+ public static final SslOption PEER_CERTIFICATE = new SslOption<>(PEER_CERTIFICATE_ID, byte[].class);
+ public static final SslOption PEER_CERTIFICATE_CHAIN = new SslOption<>(PEER_CERTIFICATE_CHAIN_ID, Object[].class);
+ public static final SslOption PEER_SUBJECT = new SslOption<>(PEER_SUBJECT_ID, String.class);
+ public static final SslOption PEER_ISSUER = new SslOption<>(PEER_ISSUER_ID, String.class);
+ public static final SslOption VERIFY_RESULT = new SslOption<>(VERIFY_RESULT_ID, String.class);
+
+ public static final SslOption SESSION_REUSED = new SslOption<>(SESSION_REUSED_ID, Boolean.class);
+ public static final SslOption SESSION_TICKET = new SslOption<>(SESSION_TICKET_ID, Integer.class);
+
+ public static final SslOption CURRENT_CIPHER = new SslOption<>(CURRENT_CIPHER_ID, String.class);
+ public static final SslOption SESSION_EARLYDATA_ACCEPTED = new SslOption<>(SESSION_EARLYDATA_ACCEPTED_ID, Boolean.class);
+ public static final SslOption SESSION_HANDSHAKE_DONE = new SslOption<>(SESSION_HANDSHAKE_DONE_ID, Boolean.class);
final int id;
final Class type;
diff --git a/src/one/nio/net/SslSessionCache.java b/src/one/nio/net/SslSessionCache.java
new file mode 100644
index 0000000..079dd9e
--- /dev/null
+++ b/src/one/nio/net/SslSessionCache.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.net;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sun.security.util.Cache;
+
+import java.util.Objects;
+
+public interface SslSessionCache {
+ Logger log = LoggerFactory.getLogger(SslSessionCache.class);
+
+ void resize(int maxSize);
+
+ void addSession(byte[] sessionId, byte[] session);
+
+ byte[] getSession(byte[] sessionId);
+
+ void removeSession(byte[] sessionId);
+
+
+ class Singleton {
+ private static volatile SslSessionCache INSTANCE;
+ private static Singleton.Factory FACTORY = Default::new;
+ private static int CAPACITY = Default.CAPACITY;
+
+ public interface Factory {
+ SslSessionCache create(int size);
+ }
+
+ public synchronized static void setFactory(Factory factory) {
+ if (INSTANCE != null) {
+ throw new IllegalStateException("Unable to change factory after lazy instantiation is done");
+ }
+ Singleton.FACTORY = Objects.requireNonNull(factory);
+ }
+
+ public synchronized static void setCapacity(int capacity) {
+ if (capacity < 0) {
+ throw new IllegalArgumentException("Capacity must be positive");
+ }
+ if (INSTANCE != null && CAPACITY != capacity) {
+ INSTANCE.resize(capacity);
+ }
+ Singleton.CAPACITY = capacity;
+ }
+
+ public static SslSessionCache getInstance() {
+ if (INSTANCE == null) {
+ synchronized (Singleton.class) {
+ if (INSTANCE == null) {
+ INSTANCE = FACTORY.create(CAPACITY);
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+ private synchronized static void clearInstance() {
+ Singleton.INSTANCE = null;
+ }
+ }
+
+ class Default implements SslSessionCache {
+ private final Cache cache;
+ static int CAPACITY = 1024;
+
+ private static Cache.EqualByteArray toKey(byte[] bytes) {
+ return new Cache.EqualByteArray(bytes);
+ }
+
+ public Default(int maxSize) {
+ this.cache = Cache.newSoftMemoryCache(maxSize);
+ }
+
+ public Default() {
+ this(Default.CAPACITY);
+ }
+
+ @Override
+ public void resize(int maxSize) {
+ cache.setCapacity(maxSize);
+ }
+
+ @Override
+ public void addSession(byte[] sessionId, byte[] session) {
+ cache.put(toKey(sessionId), session);
+ }
+
+ @Override
+ public byte[] getSession(byte[] sessionId) {
+ return cache.get(toKey(sessionId));
+ }
+
+ @Override
+ public void removeSession(byte[] sessionId) {
+ cache.remove(toKey(sessionId));
+ }
+ }
+}
diff --git a/src/one/nio/net/native/socket.c b/src/one/nio/net/native/socket.c
index 4ca28e9..6f74fd6 100755
--- a/src/one/nio/net/native/socket.c
+++ b/src/one/nio/net/native/socket.c
@@ -56,6 +56,9 @@ static pthread_t* fd_table;
#define TCP_FASTOPEN 23
#endif
+#ifndef TCP_NOTSENT_LOWAT
+#define TCP_NOTSENT_LOWAT 25
+#endif
static socklen_t sockaddr_from_java(JNIEnv* env, jobject address, jint port, struct sockaddr_storage* sa) {
// AF_UNIX
@@ -96,7 +99,7 @@ static socklen_t sockaddr_from_java(JNIEnv* env, jobject address, jint port, str
}
}
-static jobject sockaddr_to_java(JNIEnv* env, struct sockaddr_storage* sa, socklen_t len) {
+jobject sockaddr_to_java(JNIEnv* env, struct sockaddr_storage* sa, socklen_t len) {
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
int ip = ntohl(sin->sin_addr.s_addr);
@@ -252,6 +255,9 @@ Java_one_nio_net_NativeSocket_accept0(JNIEnv* env, jobject self, jboolean nonblo
end_blocking_call(fd_lock);
if (result == -1) {
+ if (errno == EWOULDBLOCK) {
+ return -1;
+ }
throw_io_exception(env);
}
return result;
@@ -882,6 +888,31 @@ Java_one_nio_net_NativeSocket_getTos(JNIEnv* env, jobject self) {
return get_int_socket_opt(fd, IPPROTO_IP, IP_TOS);
}
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSocket_setNotsentLowat(JNIEnv* env, jobject self, jint lowat) {
+ int fd = (*env)->GetIntField(env, self, f_fd);
+ setsockopt(fd, SOL_TCP, TCP_NOTSENT_LOWAT, &lowat, sizeof(lowat));
+}
+
+JNIEXPORT jint JNICALL
+Java_one_nio_net_NativeSocket_getNotsentLowat(JNIEnv* env, jobject self) {
+ int fd = (*env)->GetIntField(env, self, f_fd);
+ return get_int_socket_opt(fd, SOL_TCP, TCP_NOTSENT_LOWAT);
+}
+
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSocket_setThinLinearTimeouts(JNIEnv* env, jobject self, jboolean thinLto) {
+ int fd = (*env)->GetIntField(env, self, f_fd);
+ int value = (int) thinLto;
+ setsockopt(fd, SOL_TCP, TCP_THIN_LINEAR_TIMEOUTS, &value, sizeof(value));
+}
+
+JNIEXPORT jboolean JNICALL
+Java_one_nio_net_NativeSocket_getThinLinearTimeouts(JNIEnv* env, jobject self) {
+ int fd = (*env)->GetIntField(env, self, f_fd);
+ return get_bool_socket_opt(fd, SOL_TCP, TCP_THIN_LINEAR_TIMEOUTS);
+}
+
JNIEXPORT jbyteArray JNICALL
Java_one_nio_net_NativeSocket_getOption(JNIEnv* env, jobject self, jint level, jint option) {
int fd = (*env)->GetIntField(env, self, f_fd);
diff --git a/src/one/nio/net/native/ssl.c b/src/one/nio/net/native/ssl.c
index 4bba9bf..434d5d0 100755
--- a/src/one/nio/net/native/ssl.c
+++ b/src/one/nio/net/native/ssl.c
@@ -25,21 +25,20 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
#include
+#include
#include
#include
#include
-#include
-#include
#include
#include
#include "jni_util.h"
-#include "sslcompat.h"
#define MAX_COUNTERS 32
@@ -49,7 +48,15 @@ enum SSLFlags {
SF_HANDSHAKED = 2,
SF_HAS_TICKET = 4,
SF_HAS_OLD_TICKET = 8,
- SF_NEW_TICKET = 12
+ SF_NEW_TICKET = SF_HAS_TICKET | SF_HAS_OLD_TICKET,
+ SF_EARLY_DATA_ENABLED = 16,
+ SF_EARLY_DATA_FINISHED = 32,
+};
+
+enum SSLCacheMode {
+ CACHE_MODE_NONE = 0,
+ CACHE_MODE_INTERNAL = 1,
+ CACHE_MODE_EXTERNAL = 2,
};
typedef struct {
@@ -90,8 +97,25 @@ typedef struct {
static jfieldID f_ctx;
static jfieldID f_ssl;
+static jfieldID f_isEarlyDataAccepted;
+static jfieldID f_isHandshakeDone;
static int preclosed_socket;
+static jfieldID f_sslSessionCache;
+
+static JavaVM* global_vm;
+static jclass c_KeylogHolder;
+static jmethodID m_log;
+
+static jclass c_SslSessionCacheSingleton;
+static jclass c_SslSessionCache;
+static jmethodID m_getInstance;
+static jmethodID m_clearInstance;
+
+static jmethodID m_addSession;
+static jmethodID m_getSession;
+static jmethodID m_removeSession;
+
// openssl dhparam -C 2048
static unsigned char dh2048_p[] = {
0xF5, 0x03, 0x6F, 0xFC, 0xA7, 0xFD, 0xC7, 0xD2, 0x69, 0xD8, 0xED, 0x73, 0x7D, 0x4D, 0x2A, 0x05,
@@ -115,6 +139,7 @@ static unsigned char dh2048_g[] = { 0x02 };
extern void throw_socket_closed_cached(JNIEnv* env);
+extern jobject sockaddr_to_java(JNIEnv* env, struct sockaddr_storage* sa, socklen_t len);
static void throw_ssl_exception(JNIEnv* env) {
char buf[256];
@@ -134,7 +159,9 @@ static int check_ssl_error(JNIEnv* env, SSL* ssl, int ret) {
throw_socket_closed_cached(env);
return 0;
case SSL_ERROR_SYSCALL:
- if (ERR_peek_error()) {
+ {
+ unsigned long e = ERR_peek_error();
+ if (e && !ERR_SYSTEM_ERROR(e)) {
throw_ssl_exception(env);
} else if (ret == 0 || errno == 0) {
// OpenSSL 1.0 and 1.1 return different error code in case of "dirty" connection close
@@ -143,7 +170,16 @@ static int check_ssl_error(JNIEnv* env, SSL* ssl, int ret) {
throw_io_exception(env);
}
return 0;
+ }
case SSL_ERROR_SSL:
+ // workaround for SSL_sendfile() OpenSSL issue #23722 [ https://github.com/openssl/openssl/issues/23722 ]
+ {
+ int reason = ERR_GET_REASON(ERR_peek_error());
+ if ((errno == EPIPE || errno == ECONNRESET) && reason == SSL_R_UNINITIALIZED) {
+ throw_io_exception(env);
+ return 0;
+ }
+ }
throw_ssl_exception(env);
return 0;
case SSL_ERROR_WANT_READ:
@@ -159,7 +195,7 @@ static int check_ssl_error(JNIEnv* env, SSL* ssl, int ret) {
return err;
}
default:
- sprintf(buf, "Unexpected SSL error code (%d)", err);
+ snprintf(buf, sizeof(buf), "Unexpected SSL error code (%d)", err);
throw_by_name(env, "javax/net/ssl/SSLException", buf);
return 0;
}
@@ -194,6 +230,7 @@ static void ssl_debug(const SSL* ssl, const char* fmt, ...) {
char buf[128];
printf("ssl_debug [%s]: %s\n", ssl_get_peer_ip(ssl, buf, sizeof(buf)), message);
+ fflush(stdout);
}
static long get_session_counter(SSL_CTX* ctx, int key) {
@@ -238,14 +275,6 @@ static void setup_dh_params(SSL_CTX* ctx) {
}
}
-static void setup_ecdh_params(SSL_CTX* ctx) {
- EC_KEY* ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
- if (ecdh != NULL) {
- SSL_CTX_set_tmp_ecdh(ctx, ecdh);
- EC_KEY_free(ecdh);
- }
-}
-
static AppData* create_app_data() {
AppData* appData = calloc(1, sizeof(AppData));
if (appData != NULL) {
@@ -307,6 +336,7 @@ static int ticket_key_callback(SSL* ssl, unsigned char key_name[16], unsigned ch
TicketArray* tickets = &appData->tickets;
Ticket* ticket = tickets->data;
+ intptr_t ssl_flags = (intptr_t)SSL_get_app_data(ssl);
if (ticket == NULL) {
// No ticket keys set
} else if (new_session) {
@@ -314,7 +344,7 @@ static int ticket_key_callback(SSL* ssl, unsigned char key_name[16], unsigned ch
memcpy(key_name, ticket->name, 16);
EVP_EncryptInit_ex(evp_ctx, EVP_aes_128_cbc(), NULL, ticket->aes_key, iv);
HMAC_Init_ex(hmac_ctx, ticket->hmac_key, 16, EVP_sha256(), NULL);
- SSL_set_app_data(ssl, (char*)(SF_SERVER | SF_NEW_TICKET));
+ SSL_set_app_data(ssl, (char*)(ssl_flags | SF_NEW_TICKET));
result = 1;
}
} else {
@@ -323,8 +353,8 @@ static int ticket_key_callback(SSL* ssl, unsigned char key_name[16], unsigned ch
if (memcmp(key_name, ticket->name, 16) == 0) {
HMAC_Init_ex(hmac_ctx, ticket->hmac_key, 16, EVP_sha256(), NULL);
EVP_DecryptInit_ex(evp_ctx, EVP_aes_128_cbc(), NULL, ticket->aes_key, iv);
- intptr_t ticket_options = i == 0 ? SF_SERVER | SF_HAS_TICKET : SF_SERVER | SF_HAS_OLD_TICKET;
- SSL_set_app_data(ssl, (char*)ticket_options);
+ intptr_t ticket_options = i == 0 ? SF_HAS_TICKET : SF_HAS_OLD_TICKET;
+ SSL_set_app_data(ssl, (char*)(ssl_flags | ticket_options));
result = i == 0 ? 1 : 2;
break;
}
@@ -375,7 +405,7 @@ static int ocsp_callback(SSL* ssl, void* arg) {
if (appData->debug) {
ssl_debug(ssl, "ocsp_callback: result=%d", result);
}
-
+
pthread_rwlock_unlock(&appData->lock);
return result;
}
@@ -418,11 +448,13 @@ static int sni_callback(SSL* ssl, int* unused, void* arg) {
static void ssl_info_callback(const SSL* ssl, int cb, int ret) {
if (cb == SSL_CB_HANDSHAKE_START) {
+#ifndef SSL_OP_NO_RENEGOTIATION
// Reject any renegotiation by replacing actual socket with a dummy
intptr_t flags = (intptr_t)SSL_get_app_data(ssl);
if (flags & SF_HANDSHAKED) {
SSL_set_fd((SSL*)ssl, preclosed_socket);
}
+#endif
} else if (cb == SSL_CB_HANDSHAKE_DONE) {
intptr_t flags = (intptr_t)SSL_get_app_data(ssl);
if (flags & SF_SERVER) {
@@ -431,6 +463,10 @@ static void ssl_info_callback(const SSL* ssl, int cb, int ret) {
}
}
+static void update_NativeSslSocket_isHandshakeDone_field(JNIEnv* env, jobject self, SSL* ssl);
+static void update_NativeSslSocket_isEarlyDataAccepted_field(JNIEnv* env, jobject self, SSL* ssl);
+
+
static jbyteArray X509_cert_to_jbyteArray(JNIEnv* env, X509* cert) {
jbyteArray result = NULL;
@@ -449,11 +485,8 @@ static jbyteArray X509_cert_to_jbyteArray(JNIEnv* env, X509* cert) {
JNIEXPORT void JNICALL
Java_one_nio_net_NativeSslContext_init(JNIEnv* env, jclass cls) {
- if (dlopen("libssl.so.3", RTLD_LAZY | RTLD_GLOBAL) == NULL &&
- dlopen("libssl.so", RTLD_LAZY | RTLD_GLOBAL) == NULL &&
- dlopen("libssl.so.1.0.0", RTLD_LAZY | RTLD_GLOBAL) == NULL &&
- dlopen("libssl.so.10", RTLD_LAZY | RTLD_GLOBAL) == NULL) {
- throw_by_name(env, "java/lang/UnsupportedOperationException", "Failed to load libssl.so");
+ if (dlopen("libssl.so.3", RTLD_LAZY | RTLD_GLOBAL) == NULL) {
+ throw_by_name(env, "java/lang/UnsupportedOperationException", "Failed to load libssl.so.3");
return;
}
@@ -464,8 +497,113 @@ Java_one_nio_net_NativeSslContext_init(JNIEnv* env, jclass cls) {
f_ctx = cache_field(env, "one/nio/net/NativeSslContext", "ctx", "J");
f_ssl = cache_field(env, "one/nio/net/NativeSslSocket", "ssl", "J");
+ f_isEarlyDataAccepted = cache_field(env, "one/nio/net/NativeSslSocket", "isEarlyDataAccepted", "Z");
+ f_isHandshakeDone = cache_field(env, "one/nio/net/NativeSslSocket", "isHandshakeDone", "Z");
preclosed_socket = socket(PF_INET, SOCK_STREAM, 0);
+
+ (*env)->GetJavaVM(env, &global_vm);
+ c_KeylogHolder = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "one/nio/net/KeylogHolder"));
+ m_log = (*env)->GetStaticMethodID(env, c_KeylogHolder, "log", "(Ljava/lang/String;Ljava/net/InetSocketAddress;)V");
+
+ c_SslSessionCacheSingleton = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "one/nio/net/SslSessionCache$Singleton"));
+ c_SslSessionCache = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "one/nio/net/SslSessionCache"));
+
+ m_getInstance = (*env)->GetStaticMethodID(env, c_SslSessionCacheSingleton, "getInstance", "()Lone/nio/net/SslSessionCache;");
+ m_clearInstance = (*env)->GetStaticMethodID(env, c_SslSessionCacheSingleton, "clearInstance", "()V");
+ m_addSession = (*env)->GetMethodID(env, c_SslSessionCache, "addSession", "([B[B)V");
+ m_getSession = (*env)->GetMethodID(env, c_SslSessionCache, "getSession", "([B)[B");
+ m_removeSession = (*env)->GetMethodID(env, c_SslSessionCache, "removeSession", "([B)V");
+}
+
+static int new_session_cb(SSL* ssl, SSL_SESSION* ssl_session) {
+ JNIEnv* env;
+ if (JNI_OK != (*global_vm)->GetEnv(global_vm, (void**)&env, JNI_VERSION_1_8)) {
+ return 0;
+ }
+ jobject sslSessionCache = (*env)->CallStaticObjectMethod(env, c_SslSessionCacheSingleton, m_getInstance);
+ if (sslSessionCache == NULL) {
+ return 0;
+ }
+
+ int session_id_len;
+ const char* session_id = SSL_SESSION_get_id(ssl_session, &session_id_len);
+ jbyteArray sessionId = (*env)->NewByteArray(env, session_id_len);
+ if (sessionId == NULL) {
+ return 0;
+ }
+ (*env)->SetByteArrayRegion(env, sessionId, 0, session_id_len, (jbyte*)session_id);
+
+ int session_len = i2d_SSL_SESSION(ssl_session, NULL);
+ if (session_len == 0) {
+ return 0;
+ }
+ jbyteArray session = (*env)->NewByteArray(env, session_len);
+ if (session == NULL) {
+ return 0;
+ }
+
+ jbyte* b_session = (*env)->GetByteArrayElements(env, session, NULL);
+ unsigned char* ptr = (unsigned char*)b_session;
+ i2d_SSL_SESSION(ssl_session, &ptr);
+ (*env)->ReleaseByteArrayElements(env, session, b_session, 0);
+
+
+ (*env)->CallObjectMethod(env, sslSessionCache, m_addSession, sessionId, session);
+ return 0;
+}
+
+static SSL_SESSION* get_session_cb(SSL* ssl, const unsigned char* session_id, int session_id_len, int* copy) {
+ *copy = 0;
+
+ JNIEnv* env;
+ if (JNI_OK != (*global_vm)->GetEnv(global_vm, (void**)&env, JNI_VERSION_1_8)) {
+ return NULL;
+ }
+ jobject sslSessionCache = (*env)->CallStaticObjectMethod(env, c_SslSessionCacheSingleton, m_getInstance);
+ if (sslSessionCache == NULL) {
+ return NULL;
+ }
+
+ jbyteArray sessionId = (*env)->NewByteArray(env, session_id_len);
+ if (sessionId == NULL) {
+ return NULL;
+ }
+ (*env)->SetByteArrayRegion(env, sessionId, 0, session_id_len, (jbyte*)session_id);
+
+ jbyteArray session = (*env)->CallObjectMethod(env, sslSessionCache, m_getSession, sessionId);
+ if (session == NULL) {
+ return NULL;
+ }
+ jbyte* b_session = (*env)->GetByteArrayElements(env, session, NULL);
+ if (b_session == NULL) {
+ return NULL;
+ }
+
+ int session_len = (*env)->GetArrayLength(env, session);
+ const unsigned char* ptr = (const unsigned char*)b_session;
+ SSL_SESSION* ssl_session = d2i_SSL_SESSION(NULL, &ptr, session_len);
+ (*env)->ReleaseByteArrayElements(env, session, b_session, JNI_ABORT);
+ return ssl_session;
+}
+
+static void remove_session_cb(SSL_CTX* ssl, SSL_SESSION* ssl_session) {
+ JNIEnv* env;
+ if (JNI_OK != (*global_vm)->GetEnv(global_vm, (void**)&env, JNI_VERSION_1_8)) {
+ return;
+ }
+ jobject sslSessionCache = (*env)->CallStaticObjectMethod(env, c_SslSessionCacheSingleton, m_getInstance);
+ if (sslSessionCache == NULL) {
+ return;
+ }
+
+ int session_id_len;
+ const char* session_id = SSL_SESSION_get_id(ssl_session, &session_id_len);
+ jbyteArray sessionId = (*env)->NewByteArray(env, session_id_len);
+ if (sessionId != NULL) {
+ (*env)->SetByteArrayRegion(env, sessionId, 0, session_id_len, (jbyte*)session_id);
+ (*env)->CallObjectMethod(env, sslSessionCache, m_removeSession, sessionId);
+ }
}
JNIEXPORT jlong JNICALL
@@ -494,7 +632,6 @@ Java_one_nio_net_NativeSslContext_ctxNew(JNIEnv* env, jclass cls) {
SSL_CTX_set_app_data(ctx, appData);
setup_dh_params(ctx);
- setup_ecdh_params(ctx);
return (jlong)(intptr_t)ctx;
}
@@ -523,13 +660,13 @@ Java_one_nio_net_NativeSslContext_getDebug(JNIEnv* env, jobject self) {
}
JNIEXPORT void JNICALL
-Java_one_nio_net_NativeSslContext_setOptions(JNIEnv* env, jobject self, jint options) {
+Java_one_nio_net_NativeSslContext_setOptions(JNIEnv* env, jobject self, jlong options) {
SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
SSL_CTX_set_options(ctx, options);
}
JNIEXPORT void JNICALL
-Java_one_nio_net_NativeSslContext_clearOptions(JNIEnv* env, jobject self, jint options) {
+Java_one_nio_net_NativeSslContext_clearOptions(JNIEnv* env, jobject self, jlong options) {
SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
SSL_CTX_clear_options(ctx, options);
}
@@ -539,14 +676,20 @@ Java_one_nio_net_NativeSslContext_setRdrand(JNIEnv* env, jobject self, jboolean
if (rdrand) {
OPENSSL_init_crypto(/* OPENSSL_INIT_ENGINE_RDRAND */ 0x200L, NULL);
ENGINE* e = ENGINE_by_id("rdrand");
- if (e == NULL || !ENGINE_init(e) || !ENGINE_set_default_RAND(e)) {
- throw_ssl_exception(env);
+ if (e != NULL) {
+ if (ENGINE_init(e) && ENGINE_set_default_RAND(e)) {
+ RAND_set_rand_method(ENGINE_get_RAND(e));
+ ENGINE_free(e);
+ return;
+ }
+ ENGINE_free(e);
}
- RAND_set_rand_method(ENGINE_get_RAND(e));
+ throw_ssl_exception(env);
} else {
ENGINE* e = ENGINE_by_id("rdrand");
if (e != NULL) {
ENGINE_unregister_RAND(e);
+ ENGINE_free(e);
}
ERR_clear_error();
}
@@ -566,6 +709,19 @@ Java_one_nio_net_NativeSslContext_setCiphers(JNIEnv* env, jobject self, jstring
}
}
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSslContext_setCurve(JNIEnv* env, jobject self, jstring curve) {
+ SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
+ if (curve != NULL) {
+ const char* value = (*env)->GetStringUTFChars(env, curve, NULL);
+ int result = SSL_CTX_set1_curves_list(ctx, value);
+ (*env)->ReleaseStringUTFChars(env, curve, value);
+ if (result == 0) {
+ throw_ssl_exception(env);
+ }
+ }
+}
+
JNIEXPORT void JNICALL
Java_one_nio_net_NativeSslContext_setCertificate(JNIEnv* env, jobject self, jstring certFile) {
SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
@@ -783,7 +939,7 @@ Java_one_nio_net_NativeSslContext_setSNI0(JNIEnv* env, jobject self, jbyteArray
contexts = (jlong*)(names + names_len);
(*env)->GetLongArrayRegion(env, sniContexts, 0, contexts_len, contexts);
}
-
+
if (pthread_rwlock_wrlock(&appData->lock) != 0) {
throw_by_name(env, "javax/net/ssl/SSLException", "Invalid state of appData lock");
free(names);
@@ -793,7 +949,7 @@ Java_one_nio_net_NativeSslContext_setSNI0(JNIEnv* env, jobject self, jbyteArray
free(sni->names);
sni->names = names;
sni->contexts = contexts;
-
+
if (names != NULL) {
SSL_CTX_set_tlsext_servername_callback(ctx, sni_callback);
}
@@ -801,6 +957,25 @@ Java_one_nio_net_NativeSslContext_setSNI0(JNIEnv* env, jobject self, jbyteArray
pthread_rwlock_unlock(&appData->lock);
}
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSslContext_setCompressionAlgorithms0(JNIEnv* env, jobject self, jintArray algorithms) {
+#ifdef TLSEXT_comp_cert_limit
+ SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
+ if (algorithms != NULL) {
+ jint len = (*env)->GetArrayLength(env, algorithms);
+ jint* algs = (*env)->GetIntArrayElements(env, algorithms, NULL);
+
+ int result = SSL_CTX_set1_cert_comp_preference(ctx, (int*)algs, len);
+ (*env)->ReleaseIntArrayElements(env, algorithms, algs, JNI_ABORT);
+ if (result == 0) {
+ throw_by_name(env, "javax/net/ssl/SSLException", "Cannot set certificate compression algorithm");
+ return;
+ }
+ SSL_CTX_compress_certs(ctx, 0);
+ }
+#endif
+}
+
JNIEXPORT void JNICALL
Java_one_nio_net_NativeSslContext_setSessionId(JNIEnv* env, jobject self, jbyteArray sessionId) {
SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
@@ -816,7 +991,32 @@ Java_one_nio_net_NativeSslContext_setSessionId(JNIEnv* env, jobject self, jbyteA
}
JNIEXPORT void JNICALL
-Java_one_nio_net_NativeSslContext_setCacheSize(JNIEnv* env, jobject self, jint size) {
+Java_one_nio_net_NativeSslContext_setCacheMode(JNIEnv* env, jobject self, jint mode) {
+ SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
+
+ SSL_CTX_sess_set_get_cb(ctx, mode == CACHE_MODE_EXTERNAL ? get_session_cb : NULL);
+ SSL_CTX_sess_set_new_cb(ctx, mode == CACHE_MODE_EXTERNAL ? new_session_cb : NULL);
+ SSL_CTX_sess_set_remove_cb(ctx, mode == CACHE_MODE_EXTERNAL ? remove_session_cb : NULL);
+
+ switch (mode) {
+ case CACHE_MODE_NONE:
+ (*env)->CallStaticObjectMethod(env, c_SslSessionCacheSingleton, m_clearInstance);
+ SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
+ break;
+ case CACHE_MODE_INTERNAL:
+ (*env)->CallStaticObjectMethod(env, c_SslSessionCacheSingleton, m_clearInstance);
+ SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER);
+ break;
+ case CACHE_MODE_EXTERNAL:
+ SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL_LOOKUP);
+ break;
+ default:
+ throw_illegal_argument_msg(env, "Unknown cache mode value");
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSslContext_setInternalCacheSize(JNIEnv* env, jobject self, jint size) {
SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
SSL_CTX_sess_set_cache_size(ctx, size);
}
@@ -852,16 +1052,32 @@ Java_one_nio_net_NativeSslContext_getSessionCounters(JNIEnv* env, jobject self,
return values;
}
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSslContext_setMaxEarlyData(JNIEnv* env, jobject self, jint size) {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ SSL_CTX* ctx = (SSL_CTX*)(intptr_t)(*env)->GetLongField(env, self, f_ctx);
+ SSL_CTX_set_max_early_data(ctx, size);
+#endif
+}
+
JNIEXPORT jlong JNICALL
Java_one_nio_net_NativeSslSocket_sslNew(JNIEnv* env, jclass cls, jint fd, jlong ctx, jboolean serverMode) {
SSL* ssl = SSL_new((SSL_CTX*)(intptr_t)ctx);
if (ssl != NULL && SSL_set_fd(ssl, fd)) {
if (serverMode) {
SSL_set_accept_state(ssl);
- SSL_set_app_data(ssl, (char*)SF_SERVER);
+
+ intptr_t flags = SF_SERVER;
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ flags |= SSL_CTX_get_max_early_data((SSL_CTX*)ctx) > 0 ? SF_EARLY_DATA_ENABLED : 0;
+#endif
+ SSL_set_app_data(ssl, (char*)flags);
} else {
SSL_set_connect_state(ssl);
}
+#ifdef SSL_OP_NO_RENEGOTIATION
+ SSL_set_options(ssl, SSL_OP_NO_RENEGOTIATION);
+#endif
return (jlong)(intptr_t)ssl;
}
@@ -899,8 +1115,22 @@ Java_one_nio_net_NativeSslSocket_writeRaw(JNIEnv* env, jobject self, jlong buf,
throw_socket_closed(env);
return 0;
} else {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ if (!SSL_is_init_finished(ssl)) {
+ while (1) {
+ size_t written;
+ int result = SSL_write_early_data(ssl, (void*)(intptr_t)buf, count, &written);
+ if (result == 1) {
+ return written;
+ } else if ((result = check_ssl_error(env, ssl, 0)) != SSL_ERROR_WANT_WRITE || errno != EINTR) {
+ return result == SSL_ERROR_WANT_READ ? -1 : 0;
+ }
+ }
+ }
+#endif
while (1) {
int result = SSL_write(ssl, (void*)(intptr_t)buf, count);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
if (result > 0) {
return result;
} else if (check_ssl_error(env, ssl, result) != SSL_ERROR_WANT_WRITE || errno != EINTR) {
@@ -920,9 +1150,24 @@ Java_one_nio_net_NativeSslSocket_write(JNIEnv* env, jobject self, jbyteArray dat
return 0;
} else {
if (count > MAX_STACK_BUF) count = MAX_STACK_BUF;
- (*env)->GetByteArrayRegion(env, data, offset, count, buf);
+ (*env)->GetByteArrayRegion(env, data, offset, count <= MAX_STACK_BUF ? count : MAX_STACK_BUF, buf);
+
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ if (!SSL_is_init_finished(ssl)) {
+ while (1) {
+ size_t written;
+ int result = SSL_write_early_data(ssl, (void*)(intptr_t)buf, count, &written);
+ if (result == 1) {
+ return written;
+ } else if ((result = check_ssl_error(env, ssl, 0)) != SSL_ERROR_WANT_WRITE || errno != EINTR) {
+ return result == SSL_ERROR_WANT_READ ? -1 : 0;
+ }
+ }
+ }
+#endif
while (1) {
int result = SSL_write(ssl, (void*)(intptr_t)buf, count);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
if (result > 0) {
return result;
} else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_WRITE || errno != EINTR) {
@@ -932,6 +1177,32 @@ Java_one_nio_net_NativeSslSocket_write(JNIEnv* env, jobject self, jbyteArray dat
}
}
+JNIEXPORT jlong JNICALL
+Java_one_nio_net_NativeSslSocket_sendFile0(JNIEnv* env, jobject self, jint sourceFD, jlong offset, jlong count) {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ SSL* ssl = (SSL*)(intptr_t) (*env)->GetLongField(env, self, f_ssl);
+ if (ssl == NULL) {
+ throw_socket_closed(env);
+ } else if (count != 0) {
+ while (1) {
+ int result = SSL_sendfile(ssl, sourceFD, (off_t)offset, count, 0);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
+ if (result > 0) {
+ return result;
+ } else if (result == 0) {
+ throw_socket_closed_cached(env);
+ break;
+ } else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_WRITE || errno != EINTR) {
+ return result == SSL_ERROR_WANT_READ ? -1 : 0;
+ }
+ }
+ }
+#else
+ throw_by_name(env, "javax/net/ssl/SSLException", "Cannot use sendFile with SSL");
+#endif
+}
+
+
JNIEXPORT void JNICALL
Java_one_nio_net_NativeSslSocket_writeFully(JNIEnv* env, jobject self, jbyteArray data, jint offset, jint count) {
SSL* ssl = (SSL*)(intptr_t) (*env)->GetLongField(env, self, f_ssl);
@@ -939,12 +1210,13 @@ Java_one_nio_net_NativeSslSocket_writeFully(JNIEnv* env, jobject self, jbyteArra
if (ssl == NULL) {
throw_socket_closed(env);
- } else {
+ } else if (SSL_is_init_finished(ssl)) {
while (count > 0) {
int to_write = count <= MAX_STACK_BUF ? count : MAX_STACK_BUF;
(*env)->GetByteArrayRegion(env, data, offset, to_write, buf);
int result = SSL_write(ssl, (void*)(intptr_t)buf, to_write);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
if (result > 0) {
offset += result;
count -= result;
@@ -952,7 +1224,43 @@ Java_one_nio_net_NativeSslSocket_writeFully(JNIEnv* env, jobject self, jbyteArra
break;
}
}
+ } else {
+ throw_by_name(env, "javax/net/ssl/SSLException", "Too early. SSL Handshake is not finished");
+ }
+}
+
+static int ssl_socket_readRaw_early_data(JNIEnv* env, jobject self, SSL* ssl, jlong buf, jint count) {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ intptr_t ssl_flags = (intptr_t)SSL_get_app_data(ssl);
+
+ while (1) {
+ size_t bytes_read = 0;
+ int result;
+ int ed_status = SSL_read_early_data(ssl, (void*)buf, count, &bytes_read);
+
+ switch (ed_status) {
+ case SSL_READ_EARLY_DATA_FINISH:
+ SSL_set_app_data(ssl, (char*)(ssl_flags | SF_EARLY_DATA_FINISHED));
+ case SSL_READ_EARLY_DATA_SUCCESS:
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ return bytes_read;
+ case SSL_READ_EARLY_DATA_ERROR:
+ if ((result = check_ssl_error(env, ssl, ed_status)) != SSL_ERROR_WANT_READ || errno != EINTR) {
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ }
+ default: {
+ char error[64];
+ snprintf(error, sizeof(error), "Unexpected Early data status (%d)", ed_status);
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ throw_by_name(env, "javax/net/ssl/SSLException", error);
+ }
+ }
}
+#else
+ // it may happen if early data settings are enabled on openssl ver < 3.0.0
+ throw_by_name(env, "javax/net/ssl/SSLException", "Early data is not supported in this openssl version");
+#endif
}
JNIEXPORT jint JNICALL
@@ -962,15 +1270,58 @@ Java_one_nio_net_NativeSslSocket_readRaw(JNIEnv* env, jobject self, jlong buf, j
throw_socket_closed(env);
return 0;
} else {
- while (1) {
- int result = SSL_read(ssl, (void*)(intptr_t)buf, count);
- if (result > 0) {
- return result;
- } else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_READ || errno != EINTR) {
- return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ intptr_t ssl_flags = (intptr_t)SSL_get_app_data(ssl);
+ bool early_data = ssl_flags & SF_EARLY_DATA_ENABLED;
+ if (!early_data || ssl_flags & SF_EARLY_DATA_FINISHED) {
+ while (1) {
+ int result = SSL_read(ssl, (void*)(intptr_t)buf, count);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
+ if (result > 0) {
+ return result;
+ } else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_READ || errno != EINTR) {
+ return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ }
+ }
+ } else {
+ return ssl_socket_readRaw_early_data(env, self, ssl, buf, count);
+ }
+ }
+}
+
+static int ssl_socket_read_early_data(JNIEnv* env, jobject self, SSL* ssl, jbyteArray data, jint offset, jint count) {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ jbyte buf[MAX_STACK_BUF];
+ intptr_t ssl_flags = (intptr_t)SSL_get_app_data(ssl);
+
+ while (1) {
+ size_t bytes_read = 0;
+ int result;
+ int ed_status = SSL_read_early_data(ssl, (void*)buf, count <= MAX_STACK_BUF ? count : MAX_STACK_BUF, &bytes_read);
+
+ switch (ed_status) {
+ case SSL_READ_EARLY_DATA_FINISH:
+ SSL_set_app_data(ssl, (char*)(ssl_flags | SF_EARLY_DATA_FINISHED));
+ case SSL_READ_EARLY_DATA_SUCCESS:
+ (*env)->SetByteArrayRegion(env, data, offset, bytes_read, buf);
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ return bytes_read;
+ case SSL_READ_EARLY_DATA_ERROR:
+ if ((result = check_ssl_error(env, ssl, ed_status)) != SSL_ERROR_WANT_READ || errno != EINTR) {
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ }
+ default: {
+ char error[64];
+ snprintf(error, sizeof(error), "Unexpected Early data status (%d)", ed_status);
+ update_NativeSslSocket_isEarlyDataAccepted_field(env, self, ssl);
+ throw_by_name(env, "javax/net/ssl/SSLException", error);
}
}
}
+#else
+ // it may happen if early data settings are enabled on openssl ver < 3.0.0
+ throw_by_name(env, "javax/net/ssl/SSLException", "Early data is not supported in this openssl version");
+#endif
}
JNIEXPORT int JNICALL
@@ -982,14 +1333,21 @@ Java_one_nio_net_NativeSslSocket_read(JNIEnv* env, jobject self, jbyteArray data
throw_socket_closed(env);
return 0;
} else {
- while (1) {
- int result = SSL_read(ssl, buf, count <= MAX_STACK_BUF ? count : MAX_STACK_BUF);
- if (result > 0) {
- (*env)->SetByteArrayRegion(env, data, offset, result, buf);
- return result;
- } else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_READ || errno != EINTR) {
- return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ intptr_t ssl_flags = (intptr_t)SSL_get_app_data(ssl);
+ bool early_data = ssl_flags & SF_EARLY_DATA_ENABLED;
+ if (!early_data || ssl_flags & SF_EARLY_DATA_FINISHED) {
+ while (1) {
+ int result = SSL_read(ssl, buf, count <= MAX_STACK_BUF ? count : MAX_STACK_BUF);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
+ if (result > 0) {
+ (*env)->SetByteArrayRegion(env, data, offset, result, buf);
+ return result;
+ } else if ((result = check_ssl_error(env, ssl, result)) != SSL_ERROR_WANT_READ || errno != EINTR) {
+ return result == SSL_ERROR_WANT_WRITE ? -1 : 0;
+ }
}
+ } else {
+ return ssl_socket_read_early_data(env, self, ssl, data, offset, count);
}
}
}
@@ -1004,6 +1362,7 @@ Java_one_nio_net_NativeSslSocket_readFully(JNIEnv* env, jobject self, jbyteArray
} else {
while (count > 0) {
int result = SSL_read(ssl, buf, count <= MAX_STACK_BUF ? count : MAX_STACK_BUF);
+ update_NativeSslSocket_isHandshakeDone_field(env, self, ssl);
if (result > 0) {
(*env)->SetByteArrayRegion(env, data, offset, result, buf);
offset += result;
@@ -1022,7 +1381,7 @@ Java_one_nio_net_NativeSslSocket_sslPeerCertificate(JNIEnv* env, jobject self) {
return NULL;
}
- X509* cert = SSL_get_peer_certificate(ssl);
+ X509* cert = SSL_get1_peer_certificate(ssl);
if (cert == NULL) {
return NULL;
}
@@ -1072,7 +1431,7 @@ Java_one_nio_net_NativeSslSocket_sslCertName(JNIEnv* env, jobject self, jint whi
return NULL;
}
- X509* cert = SSL_get_peer_certificate(ssl);
+ X509* cert = SSL_get1_peer_certificate(ssl);
if (cert == NULL) {
return NULL;
}
@@ -1124,6 +1483,20 @@ Java_one_nio_net_NativeSslSocket_sslSessionReused(JNIEnv* env, jobject self) {
return ssl != NULL && SSL_session_reused(ssl) ? JNI_TRUE : JNI_FALSE;
}
+static void update_NativeSslSocket_isEarlyDataAccepted_field(JNIEnv* env, jobject self, SSL* ssl) {
+#ifdef SSL_EARLY_DATA_ACCEPTED
+ jboolean isEarlyDataAccepted = ssl != NULL
+ && SSL_get_early_data_status(ssl) == SSL_EARLY_DATA_ACCEPTED ? JNI_TRUE : JNI_FALSE;
+ (*env)->SetBooleanField(env, self, f_isEarlyDataAccepted, isEarlyDataAccepted);
+#endif
+}
+
+
+static void update_NativeSslSocket_isHandshakeDone_field(JNIEnv* env, jobject self, SSL* ssl) {
+ jboolean isHandshakeDone = ssl != NULL && SSL_is_init_finished(ssl) ? JNI_TRUE : JNI_FALSE;
+ (*env)->SetBooleanField(env, self, f_isHandshakeDone, isHandshakeDone);
+}
+
JNIEXPORT jint JNICALL
Java_one_nio_net_NativeSslSocket_sslSessionTicket(JNIEnv* env, jobject self) {
SSL* ssl = (SSL*)(intptr_t) (*env)->GetLongField(env, self, f_ssl);
@@ -1140,3 +1513,29 @@ Java_one_nio_net_NativeSslSocket_sslCurrentCipher(JNIEnv* env, jobject self) {
const char* name = SSL_CIPHER_get_name(SSL_get_current_cipher(ssl));
return name == NULL ? NULL : (*env)->NewStringUTF(env, name);
}
+
+#if (OPENSSL_VERSION_MAJOR >= 3)
+static void keylog_callback(const SSL *ssl, const char *line) {
+ JNIEnv* env;
+ if (JNI_OK != (*global_vm)->GetEnv(global_vm, (void**)&env, JNI_VERSION_1_8)) {
+ return;
+ }
+
+ int fd = SSL_get_fd(ssl);
+ struct sockaddr_storage sa;
+ socklen_t len = sizeof(sa);
+ if (getpeername(fd, (struct sockaddr*)&sa, &len) == 0) {
+ jobject isa = sockaddr_to_java(env, &sa, len);
+ jstring key_line = (*env)->NewStringUTF(env, line);
+ (*env)->CallStaticVoidMethod(env, c_KeylogHolder, m_log, key_line, isa);
+ }
+}
+#endif
+
+JNIEXPORT void JNICALL
+Java_one_nio_net_NativeSslContext_setKeylog(JNIEnv* env, jobject self, jboolean keylog) {
+#if (OPENSSL_VERSION_MAJOR >= 3)
+ SSL_CTX* ctx = (SSL_CTX*)(intptr_t) (*env)->GetLongField(env, self, f_ctx);
+ SSL_CTX_set_keylog_callback(ctx, keylog ? keylog_callback : NULL);
+#endif
+}
diff --git a/src/one/nio/os/Cpus.java b/src/one/nio/os/Cpus.java
index 1f0de3f..5adcdb9 100644
--- a/src/one/nio/os/Cpus.java
+++ b/src/one/nio/os/Cpus.java
@@ -43,7 +43,7 @@ private static BitSet cpus(String rangeFile) {
String[] s = range.split("-");
int from = Integer.parseInt(s[0]);
int to = s.length == 1 ? from : Integer.parseInt(s[1]);
- cpus.set(from, to);
+ cpus.set(from, to + 1);
}
return cpus;
} catch (IOException e) {
diff --git a/src/one/nio/os/Proc.java b/src/one/nio/os/Proc.java
index 8a13680..64a0dc3 100755
--- a/src/one/nio/os/Proc.java
+++ b/src/one/nio/os/Proc.java
@@ -44,6 +44,10 @@ public final class Proc {
/**
* The same as above, but allows an arbitrary long mask
+ *
+ * @param pid an id of a thread. If pid is zero, then the calling thread is used
+ * @param mask a thread's CPU affinity mask
+ * @return 0 on success or errno on failure
*/
public static native int setAffinity(int pid, long[] mask);
public static native long[] getAffinity(int pid);
@@ -63,6 +67,7 @@ public static void setDedicatedCpu(int pid, int cpu) {
/**
* @param pid pid or tid. 0 for current thread
+ * @param policy one of the POSIX scheduling policies
* @return 0 on success or errno on failure
*/
public static native int sched_setscheduler(int pid, int policy);
@@ -90,6 +95,7 @@ public static void setDedicatedCpu(int pid, int cpu) {
* setpriority() shall set the nice value to the highest supported value.
*
* @param pid pid or tid. 0 for current thread
+ * @param value a nice value
* @return 0 on success; otherwise, -1 shall be returned and errno set to indicate the error.
*/
public static native int setpriority(int pid, int value);
diff --git a/src/one/nio/os/bpf/Bpf.java b/src/one/nio/os/bpf/Bpf.java
index b4a85bf..377ac33 100644
--- a/src/one/nio/os/bpf/Bpf.java
+++ b/src/one/nio/os/bpf/Bpf.java
@@ -43,6 +43,8 @@ public class Bpf {
static native int[] progGetMapIds(int fd) throws IOException;
+ static native void progTestRun(int fd, byte[] dataIn, int lenDataIn, byte[] dataOut, byte[] ctxIn, int lenCtxIn, byte[] ctxOut, int[] retvals /* data_size_out,ctx_size_out,duration,retval */) throws IOException;
+
static native int rawTracepointOpen(int progFd, String name) throws IOException;
static native String mapGetInfo(int fd, int[] result /*type,id,key_size,value_size,max_entries,flags*/) throws IOException;
diff --git a/src/one/nio/os/bpf/BpfProg.java b/src/one/nio/os/bpf/BpfProg.java
index 69b8189..55209f0 100644
--- a/src/one/nio/os/bpf/BpfProg.java
+++ b/src/one/nio/os/bpf/BpfProg.java
@@ -69,7 +69,36 @@ public int[] getMapIds() throws IOException {
return Bpf.progGetMapIds(fd());
}
+ public void testRun(TestRunContext context) throws IOException {
+ assert context.ctxIn == null || context.lenCtxIn <= context.ctxIn.length;
+ assert context.dataIn == null || context.lenDataIn <= context.dataIn.length;
+
+ Bpf.progTestRun(fd(), context.dataIn, context.lenDataIn, context.dataOut, context.ctxIn, context.lenCtxIn, context.ctxOut, context.retvals);
+ }
+
public static Iterable getAllIds() {
return () -> new IdsIterator(Bpf.OBJ_PROG);
}
+
+ public static class TestRunContext {
+ public byte[] dataIn;
+ public int lenDataIn;
+ public byte[] ctxIn;
+ public int lenCtxIn;
+ public byte[] dataOut;
+ public byte[] ctxOut;
+ int[] retvals = new int[4];
+ public int lenDataOut() {
+ return retvals[0];
+ }
+ public int lenCtxOut() {
+ return retvals[1];
+ }
+ public int duration() {
+ return retvals[2];
+ }
+ public int result() {
+ return retvals[3];
+ }
+ }
}
diff --git a/src/one/nio/os/native/bpf.c b/src/one/nio/os/native/bpf.c
index 91a58ef..3fa42d6 100644
--- a/src/one/nio/os/native/bpf.c
+++ b/src/one/nio/os/native/bpf.c
@@ -53,7 +53,7 @@ Java_one_nio_os_bpf_Bpf_progLoad(JNIEnv* env, jclass cls, jstring pathname, jint
if (libbpf == NULL) {
libbpf = dlopen("libbpf.so.0", RTLD_LAZY | RTLD_GLOBAL);
if (libbpf == NULL) {
- throw_by_name(env, "java/lang/UnsupportedOperationException", "Failed to load libbpf.so");
+ throw_by_name(env, "java/lang/UnsupportedOperationException", "Failed to load libbpf.so or libbpf.so.0");
return -EINVAL;
}
}
@@ -225,6 +225,70 @@ Java_one_nio_os_bpf_Bpf_progGetMapIds(JNIEnv* env, jclass cls, int bpf_fd) {
return result;
}
+JNIEXPORT void JNICALL
+Java_one_nio_os_bpf_Bpf_progTestRun(JNIEnv* env, jclass cls, jint prog_fd, jbyteArray data_in, jint len_data_in, jbyteArray data_out,
+ jbyteArray ctx_in, jint len_ctx_in, jbyteArray ctx_out, jintArray retvals /* data_size_out,ctx_size_out,duration,retval */) {
+
+ union bpf_attr attr;
+ int res;
+
+ memset(&attr, 0, sizeof(attr));
+ attr.test.prog_fd = prog_fd;
+
+ jbyte *b_ctx_in=NULL, *b_data_in=NULL, *b_ctx_out=NULL, *b_data_out=NULL;
+
+
+ if (ctx_in != NULL) {
+ attr.test.ctx_size_in = len_ctx_in;
+ b_ctx_in = (*env)->GetByteArrayElements(env, ctx_in, NULL);
+ attr.test.ctx_in = ptr_to_u64(b_ctx_in);
+ }
+ if (data_in != NULL) {
+ attr.test.data_size_in = len_data_in;
+ b_data_in = (*env)->GetByteArrayElements(env, data_in, NULL);
+ attr.test.data_in = ptr_to_u64(b_data_in);
+ }
+ if (ctx_out != NULL) {
+ attr.test.ctx_size_out = (*env)->GetArrayLength(env, ctx_out);
+ b_ctx_out = (*env)->GetByteArrayElements(env, ctx_out, NULL);
+ attr.test.ctx_out = ptr_to_u64(b_ctx_out);
+ }
+ if (data_out != NULL) {
+ attr.test.data_size_out = (*env)->GetArrayLength(env, data_out);
+ b_data_out = (*env)->GetByteArrayElements(env, data_out, NULL);
+ attr.test.data_out = ptr_to_u64(b_data_out);
+ }
+
+ res = sys_bpf(BPF_PROG_TEST_RUN, &attr, sizeof(attr));
+
+ if (retvals != NULL) {
+ const jint b_result[] = {
+ attr.test.data_size_out,
+ attr.test.ctx_size_out,
+ attr.test.duration,
+ attr.test.retval
+ };
+ (*env)->SetIntArrayRegion(env, retvals, 0, sizeof(b_result)/sizeof(int), b_result);
+ }
+
+ if (ctx_in != NULL) {
+ (*env)->ReleaseByteArrayElements(env, ctx_in, b_ctx_in, JNI_ABORT);
+ }
+ if (data_in != NULL) {
+ (*env)->ReleaseByteArrayElements(env, data_in, b_data_in, JNI_ABORT);
+ }
+ if (ctx_out != NULL) {
+ (*env)->ReleaseByteArrayElements(env, ctx_out, b_ctx_out, 0);
+ }
+ if (data_out != NULL) {
+ (*env)->ReleaseByteArrayElements(env, data_out, b_data_out, 0);
+ }
+
+ if (res < 0) {
+ throw_io_exception(env);
+ }
+}
+
JNIEXPORT jint JNICALL
Java_one_nio_os_bpf_Bpf_progGetFdById(JNIEnv* env, jclass cls, jint id) {
union bpf_attr attr;
diff --git a/src/one/nio/pool/SocketPool.java b/src/one/nio/pool/SocketPool.java
index d5b3f50..bc23db5 100755
--- a/src/one/nio/pool/SocketPool.java
+++ b/src/one/nio/pool/SocketPool.java
@@ -17,6 +17,7 @@
package one.nio.pool;
import one.nio.mgt.Management;
+import one.nio.net.SslClientContextFactory;
import one.nio.net.ConnectionString;
import one.nio.net.Proxy;
import one.nio.net.Socket;
@@ -28,6 +29,7 @@ public class SocketPool extends Pool implements SocketPoolMXBean {
protected int readTimeout;
protected int connectTimeout;
protected int tos;
+ protected boolean thinLto;
protected SslContext sslContext;
protected Proxy proxy;
@@ -42,6 +44,7 @@ public SocketPool(ConnectionString conn) {
this.connectTimeout = conn.getIntParam("connectTimeout", 1000);
this.tos = conn.getIntParam("tos", 0);
this.fifo = conn.getBooleanParam("fifo", false);
+ this.thinLto = conn.getBooleanParam("thinLto", false);
setProperties(conn);
initialize();
@@ -53,7 +56,7 @@ public SocketPool(ConnectionString conn) {
protected void setProperties(ConnectionString conn) {
if ("ssl".equals(conn.getProtocol())) {
- sslContext = SslContext.getDefault();
+ sslContext = SslClientContextFactory.create();
}
}
@@ -152,7 +155,7 @@ public void setProxy(Proxy proxy) {
public Socket createObject() throws PoolException {
Socket socket = null;
try {
- socket = Socket.create();
+ socket = Socket.createClientSocket(sslContext);
socket.setKeepAlive(true);
socket.setNoDelay(true);
@@ -160,6 +163,10 @@ public Socket createObject() throws PoolException {
socket.setTos(tos);
}
+ if (thinLto) {
+ socket.setThinLinearTimeouts(true);
+ }
+
socket.setTimeout(connectTimeout);
if (proxy == null) {
socket.connect(host, port);
diff --git a/src/one/nio/rpc/RpcClient.java b/src/one/nio/rpc/RpcClient.java
index 0fdbc58..6d31ab5 100755
--- a/src/one/nio/rpc/RpcClient.java
+++ b/src/one/nio/rpc/RpcClient.java
@@ -36,8 +36,16 @@
public class RpcClient extends SocketPool implements InvocationHandler {
protected static final byte[][] uidLocks = new byte[64][0];
+ private final StackTraceElement remoteMarkerElement;
+
public RpcClient(ConnectionString conn) {
super(conn);
+
+ this.remoteMarkerElement = new StackTraceElement(
+ "<>", // pseudo class name
+ "remoteCall",
+ this.name(), // pseudo file name, will contain host and port
+ -1);
}
public Object invoke(Object request) throws Exception {
@@ -72,11 +80,34 @@ public Object invoke(Object request, int timeout) throws Exception {
provideSerializer(Repository.requestSerializer(uid));
rawResponse = invokeRaw(request, readTimeout);
} else {
- throw (Exception) response;
+ Exception exception = (Exception) response;
+ addLocalStack(exception, request);
+ throw exception;
}
}
}
+ private void addLocalStack(Throwable e, Object remoteRequest) {
+ StackTraceElement[] remoteStackTrace = e.getStackTrace();
+ StackTraceElement[] localStackTrace = new Exception().getStackTrace();
+
+ if (remoteStackTrace == null || localStackTrace == null) {
+ return;
+ }
+ StackTraceElement[] newStackTrace = new StackTraceElement[remoteStackTrace.length + localStackTrace.length];
+
+ System.arraycopy(remoteStackTrace, 0, newStackTrace, 0, remoteStackTrace.length);
+ newStackTrace[remoteStackTrace.length] = remoteMarkerElement;
+
+ System.arraycopy(localStackTrace,
+ 1, // starting from 1 to skip 'addLocalStack' line in stack trace
+ newStackTrace,
+ remoteStackTrace.length + 1,
+ localStackTrace.length - 1);
+
+ e.setStackTrace(newStackTrace);
+ }
+
@Override
public Object invoke(Object proxy, Method method, Object... args) throws Exception {
if (method.getDeclaringClass() == Object.class) {
diff --git a/src/one/nio/rpc/RpcSession.java b/src/one/nio/rpc/RpcSession.java
index a378c9e..036ea76 100755
--- a/src/one/nio/rpc/RpcSession.java
+++ b/src/one/nio/rpc/RpcSession.java
@@ -16,6 +16,14 @@
package one.nio.rpc;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.RejectedExecutionException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import one.nio.http.Request;
import one.nio.net.ProxyProtocol;
import one.nio.net.Session;
@@ -30,11 +38,8 @@
import one.nio.serial.SerializerNotFoundException;
import one.nio.util.Utf8;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.concurrent.RejectedExecutionException;
-
public class RpcSession extends Session {
+ protected static final Logger logSerialize = LoggerFactory.getLogger("one-serializer-logger");
protected static final int BUFFER_SIZE = 8000;
protected static final byte HTTP_REQUEST_UID = (byte) Repository.get(Request.class).uid();
@@ -198,10 +203,14 @@ protected int writeResponse(Object response) throws IOException {
RpcPacket.checkWriteSize(responseSize);
byte[] buffer = new byte[responseSize + 4];
- DataStream ds = css.hasCycles() ? new SerializeStream(buffer, css.capacity()) : new DataStream(buffer);
- ds.writeInt(responseSize);
- ds.writeObject(response);
-
+ try {
+ DataStream ds = css.hasCycles() ? new SerializeStream(buffer, css.capacity()) : new DataStream(buffer);
+ ds.writeInt(responseSize);
+ ds.writeObject(response);
+ } catch (IOException | RuntimeException e) {
+ logSerialize.warn("Exception while serializing: {}", response, e);
+ return writeResponse(new NotSerializableException(e.getMessage()));
+ }
super.write(buffer, 0, buffer.length);
return responseSize;
}
diff --git a/src/one/nio/serial/DataStream.java b/src/one/nio/serial/DataStream.java
index 4a2a6a6..a74f75e 100755
--- a/src/one/nio/serial/DataStream.java
+++ b/src/one/nio/serial/DataStream.java
@@ -183,8 +183,7 @@ public void writeFrom(long address, int len) throws IOException {
}
public int read() throws IOException {
- long offset = alloc(1);
- return unsafe.getByte(array, offset);
+ return unsafe.getByte(array, alloc(1));
}
public int read(byte[] b) throws IOException {
@@ -198,13 +197,11 @@ public int read(byte[] b, int off, int len) throws IOException {
}
public void readFully(byte[] b) throws IOException {
- long offset = alloc(b.length);
- unsafe.copyMemory(array, offset, b, byteArrayOffset, b.length);
+ unsafe.copyMemory(array, alloc(b.length), b, byteArrayOffset, b.length);
}
public void readFully(byte[] b, int off, int len) throws IOException {
- long offset = alloc(len);
- unsafe.copyMemory(array, offset, b, byteArrayOffset + off, len);
+ unsafe.copyMemory(array, alloc(len), b, byteArrayOffset + off, len);
}
public long skip(long n) throws IOException {
@@ -218,43 +215,35 @@ public int skipBytes(int n) throws IOException {
}
public boolean readBoolean() throws IOException {
- long offset = alloc(1);
- return unsafe.getBoolean(array, offset);
+ return unsafe.getBoolean(array, alloc(1));
}
public byte readByte() throws IOException {
- long offset = alloc(1);
- return unsafe.getByte(array, offset);
+ return unsafe.getByte(array, alloc(1));
}
public int readUnsignedByte() throws IOException {
- long offset = alloc(1);
- return unsafe.getByte(array, offset) & 0xff;
+ return unsafe.getByte(array, alloc(1)) & 0xff;
}
public short readShort() throws IOException {
- long offset = alloc(2);
- return Short.reverseBytes(unsafe.getShort(array, offset));
+ return Short.reverseBytes(unsafe.getShort(array, alloc(2)));
}
public int readUnsignedShort() throws IOException {
- long offset = alloc(2);
- return Short.reverseBytes(unsafe.getShort(array, offset)) & 0xffff;
+ return Short.reverseBytes(unsafe.getShort(array, alloc(2))) & 0xffff;
}
public char readChar() throws IOException {
- long offset = alloc(2);
- return Character.reverseBytes(unsafe.getChar(array, offset));
+ return Character.reverseBytes(unsafe.getChar(array, alloc(2)));
}
public int readInt() throws IOException {
- long offset = alloc(4);
- return Integer.reverseBytes(unsafe.getInt(array, offset));
+ return Integer.reverseBytes(unsafe.getInt(array, alloc(4)));
}
public long readLong() throws IOException {
- long offset = alloc(8);
- return Long.reverseBytes(unsafe.getLong(array, offset));
+ return Long.reverseBytes(unsafe.getLong(array, alloc(8)));
}
public float readFloat() throws IOException {
@@ -287,8 +276,7 @@ public String readUTF() throws IOException {
if (length > 0x7fff) {
length = (length & 0x7fff) << 16 | readUnsignedShort();
}
- long offset = alloc(length);
- return Utf8.read(array, offset, length);
+ return Utf8.read(array, alloc(length), length);
}
public Object readObject() throws IOException, ClassNotFoundException {
@@ -321,8 +309,7 @@ public void read(ByteBuffer dst) throws IOException {
}
public void readTo(long address, int len) throws IOException {
- long offset = alloc(len);
- unsafe.copyMemory(array, offset, null, address, len);
+ unsafe.copyMemory(array, alloc(len), null, address, len);
}
public ByteBuffer byteBuffer(int len) throws IOException {
diff --git a/src/one/nio/serial/DeserializeStream.java b/src/one/nio/serial/DeserializeStream.java
index 7f0f8c1..264b6ee 100755
--- a/src/one/nio/serial/DeserializeStream.java
+++ b/src/one/nio/serial/DeserializeStream.java
@@ -76,7 +76,7 @@ public Object readObject() throws IOException, ClassNotFoundException {
}
@Override
- public void close() throws IOException {
+ public void close() {
context = null;
}
diff --git a/src/one/nio/serial/PersistStream.java b/src/one/nio/serial/PersistStream.java
index b476d6c..a5bf714 100755
--- a/src/one/nio/serial/PersistStream.java
+++ b/src/one/nio/serial/PersistStream.java
@@ -69,7 +69,7 @@ public void writeObject(Object obj) throws IOException {
}
@Override
- protected long alloc(int size) throws IOException {
+ protected long alloc(int size) {
long currentOffset = offset;
if ((offset = currentOffset + size) > limit) {
limit = Math.max(offset, limit * 2);
diff --git a/src/one/nio/serial/SerializationContext.java b/src/one/nio/serial/SerializationContext.java
index 4109896..b909208 100755
--- a/src/one/nio/serial/SerializationContext.java
+++ b/src/one/nio/serial/SerializationContext.java
@@ -42,6 +42,7 @@ public int capacity() {
}
/**
+ * @param obj an object to put in the context
* @return index for existing objects, -1-index for new
*/
public int put(Object obj) {
diff --git a/src/one/nio/server/AcceptorConfig.java b/src/one/nio/server/AcceptorConfig.java
index df85bbc..9c6a544 100644
--- a/src/one/nio/server/AcceptorConfig.java
+++ b/src/one/nio/server/AcceptorConfig.java
@@ -31,10 +31,13 @@ public class AcceptorConfig {
public int sendBuf;
public int tos;
public int backlog = 128;
+ @Converter(method = "size")
+ public int notsentLowat;
public boolean keepAlive = true;
public boolean noDelay = true;
public boolean tcpFastOpen = true;
public boolean deferAccept;
public boolean reusePort;
+ public boolean thinLto;
public SslConfig ssl;
}
diff --git a/src/one/nio/server/SelectorThread.java b/src/one/nio/server/SelectorThread.java
index 762a994..d35286b 100755
--- a/src/one/nio/server/SelectorThread.java
+++ b/src/one/nio/server/SelectorThread.java
@@ -34,8 +34,8 @@ public final class SelectorThread extends PayloadThread {
long sessions;
int maxReady;
- public SelectorThread(int num, int dedicatedCpu, SchedulingPolicy schedulingPolicy) throws IOException {
- super("NIO Selector #" + num);
+ public SelectorThread(int num, int dedicatedCpu, SchedulingPolicy schedulingPolicy, String name) throws IOException {
+ super(name);
this.selector = Selector.create();
this.dedicatedCpu = dedicatedCpu;
setSchedulingPolicy(schedulingPolicy);
diff --git a/src/one/nio/server/Server.java b/src/one/nio/server/Server.java
index cb1f677..a3216c5 100755
--- a/src/one/nio/server/Server.java
+++ b/src/one/nio/server/Server.java
@@ -16,22 +16,21 @@
package one.nio.server;
-import one.nio.net.Selector;
-import one.nio.net.Session;
-import one.nio.net.Socket;
-import one.nio.mgt.Management;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.io.IOException;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.LongAdder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import one.nio.mgt.Management;
+import one.nio.net.Selector;
+import one.nio.net.Session;
+import one.nio.net.Socket;
+import one.nio.server.acceptor.Acceptor;
+import one.nio.server.acceptor.AcceptorFactory;
+
public class Server implements ServerMXBean {
private static final Logger log = LoggerFactory.getLogger(Server.class);
@@ -42,34 +41,25 @@ public class Server implements ServerMXBean {
private volatile QueueStats queueStats;
protected final int port;
- protected final CountDownLatch startSync;
- protected volatile AcceptorThread[] acceptors;
+
+ protected volatile Acceptor acceptor;
+
protected volatile SelectorThread[] selectors;
protected boolean useWorkers;
protected final WorkerPool workers;
protected final CleanupThread cleanup;
protected boolean closeSessions;
+ protected boolean pinAcceptors;
public Server(ServerConfig config) throws IOException {
- List acceptors = new ArrayList<>();
- for (AcceptorConfig ac : config.acceptors) {
- for (int i = 0; i < ac.threads; i++) {
- acceptors.add(new AcceptorThread(this, ac, i));
- }
- }
-
- if (acceptors.isEmpty()) {
- throw new IllegalArgumentException("No configured acceptors");
- }
-
- this.acceptors = acceptors.toArray(new AcceptorThread[0]);
- this.startSync = new CountDownLatch(this.acceptors.length);
- this.port = this.acceptors[0].port;
+ this.acceptor = AcceptorFactory.get(config).create(this, config.acceptors);
+ this.port = acceptor.getSinglePort();
int processors = Runtime.getRuntime().availableProcessors();
SelectorThread[] selectors = new SelectorThread[config.selectors != 0 ? config.selectors : processors];
for (int i = 0; i < selectors.length; i++) {
- selectors[i] = new SelectorThread(i, config.affinity ? i % processors : -1, config.schedulingPolicy);
+ String threadName = config.formatSelectorThreadName(i);
+ selectors[i] = new SelectorThread(i, config.affinity ? i % processors : -1, config.schedulingPolicy, threadName);
selectors[i].setPriority(config.threadPriority);
}
this.selectors = selectors;
@@ -81,6 +71,7 @@ public Server(ServerConfig config) throws IOException {
this.cleanup = new CleanupThread(selectors, config.keepAlive);
this.closeSessions = config.closeSessions;
+ this.pinAcceptors = config.pinAcceptors;
this.selectorStats = new SelectorStats();
this.queueStats = new QueueStats();
@@ -88,7 +79,7 @@ public Server(ServerConfig config) throws IOException {
public synchronized void reconfigure(ServerConfig config) throws IOException {
useWorkers = config.maxWorkers > 0;
- if (config.minWorkers > workers.getMaximumPoolSize()) {
+ if (config.minWorkers > workers.getMaximumPoolSize()) {
workers.setMaximumPoolSize(useWorkers ? config.maxWorkers : 2);
workers.setCorePoolSize(config.minWorkers);
} else {
@@ -97,47 +88,15 @@ public synchronized void reconfigure(ServerConfig config) throws IOException {
}
workers.setQueueTime(config.queueTime);
- // Create a copy of the array, since the elements will be nulled out
- // to allow reconfiguring multiple acceptors with the same address:port
- AcceptorThread[] oldAcceptors = acceptors.clone();
- List newAcceptors = new ArrayList<>();
- for (AcceptorConfig ac : config.acceptors) {
- int threads = 0;
- for (int i = 0; i < oldAcceptors.length; i++) {
- AcceptorThread oldAcceptor = oldAcceptors[i];
- if (oldAcceptor != null && oldAcceptor.port == ac.port && oldAcceptor.address.equals(ac.address)) {
- if (++threads <= ac.threads) {
- log.info("Reconfiguring acceptor: {}", oldAcceptor.getName());
- oldAcceptor.reconfigure(ac);
- oldAcceptors[i] = null;
- newAcceptors.add(oldAcceptor);
- }
- }
- }
-
- for (; threads < ac.threads; threads++) {
- AcceptorThread newAcceptor = new AcceptorThread(this, ac, threads);
- log.info("New acceptor: {}", newAcceptor.getName());
- newAcceptor.start();
- newAcceptors.add(newAcceptor);
- }
- }
-
- for (AcceptorThread oldAcceptor : oldAcceptors) {
- if (oldAcceptor != null) {
- log.info("Stopping acceptor: {}", oldAcceptor.getName());
- oldAcceptor.shutdown();
- }
- }
-
- acceptors = newAcceptors.toArray(new AcceptorThread[0]);
+ acceptor.reconfigure(config.acceptors);
int processors = Runtime.getRuntime().availableProcessors();
SelectorThread[] selectors = this.selectors;
if (config.selectors > selectors.length) {
SelectorThread[] newSelectors = Arrays.copyOf(selectors, config.selectors);
for (int i = selectors.length; i < config.selectors; i++) {
- newSelectors[i] = new SelectorThread(i, config.affinity ? i % processors : -1, config.schedulingPolicy);
+ String threadName = config.formatSelectorThreadName(i);
+ newSelectors[i] = new SelectorThread(i, config.affinity ? i % processors : -1, config.schedulingPolicy, threadName);
newSelectors[i].setPriority(config.threadPriority);
newSelectors[i].start();
}
@@ -146,6 +105,7 @@ public synchronized void reconfigure(ServerConfig config) throws IOException {
cleanup.update(this.selectors, config.keepAlive);
closeSessions = config.closeSessions;
+ pinAcceptors = config.pinAcceptors;
}
public synchronized void start() {
@@ -153,13 +113,10 @@ public synchronized void start() {
selector.start();
}
- for (AcceptorThread acceptor : acceptors) {
- acceptor.start();
- }
+ acceptor.start();
- // Wait until all AcceptorThreads are listening for incoming connections
try {
- startSync.await();
+ acceptor.syncStart();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
@@ -174,9 +131,7 @@ public synchronized void stop() {
cleanup.shutdown();
- for (AcceptorThread acceptor : acceptors) {
- acceptor.shutdown();
- }
+ acceptor.shutdown();
for (SelectorThread selector : selectors) {
if (closeSessions) {
@@ -201,14 +156,64 @@ public void run() {
});
}
- protected Session createSession(Socket socket) throws RejectedSessionException {
+ public Session createSession(Socket socket) throws RejectedSessionException {
return new Session(socket);
}
- protected void register(Session session) {
+ public void register(Session session, int acceptorIndex, int acceptorGroupSize) {
+ if (pinAcceptors) {
+ getSmallestPinnedSelector(acceptorIndex, acceptorGroupSize).register(session);
+ return;
+ }
+ register(session);
+ }
+
+ public void register(Session session) {
getSmallestSelector().register(session);
}
+ /*
+ * If `pinAcceptors` is enabled for the server, accepted sessions are distributed across the disjunctive set of selectors.
+ * When server is configured to have less `acceptors`(K) less than `selectors`(N), the selectors group for the given acceptor
+ * by its indices forms an finite arithmetic sequence starting from the acceptor index with step K.
+ * Example: ServerConfig.acceptors = 3, ServerConfig.selectors = 8.
+ * Acceptor #0 -> Selectors #0, #3, #6
+ * Acceptor #1 -> Selectors #1, #4, #7
+ * Acceptor #2 -> Selectors #2, #5
+ * Across the selectors' subset, the selector to serve the session is chosen on a random basis.
+ * Provided the server is configured to have more `acceptors`(K) than `selectors`(N), the serving selector index is calculated out of acceptor index modulo N.
+ * Example: ServerConfig.acceptors = 8, ServerConfig.selectors = 3.
+ * Acceptor #0 -> Selector #0
+ * Acceptor #1 -> Selector #1
+ * Acceptor #2 -> Selector #2
+ * Acceptor #3 -> Selector #0
+ * ...
+ * Acceptor #7 -> Selector #1
+ * Base configuration 1: acceptors = 1, selectors = N. The single acceptor balances sessions across all N selectors randomly.
+ * Base configuration 2: acceptors = N, selectors = N. Each acceptor has a single designated selector to serve the sessions.
+ */
+ private Selector getSmallestPinnedSelector(int idx, int total) {
+ Selector chosen;
+ SelectorThread[] selectors = this.selectors;
+ if (total >= selectors.length) {
+ chosen = selectors[idx % selectors.length].selector;
+ } else {
+ int q = selectors.length / total;
+ if (q * total + idx < selectors.length) {
+ q++;
+ }
+ if (q == 1) {
+ chosen = selectors[idx].selector;
+ } else {
+ ThreadLocalRandom r = ThreadLocalRandom.current();
+ Selector a = selectors[r.nextInt(q) * total + idx].selector;
+ Selector b = selectors[r.nextInt(q) * total + idx].selector;
+ chosen = a.size() < b.size() ? a : b;
+ }
+ }
+ return chosen;
+ }
+
private Selector getSmallestSelector() {
SelectorThread[] selectors = this.selectors;
@@ -257,20 +262,12 @@ public int getWorkersActive() {
@Override
public long getAcceptedSessions() {
- long result = 0;
- for (AcceptorThread acceptor : acceptors) {
- result += acceptor.acceptedSessions;
- }
- return result;
+ return acceptor.getAcceptedSessions();
}
@Override
public long getRejectedSessions() {
- long result = 0;
- for (AcceptorThread acceptor : acceptors) {
- result += acceptor.rejectedSessions;
- }
- return result;
+ return acceptor.getRejectedSessions();
}
@Override
@@ -333,10 +330,7 @@ public long getRequestsRejected() {
@Override
public synchronized void reset() {
- for (AcceptorThread acceptor : acceptors) {
- acceptor.acceptedSessions = 0;
- acceptor.rejectedSessions = 0;
- }
+ acceptor.resetCounters();
for (SelectorThread selector : selectors) {
selector.operations = 0;
diff --git a/src/one/nio/server/ServerConfig.java b/src/one/nio/server/ServerConfig.java
index 3fb2334..787a1ea 100644
--- a/src/one/nio/server/ServerConfig.java
+++ b/src/one/nio/server/ServerConfig.java
@@ -16,6 +16,8 @@
package one.nio.server;
+import java.util.Locale;
+
import one.nio.config.Config;
import one.nio.config.Converter;
import one.nio.net.ConnectionString;
@@ -24,7 +26,11 @@
@Config
public class ServerConfig {
+
+ public static String DEFAULT_SELECTOR_THREAD_NAME_FORMAT = "NIO Selector #%d";
+
public AcceptorConfig[] acceptors;
+ public boolean multiAcceptor;
public int selectors;
public boolean affinity;
public int minWorkers;
@@ -36,6 +42,10 @@ public class ServerConfig {
public int threadPriority = Thread.NORM_PRIORITY;
public SchedulingPolicy schedulingPolicy;
public boolean closeSessions;
+ public boolean pinAcceptors;
+
+ @Converter(value = ServerConfig.class, method = "threadNameFormat")
+ public String selectorThreadNameFormat = DEFAULT_SELECTOR_THREAD_NAME_FORMAT;
public ServerConfig() {
}
@@ -60,6 +70,8 @@ private ServerConfig(ConnectionString conn) {
this.threadPriority = conn.getIntParam("threadPriority", Thread.NORM_PRIORITY);
this.schedulingPolicy = SchedulingPolicy.valueOf(conn.getStringParam("schedulingPolicy", "OTHER"));
this.closeSessions = conn.getBooleanParam("closeSessions", false);
+ this.keepAlive = conn.getIntParam("keepAlive", 0);
+ this.selectorThreadNameFormat = threadNameFormat(conn.getStringParam("selectorThreadNameFormat", DEFAULT_SELECTOR_THREAD_NAME_FORMAT));
}
// Do not use for new servers! Use ConfigParser instead
@@ -71,4 +83,15 @@ public static ServerConfig from(String conn) {
public static ServerConfig from(ConnectionString conn) {
return new ServerConfig(conn);
}
+
+ public String formatSelectorThreadName(int threadNumber) {
+ return String.format(Locale.ROOT, selectorThreadNameFormat, threadNumber);
+ }
+
+ public static String threadNameFormat(String s) {
+ // validate pattern
+ String.format(Locale.ROOT, s, 0);
+
+ return s;
+ }
}
diff --git a/src/one/nio/server/acceptor/Acceptor.java b/src/one/nio/server/acceptor/Acceptor.java
new file mode 100644
index 0000000..17132c6
--- /dev/null
+++ b/src/one/nio/server/acceptor/Acceptor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+
+import one.nio.server.AcceptorConfig;
+
+public interface Acceptor {
+ void reconfigure(AcceptorConfig... configs) throws IOException;
+
+ void start();
+
+ void shutdown();
+
+ void syncStart() throws InterruptedException;
+
+ long getAcceptedSessions();
+
+ long getRejectedSessions();
+
+ void resetCounters();
+
+ int getSinglePort();
+}
diff --git a/src/one/nio/server/acceptor/AcceptorFactory.java b/src/one/nio/server/acceptor/AcceptorFactory.java
new file mode 100644
index 0000000..c16273c
--- /dev/null
+++ b/src/one/nio/server/acceptor/AcceptorFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+
+import one.nio.server.AcceptorConfig;
+import one.nio.server.Server;
+import one.nio.server.ServerConfig;
+
+public abstract class AcceptorFactory {
+ private AcceptorFactory() {
+ }
+
+ public abstract Acceptor create(Server s, AcceptorConfig... configs) throws IOException;
+
+ public static AcceptorFactory get(ServerConfig sc) {
+ if (sc.multiAcceptor) {
+ return MultiAcceptorFactory.INSTANCE;
+ } else {
+ return DefaultAcceptorFactory.INSTANCE;
+ }
+ }
+
+ private static class DefaultAcceptorFactory extends AcceptorFactory {
+ private static final DefaultAcceptorFactory INSTANCE = new DefaultAcceptorFactory();
+
+ @Override
+ public Acceptor create(Server s, AcceptorConfig... configs) throws IOException {
+ return new DefaultAcceptor(s, configs);
+ }
+ }
+
+ private static class MultiAcceptorFactory extends AcceptorFactory {
+ private static final MultiAcceptorFactory INSTANCE = new MultiAcceptorFactory();
+
+ @Override
+ public Acceptor create(Server s, AcceptorConfig... configs) throws IOException {
+ return new MultiAcceptor(s, configs);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/AcceptorSupport.java b/src/one/nio/server/acceptor/AcceptorSupport.java
new file mode 100644
index 0000000..0ad8f21
--- /dev/null
+++ b/src/one/nio/server/acceptor/AcceptorSupport.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+
+import one.nio.net.Socket;
+import one.nio.net.SslContext;
+import one.nio.server.AcceptorConfig;
+
+public class AcceptorSupport {
+ private AcceptorSupport() {}
+
+ public static Socket createServerSocket(AcceptorConfig config) throws IOException {
+ Socket serverSocket = Socket.createServerSocket();
+ if (config.ssl != null) {
+ SslContext sslContext = SslContext.create();
+ sslContext.configure(config.ssl);
+ serverSocket = serverSocket.sslWrap(sslContext);
+ }
+ if (config.recvBuf != 0) serverSocket.setRecvBuffer(config.recvBuf);
+ if (config.sendBuf != 0) serverSocket.setSendBuffer(config.sendBuf);
+ if (config.tos != 0) serverSocket.setTos(config.tos);
+ if (config.notsentLowat != 0) serverSocket.setNotsentLowat(config.notsentLowat);
+ if (config.deferAccept) serverSocket.setDeferAccept(true);
+
+ serverSocket.setKeepAlive(config.keepAlive);
+ serverSocket.setNoDelay(config.noDelay);
+ serverSocket.setTcpFastOpen(config.tcpFastOpen);
+ serverSocket.setReuseAddr(true, config.reusePort);
+ serverSocket.setThinLinearTimeouts(config.thinLto);
+ return serverSocket;
+ }
+
+ public static void reconfigureSocket(Socket socket, AcceptorConfig config) throws IOException {
+ if (config.recvBuf != 0) socket.setRecvBuffer(config.recvBuf);
+ if (config.sendBuf != 0) socket.setSendBuffer(config.sendBuf);
+ if (config.tos != 0) socket.setTos(config.tos);
+ if (config.notsentLowat != 0) socket.setNotsentLowat(config.notsentLowat);
+ socket.setDeferAccept(config.deferAccept);
+ socket.setKeepAlive(config.keepAlive);
+ socket.setNoDelay(config.noDelay);
+ socket.setTcpFastOpen(config.tcpFastOpen);
+ socket.setReuseAddr(true, config.reusePort);
+ socket.setThinLinearTimeouts(config.thinLto);
+
+ SslContext sslContext = socket.getSslContext();
+ if (sslContext != null && config.ssl != null) {
+ sslContext.configure(config.ssl);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/AcceptorThread.java b/src/one/nio/server/acceptor/AcceptorThread.java
similarity index 57%
rename from src/one/nio/server/AcceptorThread.java
rename to src/one/nio/server/acceptor/AcceptorThread.java
index 0573c0f..891b1e4 100755
--- a/src/one/nio/server/AcceptorThread.java
+++ b/src/one/nio/server/acceptor/AcceptorThread.java
@@ -14,19 +14,24 @@
* limitations under the License.
*/
-package one.nio.server;
-
-import one.nio.net.Session;
-import one.nio.net.Socket;
-import one.nio.net.SslContext;
+package one.nio.server.acceptor;
import java.io.IOException;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import one.nio.net.Session;
+import one.nio.net.Socket;
+import one.nio.server.AcceptorConfig;
+import one.nio.server.RejectedSessionException;
+import one.nio.server.Server;
+
final class AcceptorThread extends Thread {
private static final Logger log = LoggerFactory.getLogger(AcceptorThread.class);
+ final DefaultAcceptorGroup group;
+ final int num;
final String address;
final int port;
final int backlog;
@@ -36,53 +41,22 @@ final class AcceptorThread extends Thread {
volatile long acceptedSessions;
volatile long rejectedSessions;
- AcceptorThread(Server server, AcceptorConfig config, int num) throws IOException {
+ AcceptorThread(Server server, AcceptorConfig config, DefaultAcceptorGroup group, int num) throws IOException {
super("NIO Acceptor " + config.address + ":" + config.port + " #" + num);
+ this.group = group;
+ this.num = num;
this.address = config.address;
this.port = config.port;
this.backlog = config.backlog;
this.server = server;
- Socket serverSocket = Socket.createServerSocket();
- if (config.ssl != null) {
- SslContext sslContext = SslContext.create();
- sslContext.configure(config.ssl);
- serverSocket = serverSocket.sslWrap(sslContext);
- }
- this.serverSocket = serverSocket;
-
- if (config.recvBuf != 0) serverSocket.setRecvBuffer(config.recvBuf);
- if (config.sendBuf != 0) serverSocket.setSendBuffer(config.sendBuf);
- if (config.tos != 0) serverSocket.setTos(config.tos);
- if (config.deferAccept) serverSocket.setDeferAccept(true);
-
- serverSocket.setKeepAlive(config.keepAlive);
- serverSocket.setNoDelay(config.noDelay);
- serverSocket.setTcpFastOpen(config.tcpFastOpen);
- serverSocket.setReuseAddr(true, config.reusePort);
+ Socket serverSocket = AcceptorSupport.createServerSocket(config);
serverSocket.bind(address, port, backlog);
+ this.serverSocket = serverSocket;
}
void reconfigure(AcceptorConfig config) throws IOException {
- if (config.recvBuf != 0) {
- serverSocket.setRecvBuffer(config.recvBuf);
- }
- if (config.sendBuf != 0) {
- serverSocket.setSendBuffer(config.sendBuf);
- }
- if (config.tos != 0) {
- serverSocket.setTos(config.tos);
- }
- serverSocket.setDeferAccept(config.deferAccept);
- serverSocket.setKeepAlive(config.keepAlive);
- serverSocket.setNoDelay(config.noDelay);
- serverSocket.setTcpFastOpen(config.tcpFastOpen);
- serverSocket.setReuseAddr(true, config.reusePort);
-
- SslContext sslContext = serverSocket.getSslContext();
- if (sslContext != null && config.ssl != null) {
- sslContext.configure(config.ssl);
- }
+ AcceptorSupport.reconfigureSocket(serverSocket, config);
}
void shutdown() {
@@ -102,7 +76,7 @@ public void run() {
log.error("Cannot start listening at {}", port, e);
return;
} finally {
- server.startSync.countDown();
+ group.syncLatch.countDown();
}
while (serverSocket.isOpen()) {
@@ -110,12 +84,10 @@ public void run() {
try {
socket = serverSocket.acceptNonBlocking();
Session session = server.createSession(socket);
- server.register(session);
+ server.register(session, num, group.size());
acceptedSessions++;
} catch (RejectedSessionException e) {
- if (log.isDebugEnabled()) {
- log.debug("Rejected session from {}", socket.getRemoteAddress(), e);
- }
+ log.debug("Rejected session from {}", socket.getRemoteAddress(), e);
rejectedSessions++;
socket.close();
} catch (Throwable e) {
diff --git a/src/one/nio/server/acceptor/DefaultAcceptor.java b/src/one/nio/server/acceptor/DefaultAcceptor.java
new file mode 100644
index 0000000..ebcdf04
--- /dev/null
+++ b/src/one/nio/server/acceptor/DefaultAcceptor.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import one.nio.server.AcceptorConfig;
+import one.nio.server.Server;
+
+public class DefaultAcceptor implements Acceptor {
+ private static final Logger log = LoggerFactory.getLogger(DefaultAcceptor.class);
+
+ private final Server server;
+
+ private volatile DefaultAcceptorGroup[] acceptorGroups;
+
+ DefaultAcceptor(Server server, AcceptorConfig... configs) throws IOException {
+ this.server = server;
+
+ DefaultAcceptorGroup[] acceptorGroups = new DefaultAcceptorGroup[configs.length];
+ for (int configIdx = 0; configIdx < configs.length; configIdx++) {
+ AcceptorConfig config = configs[configIdx];
+ acceptorGroups[configIdx] = new DefaultAcceptorGroup(server, config);
+ }
+ this.acceptorGroups = acceptorGroups;
+ }
+
+ @Override
+ public void reconfigure(AcceptorConfig... configs) throws IOException {
+ // Create a copy of the array, since the elements will be nulled out
+ // to allow reconfiguring multiple acceptors with the same address:port
+ DefaultAcceptorGroup[] oldAcceptorGroups = this.acceptorGroups.clone();
+ DefaultAcceptorGroup[] newAcceptorGroups = new DefaultAcceptorGroup[configs.length];
+ for (int configIdx = 0; configIdx < configs.length; configIdx++) {
+ AcceptorConfig ac = configs[configIdx];
+ DefaultAcceptorGroup oldGroup = configIdx < oldAcceptorGroups.length ? oldAcceptorGroups[configIdx] : null;
+ if (oldGroup != null && oldGroup.isSameAddressPort(ac)) {
+ log.info("Reconfiguring acceptor group: {}", oldGroup);
+ oldGroup.reconfigure(ac);
+ newAcceptorGroups[configIdx] = oldGroup;
+ oldAcceptorGroups[configIdx] = null;
+ } else {
+ DefaultAcceptorGroup newGroup = new DefaultAcceptorGroup(server, ac);
+ log.info("New acceptor group: {}", newGroup);
+ newAcceptorGroups[configIdx] = newGroup;
+ newGroup.start();
+ }
+ }
+
+ for (DefaultAcceptorGroup oldGroup : oldAcceptorGroups) {
+ if (oldGroup != null) {
+ log.info("Stopping acceptor group: {}", oldGroup);
+ oldGroup.shutdown();
+ }
+ }
+
+ this.acceptorGroups = newAcceptorGroups;
+ }
+
+ @Override
+ public void start() {
+ for (DefaultAcceptorGroup acceptorGroup : this.acceptorGroups) {
+ acceptorGroup.start();
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ for (DefaultAcceptorGroup acceptorGroup : this.acceptorGroups) {
+ acceptorGroup.shutdown();
+ }
+ }
+
+ @Override
+ public void syncStart() throws InterruptedException {
+ for (DefaultAcceptorGroup acceptorGroup : this.acceptorGroups) {
+ acceptorGroup.syncStart();
+ }
+ }
+
+ @Override
+ public long getAcceptedSessions() {
+ return Arrays.stream(this.acceptorGroups)
+ .mapToLong(DefaultAcceptorGroup::getAcceptedSessions)
+ .sum();
+ }
+
+ @Override
+ public long getRejectedSessions() {
+ return Arrays.stream(this.acceptorGroups)
+ .mapToLong(DefaultAcceptorGroup::getRejectedSessions)
+ .sum();
+ }
+
+ @Override
+ public void resetCounters() {
+ for (DefaultAcceptorGroup acceptorGroup : this.acceptorGroups) {
+ acceptorGroup.resetCounters();
+ }
+ }
+
+ @Override
+ public int getSinglePort() {
+ return acceptorGroups[0].getPort();
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/DefaultAcceptorGroup.java b/src/one/nio/server/acceptor/DefaultAcceptorGroup.java
new file mode 100644
index 0000000..271c6c4
--- /dev/null
+++ b/src/one/nio/server/acceptor/DefaultAcceptorGroup.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+
+import one.nio.server.AcceptorConfig;
+import one.nio.server.Server;
+
+class DefaultAcceptorGroup {
+ private final Server server;
+ private final String address;
+ private final int port;
+
+ private volatile AcceptorThread[] acceptors;
+
+ CountDownLatch syncLatch;
+
+ public DefaultAcceptorGroup(Server server, AcceptorConfig ac) throws IOException {
+ this.server = server;
+ this.address = ac.address;
+ this.port = ac.port;
+
+ AcceptorThread[] acceptors = new AcceptorThread[ac.threads];
+ for (int threadId = 0; threadId < ac.threads; threadId++) {
+ acceptors[threadId] = new AcceptorThread(server, ac, this, threadId);
+ }
+ this.acceptors = acceptors;
+ }
+
+ public void reconfigure(AcceptorConfig ac) throws IOException {
+ if (!isSameAddressPort(ac)) {
+ throw new IllegalArgumentException("Acceptor config has different address:port");
+ }
+ AcceptorThread[] oldAcceptors = this.acceptors;
+ if (ac.threads < oldAcceptors.length) {
+ for (int i = 0; i < oldAcceptors.length; i++) {
+ if (i < ac.threads) {
+ oldAcceptors[i].reconfigure(ac);
+ } else {
+ oldAcceptors[i].shutdown();
+ }
+ }
+ this.acceptors = Arrays.copyOf(oldAcceptors, ac.threads);
+ } else {
+ AcceptorThread[] newAcceptors = Arrays.copyOf(oldAcceptors, ac.threads);
+ for (int i = 0; i < newAcceptors.length; i++) {
+ if (newAcceptors[i] != null) {
+ newAcceptors[i].reconfigure(ac);
+ } else {
+ newAcceptors[i] = new AcceptorThread(server, ac, this, i);
+ newAcceptors[i].start();
+ }
+ }
+ this.acceptors = newAcceptors;
+ }
+ }
+
+ public boolean isSameAddressPort(AcceptorConfig ac) {
+ return ac.address.equals(address) && ac.port == port;
+ }
+
+ public void start() {
+ AcceptorThread[] acceptors = this.acceptors;
+ this.syncLatch = new CountDownLatch(acceptors.length);
+ for (AcceptorThread acceptor : acceptors) {
+ acceptor.start();
+ }
+ }
+
+ public void syncStart() throws InterruptedException {
+ if (this.syncLatch != null) {
+ this.syncLatch.await();
+ }
+ }
+
+ public void shutdown() {
+ for (AcceptorThread acceptor : acceptors) {
+ acceptor.shutdown();
+ }
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public int size() {
+ AcceptorThread[] acceptors = this.acceptors;
+ return acceptors == null ? 0 : acceptors.length;
+ }
+
+ public long getAcceptedSessions() {
+ long sum = 0;
+ for (AcceptorThread acceptor : acceptors) {
+ sum += acceptor.acceptedSessions;
+ }
+ return sum;
+ }
+
+ public long getRejectedSessions() {
+ long sum = 0;
+ for (AcceptorThread acceptor : acceptors) {
+ sum += acceptor.rejectedSessions;
+ }
+ return sum;
+ }
+
+ public void resetCounters() {
+ for (AcceptorThread acceptor : acceptors) {
+ acceptor.acceptedSessions = 0;
+ acceptor.rejectedSessions = 0;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultAcceptorGroup{" +
+ "address='" + address + '\'' +
+ ", port=" + port +
+ ", size=" + size() +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/MultiAcceptSession.java b/src/one/nio/server/acceptor/MultiAcceptSession.java
new file mode 100644
index 0000000..9edea3a
--- /dev/null
+++ b/src/one/nio/server/acceptor/MultiAcceptSession.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+
+import one.nio.net.Selector;
+import one.nio.net.Session;
+import one.nio.net.Socket;
+import one.nio.os.NativeLibrary;
+import one.nio.server.AcceptorConfig;
+
+class MultiAcceptSession extends Session {
+ final int backlog;
+ final MultiAcceptorGroup group;
+ final int idx;
+
+ MultiAcceptSession(Socket socket, int backlog, MultiAcceptorGroup group, int idx) {
+ super(socket, acceptOp());
+ this.backlog = backlog;
+ this.group = group;
+ this.idx = idx;
+ }
+
+ void listen(Selector selector) throws IOException {
+ socket.listen(backlog);
+ selector.register(this);
+ }
+
+ public void reconfigure(AcceptorConfig newConfig) throws IOException {
+ AcceptorSupport.reconfigureSocket(socket, newConfig);
+ }
+
+ static int acceptOp() {
+ return NativeLibrary.IS_SUPPORTED ? READABLE : SelectionKey.OP_ACCEPT;
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/MultiAcceptor.java b/src/one/nio/server/acceptor/MultiAcceptor.java
new file mode 100644
index 0000000..9f6f606
--- /dev/null
+++ b/src/one/nio/server/acceptor/MultiAcceptor.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import one.nio.server.AcceptorConfig;
+import one.nio.server.Server;
+
+class MultiAcceptor implements Acceptor {
+ private static final Logger log = LoggerFactory.getLogger(MultiAcceptor.class);
+
+ private final MultiAcceptorThread thread;
+
+ private volatile MultiAcceptorGroup[] acceptorGroups;
+
+ MultiAcceptor(Server server, AcceptorConfig... configs) throws IOException {
+ this.thread = new MultiAcceptorThread(server);
+
+ MultiAcceptorGroup[] newGroups = new MultiAcceptorGroup[configs.length];
+ for (int configIdx = 0; configIdx < configs.length; configIdx++) {
+ AcceptorConfig config = configs[configIdx];
+ validateConfig(config);
+ newGroups[configIdx] = new MultiAcceptorGroup(thread, config);
+ }
+
+ setAcceptorGroups(newGroups);
+ }
+
+ @Override
+ public void reconfigure(AcceptorConfig... configs) throws IOException {
+ MultiAcceptorGroup[] oldGroups = this.acceptorGroups.clone();
+ MultiAcceptorGroup[] newGroups = new MultiAcceptorGroup[configs.length];
+ for (int configIdx = 0; configIdx < configs.length; configIdx++) {
+ AcceptorConfig newConfig = configs[configIdx];
+ validateConfig(newConfig);
+ MultiAcceptorGroup oldGroup = configIdx < oldGroups.length ? oldGroups[configIdx] : null;
+ if (oldGroup != null && oldGroup.isSameAddressPort(newConfig)) {
+ log.info("Reconfiguring acceptor group: {}", oldGroup);
+ oldGroup.reconfigure(newConfig);
+ newGroups[configIdx] = oldGroup;
+ oldGroups[configIdx] = null;
+ } else {
+ MultiAcceptorGroup newGroup = new MultiAcceptorGroup(thread, newConfig);
+ log.info("New acceptor group: {}", newGroup);
+ newGroups[configIdx] = newGroup;
+ newGroup.start();
+ }
+ }
+
+ for (MultiAcceptorGroup oldGroup : oldGroups) {
+ if (oldGroup != null) {
+ oldGroup.close();
+ }
+ }
+
+ setAcceptorGroups(newGroups);
+ }
+
+ @Override
+ public void start() {
+ thread.start();
+ for (MultiAcceptorGroup group : acceptorGroups) {
+ try {
+ group.start();
+ } catch (IOException e) {
+ log.error("Cannot start listening at {}", group, e);
+ }
+ }
+ }
+
+ @Override
+ public void syncStart() {
+ // not needed, this is a single thread
+ }
+
+ @Override
+ public void shutdown() {
+ thread.shutdown();
+ }
+
+ @Override
+ public long getAcceptedSessions() {
+ return thread.acceptedSessions;
+ }
+
+ @Override
+ public long getRejectedSessions() {
+ return thread.rejectedSessions;
+ }
+
+ @Override
+ public void resetCounters() {
+ thread.acceptedSessions = 0;
+ thread.rejectedSessions = 0;
+ }
+
+ @Override
+ public int getSinglePort() {
+ return acceptorGroups[0].getPort();
+ }
+
+ private void validateConfig(AcceptorConfig newConfig) {
+ if (newConfig.threads <= 0) {
+ throw new IllegalArgumentException("Cannot create acceptor with 0 ports");
+ }
+ if (newConfig.threads > 1 && !newConfig.reusePort) {
+ throw new IllegalArgumentException("Cannot create multiport acceptor without reusePort");
+ }
+ }
+
+ private void setAcceptorGroups(MultiAcceptorGroup[] newGroups) {
+ this.acceptorGroups = newGroups;
+ thread.setName("NIO MultiAcceptor " + Arrays.toString(newGroups));
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/MultiAcceptorGroup.java b/src/one/nio/server/acceptor/MultiAcceptorGroup.java
new file mode 100644
index 0000000..d2c44bd
--- /dev/null
+++ b/src/one/nio/server/acceptor/MultiAcceptorGroup.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import one.nio.net.Socket;
+import one.nio.server.AcceptorConfig;
+
+public class MultiAcceptorGroup {
+ private final MultiAcceptorThread thread;
+ private final String address;
+ private final int port;
+
+ private volatile MultiAcceptSession[] sessions;
+
+ MultiAcceptorGroup(MultiAcceptorThread thread, AcceptorConfig config) throws IOException {
+ this.thread = thread;
+ this.address = config.address;
+ this.port = config.port;
+
+ MultiAcceptSession[] sessions = new MultiAcceptSession[config.threads];
+ for (int sessionIdx = 0; sessionIdx < config.threads; sessionIdx++) {
+ sessions[sessionIdx] = createMultiAcceptSession(config, sessionIdx);
+ }
+ this.sessions = sessions;
+ }
+
+ boolean isSameAddressPort(AcceptorConfig config) {
+ return this.address.equals(config.address) && this.port == config.port;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ void start() throws IOException {
+ for (MultiAcceptSession session : sessions) {
+ thread.register(session);
+ }
+ }
+
+ void close() {
+ for (MultiAcceptSession session : sessions) {
+ session.close();
+ }
+ }
+
+ int size() {
+ MultiAcceptSession[] sessions = this.sessions;
+ return sessions == null ? 0 : sessions.length;
+ }
+
+ void reconfigure(AcceptorConfig newConfig) throws IOException {
+ if (!isSameAddressPort(newConfig)) {
+ throw new IllegalArgumentException("Acceptor config has different address:port");
+ }
+ MultiAcceptSession[] oldSessions = this.sessions;
+ MultiAcceptSession[] newSessions = Arrays.copyOf(oldSessions, newConfig.threads);
+ if (oldSessions.length > newConfig.threads) {
+ for (int sessionIdx = 0; sessionIdx < oldSessions.length; sessionIdx++) {
+ if (sessionIdx < newSessions.length) {
+ oldSessions[sessionIdx].reconfigure(newConfig);
+ } else {
+ oldSessions[sessionIdx].close();
+ }
+ }
+ } else {
+ for (int sessionIdx = 0; sessionIdx < newSessions.length; sessionIdx++) {
+ MultiAcceptSession session = newSessions[sessionIdx];
+ if (session != null) {
+ session.reconfigure(newConfig);
+ } else {
+ MultiAcceptSession acceptSession = createMultiAcceptSession(newConfig, sessionIdx);
+ thread.register(acceptSession);
+ }
+ }
+ }
+ this.sessions = newSessions;
+ }
+
+ @Override
+ public String toString() {
+ return address + ':' + port + 'x' + size();
+ }
+
+ private MultiAcceptSession createMultiAcceptSession(AcceptorConfig config, int sessionIdx) throws IOException {
+ Socket serverSocket = AcceptorSupport.createServerSocket(config);
+ serverSocket.setBlocking(false);
+ serverSocket.bind(config.address, config.port, config.backlog);
+ return new MultiAcceptSession(serverSocket, config.backlog, this, sessionIdx);
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/server/acceptor/MultiAcceptorThread.java b/src/one/nio/server/acceptor/MultiAcceptorThread.java
new file mode 100644
index 0000000..6746566
--- /dev/null
+++ b/src/one/nio/server/acceptor/MultiAcceptorThread.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.server.acceptor;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.concurrent.RejectedExecutionException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import one.nio.net.Selector;
+import one.nio.net.Session;
+import one.nio.net.Socket;
+import one.nio.server.Server;
+
+class MultiAcceptorThread extends Thread {
+ private static final Logger log = LoggerFactory.getLogger(MultiAcceptorThread.class);
+
+ private static final int MAX_ACCEPTED_PER_SOCKET = 128;
+
+ private final Server server;
+ private final Selector selector;
+
+ volatile long acceptedSessions;
+ volatile long rejectedSessions;
+
+ MultiAcceptorThread(Server server) throws IOException {
+ this.server = server;
+ this.selector = Selector.create();
+ }
+
+ void register(MultiAcceptSession session) throws IOException {
+ session.listen(selector);
+ }
+
+ @Override
+ public void run() {
+ Socket clientSocket = null;
+ try {
+ while (!Thread.currentThread().isInterrupted() && selector.isOpen()) {
+ Iterator it = selector.select();
+ while (it.hasNext()) {
+ MultiAcceptSession as = (MultiAcceptSession) it.next();
+ int accepted = 0;
+ while (accepted < MAX_ACCEPTED_PER_SOCKET && (clientSocket = as.socket().acceptNonBlocking()) != null) {
+ try {
+ Session clientSession = server.createSession(clientSocket);
+ server.register(clientSession, as.idx, as.group.size());
+ clientSocket = null;
+ acceptedSessions++;
+ accepted++;
+ } catch (RejectedExecutionException e) {
+ log.debug("Rejected session from {}", clientSocket.getRemoteAddress(), e);
+ rejectedSessions++;
+ clientSocket.close();
+ clientSocket = null;
+ }
+ }
+ }
+ }
+ } catch (Throwable t) {
+ if (selector.isOpen()) {
+ log.error("Cannot accept incoming connection", t);
+ }
+ if (clientSocket != null) {
+ clientSocket.close();
+ }
+ }
+ }
+
+ void shutdown() {
+ selector.close();
+ interrupt();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/one/nio/util/JavaFeatures.java b/src/one/nio/util/JavaFeatures.java
index 29ec3ff..7298fea 100644
--- a/src/one/nio/util/JavaFeatures.java
+++ b/src/one/nio/util/JavaFeatures.java
@@ -56,7 +56,10 @@ public static void onSpinWait() {
}
/**
- * Calls Class.isRecord() since Java 14 preview; returns false otherwise
+ * Calls Class.isRecord() since Java 14 preview
+ *
+ * @param cls a class object
+ * @return the result of the Class.isRecord() method invoked. It is always false, if the version of the JVM Runtime is less than 14
*/
public static boolean isRecord(Class> cls) {
if (isRecord != null) {
diff --git a/test/one/nio/config/ConfigParserTest.java b/test/one/nio/config/ConfigParserTest.java
index a7afccb..14e550a 100644
--- a/test/one/nio/config/ConfigParserTest.java
+++ b/test/one/nio/config/ConfigParserTest.java
@@ -28,7 +28,7 @@ public class ConfigParserTest {
"keepAlive: 120s\n" +
"maxWorkers: 1000\n" +
"queueTime: 50MS\n" +
- "\n" +
+ "selectorThreadNameFormat: push sel-r #%d\n" +
"acceptors:\n" +
" - port: 443\n" +
" backlog: 10000\n" +
@@ -68,6 +68,8 @@ public void testConfigParser() throws Exception {
assertEquals(50, config.queueTime);
assertEquals(0, config.minWorkers);
assertEquals(0, config.selectors);
+ assertEquals("push sel-r #%d", config.selectorThreadNameFormat);
+ assertEquals("push sel-r #42", config.formatSelectorThreadName(42));
assertEquals(false, config.affinity);
assertEquals(Thread.NORM_PRIORITY, config.threadPriority);
diff --git a/test/one/nio/http/ChunkedEventReaderTest.java b/test/one/nio/http/ChunkedEventReaderTest.java
new file mode 100644
index 0000000..8c6ec74
--- /dev/null
+++ b/test/one/nio/http/ChunkedEventReaderTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2019 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.http;
+
+import one.nio.http.EventSource.Event;
+import one.nio.net.ConnectionString;
+import one.nio.net.SocketUtil;
+import one.nio.pool.PoolException;
+import one.nio.util.Hex;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for client and server support for HTTP Chunked transfer encoding
+ */
+public class ChunkedEventReaderTest {
+ private static final String ENDPOINT = "/echoChunked";
+
+ private static TestServer server;
+ private static HttpClient client;
+
+ @BeforeClass
+ public static void beforeAll() throws IOException {
+ int availablePort = SocketUtil.getFreePort();
+ server = new TestServer(HttpServerConfigFactory.create(availablePort));
+ server.start();
+ client = new HttpClient(new ConnectionString("http://127.0.0.1:" + availablePort+"?bufferSize=8000"));
+ }
+
+ @AfterClass
+ public static void afterAll() {
+ client.close();
+ server.stop();
+ }
+
+ private static String size(int size) {
+ return ENDPOINT + "?size=" + size;
+ }
+
+
+ @Test
+ public void testNoEvents() throws InterruptedException, PoolException, IOException, HttpException {
+ final Request req = echoReq( "".getBytes(), 0 );
+ EventSourceResponse events = client.openEvents( req, 1000 );
+
+ assertNull("poll() must return null when EOF", events.poll() );
+ }
+
+
+ @Test
+ public void testEmptyEvent() throws InterruptedException, PoolException, IOException, HttpException {
+ final Request req = echoReq( ":only comment and no data\n\n".getBytes(), 0 );
+ EventSourceResponse events = client.openEvents( req, 1000 );
+
+ Event event = events.poll();
+ assertEquals( "only comment and no data", event.comment() );
+ assertTrue("Events consisting of only comment are empty", event.isEmpty() );
+ }
+
+ @Test
+ public void testEvents() throws InterruptedException, PoolException, IOException, HttpException {
+ final Request req = echoReq(
+ ("id:CAFEBABE1\n" +
+ "event:testmebabe\n" +
+ "data:dataisgold\n" +
+ "\n" +
+ "id: CAFEBABE2\n" +
+ "event: testmebabe2\n" +
+ "nosuchfield: oioioi\n" +
+ "data: dataisgold2\n" +
+ "\n" +
+ "\n").getBytes(), 7 );
+ EventSourceResponse events = client.openEvents( req, 1000 );
+
+ {
+ Event event = events.poll();
+ assertEquals( "CAFEBABE1", event.id() );
+ assertEquals( "testmebabe", event.name() );
+ assertEquals( "dataisgold", event.data() );
+ }
+ {
+ Event event = events.poll();
+ assertEquals( "Space after colon must be ignored in id", "CAFEBABE2", event.id() );
+ assertEquals( "Space after colon must be ignored in event","testmebabe2", event.name() );
+ assertEquals( "Space after colon must be ignored in data","dataisgold2", event.data() );
+ }
+
+ assertNull("poll() must return null when EOF", events.poll() );
+ }
+
+ @Test
+ public void testLargeEvent() throws InterruptedException, PoolException, IOException, HttpException {
+ byte[] bytes = new byte[20001];
+ ThreadLocalRandom.current().nextBytes(bytes);
+ String data = Hex.toHex(bytes); // length=40000 > 32768
+ final Request req = echoReq(32786,
+ "id:CAFEBABE1\n" +
+ "event:testmebabe\n" +
+ "data:",
+ data + "\n" +
+ "\n",
+ "id: CAFEBABE2\n" +
+ "event: testmebabe2\n" +
+ "nosuchfield: oioioi\n" +
+ "data: ",
+ data+"2\n" +
+ "\n" +
+ "\n");
+ EventSourceResponse events = client.openEvents( req, 1000 );
+ {
+ Event event = events.poll();
+ assertEquals( "CAFEBABE1", event.id() );
+ assertEquals( "testmebabe", event.name() );
+ assertEquals( data, event.data() );
+ }
+ {
+ Event event = events.poll();
+ assertEquals( "Space after colon must be ignored in id", "CAFEBABE2", event.id() );
+ assertEquals( "Space after colon must be ignored in event","testmebabe2", event.name() );
+ assertEquals( "Space after colon must be ignored in data",data + "2", event.data() );
+ }
+
+ assertNull("poll() must return null when EOF", events.poll() );
+ }
+
+ private Request echoReq(byte[] body, int chunkSize) {
+ if ( chunkSize == 0 )
+ chunkSize = body.length;
+ Request req = client.createRequest( Request.METHOD_PUT, size( chunkSize ) );
+ server.data = Collections.singletonList(body);
+ return req;
+ }
+
+ private Request echoReq(int chunkSize, String... parts) {
+ Request req = client.createRequest( Request.METHOD_PUT, size( chunkSize ) );
+ server.data = new ArrayList<>();
+ for (String part : parts) {
+ server.data.add(part.getBytes(StandardCharsets.UTF_8));
+ }
+ return req;
+ }
+
+ private Request echoReqRaw(byte[] body, int chunkSize) {
+ if ( chunkSize == 0 )
+ chunkSize = body.length;
+ Request req = client.createRequest( Request.METHOD_PUT, size( chunkSize ) );
+ server.dataRaw = body;
+ return req;
+ }
+
+ private void check(final byte[] body, final int chunkSize) throws Exception {
+ final Response response = client.put(size(chunkSize), body);
+ assertEquals(200, response.getStatus());
+ if (body == null) {
+ assertEquals(0, response.getBody().length);
+ } else {
+ assertArrayEquals(body, response.getBody());
+ }
+ }
+
+ public static class TestServer extends HttpServer {
+ private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] EOF = "0\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
+
+ private TestServer(HttpServerConfig config) throws IOException {
+ super(config);
+ }
+
+ List data;
+ byte[] dataRaw;
+
+ @Path(ENDPOINT)
+ public Response echo(
+ final Request request,
+ @Param(value = "size", required = true) final int size) throws IOException {
+ if (data == null && dataRaw == null) {
+ return Response.ok(Response.EMPTY);
+ }
+
+ if (size < 1) {
+ return new Response(Response.BAD_REQUEST, Response.EMPTY);
+ }
+
+ if (dataRaw != null) {
+ final byte[] content = dataRaw;
+ dataRaw = null;
+ final Response response = new Response(Response.OK, content);
+ response.addHeader("Transfer-encoding: chunked");
+ response.addHeader("Content-Type: text/event-stream");
+
+ return response;
+ }
+
+ // Slice into parts
+ int contentLength = 0;
+ final List parts = data;
+ final List chunks = new ArrayList<>();
+ for (byte[] part : parts) {
+ for (int start = 0; start < part.length; start += size) {
+ // Encode chunk
+ final int chunkLength = Math.min(part.length - start, size);
+ final byte[] encodedLength = Integer.toHexString(chunkLength).getBytes(StandardCharsets.US_ASCII);
+ final byte[] chunk = new byte[encodedLength.length + 2 + chunkLength + 2];
+ final ByteBuffer buffer = ByteBuffer.wrap(chunk);
+ buffer.put(encodedLength);
+ buffer.put(CRLF);
+ buffer.put(part, start, chunkLength);
+ buffer.put(CRLF);
+ assert !buffer.hasRemaining();
+
+ // Save
+ chunks.add(chunk);
+ contentLength += chunk.length;
+ }
+ }
+
+ // EOF
+ chunks.add(EOF);
+ contentLength += EOF.length;
+
+ // Concat
+ final byte[] content = new byte[contentLength];
+ final ByteBuffer contentBuffer = ByteBuffer.wrap(content);
+ for (final byte[] chunk : chunks) {
+ contentBuffer.put(chunk);
+ }
+
+ final Response response = new Response(Response.OK, content);
+ response.addHeader("Transfer-encoding: chunked");
+ response.addHeader("Content-Type: text/event-stream");
+
+ return response;
+ }
+ }
+}
diff --git a/test/one/nio/http/HttpHeaderTest.java b/test/one/nio/http/HttpHeaderTest.java
index 70a2dfa..1a11095 100644
--- a/test/one/nio/http/HttpHeaderTest.java
+++ b/test/one/nio/http/HttpHeaderTest.java
@@ -23,6 +23,7 @@
import java.util.List;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
/**
* Unit tests for HTTP header processing facilities.
@@ -31,6 +32,7 @@
*/
public class HttpHeaderTest {
private static final String HEADER = "X-OK-Custom-Header: ";
+ private static final String HEADER_KEY = "X-OK-Custom-Header";
private void testHeaderConsumer(final String... values) {
final Request request = new Request(Request.METHOD_GET, "/", true);
@@ -41,6 +43,10 @@ private void testHeaderConsumer(final String... values) {
final List sink = new ArrayList<>(values.length);
request.consumeHeaders(HEADER, sink::add);
assertEquals(Arrays.asList(values), sink);
+
+ final List sinkValues = new ArrayList<>(values.length);
+ request.consumeHeaderValues(HEADER_KEY, sinkValues::add);
+ assertEquals(Arrays.asList(values), sinkValues);
}
@Test
@@ -57,4 +63,19 @@ public void consumeSingle() {
public void consumeDouble() {
testHeaderConsumer("First", "Second");
}
+
+ @Test
+ public void testHeaderValue() {
+ final Request request = new Request(Request.METHOD_GET, "/", true);
+ request.addHeader("X-Custom-Header-1: 01");
+ request.addHeader("X-Custom-Header-2: 02");
+
+ assertEquals("01", request.getHeaderValue("X-Custom-Header-1"));
+ assertEquals("02", request.getHeaderValue("X-Custom-Header-2"));
+ assertNull(request.getHeaderValue("X-Custom-Header-3"));
+ assertNull(request.getHeaderValue("X-Very-Long-Key-Custom-Header"));
+ assertNull(request.getHeaderValue("X-Custom-Header"));
+ assertNull(request.getHeaderValue("X-Custom-Header "));
+ assertNull(request.getHeaderValue("X-Custom-Header:"));
+ }
}
diff --git a/test/one/nio/mem/LongHashSetFuncTest.java b/test/one/nio/mem/LongHashSetFuncTest.java
new file mode 100644
index 0000000..293a8a4
--- /dev/null
+++ b/test/one/nio/mem/LongHashSetFuncTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 LLC VK
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.mem;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class LongHashSetFuncTest {
+ private final LongHashSet set = new LongHashSet(10);
+
+ @Test
+ public void testClear() {
+ set.putKey(1L);
+ set.putKey(2L);
+ assertEquals(2, set.size());
+ set.clear();
+ assertEquals(0, set.size());
+ }
+}
\ No newline at end of file
diff --git a/test/one/nio/mem/SharedMemoryStringMapTest.java b/test/one/nio/mem/SharedMemoryStringMapTest.java
new file mode 100644
index 0000000..fe149c5
--- /dev/null
+++ b/test/one/nio/mem/SharedMemoryStringMapTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.mem;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SharedMemoryStringMapTest {
+ @Test
+ public void create() throws IOException {
+ new SharedMemoryStringMap<>(
+ 100,
+ "/tmp/SharedMemoryStringMapTest",
+ 2 * 1024 * 1024
+ ).close();
+ }
+}
diff --git a/test/one/nio/net/SocketTest.java b/test/one/nio/net/SocketTest.java
index fbd16b1..2bd7213 100755
--- a/test/one/nio/net/SocketTest.java
+++ b/test/one/nio/net/SocketTest.java
@@ -26,7 +26,7 @@
public class SocketTest {
private static void testIPv4() throws IOException {
- Socket s = Socket.create();
+ Socket s = Socket.createClientSocket();
s.setTimeout(3000);
s.connect("google.com", 80);
@@ -35,7 +35,7 @@ private static void testIPv4() throws IOException {
}
private static void testIPv6() throws IOException {
- Socket s = Socket.create();
+ Socket s = Socket.createClientSocket();
s.setTimeout(3000);
s.connect("2a00:1450:4010:c07::71", 80);
@@ -129,6 +129,14 @@ public static void testSocketOpts(Socket socket, boolean datagram) {
socket.setTos(96);
assertEquals(96, socket.getTos());
}
+
+ if (socket instanceof NativeSocket) {
+ socket.setNotsentLowat(67890);
+ assertEquals(67890, socket.getNotsentLowat());
+
+ socket.setThinLinearTimeouts(true);
+ assertTrue(socket.getThinLinearTimeouts());
+ }
} catch (Exception e) {
throw e;
} finally {
diff --git a/test/one/nio/ssl/TLSCurveTest.java b/test/one/nio/ssl/TLSCurveTest.java
new file mode 100644
index 0000000..853d69b
--- /dev/null
+++ b/test/one/nio/ssl/TLSCurveTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2015-2016 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ssl;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import one.nio.config.ConfigParser;
+import one.nio.server.Server;
+import one.nio.server.ServerConfig;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TLSCurveTest {
+
+ private static int port = 7443;
+
+
+ private static String clientAllowedCurves = "secp521r1,secp256r1";
+
+ static String startupConfigTemplate = "\n" +
+ "acceptors:\n" +
+ " - port: " + port +"\n" +
+ " ssl:\n" +
+ " applicationProtocols: http/1.1\n" +
+ " protocols: TLSv1.3\n" +
+ " certFile: %s\n" +
+ " privateKeyFile: %s\n";
+
+ static String curveConfigTemplate = "\n" +
+ " curve: %s\n";
+
+ private Server server;
+
+ private static String cert;
+ private static String privKey;
+ SSLSocket socket;
+
+ @BeforeClass
+ public static void beforeClass() {
+ Properties systemProps = System.getProperties();
+ String truststorePath = TLSCurveTest.class.getClassLoader().getResource("ssl/trustore.jks").getFile();
+ systemProps.put("javax.net.ssl.trustStore", truststorePath);
+ systemProps.put("javax.net.ssl.trustStorePassword","changeit");
+ System.setProperties(systemProps);
+ cert = TLSCurveTest.class.getClassLoader().getResource("ssl/certificate.crt").getFile();
+ privKey = TLSCurveTest.class.getClassLoader().getResource("ssl/certificate.key").getFile();
+
+ // set allowed curves list for the client once
+ // changing jdk.tls.namedGroups after first call SSLSocketFactory.getDefault() won't take effect
+ System.setProperty("jdk.tls.namedGroups", clientAllowedCurves);
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ System.clearProperty("javax.net.ssl.trustStore");
+ System.clearProperty("javax.net.ssl.trustStorePassword");
+ System.clearProperty("jdk.tls.namedGroups");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (socket != null) socket.close();
+ if (server != null) server.stop();
+ }
+
+ private ServerConfig getServerConfig(String curve) {
+ String curveConfigPart = curve == null ? "" : String.format(curveConfigTemplate, curve);
+ return ConfigParser.parse(String.format(startupConfigTemplate, cert, privKey) + curveConfigPart,
+ ServerConfig.class);
+ }
+
+ private void setupServer(String curve) throws IOException {
+ ServerConfig config = getServerConfig(curve);
+ server = new Server(config);
+ server.start();
+ }
+
+ private void tryHandshake() throws IOException {
+ socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket("127.0.0.1", port);
+ socket.setEnabledProtocols(new String[] {"TLSv1.3"});
+ socket.startHandshake();
+ }
+
+ /**
+ * Both client and server support secp521r1 - successful handshake.
+ */
+ @Test
+ public void secp521r1() throws Exception {
+ setupServer("secp521r1");
+ tryHandshake();
+ }
+
+ /**
+ * Both client and server support prime256v1 - successful handshake.
+ *
+ * Name of the curve depends on standards organization:
+ * server curve (openssl impl.) - uses ANSI X9.62 and SECG names;
+ * client curve (java impl.) - uses SECG names only,
+ * Curve name prime256v1 (ANSI) is alias of the curve secp256r1 (SECG).
+ */
+ @Test
+ public void prime256v1() throws Exception {
+ setupServer("prime256v1");
+ tryHandshake();
+ }
+
+ /**
+ * Both client and server support intersecting sets of curves - successful handshake.
+ */
+ @Test
+ public void secp256k1_secp521r1() throws Exception {
+ setupServer("secp256k1:secp521r1");
+ tryHandshake();
+ }
+
+ /**
+ * Both client and server support not intersecting sets of curves - handshake fails.
+ */
+ @Test(expected = javax.net.ssl.SSLHandshakeException.class)
+ public void client_server_curve_mismatch() throws Exception {
+ setupServer("secp384r1");
+ tryHandshake();
+ }
+
+ /**
+ * A curve is not specified in the server config - successful handshake.
+ * Using auto selection server curve
+ */
+ @Test
+ public void no_server_curve_specified() throws Exception {
+ setupServer(null);
+ tryHandshake();
+ }
+
+ /**
+ * A curve specified in the server config not supported by linked openssl build - server fails startup.
+ */
+ @Test(expected = javax.net.ssl.SSLException.class)
+ public void bad_curve_name() throws Exception {
+ setupServer("prime256v1:BAD_CURVE:secp521r1");
+ tryHandshake();
+ }
+
+ /**
+ * The server started up with a curve not supported by the client - handshake fails.
+ * Then reconfigure the server to supported one - successful handshake.
+ */
+ @Test
+ public void reconfigure_server_curve() throws Exception {
+ setupServer("secp384r1"); // a curve not supported by the client
+ try {
+ tryHandshake();
+ Assert.fail("First handshake didn't fail");
+ } catch (javax.net.ssl.SSLHandshakeException e) {
+ }
+ ServerConfig newConfig = getServerConfig("prime256v1"); // the curve supported by the client
+ server.reconfigure(newConfig);
+ tryHandshake();
+ }
+
+}
diff --git a/test_data/ssl/ca.crt b/test_data/ssl/ca.crt
new file mode 100644
index 0000000..588ffae
--- /dev/null
+++ b/test_data/ssl/ca.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBTCCAe2gAwIBAgIUe94Iu7/EgJaMCVs4xUFlSjDaCAYwDQYJKoZIhvcNAQEL
+BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yMzAzMTAxMzAwNTdaFw00MzAzMDUx
+MzAwNTdaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCujUGscEKlm19a4AFXttAFg/HoqtSc80IYzFh+Mg7uG4supniQ
+xvrNEw7QYHp49ABPn76SdNNxD8EpIqmgRKwZ26tJkN0thR6CSIu9FouRT7wglfJD
+G+3ZU4z7zLt1U4P6dgkXGosHrZ9W76ARcNkZ8mUlXbUCCj+cMuE5TWIsk1cIk5u7
+sumslTJcUL4mQ+mlxCvo+3dS6ql+ENS1c0OqOUcOoTrctNNJRJCOyvZQNHdvJ+tn
+CXwxNwOJOkNjLkT9Iumjz/t9coTdYeRQUo2OzrP0/lwxaB+hZpX3xOaplxSHJA4x
+8H330DpJ9pQoKz+Mca9rtD0CX+HtkHqfGh/XAgMBAAGjUzBRMB0GA1UdDgQWBBR+
+2zzbdSV06uu9rnYy9t6rNVuOGDAfBgNVHSMEGDAWgBR+2zzbdSV06uu9rnYy9t6r
+NVuOGDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBdk9oL/poW
+2PXuvn7oaRCx+z3kmxF2YVjIRGzn7XQoi1/cpadk9U8bgTuc6IWewycke0hSapGE
+R8QPGi0uEIaVBpwWYPsu8DtIWk4ImYzCZXHrOS72Re+ZzeaPl7tFAVhhZm4wTI2y
+jRLs79l5iw/bCOxbqBdjOZgpq+Uhx7tA/8TWqMWD8H5J8VTm9cNSpuGGxDEKLjbC
+NAIbNqZfbcXYd4o+z2UE/ZL8zIGR9sGcJM6AaPA1y8v7Q8oShLTmoAYdWEt1Nq8l
+G6/8ZLbv3MTB7pCGkvz/Pzd/6nO3KybaKa63wzICPpeChdaq3XpTEB0QiphXgFwt
+TMt0naO2NEB0
+-----END CERTIFICATE-----
diff --git a/test_data/ssl/ca.key b/test_data/ssl/ca.key
new file mode 100644
index 0000000..cc31b06
--- /dev/null
+++ b/test_data/ssl/ca.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCujUGscEKlm19a
+4AFXttAFg/HoqtSc80IYzFh+Mg7uG4supniQxvrNEw7QYHp49ABPn76SdNNxD8Ep
+IqmgRKwZ26tJkN0thR6CSIu9FouRT7wglfJDG+3ZU4z7zLt1U4P6dgkXGosHrZ9W
+76ARcNkZ8mUlXbUCCj+cMuE5TWIsk1cIk5u7sumslTJcUL4mQ+mlxCvo+3dS6ql+
+ENS1c0OqOUcOoTrctNNJRJCOyvZQNHdvJ+tnCXwxNwOJOkNjLkT9Iumjz/t9coTd
+YeRQUo2OzrP0/lwxaB+hZpX3xOaplxSHJA4x8H330DpJ9pQoKz+Mca9rtD0CX+Ht
+kHqfGh/XAgMBAAECggEAGZCRuk40zKl9ZUNinvSk8fHZt9BxVE1idFxVZ6IwQpph
+H6N9DANAj1GHvWgr99QQyZilYjDfGPOMQFiVzGXSaPUd2KfxZO3WwpEaeka3iUBK
+1uDvOvn3s3lDsEtTd0MUlNW9zhbyntzEdgihgnxrJ5wzSS44yQQ0Pc3L48ccJlft
+/VNGmbMEKNFiBvzCl8FWvPsocfSMn22Qt5ica3n4vAV1kK+7hZd19vz5QrMn1Jis
+sZQhqRaWUux6qE27zvAP3KfaTGjsNk+GNRSa0mRkiaFYzHloM5o3H4NOHEHq151s
+0KD2MOPyqBpvTW7icIAdFXsCUSRbZbxmkvZzwEr8uQKBgQDhput0hx+zWa+CeKBz
+CUV27IKSmfcZxfbRIpcEMCZWaCFal4DbNyAaki73dvkdbKyRJjUEu/pBxuepGnQT
+BI1zIKndtBV5RBtNS78yZj5RM8wQlQLTgN0b8LCeXfsgnFxTBFeY1UJMvoXuSUHm
++LxdnKPRqPiMANnarnGX70dzvwKBgQDGBvt2I2wu5oF9Js+8dsGm8zAlD5E5sThH
+VJKayjvx9zyRLja7R3XEvr2T39At8t3P2RiUn3N5DSrWthTg1SgnJhQgGx24j0El
+fyGH3i2aAnCGHy4mhwFjC7avG5S/1iWpQJuMh5OnTrsrnVNDwjAftDE/R08qsGxE
+9APPE8P56QKBgHMdLTa4DCL5mMCNewTUcPx3MTFKnDUouX39EF9RlO01l/ZYHaM3
+XwhPFOinZ/Rr0SkG/bsZjlRp/RpWZWqiQuq9egqg6OeBKBBpmPNEF3xjWTIIYnW1
+YpbzVZakyQzc13h+WZWdDYKLG1XxR05mC+oRk7zMX2dEs96MaWSh66iPAoGAXAqQ
+xGdQeDghRYdNlN5pwexUm1Ux/eu2KclQXuvSkvOETkJ5o/Bh97FkMiAcBc9Vc68H
+MsUuCbyqBaQZ7iqFADU9s3KHDOpgsEn3zsvgzC2IGX7Sl4u2hE/EeH1MVSH23UUv
+R1EYuvHoIyx7sAnJDmNVZIEDdecMW7xDLPOV6kkCgYBo6kny6y58W00orYfjdjJv
+6GDggOWOaPhXI4hKlcsnrutHQZiRXjsIVFjaLSyj8LHpFcdtvgrRTkgxYGcrVwn1
+bsULiQ43j3Jz/K7kclOycdL366XX79NOmSO87hqFrzk34iQT8+SgVsHm7zXWhkWk
+OjgWFgZtGxCEJYJVO6n2dg==
+-----END PRIVATE KEY-----
diff --git a/test_data/ssl/certificate.crt b/test_data/ssl/certificate.crt
new file mode 100644
index 0000000..9e0bb0b
--- /dev/null
+++ b/test_data/ssl/certificate.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIUJvEKMtCoVcAv9tGqA8aPrixn71UwDQYJKoZIhvcNAQEL
+BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yMzAzMTAxMzAwNTdaFw00MzAzMDUx
+MzAwNTdaMBsxGTAXBgNVBAMMEFRlc3QgY2VydGlmaWNhdGUwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCnYBjsgsEptJnum+1fju2OF1jzLRMjp28pzzmZ
+CTtvMjpyVK9Fr2cHIcE/ZUNStvQ8hIAAnfhkz65uYAOgZEHe7Qa04YitVUOworJl
+Nr7YJ5eUEA9I9KifTS4bkQxfkn7Fbn7KPZBE6X6WO4cEMZMbTZ9rzE5xMyUcbvux
+X8wE2ZyJxxm2pKt60dM5Jy+KS944fOEMqq4vlNp37Q1CPrlXzjzRRB152Ct93aWQ
+OpDwTySv1ae0lxzrAmoUnIErnDylb0Q6W/u7fjVmWtia9CcsGgAUNwvi1fyS1oqF
++4PT6Dgl5XNoTXmCjF91eApRokvFKn6VQYvMhBy31mAoQlpjAgMBAAGjUDBOMB0G
+A1UdDgQWBBRhmg2PRgxohtpcNSt83vhOOpVASjAfBgNVHSMEGDAWgBR+2zzbdSV0
+6uu9rnYy9t6rNVuOGDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAU
+ZYj95zUjgpaCeeHyKPOpebNG/34pNpFmdMNS7Far+DMe9MHlD1X/+4qqo07813KU
+dlWCpK4opGIBMNEaHlzXj+EqZuQelwqWJbPF1kcY5tPLAcD9XEGtHevFa3zYVhcN
+n/ixPQ/RZUNJffhQ+3qzzHCCmsrsV1DfQwiPL+7ohsxO9BBBlLtsF0ZcfwObYK0u
+eXGYoB+n/rixlEYzvE0LTEo7QT62iATScKiwR0OYj98t7fkiIDPwI9IhG5cAcnGs
+bBW0ost0ekTAKIFNOQdwkOmWYXHv1kgQTU/glyIKWKzkklcRYKVvo5NcXMIeKJdS
+8PV+76i0uEye7GSU8D1I
+-----END CERTIFICATE-----
diff --git a/test_data/ssl/certificate.key b/test_data/ssl/certificate.key
new file mode 100644
index 0000000..cbae8e5
--- /dev/null
+++ b/test_data/ssl/certificate.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCnYBjsgsEptJnu
+m+1fju2OF1jzLRMjp28pzzmZCTtvMjpyVK9Fr2cHIcE/ZUNStvQ8hIAAnfhkz65u
+YAOgZEHe7Qa04YitVUOworJlNr7YJ5eUEA9I9KifTS4bkQxfkn7Fbn7KPZBE6X6W
+O4cEMZMbTZ9rzE5xMyUcbvuxX8wE2ZyJxxm2pKt60dM5Jy+KS944fOEMqq4vlNp3
+7Q1CPrlXzjzRRB152Ct93aWQOpDwTySv1ae0lxzrAmoUnIErnDylb0Q6W/u7fjVm
+Wtia9CcsGgAUNwvi1fyS1oqF+4PT6Dgl5XNoTXmCjF91eApRokvFKn6VQYvMhBy3
+1mAoQlpjAgMBAAECggEAJECxRVEQ66qdiMwnPwJjO9qcvpctxRQ4BLNIw3f3cp9L
+f4YOLKbQRwAtrkCNs3XDMvR0ES4mJhfBrVCImI3+on1ubXFIPFryfWjQARI5hfIV
+Z9GOrfuoJlD+QqRJLM47PSIwvjdVb0evznR4SxU9yrUmaZ/oAabVS/JR+9pHA2mR
+KEYkn+DCuPmmGHxLFCe5d6LCupVB7gSSzkr74CKfcRnBL7bCGpYFzjMp9mGNtc4L
+QomNf4+T6ZFWv3u9Uvl8mH+BfJLmlTuvUrB4/QPCkv06lXZRfX+A1V/Wa31Ua9V/
+e/dcru40IA9K9VzvUDaSTW4V26soD5A22HFleb3DMQKBgQDbJYtSe+dzy/aPmCKI
+wEzhHyrqWoc02BnAz9Oqmq5bv2g15nfgxUsqR2F7hJ7Xj/kwz4sDeYRlbPngIVh8
+pdnJAKqpvtCysWgggsZe0D0yLPTIV+H0ImUUbxMftIw11oLc9LgtZ/TUcz7uisJ0
+117TGgKYgW7/NknikgqHhcnHawKBgQDDhcEXWz+YpDD/vp+zv25uRuzkQiT6yd1f
+nW0wUWwNPQzXVWs7lLe0Ez8BmiTSJjcFs6jVzjvBOnQXTtqud4ikDG22og8Piv3+
+2BPgnatCkVGXIDWWW3viQz1Q9/9B/nGBeJQKWx20+sDvDnSlvfeb3zbqkoz+sj+F
+nDl/YRwO6QKBgQCPdHtIao2vssPbafapWGC7OaDpOoupnxD0s9dWpd0feCPqrMyq
+mdxDd+irZ7xnVfsE5ceVZbWyg2zrOEjph9QSDVqqtZt+bj3Aknry1BRLRTyT7Vuf
+aeiLQM7fAVyLXbnalGQAbT3K2QpIMxNqUxXi0PMEDC6x6ELji0BTSNQ2swKBgQCx
+L8JHp1KfwpQA/7/8hcvOtgmx5VtbxpoOLz0nH3J7IMtFTUyLRv+feh2MFyOtKiKM
+0T5825N9Tbqs8LHuj7bNa9H1QzHA1SXO0ARbdqcgAU9eVDsb0jYXWvXzLXsuVCaK
+vTnzlJT8UI2NVp1RIjGFGSjMNRj/K4uzSls0200xGQKBgBJFaE5rYi508XtARl+U
+LRR1SzLihy5JaSHKjJp+7K/vp0bm5CGFGpwK6QNAkEzv2Jvl47bauzcnafdgwnsC
+w4okm5CziM+uFB8rzttd9fT1pHPW5HVUqrUoVv+sImw5x12AbStJEpDudEeFUGtW
+kUeXVGvc5hQaUjsHA4r6XmL7
+-----END PRIVATE KEY-----
diff --git a/test_data/ssl/generate.sh b/test_data/ssl/generate.sh
new file mode 100755
index 0000000..6cf0842
--- /dev/null
+++ b/test_data/ssl/generate.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+rm -f ca.kley ca.crt certificate.key certificate.crt trustore.jks
+
+# CA
+openssl req -new \
+ -days 7300 \
+ -nodes \
+ -x509 \
+ -sha256 \
+ -newkey rsa:2048 \
+ -keyout ca.key \
+ -out ca.crt \
+ -subj "/CN=Test CA"
+
+openssl req -new \
+ -days 7300 \
+ -nodes \
+ -x509 \
+ -sha256 \
+ -newkey rsa:2048 \
+ -addext basicConstraints=critical,CA:FALSE \
+ -CA ca.crt \
+ -CAkey ca.key \
+ -keyout certificate.key \
+ -out certificate.crt \
+ -subj "/CN=Test certificate"
+
+keytool -import -file ca.crt -keystore trustore.jks -storepass changeit -noprompt
diff --git a/test_data/ssl/trustore.jks b/test_data/ssl/trustore.jks
new file mode 100644
index 0000000..0f5505d
Binary files /dev/null and b/test_data/ssl/trustore.jks differ