Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6166110
Bound vote listener executor queues
BenCodez Aug 18, 2026
ba94b1d
Add optional V1 rejection mode
BenCodez Aug 18, 2026
33bdd19
Bound PROXY and CONNECT header parsing
BenCodez Aug 18, 2026
b42b81c
Add Bukkit DisableV1 setting
BenCodez Aug 18, 2026
20b92b6
Add proxy DisableV1 setting
BenCodez Aug 18, 2026
319eb7b
Add Velocity DisableV1 setting
BenCodez Aug 18, 2026
95df993
Document V1 compatibility control
BenCodez Aug 18, 2026
f4d31da
Document proxy V1 compatibility control
BenCodez Aug 18, 2026
a5bf3b1
Add shared vote protocol policy
BenCodez Aug 18, 2026
97a233a
Apply Bukkit protocol policy
BenCodez Aug 18, 2026
a2248dd
Apply proxy protocol policy
BenCodez Aug 18, 2026
a0c1076
Apply Velocity protocol policy
BenCodez Aug 18, 2026
c42afd3
Wire listener to shared protocol policy
BenCodez Aug 18, 2026
8f53c0e
Test V1 policy and bounded listener queue
BenCodez Aug 18, 2026
a219ca7
Test bounded proxy and CONNECT parsing
BenCodez Aug 18, 2026
9dfb9b3
Preserve Bukkit config source line endings
BenCodez Aug 18, 2026
63bfff6
Preserve Bungee config source line endings
BenCodez Aug 18, 2026
eb58436
Preserve Velocity config source line endings
BenCodez Aug 18, 2026
627b92e
Preserve Bukkit config resource line endings
BenCodez Aug 18, 2026
e4931fd
Preserve proxy config resource line endings
BenCodez Aug 18, 2026
4de3574
Restore existing vote timeout log contract
BenCodez Aug 18, 2026
1b66f1e
Use a valid V1 username length in security test
BenCodez Aug 18, 2026
9eb7f34
Refresh DisableV1 policy on Bukkit reload
BenCodez Aug 18, 2026
ed69d28
Read fragmented V1 packets through the parser
BenCodez Aug 18, 2026
378fc73
Test fragmented V1 packet handling
BenCodez Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.nio.file.Files;
import java.util.Set;

import com.vexsoftware.votifier.net.VoteProtocolPolicy;

