Initial commit

This commit is contained in:
2021-05-25 19:31:34 +02:00
commit e0ad264432
22 changed files with 2703 additions and 0 deletions

25
.gitignore vendored Normal file

@ -0,0 +1,25 @@
*.class
bin/
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
hotspot_pid*
.settings/
.classpath
.project
*.jardesc
log
src/tests

312
LICENSE Normal file

@ -0,0 +1,312 @@
Mozilla Public License Version 2.0
1. Definitions
1.1. "Contributor" means each individual or legal entity that creates, contributes
to the creation of, or owns Covered Software.
1.2. "Contributor Version" means the combination of the Contributions of others
(if any) used by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution" means Covered Software of a particular Contributor.
1.4. "Covered Software" means Source Code Form to which the initial Contributor
has attached the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses" means
(a) that the initial Contributor has attached the notice described in Exhibit
B to the Covered Software; or
(b) that the Covered Software was made available under the terms of version
1.1 or earlier of the License, but not also under the terms of a Secondary
License.
1.6. "Executable Form" means any form of the work other than Source Code Form.
1.7. "Larger Work" means a work that combines Covered Software with other
material, in a separate file or files, that is not Covered Software.
1.8. "License" means this document.
1.9. "Licensable" means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications" means any of the following:
(a) any file in Source Code Form that results from an addition to, deletion
from, or modification of the contents of Covered Software; or
(b) any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor means any patent claim(s), including
without limitation, method, process, and apparatus claims, in any patent Licensable
by such Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import, or
transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License" means either the GNU General Public License, Version
2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form" means the form of the work preferred for making modifications.
1.14. "You" (or "Your") means an individual or a legal entity exercising rights
under this License. For legal entities, "You" includes any entity that controls,
is controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or otherwise,
or (b) ownership of more than fifty percent (50%) of the outstanding shares
or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
license:
(a) under intellectual property rights (other than patent or trademark) Licensable
by such Contributor to use, reproduce, make available, modify, display, perform,
distribute, and otherwise exploit its Contributions, either on an unmodified
basis, with Modifications, or as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its Contributions or
its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution become
effective for each Contribution on the date the Contributor first distributes
such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under this
License. No additional rights or licenses will be implied from the distribution
or licensing of Covered Software under this License. Notwithstanding Section
2.1(b) above, no patent license is granted by a Contributor:
(a) for any code that a Contributor has removed from Covered Software; or
(b) for infringements caused by: (i) Your and any other third party's modifications
of Covered Software, or (ii) the combination of its Contributions with other
software (except as part of its Contributor Version); or
(c) under Patent Claims infringed by Covered Software in the absence of its
Contributions.
This License does not grant any rights in the trademarks, service marks, or
logos of any Contributor (except as may be necessary to comply with the notice
requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to distribute
the Covered Software under a subsequent version of this License (see Section
10.2) or under the terms of a Secondary License (if permitted under the terms
of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its Contributions
are its original creation(s) or it has sufficient rights to grant the rights
to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under applicable
copyright doctrines of fair use, fair dealing, or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any Modifications
that You create or to which You contribute, must be under the terms of this
License. You must inform recipients that the Source Code Form of the Covered
Software is governed by the terms of this License, and how they can obtain
a copy of this License. You may not attempt to alter or restrict the recipients'
rights in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the Executable
Form how they can obtain a copy of such Source Code Form by reasonable means
in a timely manner, at a charge no more than the cost of distribution to the
recipient; and
(b) You may distribute such Executable Form under the terms of this License,
or sublicense it under different terms, provided that the license for the
Executable Form does not attempt to limit or alter the recipients' rights
in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice, provided
that You also comply with the requirements of this License for the Covered
Software. If the Larger Work is a combination of Covered Software with a work
governed by one or more Secondary Licenses, and the Covered Software is not
Incompatible With Secondary Licenses, this License permits You to additionally
distribute such Covered Software under the terms of such Secondary License(s),
so that the recipient of the Larger Work may, at their option, further distribute
the Covered Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices (including
copyright notices, patent notices, disclaimers of warranty, or limitations
of liability) contained within the Source Code Form of the Covered Software,
except that You may alter any license notices to the extent required to remedy
known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support, indemnity
or liability obligations to one or more recipients of Covered Software. However,
You may do so only on Your own behalf, and not on behalf of any Contributor.
You must make it absolutely clear that any such warranty, support, indemnity,
or liability obligation is offered by You alone, and You hereby agree to indemnify
every Contributor for any liability incurred by such Contributor as a result
of warranty, support, indemnity or liability terms You offer. You may include
additional disclaimers of warranty and limitations of liability specific to
any jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute, judicial
order, or regulation then You must: (a) comply with the terms of this License
to the maximum extent possible; and (b) describe the limitations and the code
they affect. Such description must be placed in a text file included with
all distributions of the Covered Software under this License. Except to the
extent prohibited by statute or regulation, such description must be sufficiently
detailed for a recipient of ordinary skill to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if
You fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor are
reinstated (a) provisionally, unless and until such Contributor explicitly
and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
fails to notify You of the non-compliance by some reasonable means prior to
60 days after You have come back into compliance. Moreover, Your grants from
a particular Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the first
time You have received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent infringement
claim (excluding declaratory judgment actions, counter-claims, and cross-claims)
alleging that a Contributor Version directly or indirectly infringes any patent,
then the rights granted to You by any and all Contributors for the Covered
Software under Section 2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end
user license agreements (excluding distributors and resellers) which have
been validly granted by You or Your distributors under this License prior
to termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an "as is" basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire risk
as to the quality and performance of the Covered Software is with You. Should
any Covered Software prove defective in any respect, You (not any Contributor)
assume the cost of any necessary servicing, repair, or correction. This disclaimer
of warranty constitutes an essential part of this License. No use of any Covered
Software is authorized under this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any character
including, without limitation, damages for lost profits, loss of goodwill,
work stoppage, computer failure or malfunction, or any and all other commercial
damages or losses, even if such party shall have been informed of the possibility
of such damages. This limitation of liability shall not apply to liability
for death or personal injury resulting from such party's negligence to the
extent applicable law prohibits such limitation. Some jurisdictions do not
allow the exclusion or limitation of incidental or consequential damages,
so this exclusion and limitation may not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a party's ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable, such
provision shall be reformed only to the extent necessary to make it enforceable.
Any law or regulation which provides that the language of a contract shall
be construed against the drafter shall not be used to construe this License
against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section 10.3,
no one other than the license steward has the right to modify or publish new
versions of this License. Each version will be given a distinguishing version
number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version of
the License under which You originally received the Covered Software, or under
the terms of any subsequent version published by the license steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to create
a new license for such software, you may create and use a modified version
of this License if you rename the license and remove any references to the
name of the license steward (except to note that such modified license differs
from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is Incompatible With Secondary
Licenses under the terms of this version of the License, the notice described
in Exhibit B of this License must be attached. Exhibit A - Source Code Form
License Notice
This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as defined
by the Mozilla Public License, v. 2.0.

3
README.md Normal file

@ -0,0 +1,3 @@
# omz-net-lib

@ -0,0 +1,451 @@
/****************************************************************************
* Copyright (c) 1998-2010 AOL Inc.
*
* 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.
*
****************************************************************************/
/*
* This file was copied from https://github.com/mendix/SSLTools/blob/master/src/main/java/com/mendix/ssltools/PrivateKeyReader.java
* THIS FILE HAS BEEN MODIFIED:
* - formatted using an auto-formatter
* - readKeyMaterial(...) modified to properly close the Scanner instance
* - all methods in PrivateKeyReader have been made static, and data will be passed directly to those methods
*/
package com.mendix.ssltools;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Base64;
import java.util.Scanner;
/**
*
* Class for reading RSA private key from PEM formatted text.
*
* <p/>
* It can read PEM files with PKCS#8 or PKCS#1 encodings. It doesn't support encrypted PEM files.
*
*/
public class PrivateKeyReader {
// Private key file using PKCS #1 encoding
public static final String P1_BEGIN_MARKER = "-----BEGIN RSA PRIVATE KEY"; //$NON-NLS-1$
public static final String P1_END_MARKER = "-----END RSA PRIVATE KEY"; //$NON-NLS-1$
// Private key file using PKCS #8 encoding
public static final String P8_BEGIN_MARKER = "-----BEGIN PRIVATE KEY"; //$NON-NLS-1$
public static final String P8_END_MARKER = "-----END PRIVATE KEY"; //$NON-NLS-1$
/**
* Read the PEM string and return the key
*
* @return PrivateKey
* @throws IOException
*/
public static PrivateKey read(String keyString) throws IOException {
KeyFactory factory;
try{
factory = KeyFactory.getInstance("RSA"); //$NON-NLS-1$
}catch(NoSuchAlgorithmException e){
throw new IOException("JCE error: " + e.getMessage()); //$NON-NLS-1$
}
if(keyString.contains(P1_BEGIN_MARKER)){
byte[] keyBytes = readKeyMaterial(keyString, P1_BEGIN_MARKER, P1_END_MARKER);
RSAPrivateCrtKeySpec keySpec = getRSAKeySpec(keyBytes);
try{
return factory.generatePrivate(keySpec);
}catch(InvalidKeySpecException e){
throw new IOException("Invalid PKCS#1 PEM file: " + e.getMessage()); //$NON-NLS-1$
}
}
if(keyString.contains(P8_BEGIN_MARKER)){
byte[] keyBytes = readKeyMaterial(keyString, P8_BEGIN_MARKER, P8_END_MARKER);
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
try{
return factory.generatePrivate(keySpec);
}catch(InvalidKeySpecException e){
throw new IOException("Invalid PKCS#8 PEM file: " + e.getMessage()); //$NON-NLS-1$
}
}
throw new IOException("Invalid PEM file: no begin marker"); //$NON-NLS-1$
}
/**
* Read the PEM file and convert it into binary DER stream
*
* @return byte[]
* @throws IOException
*/
private static byte[] readKeyMaterial(String keyString, String beginMarker, String endMarker) throws IOException {
String line = null;
StringBuffer buf = new StringBuffer();
Scanner scanner = new Scanner(keyString);
byte[] decoded = null;
while(scanner.hasNextLine()){
line = scanner.nextLine();
if(line.contains(beginMarker)){
continue;
}
if(line.contains(endMarker)){
decoded = Base64.getDecoder().decode(buf.toString());
break;
}
buf.append(line.trim());
}
scanner.close();
if(decoded == null)
throw new IOException("Invalid PEM file: No end marker"); //$NON-NLS-1$
else
return decoded;
}
/**
* Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec.
*
* <p/>
* The ASN.1 syntax for the private key with CRT is
*
* <pre>
* --
* -- Representation of RSA private key with information for the CRT algorithm.
* --
* RSAPrivateKey ::= SEQUENCE {
* version Version,
* modulus INTEGER, -- n
* publicExponent INTEGER, -- e
* privateExponent INTEGER, -- d
* prime1 INTEGER, -- p
* prime2 INTEGER, -- q
* exponent1 INTEGER, -- d mod (p-1)
* exponent2 INTEGER, -- d mod (q-1)
* coefficient INTEGER, -- (inverse of q) mod p
* otherPrimeInfos OtherPrimeInfos OPTIONAL
* }
* </pre>
*
* @param keyBytes PKCS#1 encoded key
* @return KeySpec
* @throws IOException
*/
public static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException {
DerParser parser = new DerParser(keyBytes);
Asn1Object sequence = parser.read();
if(sequence.getType() != DerParser.SEQUENCE)
throw new IOException("Invalid DER: not a sequence"); //$NON-NLS-1$
// Parse inside the sequence
parser = sequence.getParser();
parser.read(); // Skip version
BigInteger modulus = parser.read().getInteger();
BigInteger publicExp = parser.read().getInteger();
BigInteger privateExp = parser.read().getInteger();
BigInteger prime1 = parser.read().getInteger();
BigInteger prime2 = parser.read().getInteger();
BigInteger exp1 = parser.read().getInteger();
BigInteger exp2 = parser.read().getInteger();
BigInteger crtCoef = parser.read().getInteger();
RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef);
return keySpec;
}
}
/**
* A bare-minimum ASN.1 DER decoder, just having enough functions to decode PKCS#1 private keys. Especially, it doesn't handle explicitly tagged types with an outer tag.
*
* <p/>
* This parser can only handle one layer. To parse nested constructs, get a new parser for each layer using <code>Asn1Object.getParser()</code>.
*
* <p/>
* There are many DER decoders in JRE but using them will tie this program to a specific JCE/JVM.
*
*
*/
class DerParser {
// Classes
public final static int UNIVERSAL = 0x00;
public final static int APPLICATION = 0x40;
public final static int CONTEXT = 0x80;
public final static int PRIVATE = 0xC0;
// Constructed Flag
public final static int CONSTRUCTED = 0x20;
// Tag and data types
public final static int ANY = 0x00;
public final static int BOOLEAN = 0x01;
public final static int INTEGER = 0x02;
public final static int BIT_STRING = 0x03;
public final static int OCTET_STRING = 0x04;
public final static int NULL = 0x05;
public final static int OBJECT_IDENTIFIER = 0x06;
public final static int REAL = 0x09;
public final static int ENUMERATED = 0x0a;
public final static int RELATIVE_OID = 0x0d;
public final static int SEQUENCE = 0x10;
public final static int SET = 0x11;
public final static int NUMERIC_STRING = 0x12;
public final static int PRINTABLE_STRING = 0x13;
public final static int T61_STRING = 0x14;
public final static int VIDEOTEX_STRING = 0x15;
public final static int IA5_STRING = 0x16;
public final static int GRAPHIC_STRING = 0x19;
public final static int ISO646_STRING = 0x1A;
public final static int GENERAL_STRING = 0x1B;
public final static int UTF8_STRING = 0x0C;
public final static int UNIVERSAL_STRING = 0x1C;
public final static int BMP_STRING = 0x1E;
public final static int UTC_TIME = 0x17;
public final static int GENERALIZED_TIME = 0x18;
protected InputStream in;
/**
* Create a new DER decoder from an input stream.
*
* @param in The DER encoded stream
*/
public DerParser(InputStream in) throws IOException {
this.in = in;
}
/**
* Create a new DER decoder from a byte array.
*
* @param bytes encoded bytes
* @throws IOException
*/
public DerParser(byte[] bytes) throws IOException {
this(new ByteArrayInputStream(bytes));
}
/**
* Read next object. If it's constructed, the value holds encoded content and it should be parsed by a new parser from <code>Asn1Object.getParser</code>.
*
* @return A object
* @throws IOException
*/
public Asn1Object read() throws IOException {
int tag = in.read();
if(tag == -1)
throw new IOException("Invalid DER: stream too short, missing tag"); //$NON-NLS-1$
int length = getLength();
byte[] value = new byte[length];
int n = in.read(value);
if(n < length)
throw new IOException("Invalid DER: stream too short, missing value"); //$NON-NLS-1$
Asn1Object o = new Asn1Object(tag, length, value);
return o;
}
/**
* Decode the length of the field. Can only support length encoding up to 4 octets.
*
* <p/>
* In BER/DER encoding, length can be encoded in 2 forms,
* <ul>
* <li>Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length.
* <li>Long form. Two to 127 octets (only 4 is supported here). Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. Second and
* following octets give the length, base 256, most significant digit first.
* </ul>
*
* @return The length as integer
* @throws IOException
*/
private int getLength() throws IOException {
int i = in.read();
if(i == -1)
throw new IOException("Invalid DER: length missing"); //$NON-NLS-1$
// A single byte short length
if((i & ~0x7F) == 0)
return i;
int num = i & 0x7F;
// We can't handle length longer than 4 bytes
if(i >= 0xFF || num > 4)
throw new IOException("Invalid DER: length field too big (" //$NON-NLS-1$
+ i + ")"); //$NON-NLS-1$
byte[] bytes = new byte[num];
int n = in.read(bytes);
if(n < num)
throw new IOException("Invalid DER: length too short"); //$NON-NLS-1$
return new BigInteger(1, bytes).intValue();
}
}
/**
* An ASN.1 TLV. The object is not parsed. It can only handle integers and strings.
*
*
*/
class Asn1Object {
protected final int type;
protected final int length;
protected final byte[] value;
protected final int tag;
/**
* Construct a ASN.1 TLV. The TLV could be either a constructed or primitive entity.
*
* <p/>
* The first byte in DER encoding is made of following fields,
*
* <pre>
*-------------------------------------------------
*|Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
*-------------------------------------------------
*| Class | CF | + Type |
*-------------------------------------------------
* </pre>
* <ul>
* <li>Class: Universal, Application, Context or Private
* <li>CF: Constructed flag. If 1, the field is constructed.
* <li>Type: This is actually called tag in ASN.1. It indicates data type (Integer, String) or a construct (sequence, choice, set).
* </ul>
*
* @param tag Tag or Identifier
* @param length Length of the field
* @param value Encoded octet string for the field.
*/
public Asn1Object(int tag, int length, byte[] value) {
this.tag = tag;
this.type = tag & 0x1F;
this.length = length;
this.value = value;
}
public int getType() {
return type;
}
public int getLength() {
return length;
}
public byte[] getValue() {
return value;
}
public boolean isConstructed() {
return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED;
}
/**
* For constructed field, return a parser for its content.
*
* @return A parser for the construct.
* @throws IOException
*/
public DerParser getParser() throws IOException {
if(!isConstructed())
throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$
return new DerParser(value);
}
/**
* Get the value as integer
*
* @return BigInteger
* @throws IOException
*/
public BigInteger getInteger() throws IOException {
if(type != DerParser.INTEGER)
throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$
return new BigInteger(value);
}
/**
* Get value as string. Most strings are treated as Latin-1.
*
* @return Java string
* @throws IOException
*/
public String getString() throws IOException {
String encoding;
switch(type){
// Not all are Latin-1 but it's the closest thing
case DerParser.NUMERIC_STRING:
case DerParser.PRINTABLE_STRING:
case DerParser.VIDEOTEX_STRING:
case DerParser.IA5_STRING:
case DerParser.GRAPHIC_STRING:
case DerParser.ISO646_STRING:
case DerParser.GENERAL_STRING:
encoding = "ISO-8859-1"; //$NON-NLS-1$
break;
case DerParser.BMP_STRING:
encoding = "UTF-16BE"; //$NON-NLS-1$
break;
case DerParser.UTF8_STRING:
encoding = "UTF-8"; //$NON-NLS-1$
break;
case DerParser.UNIVERSAL_STRING:
throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$
default:
throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$
}
return new String(value, encoding);
}
}

@ -0,0 +1,32 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client;
import java.io.IOException;
import org.omegazero.net.client.params.InetConnectionParameters;
import org.omegazero.net.common.NetworkApplication;
import org.omegazero.net.socket.InetConnection;
public interface InetClientManager extends NetworkApplication {
/**
* Creates a new connection instance based on the given parameters to be managed by this <code>InetClientManager</code>.<br>
* <br>
* {@link InetConnection#connect()} will need to be called on the returned connection instance to initiate the connection.
*
* @param params Parameters for this connection
* @return The new connection instance
* @throws IOException
*/
public InetConnection connection(InetConnectionParameters params) throws IOException;
}

@ -0,0 +1,42 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.function.Consumer;
import org.omegazero.net.client.params.InetConnectionParameters;
import org.omegazero.net.socket.InetConnection;
import org.omegazero.net.socket.impl.PlainConnection;
public class PlainTCPClientManager extends TCPClientManager {
public PlainTCPClientManager() {
super();
}
public PlainTCPClientManager(Consumer<Runnable> worker) {
super(worker);
}
@Override
protected InetConnection createConnection(SocketChannel socketChannel, InetConnectionParameters params) throws IOException {
return new PlainConnection(socketChannel, params.getRemote());
}
@Override
protected void handleConnect(InetConnection conn) throws IOException {
conn.handleConnect();
}
}

@ -0,0 +1,119 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Objects;
import java.util.function.Consumer;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.client.params.InetConnectionParameters;
import org.omegazero.net.common.InetConnectionSelector;
import org.omegazero.net.common.SyncWorker;
import org.omegazero.net.socket.InetConnection;
public abstract class TCPClientManager extends InetConnectionSelector implements InetClientManager {
private static final Logger logger = LoggerUtil.createLogger();
protected final Consumer<Runnable> worker;
public TCPClientManager() {
this(null);
}
public TCPClientManager(Consumer<Runnable> worker) {
if(worker != null)
this.worker = worker;
else
this.worker = new SyncWorker();
}
protected abstract InetConnection createConnection(SocketChannel socketChannel, InetConnectionParameters params) throws IOException;
/**
* Called when a connection was established.
*
* @param conn The {@link InetConnection} object representing the connection that was established
*/
protected abstract void handleConnect(InetConnection conn) throws IOException;
@Override
public void init() throws IOException {
super.initSelector();
}
@Override
public void close() throws IOException {
super.closeSelector();
}
@Override
public InetConnection connection(InetConnectionParameters params) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetConnection conn = this.createConnection(socketChannel, params);
conn.setOnLocalClose(super::onConnectionClosed);
conn.setOnError((e) -> {
logger.warn("Socket Error (remote address=", conn.getRemoteAddress(), "): ", e.toString());
});
if(params.getLocal() != null)
socketChannel.bind(params.getLocal());
super.startRegister();
socketChannel.register(super.selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ).attach(conn);
super.endRegister();
return conn;
}
@Override
public void run() throws IOException {
super.selectorLoop();
}
@Override
protected void handleSelectedKey(SelectionKey key) throws IOException {
Objects.requireNonNull(key.attachment(), "SelectionKey attachment is null");
if(!(key.attachment() instanceof InetConnection))
throw new RuntimeException("SelectionKey attachment is of type " + (key.attachment() != null ? key.attachment().getClass().getName() : null)
+ ", but expected type " + InetConnection.class.getName());
InetConnection conn = (InetConnection) key.attachment();
if(key.isConnectable()){
SocketChannel channel = (SocketChannel) key.channel();
try{
if(channel.finishConnect()){
this.handleConnect(conn);
}else
throw new IOException("Socket channel was marked as connectable but finishConnect returned false");
}catch(IOException e){
conn.handleError(e);
}
}else if(key.isReadable()){
byte[] data = conn.read();
if(data != null){
this.worker.accept(() -> {
conn.handleData(data);
});
}
}
}
}

@ -0,0 +1,107 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.omegazero.net.client.params.InetConnectionParameters;
import org.omegazero.net.client.params.TLSConnectionParameters;
import org.omegazero.net.socket.InetConnection;
import org.omegazero.net.socket.impl.TLSConnection;
import org.omegazero.net.util.SSLUtil;
public class TLSClientManager extends TCPClientManager {
private static X509TrustManager trustManagerDefault;
private TrustManager[] trustManagers;
public TLSClientManager() {
this(null, null);
}
public TLSClientManager(Consumer<Runnable> worker) {
this(worker, null);
}
public TLSClientManager(Consumer<Runnable> worker, Collection<String> additionalTrustCertificateFiles) {
super(worker);
try{
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
TLSClientManager.addDefaultCertificates(ks);
if(additionalTrustCertificateFiles != null)
for(String f : additionalTrustCertificateFiles){
ks.setCertificateEntry(f, SSLUtil.loadCertificateFromPEM(f));
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
this.trustManagers = tmf.getTrustManagers();
}catch(GeneralSecurityException | IOException e){
throw new RuntimeException("Error while initializing trust manager", e);
}
}
@Override
protected InetConnection createConnection(SocketChannel socketChannel, InetConnectionParameters params) throws IOException {
try{
if(!(params instanceof TLSConnectionParameters))
throw new IllegalArgumentException("params must be an instance of " + TLSConnectionParameters.class.getName());
TLSConnectionParameters tlsParams = (TLSConnectionParameters) params;
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, this.trustManagers, null);
return new TLSConnection(socketChannel, params.getRemote(), context, true, super.worker, tlsParams.getAlpnNames(), tlsParams.getSniOptions());
}catch(GeneralSecurityException | IOException e){
throw new RuntimeException("Error while creating TLS client connection", e);
}
}
@Override
protected void handleConnect(InetConnection conn) throws IOException {
((TLSConnection) conn).doTLSHandshake();
}
private static void addDefaultCertificates(KeyStore ks) throws GeneralSecurityException {
for(X509Certificate cert : TLSClientManager.trustManagerDefault.getAcceptedIssuers()){
ks.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert);
}
}
static{
try{
TrustManagerFactory deftfm = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
deftfm.init((KeyStore) null);
for(TrustManager tm : deftfm.getTrustManagers()){
if(tm instanceof X509TrustManager){
TLSClientManager.trustManagerDefault = (X509TrustManager) tm;
break;
}
}
}catch(GeneralSecurityException e){
throw new RuntimeException("Error while getting default trust manager", e);
}
}
}

