JavaScript promise rejection: Failed to fetch. Open browser console to see more details.

Initial commit

This commit is contained in:
user94729 2021-06-09 21:02:00 +02:00
commit 53a826d340
Signed by: warp03
GPG Key ID: B6D2AC20BD3262DA
22 changed files with 3536 additions and 0 deletions

24
.gitignore vendored Normal file

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

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-proxy3

@ -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.proxy.config;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class ConfigArray implements Serializable, Iterable<Object> {
private static final long serialVersionUID = 1L;
protected final ArrayList<Object> data;
protected ConfigArray() {
this.data = new ArrayList<>();
}
protected ConfigArray(int initialCapacity) {
this.data = new ArrayList<>(initialCapacity);
}
public int size() {
return this.data.size();
}
public boolean isEmpty() {
return this.data.isEmpty();
}
public boolean contains(Object o) {
return this.data.contains(o);
}
public Object[] toArray() {
return this.data.toArray();
}
public boolean containsAll(Collection<?> c) {
return this.data.containsAll(c);
}
public Object get(int index) {
return this.data.get(index);
}
public int indexOf(Object o) {
return this.data.indexOf(o);
}
public int lastIndexOf(Object o) {
return this.data.lastIndexOf(o);
}
@Override
public Iterator<Object> iterator() {
return new Iterator<Object>(){
private int index = 0;
@Override
public boolean hasNext() {
return this.index < ConfigArray.this.size();
}
@Override
public Object next() {
return ConfigArray.this.get(this.index++);
}
};
}
}

@ -0,0 +1,203 @@
/*
* 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.proxy.config;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
public class ConfigObject implements Serializable {
private static final long serialVersionUID = 1L;
protected final HashMap<String, Object> data = new HashMap<>();
protected ConfigObject() {
}
public int size() {
return this.data.size();
}
public boolean isEmpty() {
return this.data.isEmpty();
}
public boolean containsKey(Object key) {
return this.data.containsKey(key);
}
public boolean containsValue(Object value) {
return this.data.containsValue(value);
}
public Set<String> keySet() {
return Collections.unmodifiableSet(this.data.keySet());
}
public Collection<Object> values() {
return Collections.unmodifiableCollection(this.data.values());
}
public Object get(String key) {
return this.data.get(key);
}
public ConfigObject getObject(String key) {
Object v = this.get(key);
if(v instanceof ConfigObject)
return (ConfigObject) v;
else
throw new IllegalArgumentException("Expected object for '" + key + "' but received type " + getTypeName(v));
}
public ConfigArray getArray(String key) {
Object v = this.get(key);
if(v instanceof ConfigArray)
return (ConfigArray) v;
else
throw new IllegalArgumentException("Expected array for '" + key + "' but received type " + getTypeName(v));
}
public String getString(String key) {
Object v = this.get(key);
if(v instanceof String)
return (String) v;
else
throw new IllegalArgumentException("Expected string for '" + key + "' but received type " + getTypeName(v));
}
public int getInt(String key) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).intValue();
else
throw new IllegalArgumentException("Expected integer for '" + key + "' but received type " + getTypeName(v));
}
public long getLong(String key) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).longValue();
else
throw new IllegalArgumentException("Expected integer for '" + key + "' but received type " + getTypeName(v));
}
public float getFloat(String key) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).floatValue();
else
throw new IllegalArgumentException("Expected floating point value for '" + key + "' but received type " + getTypeName(v));
}
public double getDouble(String key) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).doubleValue();
else
throw new IllegalArgumentException("Expected floating point value for '" + key + "' but received type " + getTypeName(v));
}
public boolean getBoolean(String key) {
Object v = this.get(key);
if(v instanceof Boolean)
return (boolean) v;
else
throw new IllegalArgumentException("Expected boolean for '" + key + "' but received type " + getTypeName(v));
}
public ConfigObject optObject(String key) {
Object v = this.get(key);
if(v instanceof ConfigObject)
return (ConfigObject) v;
else
return null;
}
public ConfigArray optArray(String key) {
Object v = this.get(key);
if(v instanceof ConfigArray)
return (ConfigArray) v;
else
return null;
}
public String optString(String key, String def) {
Object v = this.get(key);
if(v instanceof String)
return (String) v;
else
return def;
}
public int optInt(String key, int def) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).intValue();
else
return def;
}
public long optLong(String key, long def) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).longValue();
else
return def;
}
public float optFloat(String key, float def) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).floatValue();
else
return def;
}
public double optDouble(String key, double def) {
Object v = this.get(key);
if(v instanceof Number)
return ((Number) v).doubleValue();
else
return def;
}
public boolean optBoolean(String key, boolean def) {
Object v = this.get(key);
if(v instanceof Boolean)
return (boolean) v;
else
return def;
}
/*private <T> T getValueOfType(Class<T> type, String key) {
Object v = this.get(key);
if(v != null && type.isAssignableFrom(v.getClass()))
return type.cast(v);
else
return null;
}*/
private static String getTypeName(Object obj) {
return obj == null ? "null" : obj.getClass().getName();
}
}