import lombok.Getter;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
Expand Down Expand Up @@ -36,6 +38,7 @@ public void load() {
try {
data = ConfigurationProvider.getProvider(YamlConfiguration.class)
.load(new File(bungee.getDataFolder(), "bungeeconfig.yml"));
VoteProtocolPolicy.setDisableV1(getDisableV1());
} catch (IOException e) {
e.printStackTrace();
}
Expand Down Expand Up @@ -78,6 +81,10 @@ public boolean getTokenSupport() {
return getData().getBoolean("TokenSupport", false);
}

public boolean getDisableV1() {
return getData().getBoolean("DisableV1", false);
}

public String getToken(String key) {
return getData().getString("tokens." + key, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.bencodez.simpleapi.file.annotation.ConfigDataKeys;
import com.bencodez.simpleapi.file.annotation.ConfigDataString;
import com.vexsoftware.votifier.VotifierPlus;
import com.vexsoftware.votifier.net.VoteProtocolPolicy;

import lombok.Getter;
import lombok.Setter;
Expand All @@ -27,6 +28,13 @@ public Config(VotifierPlus plugin) {
public void loadValues() {
new AnnotationHandler().load(getData(), this);
debug = DebugLevel.getDebug(debugLevelStr);
VoteProtocolPolicy.setDisableV1(disableV1);
Comment thread
BenCodez marked this conversation as resolved.
}

@Override
public void reloadData() {
super.reloadData();
VoteProtocolPolicy.setDisableV1(getData().getBoolean("DisableV1", false));
}

@Override
Expand Down Expand Up @@ -80,6 +88,11 @@ public void onFileCreation() {
@Setter
private boolean tokenSupport = false;

@ConfigDataBoolean(path = "DisableV1")
@Getter
@Setter
private boolean disableV1 = false;

public ConfigurationSection getForwardingConfiguration(String s) {
return getData().getConfigurationSection("Forwarding." + s);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@
*/
package com.vexsoftware.votifier.net;

import java.io.ByteArrayOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.PushbackInputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

import lombok.Getter;
import lombok.Setter;

public class ProxyHeaderProcessor {

private static final int MAX_PROXY_V1_HEADER_BYTES = 107;
private static final int MAX_CONNECT_LINE_BYTES = 8192;
private static final int MAX_CONNECT_HEADERS = 100;
private static final int MAX_CONNECT_HEADER_BYTES = 32768;
private static final int HEADER_READ_TIMEOUT_MILLIS = 5000;
private static final int DISCARD_BUFFER_BYTES = 1024;

private static final byte[] PROXY_V1_PREFIX = "PROXY".getBytes(StandardCharsets.US_ASCII);
private static final byte[] CONNECT_PREFIX = "CONNECT".getBytes(StandardCharsets.US_ASCII);
private static final byte[] PROXY_V2_SIGNATURE = new byte[] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55,
0x49, 0x54, 0x0A };

Expand All @@ -27,109 +40,218 @@ public static class ProxyHeaderResult {

public ProxyHeaderResult process(PushbackInputStream in, BufferedWriter writer, VoteReceiver receiver)
throws Exception {
ProxyHeaderResult result = new ProxyHeaderResult();
return process(in, writer, receiver, null);
}

byte[] headerPeek = new byte[32];
int bytesRead = in.read(headerPeek);
if (bytesRead <= 0) {
return result;
}
public ProxyHeaderResult process(PushbackInputStream in, BufferedWriter writer, VoteReceiver receiver, Socket socket)
throws Exception {
int previousTimeout = socket == null ? 0 : socket.getSoTimeout();
long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(HEADER_READ_TIMEOUT_MILLIS);

String headerString = new String(headerPeek, 0, bytesRead, StandardCharsets.US_ASCII);
try {
return processWithDeadline(in, writer, receiver, socket, deadlineNanos);
} finally {
if (socket != null && !socket.isClosed()) {
try {
socket.setSoTimeout(previousTimeout);
} catch (SocketException ex) {
receiver.debug(ex);
}
}
}
}

if (headerString.startsWith("PROXY") && !headerString.contains("CONNECT")) {
in.unread(headerPeek, 0, bytesRead);
private ProxyHeaderResult processWithDeadline(PushbackInputStream in, BufferedWriter writer, VoteReceiver receiver,
Socket socket, long deadlineNanos) throws Exception {
ProxyHeaderResult result = new ProxyHeaderResult();
byte[] prefix = new byte[16];
int bytesRead = readPrefix(in, prefix, 1, socket, deadlineNanos);
if (bytesRead == 0) {
return result;
}

String proxyHeader = readLine(in);
receiver.debug("Discarded PROXY (v1) header: " + proxyHeader);
if (prefix[0] == PROXY_V1_PREFIX[0]) {
bytesRead = readPrefix(in, prefix, PROXY_V1_PREFIX.length, bytesRead, socket, deadlineNanos);
if (bytesRead == PROXY_V1_PREFIX.length && startsWith(prefix, bytesRead, PROXY_V1_PREFIX)) {
in.unread(prefix, 0, bytesRead);
String proxyHeader = readLine(in, socket, deadlineNanos, MAX_PROXY_V1_HEADER_BYTES, null,
"PROXY protocol v1 header exceeds " + MAX_PROXY_V1_HEADER_BYTES + " bytes");
receiver.debug("Discarded PROXY (v1) header: " + proxyHeader);

String[] parts = proxyHeader.split("\\s+");
if (parts.length >= 3) {
String srcIp = parts[2].trim();
if (!srcIp.isEmpty()) {
result.setRealIp(srcIp);
String[] parts = proxyHeader.split("\\s+");
if (parts.length >= 3) {
String srcIp = parts[2].trim();
if (!srcIp.isEmpty()) {
result.setRealIp(srcIp);
}
}
return result;
}
return result;
}

if (bytesRead >= 16 && isProxyV2(headerPeek)) {
int addrLength = ((headerPeek[14] & 0xFF) << 8) | (headerPeek[15] & 0xFF);
int totalLength = 16 + addrLength;
int remaining = totalLength - bytesRead;

if (remaining > 0) {
byte[] discard = new byte[remaining];
int read = 0;
while (read < remaining) {
int r = in.read(discard, read, remaining - read);
if (r == -1) {
if (prefix[0] == CONNECT_PREFIX[0]) {
bytesRead = readPrefix(in, prefix, CONNECT_PREFIX.length, bytesRead, socket, deadlineNanos);
if (bytesRead == CONNECT_PREFIX.length && startsWith(prefix, bytesRead, CONNECT_PREFIX)) {
in.unread(prefix, 0, bytesRead);
int[] totalHeaderBytes = new int[1];
String connectLine = readLine(in, socket, deadlineNanos, MAX_CONNECT_LINE_BYTES, totalHeaderBytes,
"HTTP CONNECT header line exceeds " + MAX_CONNECT_LINE_BYTES + " bytes");
receiver.debug("Received CONNECT request: " + connectLine);

int headerCount = 0;
while (true) {
String line = readLine(in, socket, deadlineNanos, MAX_CONNECT_LINE_BYTES, totalHeaderBytes,
"HTTP CONNECT header line exceeds " + MAX_CONNECT_LINE_BYTES + " bytes");
if (line.isEmpty()) {
break;
}
read += r;
if (++headerCount > MAX_CONNECT_HEADERS) {
throw new InvalidVoteException("Too many HTTP CONNECT headers");
}
receiver.debug("Discarding header: " + line);
}

if (read != remaining) {
throw new Exception("Incomplete PROXY protocol v2 header");
}
writer.write("HTTP/1.1 200 Connection Established\r\n\r\n");
writer.flush();
return result;
}
}

receiver.debug("Discarded PROXY protocol v2 header (" + totalLength + " bytes)");
return result;
if ((prefix[0] & 0xFF) == (PROXY_V2_SIGNATURE[0] & 0xFF)) {
bytesRead = readPrefix(in, prefix, 16, bytesRead, socket, deadlineNanos);
if (bytesRead < 16 && matchesPrefix(prefix, bytesRead, PROXY_V2_SIGNATURE)) {
throw new InvalidVoteException("Incomplete PROXY protocol v2 header");
}
if (bytesRead == 16 && startsWith(prefix, bytesRead, PROXY_V2_SIGNATURE)) {
int addressLength = ((prefix[14] & 0xFF) << 8) | (prefix[15] & 0xFF);
discardFully(in, addressLength, socket, deadlineNanos);
receiver.debug("Discarded PROXY protocol v2 header (" + (16 + addressLength) + " bytes)");
return result;
}
}

if (headerString.startsWith("CONNECT")) {
in.unread(headerPeek, 0, bytesRead);
in.unread(prefix, 0, bytesRead);
return result;
}

String connectLine = readLine(in);
receiver.debug("Received CONNECT request: " + connectLine);
private int readPrefix(PushbackInputStream in, byte[] prefix, int targetLength, Socket socket, long deadlineNanos)
throws Exception {
return readPrefix(in, prefix, targetLength, 0, socket, deadlineNanos);
}

String line;
while (!(line = readLine(in)).isEmpty()) {
receiver.debug("Discarding header: " + line);
private int readPrefix(PushbackInputStream in, byte[] prefix, int targetLength, int offset, Socket socket,
long deadlineNanos) throws Exception {
int read = offset;
while (read < targetLength) {
int value = readByteWithDeadline(in, socket, deadlineNanos);
if (value == -1) {
break;
}

writer.write("HTTP/1.1 200 Connection Established\r\n\r\n");
writer.flush();
return result;
prefix[read++] = (byte) value;
}

in.unread(headerPeek, 0, bytesRead);
return result;
return read;
}

private boolean isProxyV2(byte[] header) {
for (int i = 0; i < PROXY_V2_SIGNATURE.length; i++) {
if (header[i] != PROXY_V2_SIGNATURE[i]) {
return false;
private void discardFully(PushbackInputStream in, int length, Socket socket, long deadlineNanos) throws Exception {
byte[] discard = new byte[Math.min(DISCARD_BUFFER_BYTES, Math.max(1, length))];
int remaining = length;
while (remaining > 0) {
int read = readWithDeadline(in, discard, 0, Math.min(discard.length, remaining), socket, deadlineNanos);
if (read == -1) {
throw new InvalidVoteException("Incomplete PROXY protocol v2 header");
}
remaining -= read;
}
return true;
}

private String readLine(PushbackInputStream in) throws Exception {
ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream();
int b;
boolean seenCR = false;
private String readLine(PushbackInputStream in, Socket socket, long deadlineNanos, int maxLineBytes,
int[] totalBytes, String overflowMessage) throws Exception {
ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(Math.min(128, maxLineBytes));
int lineBytes = 0;

while (true) {
int value = readByteWithDeadline(in, socket, deadlineNanos);
if (value == -1) {
throw new InvalidVoteException("Unexpected end of stream while reading proxy/tunnel headers");
}

while ((b = in.read()) != -1) {
if (b == '\r') {
seenCR = true;
continue;
lineBytes++;
if (lineBytes > maxLineBytes) {
throw new InvalidVoteException(overflowMessage);
}
incrementTotalBytes(totalBytes);

if (b == '\n') {
if (value == '\n') {
break;
}

if (seenCR) {
in.unread(b);
if (value == '\r') {
int next = readByteWithDeadline(in, socket, deadlineNanos);
if (next == -1) {
throw new InvalidVoteException("Unexpected end of stream while reading proxy/tunnel headers");
}

lineBytes++;
if (lineBytes > maxLineBytes) {
throw new InvalidVoteException(overflowMessage);
}
incrementTotalBytes(totalBytes);

if (next != '\n') {
throw new InvalidVoteException("Invalid line ending in proxy/tunnel headers");
}
break;
}

lineBuffer.write(b);
lineBuffer.write(value);
}

return lineBuffer.toString(StandardCharsets.US_ASCII.name()).trim();
}

private void incrementTotalBytes(int[] totalBytes) throws InvalidVoteException {
if (totalBytes != null && ++totalBytes[0] > MAX_CONNECT_HEADER_BYTES) {
throw new InvalidVoteException("HTTP CONNECT headers exceed " + MAX_CONNECT_HEADER_BYTES + " bytes");
}
}

return lineBuffer.toString("ASCII").trim();
private int readByteWithDeadline(PushbackInputStream in, Socket socket, long deadlineNanos) throws Exception {
setRemainingTimeout(socket, deadlineNanos);
return in.read();
}

private int readWithDeadline(PushbackInputStream in, byte[] buffer, int offset, int length, Socket socket,
long deadlineNanos) throws Exception {
setRemainingTimeout(socket, deadlineNanos);
return in.read(buffer, offset, length);
}

private void setRemainingTimeout(Socket socket, long deadlineNanos) throws Exception {
long remainingNanos = deadlineNanos - System.nanoTime();
if (remainingNanos <= 0) {
throw new SocketTimeoutException("Timed out reading proxy/tunnel headers");
}

if (socket != null) {
long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(remainingNanos);
if (TimeUnit.MILLISECONDS.toNanos(timeoutMillis) < remainingNanos) {
timeoutMillis++;
}
socket.setSoTimeout((int) Math.max(1L, Math.min(Integer.MAX_VALUE, timeoutMillis)));
}
}

private boolean startsWith(byte[] data, int dataLength, byte[] expected) {
return dataLength >= expected.length && matchesPrefix(data, expected.length, expected);
}

private boolean matchesPrefix(byte[] data, int dataLength, byte[] expected) {
int length = Math.min(dataLength, expected.length);
for (int i = 0; i < length; i++) {
if (data[i] != expected[i]) {
return false;
}
}
return true;
}
}
}
Loading
Loading