@ -0,0 +1,53 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client.params;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
public class InetConnectionParameters {
private final InetSocketAddress remote;
private final InetSocketAddress local;
public InetConnectionParameters(InetSocketAddress remote) {
this(remote, null);
}
public InetConnectionParameters(InetSocketAddress remote, InetSocketAddress local) {
this.remote = remote;
this.local = local;
}
public InetConnectionParameters(InetAddress remoteAddress, int remotePort) {
this(new InetSocketAddress(remoteAddress, remotePort), null);
}
public InetConnectionParameters(String remoteAddress, int remotePort) {
try{
this.remote = new InetSocketAddress(InetAddress.getByName(remoteAddress), remotePort);
}catch(UnknownHostException e){
throw new RuntimeException("Invalid remoteAddress", e);
}
this.local = null;
}
public InetSocketAddress getRemote() {
return remote;
}
public InetSocketAddress getLocal() {
return local;
}
}

@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.client.params;
import java.net.InetAddress;
import java.net.InetSocketAddress;
public class TLSConnectionParameters extends InetConnectionParameters {
private String[] alpnNames;
private String[] sniOptions;
public TLSConnectionParameters(InetSocketAddress remote) {
super(remote);
}
public TLSConnectionParameters(InetSocketAddress remote, InetSocketAddress local) {
super(remote, local);
}
public TLSConnectionParameters(InetAddress remoteAddress, int remotePort) {
super(remoteAddress, remotePort);
}
public TLSConnectionParameters(String remoteAddress, int remotePort) {
super(remoteAddress, remotePort);
}
/**
* Sets the list of requested application layer protocol names to be negotiated using TLS ALPN (Application Layer Protocol Negotiation). The elements in this list should
* be ordered from most-preferred to least-preferred protocol name.<br>
* <br>
* If not set or <code>null</code> is passed, the first protocol name presented by the server is selected.
*
* @param alpnNames The list of requested protocol names
*/
public void setAlpnNames(String[] alpnNames) {
this.alpnNames = alpnNames;
}
public String[] getAlpnNames() {
return this.alpnNames;
}
/**
* Sets the list of requested server names using TLS SNI (Server Name Indication).<br>
* <br>
* If not set or <code>null</code> is passed, SNI is disabled.
*
* @param sniOptions The list of requested server names
*/
public void setSniOptions(String[] sniOptions) {
this.sniOptions = sniOptions;
}
public String[] getSniOptions() {
return sniOptions;
}
}