@ -0,0 +1,254 @@
/*
* 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.proxy.config;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.json.JSONArray;
import org.json.JSONObject;
import org.omegazero.common.config.ConfigurationOption;
import org.omegazero.common.config.JSONConfiguration;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.util.SSLUtil;
public class ProxyConfiguration extends JSONConfiguration {
private static final Logger logger = LoggerUtil.createLogger();
public static final String TLS_AUTH_DEFAULT_NAME = "default";
@ConfigurationOption(description = "The local address the proxy server should bind to")
private String bindAddress = null;
@ConfigurationOption()
private int backlog = 1024;
@ConfigurationOption(description = "Plaintext ports the proxy server should listen on")
private List<Integer> portsPlain = new ArrayList<>();
@ConfigurationOption(description = "TLS ports the proxy server should listen on")
private List<Integer> portsTls = new ArrayList<>();
@ConfigurationOption(description = "TLS key and certificate file names for different server names")
private final Map<String, Entry<String, String>> tlsAuth = new HashMap<>();
private final Map<String, Entry<PrivateKey, X509Certificate[]>> tlsAuthData = new HashMap<>();
@ConfigurationOption(description = "The period in seconds for reloading TLS key and certificate data. Disabled if 0")
private int tlsAuthReloadInterval = 0;
@ConfigurationOption(description = "The amount of time in seconds a connection with no traffic should persist before it is closed")
private int connectionIdleTimeout = 300;
@ConfigurationOption(description = "The default error documents to use to signal an error. The key is the content type, the value is the file path")
private final Map<String, String> errdocFiles = new HashMap<>();
@ConfigurationOption(description = "The address of the default upstream server")
private String upstreamServerAddress = "localhost";
@ConfigurationOption(description = "The plaintext port of the upstream server")
private int upstreamServerPortPlain = 80;
@ConfigurationOption(description = "The TLS port of the upstream server")
private int upstreamServerPortTLS = 443;
@ConfigurationOption(description = "The maximum time in seconds to wait until a connection to an upstream server is established before reporting an error")
private int upstreamConnectionTimeout = 5;
@ConfigurationOption(description = "If the proxy should add additional HTTP headers to proxied HTTP messages (for example 'Via')")
private boolean enableHeaders = true;
@ConfigurationOption(description = "List of X509 certificate file names to trust in addition to the default installed certificates")
private List<String> trustedCertificates = new ArrayList<>();
@ConfigurationOption
private Map<String, ConfigObject> pluginConfig = new HashMap<>();
public ProxyConfiguration(byte[] fileData) {
super(fileData);
}
public ProxyConfiguration(String fileData) {
super(fileData);
}
private void loadConfigurationTLSAuth(JSONObject obj) {
JSONObject tlsEntry = (JSONObject) obj;
String servername;
if(tlsEntry.has("servername"))
servername = tlsEntry.getString("servername");
else
servername = TLS_AUTH_DEFAULT_NAME;
if(!tlsEntry.has("key"))
throw new IllegalArgumentException("Value in 'tlsAuth' is missing required argument 'key'");
if(!tlsEntry.has("cert"))
throw new IllegalArgumentException("Value in 'tlsAuth' is missing required argument 'cert'");
this.tlsAuth.put(servername, new SimpleEntry<>(tlsEntry.getString("key"), tlsEntry.getString("cert")));
}
public void reloadTLSAuthData() throws GeneralSecurityException, IOException {
synchronized(this.tlsAuthData){
logger.trace("Reloading TLS auth data");
this.tlsAuthData.clear(); // remove any possible temporary entries in the map
for(Entry<String, Entry<String, String>> entry : this.tlsAuth.entrySet()){
this.tlsAuthData.put(entry.getKey(),
new SimpleEntry<>(SSLUtil.loadPrivateKeyFromPEM(entry.getValue().getKey()), SSLUtil.loadCertificatesFromPEM(entry.getValue().getValue())));
}
}
}
@Override
protected boolean setUnsupportedField(Field field, Object jsonObject) {
if(field.getName().equals("tlsAuth")){
if(jsonObject instanceof JSONArray){
((JSONArray) jsonObject).forEach((obj) -> {
if(obj instanceof JSONObject){
this.loadConfigurationTLSAuth((JSONObject) obj);
}else
throw new IllegalArgumentException("Values in 'tlsAuth' must be objects");
});
}else if(jsonObject instanceof JSONObject){
this.loadConfigurationTLSAuth((JSONObject) jsonObject);
}else
throw new IllegalArgumentException("'tlsAuth' must be either an array or object");
try{
this.reloadTLSAuthData();
}catch(GeneralSecurityException | IOException e){
throw new RuntimeException("Failed to load TLS auth data: ", e);
}
}else if(field.getName().equals("errdocFiles")){
if(jsonObject instanceof JSONObject){
JSONObject j = ((JSONObject) jsonObject);
for(String k : j.keySet()){
this.errdocFiles.put(k, j.getString(k));
}
}else
throw new IllegalArgumentException("'errdocFiles' must be an object");
}else if(field.getName().equals("pluginConfig")){
if(jsonObject instanceof JSONObject){
JSONObject j = ((JSONObject) jsonObject);
for(String k : j.keySet()){
this.pluginConfig.put(k, ProxyConfiguration.convertJSONObject(j.getJSONObject(k)));
}
}else
throw new IllegalArgumentException("'pluginConfig' must be an object");
}else
return false;
return true;
}
public void validateConfig() {
if(this.portsTls.size() > 0 && this.tlsAuthData.isEmpty()){
logger.warn("TLS ports were configured but no valid TLS data (key/certificate) was provided");
}
}
public String getBindAddress() {
return this.bindAddress;
}
public int getBacklog() {
return this.backlog;
}
public List<Integer> getPortsPlain() {
return this.portsPlain;
}
public List<Integer> getPortsTls() {
return this.portsTls;
}
public Map<String, Entry<PrivateKey, X509Certificate[]>> getTlsAuthData() {
return this.tlsAuthData;
}
public int getTlsAuthReloadInterval() {
return this.tlsAuthReloadInterval;
}
public int getConnectionIdleTimeout() {
return this.connectionIdleTimeout;
}
public Map<String, String> getErrdocFiles() {
return this.errdocFiles;
}
public String getUpstreamServerAddress() {
return this.upstreamServerAddress;
}
public int getUpstreamServerPortPlain() {
return this.upstreamServerPortPlain;
}
public int getUpstreamServerPortTLS() {
return this.upstreamServerPortTLS;
}
public int getUpstreamConnectionTimeout() {
return this.upstreamConnectionTimeout;
}
public boolean isEnableHeaders() {
return this.enableHeaders;
}
public List<String> getTrustedCertificates() {
return this.trustedCertificates;
}
public ConfigObject getPluginConfigFor(String key) {
ConfigObject o = this.pluginConfig.get(key);
if(o != null)
return o;
else
return new ConfigObject();
}
private static ConfigObject convertJSONObject(JSONObject json) {
ConfigObject obj = new ConfigObject();
for(String k : json.keySet()){
obj.data.put(k, ProxyConfiguration.convertJSONValue(json.get(k)));
}
return obj;
}
private static Object convertJSONValue(Object v) {
if(v instanceof JSONObject){
return ProxyConfiguration.convertJSONObject((JSONObject) v);
}else if(v instanceof JSONArray){
JSONArray arr = ((JSONArray) v);
ConfigArray carr = new ConfigArray(arr.length());
Iterator<Object> data = arr.iterator();
while(data.hasNext())
carr.data.add(ProxyConfiguration.convertJSONValue(data.next()));
return carr;
}else
return v;
}
}

@ -0,0 +1,71 @@
/*
* 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.proxy.core;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.function.Consumer;
import org.omegazero.common.eventbus.Event;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.net.client.PlainTCPClientManager;
import org.omegazero.net.client.TLSClientManager;
import org.omegazero.net.server.PlainTCPServer;
import org.omegazero.net.server.TLSServer;
import org.omegazero.net.socket.impl.PlainConnection;
import org.omegazero.net.socket.impl.TLSConnection;
import org.omegazero.net.util.TrustManagerUtil;
import org.omegazero.proxy.config.ProxyConfiguration;
import org.omegazero.proxy.http.engineimpl.HTTP1;
class Defaults {
private static final Logger logger = LoggerUtil.createLogger();
protected static void registerProxyDefaults(Proxy proxy) {
ProxyConfiguration config = proxy.getConfig();
Consumer<Runnable> serverTaskHandler = null; // change to proxy worker when synchronization issues are resolved (is that ever going to happen?)
if(config.getPortsPlain().size() > 0)
proxy.registerServerInstance(
new PlainTCPServer(config.getBindAddress(), config.getPortsPlain(), config.getBacklog(), serverTaskHandler, proxy.getConnectionIdleTimeout()));
if(config.getPortsTls().size() > 0){
List<Object> alpnNames = proxy.dispatchEventRes(new Event("_proxyRegisterALPNOption", false, new Class<?>[0], String.class, true)).getReturnValues();
alpnNames.add("http/1.1");
logger.debug("Registered TLS ALPN options: ", alpnNames);
TLSServer tlsServer = new TLSServer(config.getBindAddress(), config.getPortsTls(), config.getBacklog(), serverTaskHandler, proxy.getConnectionIdleTimeout(),
proxy.getSslContext());
tlsServer.setSupportedApplicationLayerProtocols(alpnNames.toArray(new String[alpnNames.size()]));
proxy.registerServerInstance(tlsServer);
}
proxy.registerClientManager(new PlainTCPClientManager(serverTaskHandler));
try{
proxy.registerClientManager(
new TLSClientManager(serverTaskHandler, TrustManagerUtil.getTrustManagersWithAdditionalCertificateFiles(config.getTrustedCertificates())));
}catch(GeneralSecurityException | IOException e){
throw new RuntimeException("Error while loading trusted certificates", e);
}
proxy.addHTTPEngineSelector((connection) -> {
if(connection instanceof PlainConnection)
return HTTP1.class;
else if(connection instanceof TLSConnection){
String alpnProtocolName = ((TLSConnection) connection).getAlpnProtocol();
if(alpnProtocolName == null || alpnProtocolName.equals("http/1.1"))
return HTTP1.class;
}
return null;
});
}
}

@ -0,0 +1,728 @@
/*
* 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.proxy.core;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import org.omegazero.common.event.EventQueueExecutor;
import org.omegazero.common.event.Tasks;
import org.omegazero.common.eventbus.Event;
import org.omegazero.common.eventbus.EventBus;
import org.omegazero.common.eventbus.EventResult;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.common.plugins.Plugin;
import org.omegazero.common.plugins.PluginManager;
import org.omegazero.common.util.Args;
import org.omegazero.net.client.NetClientManager;
import org.omegazero.net.client.params.ConnectionParameters;
import org.omegazero.net.common.NetworkApplication;
import org.omegazero.net.server.NetServer;
import org.omegazero.net.socket.SocketConnection;
import org.omegazero.proxy.config.ConfigObject;
import org.omegazero.proxy.config.ProxyConfiguration;
import org.omegazero.proxy.http.HTTPEngine;
import org.omegazero.proxy.http.HTTPErrdoc;
import org.omegazero.proxy.net.UpstreamServer;
public final class Proxy {
private static final Logger logger = LoggerUtil.createLogger();
public static final String VERSION = "3.1.0";
private static final String DEFAULT_ERRDOC_LOCATION = "/org/omegazero/proxy/resources/errdoc.html";
private static Proxy instance;
private State state = State.NEW;
private ProxyConfiguration config;
private String instanceType = "proxy";
private String instanceVersion = Proxy.VERSION;
private String instanceNameAppendage;
private String instanceName;
private PluginManager pluginManager;
private EventBus proxyEventBus;
private ProxyKeyManager keyManager;
private SSLContext sslContext;
private EventQueueExecutor serverWorker;
private ApplicationWorkerProvider serverWorkerProvider;
private final List<Function<SocketConnection, Class<? extends HTTPEngine>>> httpEngineSelectors = new ArrayList<>();
private final Map<String, HTTPErrdoc> errdocs = new HashMap<>();
private HTTPErrdoc errdocDefault;
private UpstreamServer defaultUpstreamServer;
private List<NetServer> serverInstances = new ArrayList<>();
private Map<Class<? extends NetClientManager>, NetClientManager> clientManagers = new HashMap<>();
public Proxy() {
if(instance != null)
throw new IllegalStateException("An instance of " + this.getClass().getName() + " already exists");
instance = this;
}
public synchronized void init(Args args) throws IOException {
this.requireStateMax(State.NEW);
this.updateState(State.PREINIT);
String configCmdData = args.getValue("config");
if(configCmdData != null){
this.loadConfiguration(configCmdData.getBytes());
}else{
String configFile = args.getValueOrDefault("configFile", "config.json");
logger.info("Loading configuration '", configFile, "'");
this.loadConfiguration(configFile);
}
this.config.validateConfig();
this.proxyEventBus = new EventBus();
this.loadPlugins(args.getValueOrDefault("pluginDir", "plugins").split("::"), args.getBooleanOrDefault("dirPlugins", false));
this.dispatchEvent(ProxyEvents.PREINIT);
this.updateState(State.INIT);
logger.info("Loading SSL context; ", this.config.getTlsAuthData().size(), " server names configured");
this.loadSSLContext();
this.loadErrdocs();
this.serverWorker = new EventQueueExecutor(false, "Worker");
this.serverWorker.setErrorHandler((e) -> {
logger.fatal("Error in server worker: ", e);
Proxy.this.shutdown();
});
this.serverWorkerProvider = new ApplicationWorkerProvider();
this.registerDefaults();
this.dispatchEvent(ProxyEvents.INIT);
this.updateState(State.STARTING);
for(NetServer server : this.serverInstances){
this.initServerInstance(server);
}
for(NetClientManager mgr : this.clientManagers.values()){
this.initApplication(mgr);
}
this.updateState(State.RUNNING);
}
public void shutdown() {
this.requireStateMin(State.STARTING);
this.serverWorker.queue((args) -> {
ProxyMain.shutdown();
}, 10);
}
private void loadConfiguration(String file) throws IOException {
this.loadConfiguration(Files.readAllBytes(Paths.get(file)));
}
private void loadConfiguration(byte[] data) throws IOException {
this.config = new ProxyConfiguration(data);
this.config.load();
if(this.config.getTlsAuthReloadInterval() > 0){
Tasks.interval((args) -> {
try{
Proxy.this.config.reloadTLSAuthData();
Proxy.this.keyManager.tlsDataReload();
}catch(Exception e){
logger.error("Error while reloading TLS auth data: ", e);
}
}, this.config.getTlsAuthReloadInterval() * 1000).daemon();
}
if(this.config.getUpstreamServerAddress() != null)
this.defaultUpstreamServer = new UpstreamServer(InetAddress.getByName(this.config.getUpstreamServerAddress()), this.config.getUpstreamServerPortPlain(),
this.config.getUpstreamServerPortTLS());
}
private void loadPlugins(String[] pluginDirs, boolean dirPlugins) throws IOException {
logger.debug("Loading plugins");
this.pluginManager = new PluginManager();
int pluginFlags = dirPlugins ? PluginManager.ALLOW_DIRS : PluginManager.RECURSIVE;
for(String p : pluginDirs){
Path pluginDir = Paths.get(p);
if(Files.exists(pluginDir))
this.pluginManager.loadFromDirectory(pluginDir, pluginFlags);
else
logger.warn("Plugin directory '", pluginDir, "' does not exist");
}
String[] pluginNames = new String[this.pluginManager.pluginCount()];
int pluginIndex = 0;
for(Plugin p : this.pluginManager){
try{
java.lang.reflect.Method configMethod = p.getMainClassType().getDeclaredMethod("configurationReload", ConfigObject.class);
configMethod.setAccessible(true);
configMethod.invoke(p.getMainClassInstance(), Proxy.this.config.getPluginConfigFor(p.getId()));
}catch(java.lang.reflect.InvocationTargetException e){
throw new RuntimeException("Error in config reload method of plugin '" + p.getName() + "'", e);
}catch(ReflectiveOperationException e){
logger.warn("Error while attempting to call config reload method of plugin '" + p.getName() + "': ", e.toString());
}
try{
logger.debug("Registering plugin class with event bus: ", p.getMainClassType().getName());
String eventsList = p.getAdditionalOption("events");
String[] events;
if(eventsList != null)
events = eventsList.split(",");
else
events = new String[0];
Proxy.this.proxyEventBus.register(p.getMainClassInstance(), events);
}catch(Exception e){
logger.error("Error while registering plugin '" + p.getName() + "': ", e);
}
if(p.getVersion() != null)
pluginNames[pluginIndex++] = p.getId() + "/" + p.getVersion();
else
pluginNames[pluginIndex++] = p.getId();
}
logger.info("Loaded ", pluginNames.length, " plugins: ", java.util.Arrays.toString(pluginNames));
}
private void loadSSLContext() {
try{
this.keyManager = new ProxyKeyManager(this);
this.sslContext = SSLContext.getInstance("TLS");
this.sslContext.init(new KeyManager[] { this.keyManager }, null, new SecureRandom());
}catch(GeneralSecurityException e){
throw new RuntimeException("SSL context initialization failed", e);
}
}
private void registerDefaults() {
Defaults.registerProxyDefaults(this);
}
private void loadErrdocs() throws IOException {
if(!this.config.getErrdocFiles().isEmpty()){
logger.info("Loading error documents");
for(String t : this.config.getErrdocFiles().keySet()){
String file = this.config.getErrdocFiles().get(t);
logger.debug("Loading errdoc '", file, "' (", t, ")");
HTTPErrdoc errdoc = HTTPErrdoc.fromString(new String(Files.readAllBytes(Paths.get(file))), t);
errdoc.setServername(this.getInstanceName());
this.errdocs.put(t, errdoc);
if(this.errdocDefault == null)
this.errdocDefault = errdoc;
}
if(!this.errdocs.containsKey("text/html")){
logger.debug("No errdoc of type 'text/html', loading default: ", Proxy.DEFAULT_ERRDOC_LOCATION);
this.loadDefaultErrdoc();
}
}else{
logger.debug("No errdocs configured, loading default: ", Proxy.DEFAULT_ERRDOC_LOCATION);
this.loadDefaultErrdoc();
}
}
private void loadDefaultErrdoc() throws IOException {
java.io.InputStream defErrdocStream = Proxy.class.getResourceAsStream(Proxy.DEFAULT_ERRDOC_LOCATION);
if(defErrdocStream == null)
throw new IOException("Default errdoc (" + Proxy.DEFAULT_ERRDOC_LOCATION + ") not found");
byte[] defErrdocData = new byte[defErrdocStream.available()];
defErrdocStream.read(defErrdocData);
HTTPErrdoc defErrdoc = HTTPErrdoc.fromString(new String(defErrdocData));
defErrdoc.setServername(this.getInstanceName());
this.errdocDefault = defErrdoc;
this.errdocs.put(defErrdoc.getMimeType(), defErrdoc);
}
/**
* Registers a new {@link NetServer} instance of the given type.<br>
* <br>
* A new instance of the given type is created and registered using {@link Proxy#registerServerInstance(NetServer)}. This requires that the given type has a constructor
* with no parameters. If the type requires additional arguments in the constructor, use <code>Proxy.registerServerInstance(NetServer)</code> directly instead.
*
* @param c
*/
public void registerServerInstance(Class<? extends NetServer> c) {
try{
NetServer server = c.newInstance();
this.registerServerInstance(server);
}catch(ReflectiveOperationException e){
throw new RuntimeException("Failed to register server instance of type '" + c.getName() + "'", e);
}
}
/**
* Registers a new {@link NetServer} instance. The instance is added to the list of registered instances and will be initialized at the beginning of the main
* initialization phase, meaning server instances must be registered during the {@link ProxyEvents#PREINIT} event.
*
* @param server
*/
public void registerServerInstance(NetServer server) {
this.serverInstances.add(server);
logger.info("Added server instance ", server.getClass().getName());
}
private void initServerInstance(NetServer server) {
server.setConnectionCallback(this::onNewConnection);
this.initApplication(server);
}
/**
* Registers a new {@link HTTPEngine} selector.<br>
* <br>
* For incoming connections, an appropriate <code>HTTPEngine</code> must be selected. For this, every registered selector is called until one returns a
* non-<code>null</code> value, which will be the <code>HTTPEngine</code> used for the connection. If all selectors return <code>null</code>, an
* <code>UnsupportedOperationException</code> is thrown and the connection will be closed.
*
* @param selector
*/
public void addHTTPEngineSelector(Function<SocketConnection, Class<? extends HTTPEngine>> selector) {
this.httpEngineSelectors.add(selector);
}
/**
* Registers a new {@link NetClientManager} for outgoing connections.
*
* @param mgr
*/
public void registerClientManager(NetClientManager mgr) {
this.clientManagers.put(mgr.getClass(), mgr);
}
private void initApplication(NetworkApplication app) {
try{
app.init();
ApplicationThread thread = new ApplicationThread(app);
thread.start();
}catch(IOException e){
throw new RuntimeException("Failed to initialize application of type '" + app.getClass().getName() + "'", e);
}
}
protected synchronized void close() throws IOException {
if(this.state.value() >= State.STOPPING.value())
return;
logger.info("Closing");
this.updateState(State.STOPPING);
if(this.proxyEventBus != null)
this.dispatchEvent(ProxyEvents.SHUTDOWN);
for(NetServer server : this.serverInstances){
server.close();
}
for(NetClientManager mgr : this.clientManagers.values()){
mgr.close();
}
if(this.serverWorker != null)
this.serverWorker.exit();
this.updateState(State.STOPPED);
Proxy.instance = null;
}
private Class<? extends HTTPEngine> selectHTTPEngine(SocketConnection conn) {
Class<? extends HTTPEngine> c = null;
for(Function<SocketConnection, Class<? extends HTTPEngine>> sel : this.httpEngineSelectors){
c = sel.apply(conn);
if(c != null)
break;
}
if(c == null)
throw new UnsupportedOperationException("Did not find HTTPEngine for socket of type " + conn.getClass().getName());
return c;
}
private HTTPEngine createHTTPEngineInstance(Class<? extends HTTPEngine> engineType, SocketConnection downstreamConnection) {
HTTPEngine engine = null;
try{
java.lang.reflect.Constructor<? extends HTTPEngine> cons = engineType.getConstructor(SocketConnection.class, Proxy.class);
engine = cons.newInstance(downstreamConnection, this);
}catch(ReflectiveOperationException e){
throw new RuntimeException("Failed to create instance of '" + engineType.getName() + "'", e);
}
return engine;
}
private void onNewConnection(SocketConnection conn) {
String msgToProxy = this.debugStringForConnection(conn, null);
logger.debug(msgToProxy, " Connected");
final AtomicReference<HTTPEngine> engineRef = new AtomicReference<>();
conn.setOnClose(() -> {
logger.debug(msgToProxy, " Disconnected");
HTTPEngine engine = engineRef.get();
if(engine != null)
engine.close();
Proxy.this.dispatchEvent(ProxyEvents.DOWNSTREAM_CONNECTION_CLOSED, conn);
});
this.dispatchEvent(ProxyEvents.DOWNSTREAM_CONNECTION, conn);
HTTPEngine engine = this.createHTTPEngineInstance(this.selectHTTPEngine(conn), conn);
engineRef.set(engine);
logger.debug(msgToProxy, " HTTPEngine type: ", engine.getClass().getName());
conn.setOnData((data) -> {
try{
engineRef.get().processData(data);
}catch(Exception e){
throw new RuntimeException("Error while processing data", e);
}
});
}
/**
* Creates a new connection instance for an outgoing proxy connection.
*
* @param type The {@link NetClientManager} type to use for this connection
* @param parameters Parameters for this connection
* @return The new connection instance
* @throws IOException
* @throws IllegalArgumentException If an <code>NetClientManager</code> type was given for which there is no active instance
* @see NetClientManager#connection(ConnectionParameters)
*/
public SocketConnection connection(Class<? extends NetClientManager> type, ConnectionParameters parameters) throws IOException {
this.requireState(State.RUNNING);
NetClientManager mgr = this.clientManagers.get(type);
if(mgr == null)
throw new IllegalArgumentException("Cannot connect with type " + type.getName());
return mgr.connection(parameters);
}
/**
* Delegates the given event to this proxy's {@link EventBus}.
*
* @param event The event to dispatch using this proxy's event bus
* @param args The arguments to pass the event handlers
* @return The number of handlers executed
* @see EventBus#dispatchEvent(Event, Object...)
*/
public int dispatchEvent(Event event, Object... args) {
logger.trace("Proxy EventBus event <fast>: '", event.getMethodName(), "' ", event.getEventSignature());
return ProxyEvents.runEvent(this.proxyEventBus, event, args);
}
/**
* Delegates the given event to this proxy's {@link EventBus}.
*
* @param event The event to dispatch using this proxy's event bus
* @param args The arguments to pass the event handlers
* @return An {@link EventResult} object containing information about this event execution
* @see EventBus#dispatchEventRes(Event, Object...)
*/
public EventResult dispatchEventRes(Event event, Object... args) {
logger.trace("Proxy EventBus event <res>: '", event.getMethodName(), "' ", event.getEventSignature());
return ProxyEvents.runEventRes(this.proxyEventBus, event, args);
}
/**
* Delegates the given event to this proxy's {@link EventBus} and returns a <code>boolean</code> value returned by the event handlers.<br>
* <br>
* If {@link Event#isIncludeAllReturns()} is <code>false</code>, the return value of the first event handler that returns a non-<code>null</code> value is returned. If all
* event handlers return <code>null</code> or there are none, <b>def</b> will be returned.<br>
* Otherwise, if <code>includeAllReturns</code> is <code>true</code>, all event handlers will be executed and the value of <b>def</b> is returned if all event handlers
* return either <code>null</code> or the same value as <b>def</b>. Otherwise, the return value of the first event handler is returned that does not match the <b>def</b>
* value.
*
* @param event The event to dispatch using this proxy's event bus
* @param def The value to return if all event handlers return null or there are none
* @param args The arguments to pass the event handlers
* @return The <code>boolean</code> value returned by the first event handler that returns a non-<code>null</code> value, or <b>def</b>
*/
public boolean dispatchBooleanEvent(Event event, boolean def, Object... args) {
EventResult res = this.dispatchEventRes(event, args);
if(event.isIncludeAllReturns()){
for(Object ret : res.getReturnValues()){
boolean b = (boolean) ret;
if(b != def)
return b;
}
return def;
}else{
if(res.getReturnValue() instanceof Boolean)
return (boolean) res.getReturnValue();
else
return def;
}
}
public String debugStringForConnection(SocketConnection incoming, SocketConnection outgoing) {
return "[" + incoming.getRemoteAddress() + "]->[" + this.instanceType + ":" + ((java.net.InetSocketAddress) incoming.getLocalAddress()).getPort() + "]"
+ (outgoing != null ? ("->[" + outgoing.getRemoteAddress() + "]") : "");
}
public State getState() {
return this.state;
}
public boolean isRunning() {
return this.state == State.STARTING || this.state == State.RUNNING;
}
public ProxyConfiguration getConfig() {
return this.config;
}
public String getInstanceType() {
return this.instanceType;
}
public void setInstanceType(String instanceType) {
this.requireStateMax(State.PREINIT);
this.instanceType = Objects.requireNonNull(instanceType);
this.instanceName = null;
}
public String getInstanceVersion() {
return this.instanceVersion;
}
public void setInstanceVersion(String instanceVersion) {
this.requireStateMax(State.PREINIT);
this.instanceVersion = Objects.requireNonNull(instanceVersion);
this.instanceName = null;
}
public String getInstanceNameAppendage() {
return this.instanceNameAppendage;
}
public void setInstanceNameAppendage(String instanceNameAppendage) {
this.requireStateMax(State.PREINIT);
this.instanceNameAppendage = instanceNameAppendage;
this.instanceName = null;
}
public String getInstanceName() {
if(this.instanceName == null)
this.instanceName = "omz-" + this.instanceType + "/" + this.instanceVersion + " (" + System.getProperty("os.name")
+ (this.instanceNameAppendage != null ? (") " + this.instanceNameAppendage) : ")");
return this.instanceName;
}
/**
*
* @param id The id of the plugin to search for
* @return <code>true</code> if a plugin with the given id exists
*/
public boolean isPluginLoaded(String id) {
for(Plugin p : this.pluginManager){
if(p.getId().equals(id))
return true;
}
return false;
}
/**
*
* @return The configured SSL context for this proxy
*/
public SSLContext getSslContext() {
return this.sslContext;
}
public ApplicationWorkerProvider getServerWorkerProvider() {
return this.serverWorkerProvider;
}
/**
* Returns the error document set for the given <b>type</b> using {@link Proxy#setErrdoc(String, HTTPErrdoc)}. If no error document for the given type was set, the default
* error document is returned ({@link Proxy#getDefaultErrdoc()}).
*
* @param type The MIME type of the error document
* @return A {@link HTTPErrdoc} instance of the given MIME type or the default error document if none was found
*/
public HTTPErrdoc getErrdoc(String type) {
HTTPErrdoc errdoc = this.errdocs.get(type);
if(errdoc == null){
errdoc = this.errdocDefault;
}
return errdoc;
}
public HTTPErrdoc getDefaultErrdoc() {
return this.errdocDefault;
}
/**
* Sets an error document for the given MIME type (<b>Content-Type</b> header in HTTP).<br>
* <br>
* The error document is returned by {@link Proxy#getErrdoc(String)} when given the MIME type. The {@link HTTPEngine} implementation may choose any way to determine the
* appropriate error document type for a request, but usually does so using the <b>Accept</b> HTTP request header.
*
* @param type The content type to set this error document for
* @param errdoc The <code>HTTPErrdoc</code> instance
*/
public void setErrdoc(String type, HTTPErrdoc errdoc) {
this.errdocs.put(Objects.requireNonNull(type), Objects.requireNonNull(errdoc));
}
/**
*
* @return The amount of time in milliseconds a connection with no traffic should persist before it is closed
*/
public int getConnectionIdleTimeout() {
return this.config.getConnectionIdleTimeout() * 1000;
}
/**
*
* @return The default upstream server configured in the configuration file. May be <code>null</code>
*/
public UpstreamServer getDefaultUpstreamServer() {
return this.defaultUpstreamServer;
}
/**
* Selects an upstream server based on the given hostname and path.<br>
* <br>
* This method uses the EventBus event {@link ProxyEvents#SELECT_UPSTREAM_SERVER}. If all event handlers return <code>null</code>, the default upstream server, which may
* also be <code>null</code>, is selected.
*
* @param host The hostname to choose a server for
* @param path
* @return The {@link UpstreamServer} instance for the given <b>host</b>, or <code>null</code> if no appropriate server was found
*/
public UpstreamServer getUpstreamServer(String host, String path) {
EventResult res = this.dispatchEventRes(ProxyEvents.SELECT_UPSTREAM_SERVER, host, path);
UpstreamServer serv = (UpstreamServer) res.getReturnValue();
if(serv == null)
serv = this.getDefaultUpstreamServer();
return serv;
}
/**
*
* @return The maximum time in milliseconds to wait until a connection to an upstream server is established before the connection attempt should be cancelled and an error
* be reported
*/
public int getUpstreamConnectionTimeout() {
return this.config.getUpstreamConnectionTimeout() * 1000;
}
/**
*
* @return <code>true</code> if this proxy was configured to add headers to proxied HTTP messages
*/
public boolean enableHeaders() {
return this.config.isEnableHeaders();
}
/**
*
* @return The currently active instance of <code>Proxy</code>, or <code>null</code> if there is none
*/
public static Proxy getInstance() {
return instance;
}
private void updateState(State newState) {
if(newState.value() < this.state.value())
throw new IllegalArgumentException(newState + " is lower than " + this.state);
this.state = newState;
}
private void requireState(State state) {
if(this.state.value() != state.value())
throw new IllegalStateException("Requires state " + state + " but proxy is in state " + this.state);
}
private void requireStateMin(State state) {
if(this.state.value() < state.value())
throw new IllegalStateException("Requires state " + state + " or after but proxy is in state " + this.state);
}
private void requireStateMax(State state) {
if(this.state.value() > state.value())
throw new IllegalStateException("Requires state " + state + " or before but proxy is already in state " + this.state);
}
private class ApplicationWorkerProvider implements Consumer<Runnable> {
@Override
public void accept(Runnable t) {
Proxy.this.serverWorker.queue((args) -> {
t.run();
}, 0);
}
}
private class ApplicationThread extends Thread {
private final NetworkApplication application;
private final String name;
public ApplicationThread(NetworkApplication application) {
this.application = application;
this.name = application.getClass().getSimpleName();
super.setName(this.name + "Thread");
}
@Override
public void run() {
logger.debug("Application thread for '" + this.name + "' started");
try{
this.application.start();
if(Proxy.this.isRunning())
throw new IllegalStateException("Application loop of '" + this.name + "' returned while proxy is still running");
}catch(Throwable e){
logger.fatal("Error in '" + this.name + "' application loop: ", e);
Proxy.this.shutdown();
}
}
}
}

