Initial commit
This commit is contained in:
commit
53a826d340
.gitignoreLICENSEREADME.md
src/org/omegazero/proxy
config
core
http
net
resources
util
24
.gitignore
vendored
Normal file
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
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
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# omz-proxy3
|
||||
|
||||
|
85
src/org/omegazero/proxy/config/ConfigArray.java
Normal file
85
src/org/omegazero/proxy/config/ConfigArray.java
Normal file
@ -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++);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
203
src/org/omegazero/proxy/config/ConfigObject.java
Normal file
203
src/org/omegazero/proxy/config/ConfigObject.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
254
src/org/omegazero/proxy/config/ProxyConfiguration.java
Normal file
254
src/org/omegazero/proxy/config/ProxyConfiguration.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
71
src/org/omegazero/proxy/core/Defaults.java
Normal file
71
src/org/omegazero/proxy/core/Defaults.java
Normal file
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
728
src/org/omegazero/proxy/core/Proxy.java
Normal file
728
src/org/omegazero/proxy/core/Proxy.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/org/omegazero/proxy/core/ProxyEvents.java
Normal file
74
src/org/omegazero/proxy/core/ProxyEvents.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
192
src/org/omegazero/proxy/core/ProxyKeyManager.java
Normal file
192
src/org/omegazero/proxy/core/ProxyKeyManager.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
135
src/org/omegazero/proxy/core/ProxyMain.java
Normal file
135
src/org/omegazero/proxy/core/ProxyMain.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
26
src/org/omegazero/proxy/core/State.java
Normal file
26
src/org/omegazero/proxy/core/State.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
139
src/org/omegazero/proxy/http/HTTPCommon.java
Normal file
139
src/org/omegazero/proxy/http/HTTPCommon.java
Normal file
@ -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";
|
||||
}
|
||||
}
|
42
src/org/omegazero/proxy/http/HTTPEngine.java
Normal file
42
src/org/omegazero/proxy/http/HTTPEngine.java
Normal file
@ -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);
|
||||
}
|
118
src/org/omegazero/proxy/http/HTTPErrdoc.java
Normal file
118
src/org/omegazero/proxy/http/HTTPErrdoc.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
388
src/org/omegazero/proxy/http/HTTPMessage.java
Normal file
388
src/org/omegazero/proxy/http/HTTPMessage.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
41
src/org/omegazero/proxy/http/HTTPMessageData.java
Normal file
41
src/org/omegazero/proxy/http/HTTPMessageData.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
459
src/org/omegazero/proxy/http/engineimpl/HTTP1.java
Normal file
459
src/org/omegazero/proxy/http/engineimpl/HTTP1.java
Normal file
@ -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());
|
||||
}
|
||||
}
|
58
src/org/omegazero/proxy/net/UpstreamServer.java
Normal file
58
src/org/omegazero/proxy/net/UpstreamServer.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
20
src/org/omegazero/proxy/resources/errdoc.html
Normal file
20
src/org/omegazero/proxy/resources/errdoc.html
Normal file
@ -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>
|
43
src/org/omegazero/proxy/util/ArrayUtil.java
Normal file
43
src/org/omegazero/proxy/util/ArrayUtil.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
121
src/org/omegazero/proxy/util/ProxyUtil.java
Normal file
121
src/org/omegazero/proxy/util/ProxyUtil.java
Normal file
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user