@ -0,0 +1,151 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.common;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.HashSet;
import java.util.Iterator;
import org.omegazero.net.socket.InetConnection;
public abstract class InetConnectionSelector {
protected Selector selector;
// when a socket is closed locally, using SocketChannel.close(), the SelectionKey is removed from the Selector before the next time the select() method
// returns, so there is no way to detect a socket closed locally except manually notifying and adding the connection to this list when close() is called
// on the InetSocketConnection object. Alternative would be directly calling handleClose() in the close() method in eg InetSocketConnection, but
// preferably the thread executing the selectorLoop() should emit all events and that likely wouldnt be the case because close() may be called by any thread.
// this is ugly, but i dont think there is a better way
private HashSet<InetConnection> closedConnections = new HashSet<>();
private boolean running = false;
private volatile boolean registerOperation = false;
/**
* Called when a key was selected in a select call in the {@link InetConnectionSelector#selectorLoop()} method.
*
* @param key The selected key
* @throws IOException
*/
protected abstract void handleSelectedKey(SelectionKey key) throws IOException;
/**
* Called when a connection closed.
*
* @param conn
* @throws IOException
*/
protected void handleConnectionClosed(InetConnection conn) throws IOException {
conn.handleClose();
}
/**
* Notify this <code>InetConnectionSelector</code> that a connection was closed.<br>
* <br>
* This method is intended to be called by the subclass to notify this class that a connection has been closed locally, in contrast to the similarly named method
* {@link InetConnectionSelector#handleConnectionClosed(InetConnection)}, which is called by this class to notify the subclass that any connection has closed.
*
* @param conn The connection that was closed
*/
protected void onConnectionClosed(InetConnection conn) {
this.closedConnections.add(conn);
this.selector.wakeup();
}
/**
* Initializes the {@link Selector} and sets this instance as running.<br>
* <br>
* After this method has been called successfully, {@link InetConnectionSelector#selectorLoop()} should be called to start performing IO operations.
*
* @throws IOException
*/
protected void initSelector() throws IOException {
this.selector = Selector.open();
this.running = true;
}
/**
* Stops this instance and closes the {@link Selector} instance.
*
* @throws IOException
*/
protected synchronized void closeSelector() throws IOException {
if(!this.running)
return;
this.running = false;
this.selector.close();
}
protected void startRegister() {
this.registerOperation = true;
this.selector.wakeup();
}
protected void endRegister() {
this.registerOperation = false;
}
/**
* Runs the loop that continuously selects channels using the {@link Selector}.<br>
* <br>
* Will not return until {@link InetConnectionSelector#closeSelector()} is called. If {@link InetConnectionSelector#initSelector()} was never called, this method returns
* immediately.
*
* @throws IOException
*/
protected void selectorLoop() throws IOException {
while(this.running){
if(this.closedConnections.size() > 0){
Iterator<InetConnection> closeIterator = this.closedConnections.iterator();
while(closeIterator.hasNext()){
InetConnection conn = closeIterator.next();
this.handleConnectionClosed(conn);
closeIterator.remove();
}
}
if(this.selector.select() != 0){
synchronized(this){
if(!this.selector.isOpen())
continue;
Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
this.handleSelectedKey(key);
iterator.remove();
}
}
}
if(this.registerOperation){
long start = System.currentTimeMillis();
while(this.registerOperation){
if(System.currentTimeMillis() - start > 1000)
throw new RuntimeException("Waiting time for register operation exceeded");
}
}
}
}
public boolean isRunning() {
return running;
}
}