@ -0,0 +1,74 @@
/*
* 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.proxy.core;
import org.omegazero.common.eventbus.Event;
import org.omegazero.common.eventbus.EventBus;
import org.omegazero.common.eventbus.EventResult;
import org.omegazero.net.socket.SocketConnection;
import org.omegazero.proxy.http.HTTPMessage;
import org.omegazero.proxy.http.HTTPMessageData;
import org.omegazero.proxy.net.UpstreamServer;
public final class ProxyEvents {
public static final Event PREINIT = new Event("onPreinit", new Class<?>[] {});
public static final Event INIT = new Event("onInit", new Class<?>[] {});
public static final Event SHUTDOWN = new Event("onShutdown", new Class<?>[] {});
public static final Event DOWNSTREAM_CONNECTION = new Event("onDownstreamConnection", new Class<?>[] { SocketConnection.class });
public static final Event DOWNSTREAM_CONNECTION_CLOSED = new Event("onDownstreamConnectionClosed", new Class<?>[] { SocketConnection.class });
public static final Event INVALID_HTTP_REQUEST = new Event("onInvalidHTTPRequest", new Class<?>[] { SocketConnection.class, byte[].class });
public static final Event INVALID_UPSTREAM_SERVER = new Event("onInvalidUpstreamServer", new Class<?>[] { SocketConnection.class, HTTPMessage.class });
public static final Event INVALID_HTTP_RESPONSE = new Event("onInvalidHTTPResponse",
new Class<?>[] { SocketConnection.class, SocketConnection.class, HTTPMessage.class, byte[].class });
public static final Event HTTP_REQUEST_PRE = new Event("onHTTPRequestPre", new Class<?>[] { SocketConnection.class, HTTPMessage.class });
public static final Event HTTP_REQUEST = new Event("onHTTPRequest", new Class<?>[] { SocketConnection.class, HTTPMessage.class, UpstreamServer.class });
public static final Event HTTP_REQUEST_DATA = new Event("onHTTPRequestData", new Class<?>[] { SocketConnection.class, HTTPMessageData.class, UpstreamServer.class });
public static final Event HTTP_RESPONSE = new Event("onHTTPResponse",
new Class<?>[] { SocketConnection.class, SocketConnection.class, HTTPMessage.class, UpstreamServer.class });
public static final Event HTTP_RESPONSE_DATA = new Event("onHTTPResponseData",
new Class<?>[] { SocketConnection.class, SocketConnection.class, HTTPMessageData.class, UpstreamServer.class });
public static final Event SELECT_UPSTREAM_SERVER = new Event("selectUpstreamServer", new Class<?>[] { String.class, String.class }, UpstreamServer.class);
public static final Event UPSTREAM_CONNECTION_PERMITTED = new Event("isUpstreamConnectionPermitted", false, new Class<?>[] { HTTPMessage.class, UpstreamServer.class },
Boolean.class, true);
public static final Event UPSTREAM_CONNECTION = new Event("onUpstreamConnection", new Class<?>[] { SocketConnection.class });
public static final Event UPSTREAM_CONNECTION_CLOSED = new Event("onUpstreamConnectionClosed", new Class<?>[] { SocketConnection.class });
public static final Event UPSTREAM_CONNECTION_ERROR = new Event("onUpstreamConnectionError", new Class<?>[] { SocketConnection.class, Throwable.class });
public static final Event UPSTREAM_CONNECTION_TIMEOUT = new Event("onUpstreamConnectionTimeout", new Class<?>[] { SocketConnection.class });
public static final Event MISSING_TLS_DATA = new Event("getMissingTLSData", new Class<?>[] { String.class, String.class }, java.util.Map.Entry.class);
public static int runEvent(EventBus eventBus, Event event, Object... args) {
int c;
if(event.isStateful()){
synchronized(event){
c = eventBus.dispatchEvent(event, args);
event.reset();
}
}else{
c = eventBus.dispatchEvent(event, args);
}
return c;
}
public static EventResult runEventRes(EventBus eventBus, Event event, Object... args) {
EventResult res;
if(event.isStateful()){
synchronized(event){
res = eventBus.dispatchEventRes(event, args);
event.reset();
}
}else{
res = eventBus.dispatchEventRes(event, args);
}
return res;
}
}

@ -0,0 +1,192 @@
/*
* 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.proxy.core;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509ExtendedKeyManager;
import org.omegazero.common.eventbus.EventResult;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.common.util.PropertyUtil;
import org.omegazero.proxy.config.ProxyConfiguration;
class ProxyKeyManager extends X509ExtendedKeyManager {
private static final Logger logger = LoggerUtil.createLogger();
private static final int maxSNICacheNameLen = PropertyUtil.getInt("org.omegazero.proxy.sni.maxCacheNameLen", 64);
private static final int maxSNICacheMappings = PropertyUtil.getInt("org.omegazero.proxy.sni.maxCacheMappings", 4096);
private final Proxy proxy;
private final Map<String, String> aliases = new HashMap<String, String>();
private Map<String, Entry<PrivateKey, X509Certificate[]>> tlsAuthData;
public ProxyKeyManager(Proxy proxy) {
this.proxy = proxy;
this.tlsDataReload();
}
public void tlsDataReload() {
this.aliases.clear();
// create copy to not modify the original map when adding new entries
this.tlsAuthData = new HashMap<>(this.proxy.getConfig().getTlsAuthData());
}
private Entry<PrivateKey, X509Certificate[]> getTlsAuthEntry(String name) {
synchronized(this.tlsAuthData){
return this.tlsAuthData.get(name);
}
}
@SuppressWarnings("unchecked")
private boolean getExternalEntry(String name, String keyType) {
EventResult res = this.proxy.dispatchEventRes(ProxyEvents.MISSING_TLS_DATA, name, keyType);
if(res.getReturnValue() != null){
Entry<Object, Object> e = (Entry<Object, Object>) res.getReturnValue();
if(!(e.getKey() instanceof PrivateKey)){
logger.warn("Entry key must be of type ", PrivateKey.class.getName(), " but received ", getClassName(e.getKey()));
return false;
}
if(!(e.getValue() instanceof X509Certificate[])){
logger.warn("Entry value must be of type ", X509Certificate[].class.getName(), " but received ", getClassName(e.getValue()));
return false;
}
synchronized(this.tlsAuthData){
this.tlsAuthData.put(name, (Entry<PrivateKey, X509Certificate[]>) res.getReturnValue());
}
return true;
}else
return false;
}
private static String getClassName(Object o) {
return o != null ? o.getClass().getName() : "null";
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return null;
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return null;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
Entry<PrivateKey, X509Certificate[]> e = this.getTlsAuthEntry(alias);
if(e != null)
return e.getValue();
else
return null;
}
@Override
public PrivateKey getPrivateKey(String alias) {
Entry<PrivateKey, X509Certificate[]> e = this.getTlsAuthEntry(alias);
if(e != null)
return e.getKey();
else
return null;
}
@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
SSLSession session = engine.getHandshakeSession();
if(!(session instanceof ExtendedSSLSession)){
logger.debug("session is not of type ", ExtendedSSLSession.class.getName(), " but ", session.getClass().getName());
return null;
}
ExtendedSSLSession esession = (ExtendedSSLSession) session;
List<SNIServerName> servernames = esession.getRequestedServerNames();
String available = null;
String servername = null;
synchronized(this.tlsAuthData){
for(SNIServerName s : servernames){
servername = new String(s.getEncoded());
if(this.tlsAuthData.containsKey(servername)){
available = servername;
break;
}
if(this.aliases.containsKey(servername)){
available = this.aliases.get(servername);
break;
}
// no direct mapping found, try with higher level names (ie select 'example.com' for sni name 'subdomain.example.com' and cache it if found)
String c = servername;
int di;
while((di = c.indexOf('.')) >= 0){
c = c.substring(di + 1);
if(this.tlsAuthData.containsKey(c)){
if(servername.length() < maxSNICacheNameLen && this.aliases.size() < maxSNICacheMappings)
this.aliases.put(servername, c);
available = c;
break;
}else if(this.getExternalEntry(c, keyType)){
available = c;
break;
}
}
if(available != null)
break;
}
// no matching server name, try default
if(available == null && this.tlsAuthData.containsKey(ProxyConfiguration.TLS_AUTH_DEFAULT_NAME))
available = ProxyConfiguration.TLS_AUTH_DEFAULT_NAME;
// check if found entry is correct key type
if(available != null){
Entry<PrivateKey, X509Certificate[]> e = this.tlsAuthData.get(available);
if(!e.getKey().getAlgorithm().equals(keyType))
available = null;
}
}
logger.trace("SNI: Selected '", available, "' for '", servername, "' (keyType=", keyType, ")");
return available;
}
}

@ -0,0 +1,135 @@
/*
* 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.proxy.core;
import java.lang.Thread.UncaughtExceptionHandler;
import org.omegazero.common.OmzLib;
import org.omegazero.common.event.Tasks;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.common.util.Args;
import org.omegazero.common.util.Util;
public class ProxyMain {
private static Logger logger = LoggerUtil.createLogger();
private static Proxy proxy;
private static final int shutdownTimeout = 2000;
private static boolean shuttingDown = false;
public static void main(String[] pargs) {
Args args = Args.parse(pargs);
LoggerUtil.redirectStandardOutputStreams();
String logFile = args.getValueOrDefault("logFile", "log");
LoggerUtil.init(LoggerUtil.resolveLogLevel(args.getValue("logLevel")), logFile.equals("null") ? null : logFile);
Util.onClose(ProxyMain::shutdown);
OmzLib.printBrand();
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(){
private final boolean exitOnDoubleFault = args.getBooleanOrDefault("exitOnDoubleFault", true);
private final byte[] vmerrMsg = "Virtual Machine Error\n".getBytes();
private final byte[] dfMsg = "Uncaught error in exception handler\n".getBytes();
@Override
public void uncaughtException(Thread t, Throwable err) {
try{
logger.fatal("Uncaught exception in thread '", t.getName(), "': ", err);
ProxyMain.shutdown();
}catch(VirtualMachineError e){ // things have really gotten out of hand now
handleError(e, vmerrMsg);
throw e;
}catch(Throwable e){
handleError(e, dfMsg);
throw e;
}
}
private void handleError(Throwable err, byte[] msg) {
try{
System.setErr(LoggerUtil.sysErr);
err.printStackTrace();
}catch(Throwable e){
for(int i = 0; i < vmerrMsg.length; i++)
LoggerUtil.sysOut.write(vmerrMsg[i]);
}
// exceptions thrown in the uncaught exception handler dont cause the VM to exit, even if it is a OOM error, so just exit manually
// (because everything is definitely in a very broken state and it is easier for supervisor programs to detect that there is a problem when exiting)
// exitOnDoubleFault option can be set to false for debugging
if(exitOnDoubleFault)
Runtime.getRuntime().halt(3);
}
});
proxy = new Proxy();
try{
Thread.currentThread().setName("InitializationThread");
proxy.init(args);
}catch(Throwable e){
logger.fatal("Error during proxy initialization: ", e);
}
}
protected static synchronized void shutdown() {
try{
// this function may be called multiple times unintentionally, for example when this method is called explicitly (through shutdown()),
// it shuts down all non-daemon threads, causing shutdown hooks to execute, one of which (at least, in Util.onClose) will also call this method
if(shuttingDown)
return;
shuttingDown = true;
logger.info("Shutting down");
try{
if(proxy != null)
proxy.close();
}catch(Throwable e){
logger.fatal("Error while closing proxy: ", e);
}
Tasks.timeout((args) -> {
ProxyMain.closeTimeout();
}, 2000).daemon();
LoggerUtil.close();
Tasks.exit();
}catch(Exception e){
logger.fatal("Error while shutting down", e);
}finally{
if(!Util.waitForNonDaemonThreads(shutdownTimeout))
ProxyMain.closeTimeout();
}
}
private static void closeTimeout() {
try{
logger.warn("A non-daemon thread has not exited " + shutdownTimeout + " milliseconds after an exit request was issued, JVM will be forcibly terminated");
for(Thread t : Thread.getAllStackTraces().keySet()){
if(!t.isDaemon() && !"DestroyJavaVM".equals(t.getName())){
logger.warn("Still running thread (stack trace below): " + t.getName());
for(StackTraceElement ste : t.getStackTrace())
logger.warn(" " + ste);
break;
}
}
}catch(Throwable e){
e.printStackTrace();
}finally{
Runtime.getRuntime().halt(2);
}
}
}

@ -0,0 +1,26 @@
/*
* 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.proxy.core;
public enum State {
NEW(0), PREINIT(1), INIT(2), STARTING(3), RUNNING(4), STOPPING(5), STOPPED(6);
private final int VALUE;
private State(int value) {
this.VALUE = value;
}
public int value() {
return VALUE;
}
}

@ -0,0 +1,139 @@
/*
* 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.proxy.http;
import java.net.InetAddress;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Random;
import org.omegazero.net.socket.SocketConnection;
import org.omegazero.proxy.core.Proxy;
public final class HTTPCommon {
public static final int STATUS_CONTINUE = 100;
public static final int STATUS_SWITCHING_PROTOCOLS = 101;
public static final int STATUS_PROCESSING = 102;
public static final int STATUS_OK = 200;
public static final int STATUS_CREATED = 201;
public static final int STATUS_ACCEPTED = 202;
public static final int STATUS_NON_AUTHORITATIVE = 203;
public static final int STATUS_NO_CONTENT = 204;
public static final int STATUS_RESET_CONTENT = 205;
public static final int STATUS_PARTIAL_CONTENT = 206;
public static final int STATUS_MULTIPLE_CHOICES = 300;
public static final int STATUS_MOVED_PERMANENTLY = 301;
public static final int STATUS_FOUND = 302;
public static final int STATUS_SEE_OTHER = 303;
public static final int STATUS_NOT_MODIFIED = 304;
public static final int STATUS_TEMPORARY_REDIRECT = 307;
public static final int STATUS_PERMANENT_REDIRECT = 308;
public static final int STATUS_BAD_REQUEST = 400;
public static final int STATUS_UNAUTHORIZED = 401;
public static final int STATUS_FORBIDDEN = 403;
public static final int STATUS_NOT_FOUND = 404;
public static final int STATUS_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_NOT_ACCEPTABLE = 406;
public static final int STATUS_PROXY_AUTHENTICATION_REQUIRED = 407;
public static final int STATUS_REQUEST_TIMEOUT = 408;
public static final int STATUS_CONFLICT = 409;
public static final int STATUS_GONE = 410;
public static final int STATUS_LENGTH_REQUIRED = 411;
public static final int STATUS_PRECONDITION_REQUIRED = 412;
public static final int STATUS_PAYLOAD_TOO_LARGE = 413;
public static final int STATUS_URI_TOO_LONG = 414;
public static final int STATUS_UNSUPPORTED_MEDIA_TYPE = 415;
public static final int STATUS_RANGE_NOT_SATISFIABLE = 416;
public static final int STATUS_EXPECTATION_FAILED = 417;
public static final int STATUS_IM_A_TEAPOT = 418;
public static final int STATUS_UPGRADE_REQUIRED = 426;
public static final int STATUS_PRECONDITION_FAILED = 428;
public static final int STATUS_TOO_MANY_REQUESTS = 429;
public static final int STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public static final int STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public static final int STATUS_INTERNAL_SERVER_ERROR = 500;
public static final int STATUS_NOT_IMPLEMENTED = 501;
public static final int STATUS_BAD_GATEWAY = 502;
public static final int STATUS_SERVICE_UNAVAILABLE = 503;
public static final int STATUS_GATEWAY_TIMEOUT = 504;
public static final int STATUS_HTTP_VERSION_NOT_SUPPORTED = 505;
public static final int STATUS_VARIANT_ALSO_NEGOTIATES = 506;
public static final int STATUS_LOOP_DETECTED = 508;
public static final int STATUS_NOT_EXTENDED = 510;
public static final int STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511;
private static final Random RANDOM = new Random();
private static final DateTimeFormatter DATE_HEADER_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH).withZone(ZoneId.of("GMT"));
private static final String UPSTREAM_CONNECT_ERROR_MESSAGE = "An error occurred while connecting to the upstream server";
private static final String UPSTREAM_ERROR_MESSAGE = "Error in connection to upstream server";
public static String dateString() {
return DATE_HEADER_FORMATTER.format(ZonedDateTime.now());
}
public static String hstrFromInetAddress(InetAddress address) {
byte[] addrBytes = address.getAddress();
int nf = 0;
int bytesPerF = addrBytes.length / 4;
for(int i = 0; i < 4; i++){
int n = 0;
for(int j = 0; j < bytesPerF; j++){
n += (addrBytes[bytesPerF * i + j] + nf + 46) << i;
}
nf |= (n & 0xff) << (i * 8);
}
if(nf <= 0x0fffffff)
nf |= 0x80000000;
return Integer.toHexString(nf);
}
public static String requestId(SocketConnection connection) {
StringBuilder sb = new StringBuilder(32);
int n = RANDOM.nextInt();
if(n <= 0x0fffffff)
n |= 0x10000000;
sb.append(Integer.toHexString(n)).append(HTTPCommon.hstrFromInetAddress(((java.net.InetSocketAddress) connection.getRemoteAddress()).getAddress())).append('-')
.append(Long.toHexString(System.currentTimeMillis()));
return sb.toString();
}
public static String shortenRequestId(String full) {
return full.substring(0, 8);
}
public static void setDefaultHeaders(Proxy proxy, HTTPMessage msg) {
String via = msg.getHeader("via");
via = ((via != null) ? (via + ", ") : "") + msg.getVersion() + " " + proxy.getInstanceName();
msg.setHeader("via", via);
String reqid = msg.getHeader("x-request-id");
reqid = ((reqid != null) ? (reqid + ",") : "") + msg.getRequestId();
msg.setHeader("x-request-id", reqid);
}
public static String getUpstreamErrorMessage(Throwable e) {
if(e instanceof javax.net.ssl.SSLHandshakeException)
return UPSTREAM_CONNECT_ERROR_MESSAGE + ": TLS handshake error";
else if(e instanceof java.net.ConnectException)
return UPSTREAM_CONNECT_ERROR_MESSAGE + ": " + e.getMessage();
else if(e instanceof java.net.SocketException)
return UPSTREAM_ERROR_MESSAGE + ": " + e.getMessage();
else if(e instanceof java.io.IOException)
return UPSTREAM_ERROR_MESSAGE + ": Socket error";
else
return UPSTREAM_ERROR_MESSAGE + ": Unexpected error";
}
}

@ -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.proxy.http;
import org.omegazero.net.socket.SocketConnection;
/**
* HTTP server implementation interface.<br>
* <br>
* An instance of a <code>HTTPEngine</code> is associated with one connection by a client.
*/
public interface HTTPEngine {
/**
*
* @param data Incoming data of a client to process
*/
public void processData(byte[] data);
public void close();
/**
*
* @return The {@link SocketConnection} to the client associated with this instance
*/
public SocketConnection getDownstreamConnection();
public void respond(HTTPMessage request, HTTPMessage response);
public void respond(HTTPMessage request, int status, byte[] data, String... headers);
public void respondError(HTTPMessage request, int status, String title, String message, String... headers);
}