@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.common;
import java.io.IOException;
public interface NetworkApplication {
/**
* Initializes this application.
*
* @throws IOException If an IO error occurs during initialization
*/
public void init() throws IOException;
/**
* Closes this application, closing all bound and connected sockets and stopping the {@link NetworkApplication#loop}.
*
* @throws IOException If an IO error occurs
*/
public void close() throws IOException;
/**
* Runs the main loop of this instance. This loop processes incoming or outgoing connection requests and network traffic.<br>
* <br>
* Under normal circumstances, should never return before {@link NetworkApplication#close()} is called, otherwise, the function should return as soon as possible.
*
* @throws IOException If an IO error occurs during any networking operation
*/
public void run() throws IOException;
}

@ -0,0 +1,22 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.common;
import java.util.function.Consumer;
public class SyncWorker implements Consumer<Runnable> {
@Override
public void accept(Runnable t) {
t.run();
}
}

@ -0,0 +1,32 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.server;
import java.util.function.Consumer;
import org.omegazero.net.common.NetworkApplication;
import org.omegazero.net.socket.InetConnection;
/**
* A server accepting stateful connection requests based on the Internet Protocol.
*/
public interface InetServer extends NetworkApplication {
/**
* Sets the callback for a new incoming request.<br>
* <br>
* The first parameter of this callback is an {@link InetConnection} instance representing the new connection from the client.
*
* @param handler The connection callback
*/
public void setConnectionCallback(Consumer<InetConnection> handler);
}

@ -0,0 +1,67 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.server;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.function.Consumer;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.socket.InetConnection;
import org.omegazero.net.socket.impl.PlainConnection;
/**
* {@link TCPServer} implementation for plaintext sockets.
*/
public class PlainTCPServer extends TCPServer {
private static final Logger logger = LoggerUtil.createLogger();
/**
*
* @see TCPServer#TCPServer(Collection)
*/
public PlainTCPServer(Collection<Integer> ports) {
super(ports);
}
/**
*
* @see TCPServer#TCPServer(String, Collection, int, Consumer, long)
*/
public PlainTCPServer(String bindAddress, Collection<Integer> ports, int backlog, Consumer<Runnable> worker, long idleTimeout) {
super(bindAddress, ports, backlog, worker, idleTimeout);
}
@Override
protected InetConnection handleConnection(SocketChannel socketChannel) throws IOException {
InetConnection conn = new PlainConnection(socketChannel, null);
// this is to handle errors that happen before another error handler was set, for example because a TLS handshake error occurred
// and there was no way to set another error handler because the onConnect was not called yet
conn.setOnError((e) -> {
if(e instanceof IOException)
logger.warn("Socket Error (remote address=", conn.getRemoteAddress(), "): ", e.toString());
else
logger.error("Error in connection (remote address=", conn.getRemoteAddress(), "): ", e);
});
return conn;
}
@Override
protected void handleConnectionPost(InetConnection connection) {
connection.handleConnect(); // plain connections have no additional handshake and are considered connected immediately
}
}

@ -0,0 +1,212 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.server;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import org.omegazero.common.event.Tasks;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.common.InetConnectionSelector;
import org.omegazero.net.common.SyncWorker;
import org.omegazero.net.socket.InetConnection;
/**
* TCP server implementation of an {@link InetServer} based on java.nio channels.
*/
public abstract class TCPServer extends InetConnectionSelector implements InetServer {
private static final Logger logger = LoggerUtil.createLogger();
private Consumer<InetConnection> onNewConnection;
private List<ServerSocketChannel> serverSockets = new ArrayList<>();
private long connectionTimeoutCheckInterval;
protected final InetAddress bindAddress;
protected final Collection<Integer> ports;
protected final int backlog;
protected final Consumer<Runnable> worker;
private long idleTimeout;
/**
*
* @see TCPServer#TCPServer(String, Collection, int, Consumer, long)
*/
public TCPServer(Collection<Integer> ports) {
this(null, ports, 0, null, 0);
}
/**
* Constructs a new <code>TCPServer</code> instance.<br>
* <br>
* To initialize the server, {@link InetServer#init()} of this object must be called. After the call succeeded, the server will listen on the specified local address
* (<b>bindAddress</b>) on the given <b>ports</b> and {@link InetServer#serverLoop()} must be called to start processing incoming connection requests and data.
*
* @param bindAddress The local address to bind to (see {@link ServerSocketChannel#bind(java.net.SocketAddress, int)})
* @param ports The list of ports to listen on
* @param backlog The maximum number of pending connections (see {@link ServerSocketChannel#bind(java.net.SocketAddress, int)}). May be 0 to use a default value
* @param worker A callback accepting tasks to run that may require increased processing time. May be <code>null</code> to run everything using a single thread
* @param idleTimeout The time in milliseconds to keep connections that had no traffic. May be 0 to disable closing idle connections
*/
public TCPServer(String bindAddress, Collection<Integer> ports, int backlog, Consumer<Runnable> worker, long idleTimeout) {
if(bindAddress != null)
try{
this.bindAddress = InetAddress.getByName(bindAddress);
}catch(Exception e){
throw new IllegalArgumentException("bindAddress is invalid", e);
}
else
this.bindAddress = null;
this.ports = Objects.requireNonNull(ports, "ports must not be null");
this.backlog = backlog;
if(worker != null)
this.worker = worker;
else
this.worker = new SyncWorker();
this.idleTimeout = idleTimeout;
}
private void listen() throws IOException {
super.initSelector();
for(int p : this.ports){
this.listenOnPort(p);
logger.info("Listening plain on port " + p);
}
}
private void listenOnPort(int port) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(this.bindAddress, port), this.backlog);
ssc.configureBlocking(false);
ssc.register(super.selector, SelectionKey.OP_ACCEPT);
this.serverSockets.add(ssc);
}
protected abstract InetConnection handleConnection(SocketChannel socketChannel) throws IOException;
protected void handleConnectionPost(InetConnection connection) {
}
@Override
public void init() throws IOException {
this.listen();
this.connectionTimeoutCheckInterval = Tasks.interval((args) -> {
// still need to check for idle timeout even if it wasn't configured because it may change during runtime
long timeout = TCPServer.this.idleTimeout;
if(timeout <= 0)
return;
long currentTime = System.currentTimeMillis();
try{
for(SelectionKey k : super.selector.keys()){
if(k.attachment() instanceof InetConnection){
InetConnection conn = (InetConnection) k.attachment();
long delta = currentTime - conn.getLastIOTime();
if(delta < 0 || delta > timeout){
logger.debug("Idle Timeout: ", conn.getRemoteAddress(), " (", delta, "ms)");
conn.close();
}
}
}
}catch(Exception e){
logger.warn("Error while checking idle timeouts: ", e.toString());
}
}, 5000).getId();
}
@Override
public void close() throws IOException {
Tasks.clear(this.connectionTimeoutCheckInterval);
for(ServerSocketChannel ssc : this.serverSockets){
ssc.close();
}
super.closeSelector();
}
@Override
public void setConnectionCallback(Consumer<InetConnection> handler) {
this.onNewConnection = handler;
}
@Override
public void run() throws IOException {
super.selectorLoop();
}
@Override
protected void handleSelectedKey(SelectionKey key) throws IOException {
if(key.isAcceptable()){
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
InetConnection conn = this.handleConnection(socketChannel);
conn.setOnLocalClose(super::onConnectionClosed);
conn.setOnConnect(() -> {
if(TCPServer.this.onNewConnection != null){
TCPServer.this.onNewConnection.accept(conn);
}else
throw new IllegalStateException("No connection handler is set");
});
this.handleConnectionPost(conn);
// no need to call super.startRegister because we're in handleSelectedKey (not in a select call)
socketChannel.register(super.selector, SelectionKey.OP_READ).attach(conn);
}else if(key.isReadable() && key.attachment() instanceof InetConnection){
InetConnection conn = (InetConnection) key.attachment();
byte[] data = conn.read();
if(data != null){
this.worker.accept(() -> {
conn.handleData(data);
});
}
}
}
public long getIdleTimeout() {
return idleTimeout;
}
public void setIdleTimeout(long idleTimeout) {
this.idleTimeout = idleTimeout;
}
public InetAddress getBindAddress() {
return bindAddress;
}
public int getBacklog() {
return backlog;
}
}