@ -0,0 +1,118 @@
/*
* 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.proxy.http;
import java.util.ArrayList;
import java.util.List;
public class HTTPErrdoc {
private final String mimeType;
private List<Object> elements = new ArrayList<>();
private int len = 0;
private String servername = null;
private HTTPErrdoc(String mimeType) {
this.mimeType = mimeType;
}
private void addElement(Object element) {
this.elements.add(element);
if(element instanceof String)
this.len += ((String) element).length();
}
public String generate(int status, String title, String message, String requestId, String clientAddress) {
StringBuilder sb = new StringBuilder(this.len);
for(Object el : this.elements){
if(el instanceof Type){
if(el == Type.STATUS)
sb.append(status);
else if(el == Type.TITLE)
sb.append(title);
else if(el == Type.MESSAGE)
sb.append(message);
else if(el == Type.REQUESTID)
sb.append(requestId);
else if(el == Type.CLIENTADDRESS)
sb.append(clientAddress);
else if(el == Type.SERVERNAME)
sb.append(this.servername);
else if(el == Type.TIME)
sb.append(HTTPCommon.dateString());
}else
sb.append(el);
}
return sb.toString();
}
public void setServername(String servername) {
this.servername = servername;
}
public String getMimeType() {
return mimeType;
}
public static HTTPErrdoc fromString(String data) {
return HTTPErrdoc.fromString(data, "text/html");
}
/**
* Generates a new <code>HTTPErrdoc</code> instance from the given <b>data</b>.<br>
* <br>
* The <b>mimeType</b> argument is the type returned by {@link HTTPErrdoc#getMimeType()}.
*
* @param data The string data
* @param mimeType The MIME type of the error document
* @return The new <code>HTTPErrdoc</code> instance
*/
public static HTTPErrdoc fromString(String data, String mimeType) {
HTTPErrdoc errdoc = new HTTPErrdoc(mimeType);
int lastEnd = 0;
while(true){
int startIndex = data.indexOf("${", lastEnd);
if(startIndex < 0)
break;
errdoc.addElement(data.substring(lastEnd, startIndex));
int endIndex = data.indexOf('}', startIndex);
if(endIndex < 0)
throw new RuntimeException("Missing closing '}' at position " + startIndex);
lastEnd = endIndex + 1;
String typename = data.substring(startIndex + 2, endIndex);
Type type = HTTPErrdoc.resolveType(typename);
if(type == null)
throw new IllegalArgumentException("Invalid variable name '" + typename + "' at position " + startIndex);
errdoc.addElement(type);
}
errdoc.addElement(data.substring(lastEnd));
return errdoc;
}
private static Type resolveType(String name) {
for(Type t : Type.values()){
if(name.toUpperCase().equals(t.toString()))
return t;
}
return null;
}
private static enum Type {
STATUS, TITLE, MESSAGE, REQUESTID, CLIENTADDRESS, SERVERNAME, TIME;
}
}