@ -0,0 +1,104 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.server;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.socket.InetConnection;
import org.omegazero.net.socket.impl.TLSConnection;
/**
* {@link TCPServer} implementation for sockets encrypted using SSL or TLS.
*/
public class TLSServer extends TCPServer {
private static final Logger logger = LoggerUtil.createLogger();
private final SSLContext sslContext;
private String[] supportedApplicationLayerProtocols = null;
/**
*
* @param sslContext The SSL context to be used by the server
* @see TCPServer#TCPServer(String, Collection, int, Consumer, long)
*/
public TLSServer(Collection<Integer> ports, SSLContext sslContext) {
super(ports);
this.sslContext = sslContext;
}
/**
*
* @param sslContext The SSL context to be used by the server
* @see TCPServer#TCPServer(String, Collection, int, Consumer, long)
*/
public TLSServer(String bindAddress, Collection<Integer> ports, int backlog, Consumer<Runnable> worker, long idleTimeout, SSLContext sslContext) {
super(bindAddress, ports, backlog, worker, idleTimeout);
this.sslContext = sslContext;
}
/**
* Sets the list of supported application layer protocol names to be negotiated using TLS ALPN (Application Layer Protocol Negotiation). The elements in this list should
* be ordered from most-preferred to least-preferred protocol name.<br>
* <br>
* If not set or <code>null</code> is passed, the first protocol name presented by the client is selected. If the client does not request ALPN, this list is ignored.
*
* @param supportedApplicationLayerProtocols The list of supported protocol names
*/
public void setSupportedApplicationLayerProtocols(String[] supportedApplicationLayerProtocols) {
this.supportedApplicationLayerProtocols = supportedApplicationLayerProtocols;
}
/**
*
* @return The list of configured supported application layer protocol names, or <code>null</code> of none were configured
*/
public String[] getSupportedApplicationLayerProtocols() {
return supportedApplicationLayerProtocols;
}
@Override
protected InetConnection handleConnection(SocketChannel socketChannel) throws IOException {
InetConnection conn = new TLSConnection(socketChannel, this.sslContext, false, (r) -> {
TLSServer.super.worker.accept(() -> {
r.run();
});
}, this.supportedApplicationLayerProtocols);
// see note in PlainTCPServer
conn.setOnError((e) -> {
// if it is a SSLHandshakeException, no need to be verbose because it isn't a fatal error (may just be caused because a client
// is bad or doesn't accept a certificate)
if(e instanceof SSLHandshakeException)
logger.warn("TLS handshake failed (remote address=", conn.getRemoteAddress(), "): ", e.toString());
else if(e instanceof SSLException) // same applies to SSLException, for example when a client sends a malformed SSL packet, a SSLException is thrown
logger.warn("TLS error (remote address=", conn.getRemoteAddress(), "): ", e.toString());
else if(e instanceof IOException)
logger.warn("Socket Error (remote address=", conn.getRemoteAddress(), "): ", e.toString());
else
logger.error("Error in connection (remote address=", conn.getRemoteAddress(), "): ", e);
});
return conn;
}
}

@ -0,0 +1,262 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.socket;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Represents any type of stateful connection based on the Internet Protocol.
*/
public abstract class InetConnection {
private Runnable onConnect;
private Runnable onTimeout;
private Consumer<byte[]> onData;
private Runnable onClose;
private Consumer<Throwable> onError;
private Consumer<InetConnection> onLocalClose;
private InetSocketAddress apparentRemoteAddress;
private Object attachment;
private List<byte[]> writeQueue = new ArrayList<>();
private boolean closed = false;
/**
* Connects this <code>InetConnection</code> to the previously specified remote address in the constructor. If no address was specified, this method will throw an
* <code>UnsupportedOperationException</code><br>
* <br>
* This function is non-blocking.<br>
* <br>
* A connection timeout in milliseconds may be specified in the <b>timeout</b> parameter. If the connection has not been established within this timeout, the handler set
* using {@link #setOnTimeout(Runnable)} is called and the connection is closed. Depending on the implementation, a timeout may occur earlier and may instead cause the
* <code>onError</code> callback to be called.
*
* @param timeout The connection timeout in milliseconds. Disabled if 0
*/
public abstract void connect(int timeout);
/**
* Reads data received from the peer host on this connection. <br>
* <br>
* This function is non-blocking. If no data was available, <code>null</code> is returned.
*
* @return The read data or <code>null</code> if no data is available.
*/
public abstract byte[] read();
/**
* Writes data to this connection for delivery to the peer host. <br>
* <br>
* This function returns after all data has been written to the write buffer.
*
* @param a The data to be written to this connection
*/
public abstract void write(byte[] a);
/**
* Closes this connection.
*/
public abstract void close();
/**
*
* @return <code>true</code> if this socket is connected.
*/
public abstract boolean isConnected();
/**
*
* @return The address of the peer host
*/
public abstract InetSocketAddress getRemoteAddress();
/**
*
* @return The local address of this connection
*/
public abstract InetSocketAddress getLocalAddress();
/**
*
* @return The last time any data was sent over this connection, either incoming or outgoing, as returned by {@link System#currentTimeMillis()}
*/
public abstract long getLastIOTime();
/**
* Sets a possibly different remote address a client claims to be or act on behalf of.<br>
* <br>
* For example, if a connection received by a server was proxied through a proxy, this should be set to the actual client address.
*
* @param apparentRemoteAddress The apparent address of the peer
*/
public final void setApparentRemoteAddress(InetSocketAddress apparentRemoteAddress) {
this.apparentRemoteAddress = apparentRemoteAddress;
}
/**
*
* @return The apparent remote address previously set by {@link InetConnection#setApparentRemoteAddress(InetSocketAddress)}, or the address returned by
* {@link InetConnection#getRemoteAddress()} if none was set
*/
public final InetSocketAddress getApparentRemoteAddress() {
if(this.apparentRemoteAddress != null)
return this.apparentRemoteAddress;
else
return this.getRemoteAddress();
}
public final void handleConnect() {
try{
if(this.onConnect != null)
this.onConnect.run();
this.flushWriteQueue();
}catch(Exception e){
this.handleError(e);
}
}
public final void handleTimeout() {
try{
if(this.onTimeout != null)
this.onTimeout.run();
}catch(Exception e){
this.handleError(e);
}
}
/**
*
* @param data The data that was received on this connection
* @return <code>false</code> if no <code>onData</code> handler was set
*/
public final boolean handleData(byte[] data) {
if(this.onData == null)
return false;
try{
this.onData.accept(data);
}catch(Exception e){
this.handleError(e);
}
return true;
}
public final void handleError(Throwable e) {
if(this.onError != null){
this.onError.accept(e);
this.close();
}else
throw new RuntimeException("Socket Error", e);
}
public final void handleClose() {
if(this.closed)
return;
this.closed = true;
try{
if(this.onClose != null)
this.onClose.run();
}catch(Exception e){
this.handleError(e);
}
}
/**
* Sets a callback that is called when this socket is connected and ready to receive or send data.
*
* @param onConnect The callback
*/
public final void setOnConnect(Runnable onConnect) {
this.onConnect = onConnect;
}
/**
* Sets a callback that is called when the connect operation started using {@link #connect(int)} times out.
*
* @param onTimeout The callback
*/
public final void setOnTimeout(Runnable onTimeout) {
this.onTimeout = onTimeout;
}
/**
* Sets a callback that is called when data is received on this connection.
*
* @param onData The callback
*/
public final void setOnData(Consumer<byte[]> onData) {
this.onData = onData;
}
/**
* Sets a callback that is called when this connection closes and can no longer receive or send data.
*
* @param onClose The callback
*/
public final void setOnClose(Runnable onClose) {
this.onClose = onClose;
}
/**
* Sets a callback that is called when an error occurs on this connection.<br>
* <br>
* This callback is usually followed by a <code>onClose</code> (set using {@link InetConnection#setOnClose(Runnable)}) callback.
*
* @param onError The callback
*/
public final void setOnError(Consumer<Throwable> onError) {
this.onError = onError;
}
public void setOnLocalClose(Consumer<InetConnection> onLocalClose) {
if(this.onLocalClose != null)
throw new IllegalStateException("onLocalClose is already set");
this.onLocalClose = onLocalClose;
}
protected final void localClose() {
if(this.onLocalClose != null)
this.onLocalClose.accept(this);
}
protected final synchronized void queueWrite(byte[] data) {
if(this.writeQueue != null)
this.writeQueue.add(data);
else
throw new IllegalStateException("Tried to queue write after connection finished");
}
private synchronized void flushWriteQueue() {
for(byte[] d : this.writeQueue){
this.write(d);
}
}
public final Object getAttachment() {
return attachment;
}
public final void setAttachment(Object attachment) {
this.attachment = attachment;
}
}

@ -0,0 +1,145 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.socket;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.SocketChannel;
import org.omegazero.common.event.Tasks;
/**
* Represents an {@link InetConnection} based on a java.nio {@link SocketChannel}.
*/
public abstract class InetSocketConnection extends InetConnection {
private final SocketChannel socket;
private final InetSocketAddress remoteAddress;
private final InetSocketAddress localAddress;
private long lastIOTime;
protected ByteBuffer readBuf;
protected ByteBuffer writeBuf;
private long connectTimeout = -1;
public InetSocketConnection(SocketChannel socket) throws IOException {
this(socket, null);
}
public InetSocketConnection(SocketChannel socket, InetSocketAddress remote) throws IOException {
this.socket = socket;
InetSocketAddress socketRemote = (InetSocketAddress) this.socket.getRemoteAddress();
if(socketRemote != null && remote != null)
throw new AlreadyConnectedException();
else if(socketRemote != null)
this.remoteAddress = socketRemote;
else
this.remoteAddress = remote;
this.localAddress = (InetSocketAddress) this.socket.getLocalAddress();
this.lastIOTime = System.currentTimeMillis();
this.ensureNonBlocking();
}
protected abstract void createBuffers();
@Override
public void connect(int timeout) {
try{
this.ensureNonBlocking();
if(this.remoteAddress == null)
throw new UnsupportedOperationException("Cannot connect because no remote address was specified");
this.socket.connect(this.remoteAddress);
if(timeout > 0)
this.connectTimeout = Tasks.timeout((args) -> {
if(!InetSocketConnection.this.isConnected()){ // definitely before this ever connected because this handler gets canceled if socket closes (or errors)
InetSocketConnection.this.handleTimeout();
InetSocketConnection.this.close();
}
}, timeout).getId();
}catch(Exception e){
super.handleError(e);
}
}
@Override
public void close() {
super.localClose();
try{
if(this.connectTimeout >= 0)
Tasks.clear(this.connectTimeout);
this.socket.close();
}catch(IOException e){
throw new RuntimeException("Error while closing channel", e);
}
}
/**
*
* @see SocketChannel#isConnected()
*/
@Override
public final boolean isConnected() {
return this.socket.isConnected();
}
@Override
public InetSocketAddress getRemoteAddress() {
return this.remoteAddress;
}
@Override
public InetSocketAddress getLocalAddress() {
return this.localAddress;
}
@Override
public long getLastIOTime() {
return this.lastIOTime;
}
private void ensureNonBlocking() throws IOException {
if(this.socket.isBlocking())
this.socket.configureBlocking(false);
}
protected final int readFromSocket() throws IOException {
this.lastIOTime = System.currentTimeMillis();
return this.socket.read(this.readBuf);
}
protected final int writeToSocket() throws IOException {
this.lastIOTime = System.currentTimeMillis();
int written = 0;
while(this.writeBuf.hasRemaining()){
int w = this.socket.write(this.writeBuf);
written += w;
if(w == 0)
try{
Thread.sleep(1);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
return written;
}
}

@ -0,0 +1,85 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.socket.impl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import org.omegazero.net.socket.InetSocketConnection;
public class PlainConnection extends InetSocketConnection {
public PlainConnection(SocketChannel socket) throws IOException {
this(socket, null);
}
public PlainConnection(SocketChannel socket, InetSocketAddress remote) throws IOException {
super(socket, remote);
this.createBuffers();
}
@Override
protected void createBuffers() {
super.readBuf = ByteBuffer.allocate(8192);
super.writeBuf = ByteBuffer.allocate(8192);
}
@Override
public synchronized byte[] read() {
try{
if(!super.isConnected())
return null;
super.readBuf.clear();
if(super.readFromSocket() >= 0){
super.readBuf.flip();
if(super.readBuf.hasRemaining()){
byte[] a = new byte[super.readBuf.remaining()];
super.readBuf.get(a);
super.readBuf.compact();
return a;
}else
return null;
}
}catch(Throwable e){
super.handleError(e);
}
// error or read returned below 0 (eof)
super.close();
return null;
}
@Override
public synchronized void write(byte[] a) {
try{
if(!super.isConnected()){
super.queueWrite(a);
return;
}
super.writeBuf.clear();
int written = 0;
while(written < a.length){
int wr = Math.min(super.writeBuf.remaining(), a.length - written);
super.writeBuf.put(a, written, wr);
super.writeBuf.flip();
super.writeToSocket();
super.writeBuf.compact();
written += wr;
}
}catch(Throwable e){
super.handleError(e);
}
}
}

@ -0,0 +1,245 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.socket.impl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import org.omegazero.net.socket.InetSocketConnection;
public class TLSConnection extends InetSocketConnection {
private final SSLContext sslContext;
private final Consumer<Runnable> taskRunner;
private final String[] alpnNames;
private final SSLEngine sslEngine;
protected ByteBuffer readBufUnwrapped;
protected ByteBuffer writeBufUnwrapped;
private boolean handshakeComplete = false;
private String alpnProtocol = null;
public TLSConnection(SocketChannel socket, SSLContext sslContext, boolean client, Consumer<Runnable> taskRunner, String[] alpnNames) throws IOException {
this(socket, null, sslContext, client, taskRunner, alpnNames, null);
}
public TLSConnection(SocketChannel socket, InetSocketAddress remote, SSLContext sslContext, boolean client, Consumer<Runnable> taskRunner, String[] alpnNames,
String[] requestedServerNames) throws IOException {
super(socket, remote);
this.sslContext = sslContext;
this.taskRunner = taskRunner;
this.alpnNames = alpnNames;
this.sslEngine = this.sslContext.createSSLEngine();
this.sslEngine.setUseClientMode(client);
this.sslEngine.setHandshakeApplicationProtocolSelector((sslEngine, list) -> {
if(TLSConnection.this.alpnNames != null){
for(String s : TLSConnection.this.alpnNames){
if(list.contains(s))
return s;
}
}else if(list.size() > 0)
return list.get(0);
return null;
});
// GCM ciphers are broken or something (or im doing something wrong which is far more likely)
// if data is received immediately after the handshake completed, the next read call will cause a "AEADBadTagException: Tag mismatch!"
String[] ciphersList = this.sslEngine.getEnabledCipherSuites();
List<String> ciphers = new ArrayList<>(ciphersList.length);
for(String s : ciphersList){
if(!s.contains("GCM"))
ciphers.add(s);
}
this.sslEngine.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]));
if(requestedServerNames != null){
SNIServerName[] serverNames = new SNIServerName[requestedServerNames.length];
for(int i = 0; i < requestedServerNames.length; i++){
serverNames[i] = new SNIHostName(requestedServerNames[i]);
}
this.sslEngine.getSSLParameters().setServerNames(Arrays.asList(serverNames));
}
this.createBuffers();
}
@Override
protected void createBuffers() {
super.readBuf = ByteBuffer.allocate(this.sslEngine.getSession().getPacketBufferSize());
super.writeBuf = ByteBuffer.allocate(this.sslEngine.getSession().getPacketBufferSize());
this.readBufUnwrapped = ByteBuffer.allocate(this.sslEngine.getSession().getApplicationBufferSize());
this.writeBufUnwrapped = ByteBuffer.allocate(this.sslEngine.getSession().getApplicationBufferSize());
}
@Override
public synchronized byte[] read() {
try{
if(!super.isConnected())
return null;
super.readBuf.clear();
if(handshakeComplete){
return this.readApplicationData();
}else{
this.doTLSHandshake();
}
}catch(Throwable e){
super.handleError(e);
}
return null;
}
@Override
public synchronized void write(byte[] a) {
try{
if(!this.handshakeComplete){
super.queueWrite(a);
return;
}
this.writeBufUnwrapped.clear();
int written = 0;
while(written < a.length){
int wr = Math.min(this.writeBufUnwrapped.remaining(), a.length - written);
this.writeBufUnwrapped.put(a, written, wr);
this.writeBufUnwrapped.flip();
while(this.writeBufUnwrapped.hasRemaining()){
super.writeBuf.clear();
SSLEngineResult result = this.sslEngine.wrap(this.writeBufUnwrapped, super.writeBuf);
if(result.getStatus() == Status.OK){
super.writeBuf.flip();
super.writeToSocket();
}else
throw new SSLException("Write SSL wrap failed: " + result.getStatus());
}
this.writeBufUnwrapped.compact();
written += wr;
}
}catch(Throwable e){
super.handleError(e);
}
}
@Override
public synchronized void close() {
this.sslEngine.closeOutbound();
super.close();
}
/**
*
* @return The negotiated ALPN protocol name, or <code>null</code> if ALPN did not occur
*/
public String getAlpnProtocol() {
// an empty string means that no ALPN happened (as specified by SSLEngine.getApplicationProtocol())
if(alpnProtocol == null || alpnProtocol.length() < 1)
return null;
else
return alpnProtocol;
}
private byte[] readApplicationData() throws IOException {
int read = super.readFromSocket();
if(read > 0){
super.readBuf.flip();
SSLEngineResult result = this.sslEngine.unwrap(super.readBuf, this.readBufUnwrapped);
if(result.getStatus() == Status.CLOSED)
this.close();
else if(result.getStatus() == Status.OK){
this.readBufUnwrapped.flip();
if(this.readBufUnwrapped.hasRemaining()){
byte[] a = new byte[this.readBufUnwrapped.remaining()];
this.readBufUnwrapped.get(a);
this.readBufUnwrapped.compact();
return a;
}
}else
throw new SSLException("Read SSL unwrap failed: " + result.getStatus());
}else if(read < 0){
this.close();
}
return null;
}
public void doTLSHandshake() throws IOException {
if(this.handshakeComplete)
throw new IllegalStateException("Handshake is already completed");
HandshakeStatus status = sslEngine.getHandshakeStatus();
if(status == HandshakeStatus.NOT_HANDSHAKING){
sslEngine.beginHandshake();
status = sslEngine.getHandshakeStatus();
}
while(true){
if(status == HandshakeStatus.NEED_UNWRAP){
int read = super.readFromSocket();
if(read < 0)
throw new SSLHandshakeException("Socket disconnected before handshake completed");
super.readBuf.flip();
SSLEngineResult result = this.sslEngine.unwrap(super.readBuf, this.readBufUnwrapped);
super.readBuf.compact();
// BUFFER_UNDERFLOW here means no data is available; for some reason we can't just immediately return when we read 0 bytes above,
// because then SSLEngine will break; instead, cause buffer underflow and loop around to return here
if(read == 0 && result.getStatus() == Status.BUFFER_UNDERFLOW)
return;
else if(result.getStatus() != Status.OK)
throw new SSLHandshakeException("Unexpected status after SSL unwrap: " + result.getStatus());
status = sslEngine.getHandshakeStatus();
}else if(status == HandshakeStatus.NEED_WRAP){
super.writeBuf.clear();
SSLEngineResult result = this.sslEngine.wrap(this.writeBufUnwrapped, super.writeBuf);
if(result.getStatus() == Status.OK){
super.writeBuf.flip();
super.writeToSocket();
}else
throw new SSLHandshakeException("Unexpected status after SSL wrap: " + result.getStatus());
status = sslEngine.getHandshakeStatus();
}else if(status == HandshakeStatus.NEED_TASK){
Runnable r;
while((r = this.sslEngine.getDelegatedTask()) != null){
this.taskRunner.accept(r);
}
status = sslEngine.getHandshakeStatus();
}else if(status == HandshakeStatus.FINISHED || status == HandshakeStatus.NOT_HANDSHAKING){
this.handshakeComplete = true;
this.alpnProtocol = this.sslEngine.getApplicationProtocol();
super.handleConnect();
return;
}else
throw new SSLHandshakeException("Unexpected engine status: " + status);
}
}
}