@ -0,0 +1,388 @@
/*
* 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.proxy.http;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
/**
* Represents a generic HTTP request or response message, agnostic of the HTTP version used.
*/
public class HTTPMessage implements Serializable {
private static final long serialVersionUID = 1L;
private final boolean request;
private final long createdTime = System.currentTimeMillis();
@SideOnly(side = SideOnly.Side.REQUEST)
private String method;
@SideOnly(side = SideOnly.Side.REQUEST)
private String scheme;
@SideOnly(side = SideOnly.Side.REQUEST)
private String authority;
@SideOnly(side = SideOnly.Side.REQUEST)
private String path;
@SideOnly(side = SideOnly.Side.RESPONSE)
private int status;
private String version;
private final Map<String, String> headerFields;
private byte[] data;
private String origAuthority;
private String origPath;
private HTTPMessage correspondingMessage;
private String requestId;
private HTTPEngine engine;
private int size;
private HTTPMessage(boolean request, Map<String, String> headers) {
this.request = request;
if(headers == null)
this.headerFields = new HashMap<>();
else
this.headerFields = headers;
}
/**
* @see #HTTPMessage(int, String, Map)
*/
public HTTPMessage(int status, String version) {
this(status, version, null);
}
/**
* Creates a new <code>HTTPMessage</code> representing a HTTP response.
*
* @param status The status in the response
* @param version The HTTP version
*/
public HTTPMessage(int status, String version, Map<String, String> headers) {
this(false, headers);
this.status = status;
this.version = version;
}
/**
* @see #HTTPMessage(String, String, String, String, String, Map)
*/
public HTTPMessage(String method, String scheme, String authority, String path, String version) {
this(method, scheme, authority, path, version, null);
}
/**
* Creates a new <code>HTTPMessage</code> representing a HTTP request.
*
* @param method The request method
* @param scheme The URL scheme (e.g. "http")
* @param authority The URL authority or, if not provided, the value of the "Host" header (e.g. "example.com")
* @param path The requested path or URL
* @param version The HTTP version
*/
public HTTPMessage(String method, String scheme, String authority, String path, String version, Map<String, String> headers) {
this(true, headers);
this.method = method;
this.scheme = scheme;
this.authority = authority;
this.path = path;
this.version = version;
this.origAuthority = authority;
this.origPath = path;
}
/**
*
* @return <code>true</code> if this object represents a HTTP request, or <code>false</code> if this object represents a HTTP response
*/
public final boolean isRequest() {
return this.request;
}
public long getCreatedTime() {
return this.createdTime;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public String getMethod() {
return this.method;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public String getScheme() {
return this.scheme;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public String getAuthority() {
return this.authority;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public String getPath() {
return this.path;
}
@SideOnly(side = SideOnly.Side.RESPONSE)
public int getStatus() {
return this.status;
}
public String getVersion() {
return this.version;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public void setAuthority(String authority) {
this.authority = authority;
}
@SideOnly(side = SideOnly.Side.REQUEST)
public void setPath(String path) {
this.path = path;
}
public String getOrigAuthority() {
return this.origAuthority;
}
public String getOrigPath() {
return this.origPath;
}
/**
*
* @param key The HTTP field name of this header field
* @return The value of this header field, or <code>null</code> if it does not exist
*/
public String getHeader(String key) {
return this.headerFields.get(key);
}
/**
*
* @param key The HTTP field name of this header field
* @param def A value to return if a header field with the specified name does not exist
* @return The value of this header field, or <b>def</b> if it does not exist
*/
public String getHeader(String key, String def) {
String v = this.getHeader(key);
if(v == null)
v = def;
return v;
}
/**
*
* @param key The HTTP field name of this header field
* @param value The value of this header field. If <code>null</code>, the header will be deleted
*/
public void setHeader(String key, String value) {
Objects.requireNonNull(key);
if(value == null)
this.headerFields.remove(key);
else
this.headerFields.put(key, value);
}
/**
*
* @param key The HTTP field name of the header to search for
* @return <code>true</code> if a header with the given key exists
*/
public boolean headerExists(String key) {
return this.headerFields.containsKey(key);
}
/**
* Deletes the header entry with the given key.<br>
* <br>
* This call is equivalent to <code>setHeader(key, null)</code>.
*
* @param key The header to delete
*/
public void deleteHeader(String key) {
this.setHeader(key, null);
}
public Set<Entry<String, String>> getHeaderSet() {
return this.headerFields.entrySet();
}
public byte[] getData() {
return this.data;
}
public void setData(byte[] data) {
this.data = data;
}
public HTTPMessage getCorrespondingMessage() {
return this.correspondingMessage;
}
public void setCorrespondingMessage(HTTPMessage correspondingMessage) {
this.correspondingMessage = correspondingMessage;
}
/**
*
* @return A request ID associated with this request using {@link HTTPMessage#setRequestId(String)}, <code>null</code> otherwise
*/
public String getRequestId() {
return this.requestId;
}
/**
* Sets a request ID associated with this request.
*
* @param requestId The request ID
*/
public void setRequestId(String requestId) {
this.requestId = Objects.requireNonNull(requestId);
}
public HTTPEngine getEngine() {
return this.engine;
}
public void setEngine(HTTPEngine engine) {
this.engine = engine;
}
public int getSize() {
return this.size;
}
public void setSize(int size) {
this.size = size;
}
/**
*
* @return The request URI of this <code>HTTPMessage</code>
* @throws IllegalStateException If this object does not represent a HTTP request
*/
public String requestURI() {
if(!this.request)
throw new IllegalStateException("Called requestURI on a response object");
return this.scheme + "://" + this.authority + ("*".equals(this.path) ? "" : this.path);
}
/**
*
* @return A HTTP/1-style request line of the form <code>[method] [requestURI] HTTP/[version]</code>
* @throws IllegalStateException If this object does not represent a HTTP request
*/
public String requestLine() {
if(!this.request)
throw new IllegalStateException("Called requestLine on a response object");
return this.method + " " + this.requestURI() + " " + this.version;
}
/**
*
* @return A HTTP/1-style response line of the form <code>HTTP/[version] [status]</code>
* @throws IllegalStateException If this object does not represent a HTTP response
*/
public String responseLine() {
if(this.request)
throw new IllegalStateException("Called responseLine on a request object");
return this.version + " " + this.status;
}
/**
* Checks if the start line of this <code>HTTPMessage</code> is equal to the start line of <b>msg</b>. The start line consists of:
* <ul>
* <li>for requests: the request method, the full URI (scheme, authority and path) and the HTTP version string</li>
* <li>for responses: the HTTP version string and status code</li>
* </ul>
*
* This means that all values not relevant to the type of message are ignored. For example, if a HTTPMessage is a request ({@link HTTPMessage#isRequest()} returns
* <code>true</code>), the value of {@link HTTPMessage#status} is ignored, since it is only relevant for HTTP responses.<br>
*
* @param msg The <code>HTTPMessage</code> to compare against
* @return <code>true</code> if the start line matches
*/
public boolean equalStartLine(HTTPMessage msg) {
boolean startLineEqual;
if(this.request != msg.request)
startLineEqual = false;
else if(this.request)
startLineEqual = Objects.equals(this.method, msg.method) && Objects.equals(this.scheme, msg.scheme) && Objects.equals(this.authority, msg.authority)
&& Objects.equals(this.path, msg.path) && Objects.equals(this.version, msg.version);
else
startLineEqual = Objects.equals(this.version, msg.version) && this.status == msg.status;
return startLineEqual;
}
/**
* Creates a mostly shallow copy of this <code>HTTPMessage</code> object.<br>
* <br>
* {@link HTTPMessage#headerFields} is the only object where a new instance is created.
*/
@Override
public synchronized HTTPMessage clone() {
HTTPMessage c = new HTTPMessage(this.request, null);
c.method = this.method;
c.scheme = this.scheme;
c.authority = this.authority;
c.path = this.path;
c.status = this.status;
c.version = this.version;
c.headerFields.putAll(this.headerFields);
c.data = this.data;
c.origAuthority = this.origAuthority;
c.origPath = this.origPath;
c.correspondingMessage = this.correspondingMessage;
c.requestId = this.requestId;
c.engine = this.engine;
c.size = this.size;
return c;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(32);
sb.append("HTTPMessage[");
if(this.request){
sb.append("request").append("; method=").append(this.method).append("; path=").append(this.path).append("; version=").append(this.version);
}else{
sb.append("response").append("; status=").append(this.status).append("; version=").append(this.version);
}
sb.append("]");
return sb.toString();
}
public @interface SideOnly {
Side side();
public enum Side {
REQUEST, RESPONSE;
}
}
}

@ -0,0 +1,41 @@
/*
* 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.proxy.http;
import java.io.Serializable;
public class HTTPMessageData implements Serializable {
private static final long serialVersionUID = 1L;
private final HTTPMessage httpMessage;
private byte[] data;
public HTTPMessageData(HTTPMessage httpMessage, byte[] data) {
this.httpMessage = httpMessage;
this.data = data;
}
public byte[] getData() {
return this.data;
}
public void setData(byte[] data) {
this.data = data;
}
public HTTPMessage getHttpMessage() {
return this.httpMessage;
}
}

@ -0,0 +1,459 @@
/*
* 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.proxy.http.engineimpl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import org.omegazero.common.logging.Logger;
import org.omegazero.common.logging.LoggerUtil;
import org.omegazero.common.util.PropertyUtil;
import org.omegazero.net.client.NetClientManager;
import org.omegazero.net.client.PlainTCPClientManager;
import org.omegazero.net.client.TLSClientManager;
import org.omegazero.net.client.params.ConnectionParameters;
import org.omegazero.net.client.params.TLSConnectionParameters;
import org.omegazero.net.socket.SocketConnection;
import org.omegazero.net.socket.impl.TLSConnection;
import org.omegazero.proxy.core.Proxy;
import org.omegazero.proxy.core.ProxyEvents;
import org.omegazero.proxy.http.HTTPCommon;
import org.omegazero.proxy.http.HTTPEngine;
import org.omegazero.proxy.http.HTTPErrdoc;
import org.omegazero.proxy.http.HTTPMessage;
import org.omegazero.proxy.http.HTTPMessageData;
import org.omegazero.proxy.net.UpstreamServer;
import org.omegazero.proxy.util.ArrayUtil;
import org.omegazero.proxy.util.ProxyUtil;
public class HTTP1 implements HTTPEngine {
private static final Logger logger = LoggerUtil.createLogger();
private static final boolean disableDefaultRequestLog = PropertyUtil.getBoolean("org.omegazero.proxy.disableDefaultRequestLog", false);
private static final byte[] HTTP1_HEADER_END = new byte[] { 0x0d, 0x0a, 0x0d, 0x0a };
private static final String[] HTTP1_ALPN = new String[] { "http/1.1" };
private final SocketConnection downstreamConnection;
private final Proxy proxy;
private final String downstreamConnectionDbgstr;
private final boolean downstreamSecurity;
private boolean downstreamClosed;
private HTTPMessage lastRequest;
private UpstreamServer lastUpstreamServer;
private Map<UpstreamServer, SocketConnection> upstreamConnections = new java.util.HashMap<>();
public HTTP1(SocketConnection downstreamConnection, Proxy proxy) {
this.downstreamConnection = Objects.requireNonNull(downstreamConnection);
this.proxy = Objects.requireNonNull(proxy);
this.downstreamConnectionDbgstr = this.proxy.debugStringForConnection(this.downstreamConnection, null);
this.downstreamSecurity = this.downstreamConnection instanceof TLSConnection;
}
@Override
public synchronized void processData(byte[] data) {
try{
this.processPacket(data);
}catch(Exception e){
if(this.lastRequest != null){
logger.error("Error while processing packet: ", e);
this.respondError(this.lastRequest, HTTPCommon.STATUS_INTERNAL_SERVER_ERROR, "Internal Server Error", "An unexpected error has occurred");
}else
throw e;
}
}
@Override
public synchronized void close() {
this.downstreamClosed = true;
for(SocketConnection uconn : this.upstreamConnections.values())
uconn.close();
}
@Override
public SocketConnection getDownstreamConnection() {
return this.downstreamConnection;
}
@Override
public void respond(HTTPMessage request, HTTPMessage response) {
if(request != null){
if(request.getCorrespondingMessage() != null) // received response already
return;
request.setCorrespondingMessage(response);
}
HTTP1.writeHTTPMsg(this.downstreamConnection, response);
}
@Override
public void respond(HTTPMessage request, int status, byte[] data, String... headers) {
this.respondEx(request, status, data, headers);
}
@Override
public void respondError(HTTPMessage request, int status, String title, String message, String... headers) {
if(request != null && request.getCorrespondingMessage() != null)
return;
String accept = request != null ? request.getHeader("accept") : null;
String[] acceptParts = accept != null ? accept.split(",") : new String[0];
HTTPErrdoc errdoc = null;
for(String ap : acceptParts){
int pe = ap.indexOf(';');
if(pe >= 0)
ap = ap.substring(0, pe);
errdoc = this.proxy.getErrdoc(ap.trim());
if(errdoc != null)
break;
}
if(errdoc == null)
errdoc = this.proxy.getDefaultErrdoc();
byte[] errdocData = errdoc
.generate(status, title, message, request != null ? request.getHeader("x-request-id") : null, this.downstreamConnection.getApparentRemoteAddress().toString())
.getBytes();
this.respondEx(request, status, errdocData, headers, "content-type", errdoc.getMimeType());
}
private synchronized void respondEx(HTTPMessage request, int status, byte[] data, String[] h1, String... hEx) {
if(request != null && request.getCorrespondingMessage() != null) // received response already
return;
logger.debug(this.downstreamConnectionDbgstr, " Responding with status ", status);
HTTPMessage response = new HTTPMessage(status, "HTTP/1.1");
for(int i = 0; i + 1 < hEx.length; i += 2){
response.setHeader(hEx[i], hEx[i + 1]);
}
for(int i = 0; i + 1 < h1.length; i += 2){
response.setHeader(h1[i], h1[i + 1]);
}
if(!response.headerExists("content-length"))
response.setHeader("content-length", String.valueOf(data.length));
if(!response.headerExists("date"))
response.setHeader("date", HTTPCommon.dateString());
if(!response.headerExists("connection"))
response.setHeader("connection", "close");
response.setHeader("server", this.proxy.getInstanceName());
response.setHeader("x-proxy-engine", this.getClass().getSimpleName());
if(request != null)
response.setHeader("x-request-id", request.getRequestId());
response.setData(data);
if(request != null)
request.setCorrespondingMessage(response);
HTTP1.writeHTTPMsg(this.downstreamConnection, response);
}
// called in synchronized context
private void processPacket(byte[] data) {
HTTPMessage request = this.parseHTTPRequest(data);
if(request != null){
this.lastRequest = request;
request.setRequestId(HTTPCommon.requestId(this.downstreamConnection));
if(this.proxy.enableHeaders()){
HTTPCommon.setDefaultHeaders(this.proxy, request);
}
this.proxy.dispatchEvent(ProxyEvents.HTTP_REQUEST_PRE, this.downstreamConnection, request);
if(!disableDefaultRequestLog)
logger.info(this.downstreamConnection.getApparentRemoteAddress(), "/", HTTPCommon.shortenRequestId(request.getRequestId()), " - '", request.requestLine(),
"'");
if(this.hasReceivedResponse())
return;
String hostname = request.getAuthority();
if(hostname == null){
logger.info(this.downstreamConnectionDbgstr, " No Host header");
this.respondError(request, HTTPCommon.STATUS_BAD_REQUEST, "Bad Request", "Missing Host header");
return;
}
this.lastUpstreamServer = this.proxy.getUpstreamServer(hostname, request.getPath());
if(this.lastUpstreamServer == null){
logger.info(this.downstreamConnectionDbgstr, " No upstream server found");
this.proxy.dispatchEvent(ProxyEvents.INVALID_UPSTREAM_SERVER, this.downstreamConnection, request);
this.respondError(request, HTTPCommon.STATUS_NOT_FOUND, "Not Found", "No appropriate upstream server was found for this request");
return;
}
}
UpstreamServer userver = this.lastUpstreamServer;
if(userver == null){ // here: implies request is null (invalid)
this.proxy.dispatchEvent(ProxyEvents.INVALID_HTTP_REQUEST, this.downstreamConnection, data);
if(this.hasReceivedResponse())
return;
logger.info(this.downstreamConnectionDbgstr, " Invalid Request");
this.respondError(request, HTTPCommon.STATUS_BAD_REQUEST, "Bad Request", "The proxy server did not understand the request");
return;
}
SocketConnection uconn = null;
if((uconn = this.upstreamConnections.get(userver)) == null){
if((uconn = this.connectUpstream(userver)) == null)
return;
}
if(request != null){
this.proxy.dispatchEvent(ProxyEvents.HTTP_REQUEST, this.downstreamConnection, request, userver);
HTTP1.writeHTTPMsg(uconn, request);
}else{
HTTPMessageData hmd = new HTTPMessageData(this.lastRequest, data);
this.proxy.dispatchEvent(ProxyEvents.HTTP_REQUEST_DATA, this.downstreamConnection, hmd, userver);
ProxyUtil.handleBackpressure(uconn, this.downstreamConnection);
uconn.write(hmd.getData());
}
}
// called in synchronized context
private SocketConnection connectUpstream(UpstreamServer userver) {
if(!this.proxy.dispatchBooleanEvent(ProxyEvents.UPSTREAM_CONNECTION_PERMITTED, true, this.lastRequest, userver)){
logger.info(this.downstreamConnectionDbgstr, " Connection to ", userver, " blocked");
this.respondError(this.lastRequest, HTTPCommon.STATUS_FORBIDDEN, "Forbidden", "You are not permitted to access this resource");
return null;
}
Class<? extends NetClientManager> type;
ConnectionParameters params;
if((this.downstreamSecurity || userver.getPlainPort() <= 0) && userver.getSecurePort() > 0){
type = TLSClientManager.class;
params = new TLSConnectionParameters(new InetSocketAddress(userver.getAddress(), userver.getSecurePort()));
((TLSConnectionParameters) params).setAlpnNames(HTTP1_ALPN);
((TLSConnectionParameters) params).setSniOptions(new String[] { userver.getAddress().getHostName() });
}else if(userver.getPlainPort() > 0){
type = PlainTCPClientManager.class;
params = new ConnectionParameters(new InetSocketAddress(userver.getAddress(), userver.getPlainPort()));
}else
throw new RuntimeException("Upstream server " + userver.getAddress() + " neither has a plain nor a secure port set");
SocketConnection uconn;
try{
uconn = this.proxy.connection(type, params);
}catch(IOException e){
logger.error("Connection failed: ", e);
this.respondError(this.lastRequest, HTTPCommon.STATUS_INTERNAL_SERVER_ERROR, "Internal Server Error", "Upstream connection creation failed");
return null;
}
uconn.setAttachment(this.proxy.debugStringForConnection(this.downstreamConnection, uconn));
uconn.setOnConnect(() -> {
logger.debug(uconn.getAttachment(), " Connected");
HTTP1.this.proxy.dispatchEvent(ProxyEvents.UPSTREAM_CONNECTION, uconn);
});
uconn.setOnTimeout(() -> {
logger.error(uconn.getAttachment(), " Connect timed out");
HTTP1.this.proxy.dispatchEvent(ProxyEvents.UPSTREAM_CONNECTION_TIMEOUT, uconn);
if(userver.equals(HTTP1.this.lastUpstreamServer))
this.respondError(HTTP1.this.lastRequest, HTTPCommon.STATUS_GATEWAY_TIMEOUT, "Gateway Timeout", "Connection to the upstream server timed out");
});
uconn.setOnError((e) -> {
// error in connection to upstream server is log level error instead of warn (as for downstream connections) because they usually indicate a problem with the
// upstream server, which is more severe than a client connection getting RSTed
if(e instanceof IOException)
logger.error(uconn.getAttachment(), " Error: ", e.toString());
else
logger.error(uconn.getAttachment(), " Internal error: ", e);
HTTP1.this.proxy.dispatchEvent(ProxyEvents.UPSTREAM_CONNECTION_ERROR, uconn, e);
if(userver.equals(HTTP1.this.lastUpstreamServer)){
if(e instanceof IOException)
this.respondError(HTTP1.this.lastRequest, HTTPCommon.STATUS_BAD_GATEWAY, "Bad Gateway", HTTPCommon.getUpstreamErrorMessage(e));
else
this.respondError(HTTP1.this.lastRequest, HTTPCommon.STATUS_INTERNAL_SERVER_ERROR, "Internal Server Error",
"An internal error occurred in the connection to the upstream server");
}
});
uconn.setOnClose(() -> {
logger.debug(uconn.getAttachment(), " Disconnected");
HTTP1.this.proxy.dispatchEvent(ProxyEvents.UPSTREAM_CONNECTION_CLOSED, uconn);
if(userver.equals(HTTP1.this.lastUpstreamServer) && HTTP1.this.lastRequest.getCorrespondingMessage() == null && !HTTP1.this.downstreamClosed){
// did not receive a response
logger.error(uconn.getAttachment(), " Connection closed unexpectedly");
this.respondError(HTTP1.this.lastRequest, HTTPCommon.STATUS_BAD_GATEWAY, "Bad Gateway", "Connection to the upstream server closed unexpectedly");
}
HTTP1.this.upstreamConnections.remove(userver, uconn);
});
uconn.setOnData((d) -> {
if(!userver.equals(HTTP1.this.lastUpstreamServer)){
logger.warn(uconn.getAttachment(), " Received unexpected data");
return;
}
HTTPMessage response = HTTP1.this.parseHTTPResponse(d);
HTTPMessage req = HTTP1.this.lastRequest;
if(response != null){
response.setRequestId(req.getRequestId());
if(HTTP1.this.proxy.enableHeaders()){
if(!response.headerExists("date"))
response.setHeader("Date", HTTPCommon.dateString());
HTTPCommon.setDefaultHeaders(HTTP1.this.proxy, response);
}
response.setCorrespondingMessage(req);
req.setCorrespondingMessage(response);
try{
HTTP1.this.proxy.dispatchEvent(ProxyEvents.HTTP_RESPONSE, HTTP1.this.downstreamConnection, uconn, response, userver);
}catch(Exception e){
// reset setCorrespondingMessage to enable respondError in the onError callback to write the 500 response
req.setCorrespondingMessage(null);
throw e;
}
HTTP1.writeHTTPMsg(HTTP1.this.downstreamConnection, response);
}else{
response = HTTP1.this.lastRequest.getCorrespondingMessage();
if(response != null){
HTTPMessageData hmd = new HTTPMessageData(response, d);
HTTP1.this.proxy.dispatchEvent(ProxyEvents.HTTP_RESPONSE_DATA, HTTP1.this.downstreamConnection, uconn, hmd, userver);
ProxyUtil.handleBackpressure(HTTP1.this.downstreamConnection, uconn);
HTTP1.this.downstreamConnection.write(hmd.getData());
}else{
HTTP1.this.proxy.dispatchEvent(ProxyEvents.INVALID_HTTP_RESPONSE, HTTP1.this.downstreamConnection, uconn, req, d);
if(HTTP1.this.hasReceivedResponse())
return;
logger.warn(uconn.getAttachment(), " Invalid response");
this.respondError(HTTP1.this.lastRequest, HTTPCommon.STATUS_BAD_GATEWAY, "Bad Gateway", "The upstream server did not send a valid response");
}
}
});
uconn.connect(this.proxy.getUpstreamConnectionTimeout());
this.upstreamConnections.put(userver, uconn);
return uconn;
}
private boolean hasReceivedResponse() {
return this.lastRequest != null && this.lastRequest.getCorrespondingMessage() != null;
}
private HTTPMessage parseHTTPRequest(byte[] data) {
if(data[0] < 'A' || data[0] > 'Z')
return null;
// the full header must be sent at once (this was never a problem for real clients)
// because the read buffer is 8192 bytes large, this also means the header size is limited to 8KiB (same applies to responses)
int headerEnd = ArrayUtil.byteArrayIndexOf(data, HTTP1_HEADER_END);
if(headerEnd < 0)
return null;
String headerData = new String(data, 0, headerEnd);
int startLineEnd = headerData.indexOf("\r\n");
if(startLineEnd < 0)
return null;
String[] startLine = headerData.substring(0, startLineEnd).split(" ");
if(!(startLine.length == 3 && startLine[0].matches("[A-Z]{2,10}") && startLine[2].matches("HTTP/1\\.[01]")))
return null;
String requestURI = startLine[1];
String host = null;
if(requestURI.charAt(0) != '/' && !requestURI.equals("*")){
// assuming absolute URI with net_path and abs_path
int authStart = requestURI.indexOf("://");
if(authStart < 0)
return null;
authStart += 3;
int pathStart = requestURI.indexOf('/', authStart);
host = requestURI.substring(authStart, pathStart);
requestURI = requestURI.substring(pathStart);
}
// make sure the request uri only contains printable ASCII characters
for(int i = 0; i < requestURI.length(); i++){
char c = requestURI.charAt(i);
if(c <= 32 || c >= 127)
return null;
}
String[] headerLines = headerData.substring(startLineEnd + 2).split("\r\n");
Map<String, String> headers = new java.util.HashMap<>(headerLines.length);
for(String headerLine : headerLines){
int sep = headerLine.indexOf(':');
if(sep < 0)
return null;
headers.put(headerLine.substring(0, sep).trim().toLowerCase(), headerLine.substring(sep + 1).trim());
}
if(host == null){
host = headers.get("host");
}
HTTPMessage msg = new HTTPMessage(startLine[0], this.downstreamSecurity ? "https" : "http", host, requestURI, startLine[2], headers);
msg.setEngine(this);
msg.setSize(headerEnd);
int edataStart = headerEnd + HTTP1_HEADER_END.length;
int edataLen = data.length - edataStart;
byte[] edata = new byte[edataLen];
System.arraycopy(data, edataStart, edata, 0, edataLen);
msg.setData(edata);
return msg;
}
private HTTPMessage parseHTTPResponse(byte[] data) {
if(data[0] != 'H')
return null;
int headerEnd = ArrayUtil.byteArrayIndexOf(data, HTTP1_HEADER_END);
if(headerEnd < 0)
return null;
String headerData = new String(data, 0, headerEnd);
int startLineEnd = headerData.indexOf("\r\n");
if(startLineEnd < 0)
return null;
String[] startLine = headerData.substring(0, startLineEnd).split(" ");
if(!(startLine.length >= 2 && startLine[0].matches("HTTP/1\\.[01]") && startLine[1].matches("[0-9]{2,4}")))
return null;
HTTPMessage msg = new HTTPMessage(Integer.parseInt(startLine[1]), startLine[0]);
msg.setEngine(this);
String[] headerLines = headerData.substring(startLineEnd + 2).split("\r\n");
for(String headerLine : headerLines){
int sep = headerLine.indexOf(':');
if(sep < 0)
return null;
msg.setHeader(headerLine.substring(0, sep).trim().toLowerCase(), headerLine.substring(sep + 1).trim());
}
msg.setSize(headerEnd);
int edataStart = headerEnd + HTTP1_HEADER_END.length;
int edataLen = data.length - edataStart;
byte[] edata = new byte[edataLen];
System.arraycopy(data, edataStart, edata, 0, edataLen);
msg.setData(edata);
return msg;
}
private static void writeHTTPMsg(SocketConnection conn, HTTPMessage msg) {
StringBuilder sb = new StringBuilder(msg.getSize());
if(msg.isRequest()){
sb.append(msg.getMethod() + ' ' + msg.getPath() + ' ' + msg.getVersion());
}else{
sb.append(msg.getVersion() + ' ' + msg.getStatus());
}
sb.append("\r\n");
for(Entry<String, String> header : msg.getHeaderSet()){
sb.append(header.getKey()).append(": ").append(header.getValue()).append("\r\n");
}
sb.append("\r\n");
conn.write(sb.toString().getBytes());
conn.write(msg.getData());
}
}

@ -0,0 +1,58 @@
/*
* 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.proxy.net;
import java.net.InetAddress;
public class UpstreamServer {
private final InetAddress address;
private final int plainPort;
private final int securePort;
public UpstreamServer(InetAddress address, int plainPort, int securePort) {
this.address = address;
this.plainPort = plainPort;
this.securePort = securePort;
}
public InetAddress getAddress() {
return this.address;
}
public int getPlainPort() {
return this.plainPort;
}
public int getSecurePort() {
return this.securePort;
}
@Override
public int hashCode() {
return this.address.hashCode() + 36575 * this.plainPort + 136575 * this.securePort;
}
@Override
public boolean equals(Object o) {
if(o == null || !(o instanceof UpstreamServer))
return false;
UpstreamServer u = (UpstreamServer) o;
return u.address.equals(this.address) && u.plainPort == this.plainPort && u.securePort == this.securePort;
}
@Override
public String toString() {
return this.address + ":" + this.plainPort + "/" + this.securePort;
}
}

@ -0,0 +1,20 @@
<!-- The default error document of omz_proxy3 -->
<!DOCTYPE html>
<html>
<head>
<title>${status} ${title}</title>
<meta charset="utf-8" />
</head>
<body>
<h1>${title} (${status})</h1>
${message}<br />
<br />
<hr />
<br />
Request ID: ${requestId}<br />
Client address: ${clientAddress}<br />
Time: ${time}<br />
<br />
<i>${servername}</i>
</body>
</html>

@ -0,0 +1,43 @@
/*
* 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.proxy.util;
public final class ArrayUtil {
private ArrayUtil() {
}
/**
* Searches for the sequence of bytes given in the <b>seq</b> in the larger <b>arr</b> byte array.
*
* @param arr The array in which to search for <b>seq</b>
* @param seq The sequence of bytes to search in the larger <b>arr</b> array
* @return The index at which the given sequence starts in the <b>arr</b> array, or -1 if the sequence was not found
*/
public static int byteArrayIndexOf(byte[] arr, byte[] seq) {
for(int i = 0; i < arr.length - seq.length + 1; i++){
boolean match = true;
for(int j = 0; j < seq.length; j++){
if(arr[i + j] != seq[j]){
match = false;
if(j > 1)
i += j - 1;
break;
}
}
if(match)
return i;
}
return -1;
}
}

@ -0,0 +1,121 @@
/*
* 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.proxy.util;
import org.omegazero.net.socket.SocketConnection;
public class ProxyUtil {
/**
* Checks if the given <b>hostname</b> matches the expression (<b>expr</b>) containing the wildcard character '<code>*</code>'. The wildcard character matches any
* character, including '<code>.</code>' (dot). If no wildcard character is used in <b>expr</b>, this function behaves exactly the same as
* {@link String#equals(Object)}.<br>
* <br>
* <b>Examples:</b>
* <table>
* <tr>
* <td><b>expr</b></td>
* <td><b>hostname</b></td>
* <td><b>Return value</b></td>
* </tr>
* <tr>
* <td>*.example.com</td>
* <td>foo.example.com</td>
* <td><code>true</code></td>
* </tr>
* <tr>
* <td>*.example.com</td>
* <td>foo.example.org</td>
* <td><code>false</code></td>
* </tr>
* <tr>
* <td>subdomain.*.net</td>
* <td>subdomain.example.net</td>
* <td><code>true</code></td>
* </tr>
* <tr>
* <td>*subdomain.*.net</td>
* <td>othersubdomain.example.net</td>
* <td><code>true</code></td>
* </tr>
* </table>
*
* @param expr
* @param hostname
* @return <code>true</code> if the given hostname matches the expression
* @implNote This function currently cannot handle certain edge cases, for example: expr = <code>a.n*n.a</code> and hostname = <code>a.nnnn.a</code> returns
* <code>false</code>, even though it should return <code>true</code>. Given that such a hostname expression is quite unlikely to be used in actual
* configurations, this is fine for now.
*/
public static boolean hostMatches(String expr, String hostname) {
int exprlen = expr.length();
int hnlen = hostname.length();
int exprindex = 0;
int hnindex = 0;
int lastbranch = -1;
boolean reset = false;
while(true){
if(hnindex >= hnlen)
break;
char exprchar = expr.charAt(exprindex);
if(exprchar == '*'){
if(exprindex < exprlen - 1 && hnindex < hnlen - 1){
if(expr.charAt(exprindex + 1) == hostname.charAt(hnindex + 1)){
lastbranch = exprindex;
exprindex++;
}
hnindex++;
}else if(exprindex < exprlen - 1 && hnindex == hnlen - 1){ // at the end of hostname string, but not at end of expr -> missing characters
return false;
}else // at end of expr or hostname and expr is *
hnindex++;
}else if(exprchar == hostname.charAt(hnindex)){
exprindex++;
hnindex++;
}else
reset = true;
if(exprindex >= exprlen && hnindex < hnlen)
reset = true;
if(reset){
if(lastbranch >= 0){
exprindex = lastbranch;
hnindex++;
}else
return false;
reset = false;
}
}
return true;
}
/**
* Checks if the <b>writeStream</b> (the connection where data is being written to) is buffering write calls ({@link SocketConnection#isWritable()} returns
* <code>false</code>). If that is the case, reads from the <b>readStream</b> will be blocked using {@link SocketConnection#setReadBlock(boolean)} until the
* <b>writeStream</b> is writable again.<br>
* <br>
* This method uses the <code>onWrite</code> callback of the <code>SocketConnection</code>, which should not be used when this function is in use.
*
* @param writeStream The connection where data is being written to
* @param readStream The connection where reads should be blocked until <b>writeStream</b> is writable again
*/
public static void handleBackpressure(SocketConnection writeStream, SocketConnection readStream) {
if(!writeStream.isWritable()){
readStream.setReadBlock(true);
writeStream.setOnWritable(() -> {
readStream.setReadBlock(false);
writeStream.setOnWritable(null);
});
}
}
}