@ -0,0 +1,125 @@
/*
* Copyright (C) 2021 omegazero.org
*
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is" basis, without warranty of any kind,
* either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or non-infringing.
* The entire risk as to the quality and performance of the Covered Software is with You.
*/
package org.omegazero.net.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Scanner;
import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocketFactory;
import com.mendix.ssltools.PrivateKeyReader;
public final class SSLUtil {
private SSLUtil() {
}
public static ServerSocketFactory getServerSocketFactory() {
return ServerSocketFactory.getDefault();
}
public static SSLContext getSSLContextFromKeyStore(String filename, String password, String keypassword) throws GeneralSecurityException, IOException {
if(filename == null)
throw new IllegalArgumentException("Tried to create a secure server socket factory but filename is null");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(Files.newInputStream(Paths.get(filename)), password.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keypassword.toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
return sslContext;
}
public static SSLContext getSSLContextFromPEM(String keyFile, String certFile) throws GeneralSecurityException, IOException {
if(keyFile == null || certFile == null)
throw new IllegalArgumentException("Tried to create a secure server socket factory but a filename is null");
PrivateKey key = SSLUtil.loadPrivateKeyFromPEM(keyFile);
X509Certificate cert = SSLUtil.loadCertificateFromPEM(certFile);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("certificate", cert);
keyStore.setKeyEntry("private-key", key, "password".toCharArray(), new Certificate[] { cert });
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "password".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
return sslContext;
}
public static SSLServerSocketFactory getSecureServerSocketFactory(String keyFile, String certFile) throws GeneralSecurityException, IOException {
return getSSLContextFromPEM(keyFile, certFile).getServerSocketFactory();
}
public static SSLEngine getSSLEngineWithPEM(String keyFile, String certFile) throws GeneralSecurityException, IOException {
return getSSLContextFromPEM(keyFile, certFile).createSSLEngine();
}
public static byte[] readCertificatePEM(String data) {
StringBuilder sb = new StringBuilder();
Scanner scanner = new Scanner(data);
byte[] rdata = null;
while(scanner.hasNextLine()){
String line = scanner.nextLine();
if(line.contains("-----BEGIN CERTIFICATE-----")){
continue;
}else if(line.contains("-----END CERTIFICATE-----")){
rdata = Base64.getDecoder().decode(sb.toString());
break;
}
sb.append(line.trim());
}
scanner.close();
if(rdata == null)
throw new RuntimeException("Invalid PEM certificate format");
return rdata;
}
public static PrivateKey loadPrivateKeyFromPEM(String keyFile) throws GeneralSecurityException, IOException {
String keyFileData = new String(Files.readAllBytes(Paths.get(keyFile)));
PrivateKey key = PrivateKeyReader.read(keyFileData);
return key;
}
public static X509Certificate loadCertificateFromPEM(String certFile) throws GeneralSecurityException, IOException {
String certFileData = new String(Files.readAllBytes(Paths.get(certFile)));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream tlsStream = new ByteArrayInputStream(SSLUtil.readCertificatePEM(certFileData));
X509Certificate cert = (X509Certificate) cf.generateCertificate(tlsStream);
return cert;
}
}