001/*
002 * Copyright 2024-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet.servlet.jakarta;
018
019import com.soklet.QueryFormat;
020import com.soklet.Request;
021import com.soklet.Utilities;
022import com.soklet.Utilities.EffectiveOriginResolver;
023import com.soklet.Utilities.EffectiveOriginResolver.TrustPolicy;
024import jakarta.servlet.AsyncContext;
025import jakarta.servlet.DispatcherType;
026import jakarta.servlet.RequestDispatcher;
027import jakarta.servlet.ServletConnection;
028import jakarta.servlet.ServletContext;
029import jakarta.servlet.ServletException;
030import jakarta.servlet.ServletInputStream;
031import jakarta.servlet.ServletRequest;
032import jakarta.servlet.ServletResponse;
033import jakarta.servlet.http.Cookie;
034import jakarta.servlet.http.HttpServletRequest;
035import jakarta.servlet.http.HttpServletResponse;
036import jakarta.servlet.http.HttpSession;
037import jakarta.servlet.http.HttpUpgradeHandler;
038import jakarta.servlet.http.Part;
039import org.jspecify.annotations.NonNull;
040import org.jspecify.annotations.Nullable;
041
042import javax.annotation.concurrent.NotThreadSafe;
043import java.io.BufferedReader;
044import java.io.ByteArrayInputStream;
045import java.io.IOException;
046import java.io.InputStream;
047import java.io.InputStreamReader;
048import java.io.UnsupportedEncodingException;
049import java.net.InetAddress;
050import java.net.InetSocketAddress;
051import java.net.URI;
052import java.nio.charset.Charset;
053import java.nio.charset.IllegalCharsetNameException;
054import java.nio.charset.StandardCharsets;
055import java.nio.charset.UnsupportedCharsetException;
056import java.security.Principal;
057import java.time.Instant;
058import java.time.ZoneOffset;
059import java.time.format.DateTimeFormatter;
060import java.time.format.DateTimeFormatterBuilder;
061import java.time.format.SignStyle;
062import java.time.temporal.ChronoField;
063import java.util.ArrayList;
064import java.util.Collection;
065import java.util.Collections;
066import java.util.Enumeration;
067import java.util.HashMap;
068import java.util.LinkedHashMap;
069import java.util.LinkedHashSet;
070import java.util.List;
071import java.util.Locale;
072import java.util.Map;
073import java.util.Map.Entry;
074import java.util.Optional;
075import java.util.Set;
076import java.util.UUID;
077import java.util.function.Predicate;
078
079import static java.lang.String.format;
080import static java.util.Locale.ROOT;
081import static java.util.Locale.US;
082import static java.util.Locale.getDefault;
083import static java.util.Objects.requireNonNull;
084
085/**
086 * Soklet integration implementation of {@link HttpServletRequest}.
087 *
088 * @author <a href="https://www.revetkn.com">Mark Allen</a>
089 */
090@NotThreadSafe
091public final class SokletHttpServletRequest implements HttpServletRequest {
092        @NonNull
093        private static final Charset DEFAULT_CHARSET;
094        @NonNull
095        private static final DateTimeFormatter RFC_1123_PARSER;
096        @NonNull
097        private static final DateTimeFormatter RFC_1036_PARSER;
098        @NonNull
099        private static final DateTimeFormatter ASCTIME_PARSER;
100        @NonNull
101        private static final String SESSION_COOKIE_NAME;
102        @NonNull
103        private static final String SESSION_URL_PARAM;
104
105        static {
106                DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; // Per Servlet spec
107                RFC_1123_PARSER = DateTimeFormatter.RFC_1123_DATE_TIME;
108                // RFC 1036: spaces between day/month/year + 2-digit year reduced to 19xx baseline.
109                RFC_1036_PARSER = new DateTimeFormatterBuilder()
110                                .parseCaseInsensitive()
111                                .appendPattern("EEE, dd MMM ")
112                                .appendValueReduced(ChronoField.YEAR, 2, 2, 1900) // 94 -> 1994
113                                .appendPattern(" HH:mm:ss zzz")
114                                .toFormatter(US)
115                                .withZone(ZoneOffset.UTC);
116
117                // asctime: "EEE MMM  d HH:mm:ss yyyy" — allow 1 or 2 spaces before day, no zone in text → default GMT.
118                ASCTIME_PARSER = new DateTimeFormatterBuilder()
119                                .parseCaseInsensitive()
120                                .appendPattern("EEE MMM")
121                                .appendLiteral(' ')
122                                .optionalStart().appendLiteral(' ').optionalEnd() // tolerate double space before single-digit day
123                                .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
124                                .appendPattern(" HH:mm:ss yyyy")
125                                .toFormatter(US)
126                                .withZone(ZoneOffset.UTC);
127
128                SESSION_COOKIE_NAME = "JSESSIONID";
129                SESSION_URL_PARAM = "jsessionid";
130        }
131
132        @NonNull
133        private final Request request;
134        @Nullable
135        private final String host;
136        @Nullable
137        private final Integer port;
138        @NonNull
139        private final ServletContext servletContext;
140        @Nullable
141        private HttpSession httpSession;
142        @NonNull
143        private final Map<@NonNull String, @NonNull Object> attributes;
144        @NonNull
145        private final List<@NonNull Cookie> cookies;
146        @Nullable
147        private Charset charset;
148        @Nullable
149        private String contentType;
150        @Nullable
151        private Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters;
152        @Nullable
153        private Map<@NonNull String, @NonNull Set<@NonNull String>> formParameters;
154        private boolean parametersAccessed;
155        private boolean bodyParametersAccessed;
156        private boolean sessionCreated;
157        @NonNull
158        private final TrustPolicy forwardedHeaderTrustPolicy;
159        @Nullable
160        private final Predicate<@NonNull InetSocketAddress> trustedProxyPredicate;
161        @Nullable
162        private final Boolean allowOriginFallback;
163        @Nullable
164        private SokletServletInputStream servletInputStream;
165        @Nullable
166        private BufferedReader reader;
167        @NonNull
168        private RequestReadMethod requestReadMethod;
169        @NonNull
170        private final String requestId;
171        @NonNull
172        private final ServletConnection servletConnection;
173
174        @NonNull
175        public static SokletHttpServletRequest fromRequest(@NonNull Request request) {
176                requireNonNull(request);
177                return withRequest(request).build();
178        }
179
180        @NonNull
181        public static Builder withRequest(@NonNull Request request) {
182                return new Builder(request);
183        }
184
185        private SokletHttpServletRequest(@NonNull Builder builder) {
186                requireNonNull(builder);
187                requireNonNull(builder.request);
188
189                this.request = builder.request;
190                this.attributes = new HashMap<>();
191                this.cookies = parseCookies(request);
192                this.charset = parseCharacterEncoding(request).orElse(null);
193                this.contentType = parseContentType(request).orElse(null);
194                this.host = builder.host;
195                this.port = builder.port;
196                this.servletContext = builder.servletContext == null ? SokletServletContext.fromDefaults() : builder.servletContext;
197                this.httpSession = builder.httpSession;
198                this.forwardedHeaderTrustPolicy = builder.forwardedHeaderTrustPolicy;
199                this.trustedProxyPredicate = builder.trustedProxyPredicate;
200                this.allowOriginFallback = builder.allowOriginFallback;
201                this.requestReadMethod = RequestReadMethod.UNSPECIFIED;
202                this.requestId = UUID.randomUUID().toString();
203                this.servletConnection = buildServletConnection();
204        }
205
206        @NonNull
207        private Request getRequest() {
208                return this.request;
209        }
210
211        @NonNull
212        private Map<@NonNull String, @NonNull Object> getAttributes() {
213                return this.attributes;
214        }
215
216        @NonNull
217        private List<@NonNull Cookie> parseCookies(@NonNull Request request) {
218                requireNonNull(request);
219
220                List<@NonNull Cookie> convertedCookies = new ArrayList<>();
221                Map<@NonNull String, @NonNull Set<@NonNull String>> headers = request.getHeaders();
222
223                for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : headers.entrySet()) {
224                        String headerName = entry.getKey();
225
226                        if (headerName == null || !"cookie".equalsIgnoreCase(headerName.trim()))
227                                continue;
228
229                        Set<@NonNull String> headerValues = entry.getValue();
230
231                        if (headerValues == null)
232                                continue;
233
234                        for (String headerValue : headerValues) {
235                                headerValue = Utilities.trimAggressivelyToNull(headerValue);
236
237                                if (headerValue == null)
238                                        continue;
239
240                                for (String cookieComponent : splitCookieHeaderRespectingQuotes(headerValue)) {
241                                        cookieComponent = Utilities.trimAggressivelyToNull(cookieComponent);
242
243                                        if (cookieComponent == null)
244                                                continue;
245
246                                        String[] cookiePair = cookieComponent.split("=", 2);
247                                        String rawName = Utilities.trimAggressivelyToNull(cookiePair[0]);
248                                        if (cookiePair.length != 2)
249                                                continue;
250
251                                        String rawValue = Utilities.trimAggressivelyToEmpty(cookiePair[1]);
252
253                                        if (rawName == null)
254                                                continue;
255
256                                        String cookieValue = unquoteCookieValueIfNeeded(rawValue);
257                                        convertedCookies.add(new Cookie(rawName, cookieValue));
258                                }
259                        }
260                }
261
262                return convertedCookies;
263        }
264
265        /**
266         * Splits a Cookie header string into components on ';' but ONLY when not inside a quoted value.
267         * Supports backslash-escaped quotes within quoted strings.
268         */
269        @NonNull
270        private static List<@NonNull String> splitCookieHeaderRespectingQuotes(@NonNull String headerValue) {
271                List<@NonNull String> parts = new ArrayList<>();
272                StringBuilder current = new StringBuilder(headerValue.length());
273                boolean inQuotes = false;
274                boolean escape = false;
275
276                for (int i = 0; i < headerValue.length(); i++) {
277                        char c = headerValue.charAt(i);
278
279                        if (escape) {
280                                current.append(c);
281                                escape = false;
282                                continue;
283                        }
284
285                        if (c == '\\') {
286                                escape = true;
287                                current.append(c);
288                                continue;
289                        }
290
291                        if (c == '"') {
292                                inQuotes = !inQuotes;
293                                current.append(c);
294                                continue;
295                        }
296
297                        if (c == ';' && !inQuotes) {
298                                parts.add(current.toString());
299                                current.setLength(0);
300                                continue;
301                        }
302
303                        current.append(c);
304                }
305
306                if (current.length() > 0)
307                        parts.add(current.toString());
308
309                return parts;
310        }
311
312        /**
313         * Splits a header value on the given delimiter, ignoring delimiters inside quoted strings.
314         * Supports backslash-escaped quotes within quoted strings.
315         */
316        @NonNull
317        private static List<@NonNull String> splitHeaderValueRespectingQuotes(@NonNull String headerValue,
318                                                                                                                                                                                                                                                                                                char delimiter) {
319                List<@NonNull String> parts = new ArrayList<>();
320                StringBuilder current = new StringBuilder(headerValue.length());
321                boolean inQuotes = false;
322                boolean escape = false;
323
324                for (int i = 0; i < headerValue.length(); i++) {
325                        char c = headerValue.charAt(i);
326
327                        if (escape) {
328                                current.append(c);
329                                escape = false;
330                                continue;
331                        }
332
333                        if (c == '\\') {
334                                escape = true;
335                                current.append(c);
336                                continue;
337                        }
338
339                        if (c == '"') {
340                                inQuotes = !inQuotes;
341                                current.append(c);
342                                continue;
343                        }
344
345                        if (c == delimiter && !inQuotes) {
346                                parts.add(current.toString());
347                                current.setLength(0);
348                                continue;
349                        }
350
351                        current.append(c);
352                }
353
354                if (current.length() > 0)
355                        parts.add(current.toString());
356
357                return parts;
358        }
359
360        /**
361         * If the cookie value is a quoted-string, remove surrounding quotes and unescape \" \\ and \; .
362         * Otherwise returns the input as-is.
363         */
364        @NonNull
365        private static String unquoteCookieValueIfNeeded(@NonNull String rawValue) {
366                requireNonNull(rawValue);
367
368                if (rawValue.length() >= 2 && rawValue.charAt(0) == '"' && rawValue.charAt(rawValue.length() - 1) == '"') {
369                        String inner = rawValue.substring(1, rawValue.length() - 1);
370                        StringBuilder sb = new StringBuilder(inner.length());
371                        boolean escape = false;
372
373                        for (int i = 0; i < inner.length(); i++) {
374                                char c = inner.charAt(i);
375
376                                if (escape) {
377                                        sb.append(c);
378                                        escape = false;
379                                } else if (c == '\\') {
380                                        escape = true;
381                                } else {
382                                        sb.append(c);
383                                }
384                        }
385
386                        if (escape)
387                                sb.append('\\');
388
389                        return sb.toString();
390                }
391
392                return rawValue;
393        }
394
395        /**
396         * Remove a single pair of surrounding quotes if present.
397         */
398        @NonNull
399        private static String stripOptionalQuotes(@NonNull String value) {
400                requireNonNull(value);
401
402                if (value.length() >= 2) {
403                        char first = value.charAt(0);
404                        char last = value.charAt(value.length() - 1);
405
406                        if ((first == '"' && last == '"') || (first == '\'' && last == '\''))
407                                return value.substring(1, value.length() - 1);
408                }
409
410                return value;
411        }
412
413        @NonNull
414        private Optional<Charset> parseCharacterEncoding(@NonNull Request request) {
415                requireNonNull(request);
416                return Utilities.extractCharsetFromHeaders(request.getHeaders());
417        }
418
419        @NonNull
420        private Optional<String> parseContentType(@NonNull Request request) {
421                requireNonNull(request);
422                return Utilities.extractContentTypeFromHeaders(request.getHeaders());
423        }
424
425        @NonNull
426        private Optional<HttpSession> getHttpSession() {
427                HttpSession current = this.httpSession;
428
429                if (current instanceof SokletHttpSession && ((SokletHttpSession) current).isInvalidated()) {
430                        this.httpSession = null;
431                        return Optional.empty();
432                }
433
434                return Optional.ofNullable(current);
435        }
436
437        private void setHttpSession(@Nullable HttpSession httpSession) {
438                this.httpSession = httpSession;
439        }
440
441        private void touchSession(@NonNull HttpSession httpSession,
442                                                                                                                boolean createdNow) {
443                requireNonNull(httpSession);
444
445                if (httpSession instanceof SokletHttpSession) {
446                        SokletHttpSession sokletSession = (SokletHttpSession) httpSession;
447                        sokletSession.markAccessed();
448
449                        if (!createdNow && !this.sessionCreated)
450                                sokletSession.markNotNew();
451                }
452        }
453
454        @NonNull
455        private Optional<Charset> getCharset() {
456                return Optional.ofNullable(this.charset);
457        }
458
459        @Nullable
460        private Charset getContextRequestCharset() {
461                String encoding = getServletContext().getRequestCharacterEncoding();
462
463                if (encoding == null || encoding.isBlank())
464                        return null;
465
466                try {
467                        return Charset.forName(encoding);
468                } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
469                        return null;
470                }
471        }
472
473        @NonNull
474        private Charset getEffectiveCharset() {
475                Charset explicit = this.charset;
476
477                if (explicit != null)
478                        return explicit;
479
480                Charset context = getContextRequestCharset();
481                return context == null ? DEFAULT_CHARSET : context;
482        }
483
484        @Nullable
485        private Long getContentLengthHeaderValue() {
486                String value = getHeader("Content-Length");
487
488                if (value == null)
489                        return null;
490
491                value = value.trim();
492
493                if (value.isEmpty())
494                        return null;
495
496                try {
497                        long parsed = Long.parseLong(value, 10);
498                        return parsed < 0 ? null : parsed;
499                } catch (NumberFormatException e) {
500                        return null;
501                }
502        }
503
504        private boolean hasContentLengthHeader() {
505                Set<@NonNull String> values = getRequest().getHeaders().get("Content-Length");
506                return values != null && !values.isEmpty();
507        }
508
509        private void setCharset(@Nullable Charset charset) {
510                this.charset = charset;
511        }
512
513        @NonNull
514        private Map<@NonNull String, @NonNull Set<@NonNull String>> getQueryParameters() {
515                if (this.queryParameters != null)
516                        return this.queryParameters;
517
518                String rawQuery = getRequest().getRawQuery().orElse(null);
519
520                if (rawQuery == null || rawQuery.isEmpty()) {
521                        this.queryParameters = Map.of();
522                        return this.queryParameters;
523                }
524
525                Charset charset = getEffectiveCharset();
526                Map<@NonNull String, @NonNull Set<@NonNull String>> parsed =
527                                Utilities.extractQueryParametersFromQuery(rawQuery, QueryFormat.X_WWW_FORM_URLENCODED, charset);
528                this.queryParameters = Collections.unmodifiableMap(parsed);
529                return this.queryParameters;
530        }
531
532        @NonNull
533        private Map<@NonNull String, @NonNull Set<@NonNull String>> getFormParameters() {
534                if (this.formParameters != null)
535                        return this.formParameters;
536
537                if (getRequestReadMethod() != RequestReadMethod.UNSPECIFIED) {
538                        this.formParameters = Map.of();
539                        return this.formParameters;
540                }
541
542                if (this.contentType == null || !this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) {
543                        this.formParameters = Map.of();
544                        return this.formParameters;
545                }
546
547                markBodyParametersAccessed();
548
549                byte[] body = getRequest().getBody().orElse(null);
550
551                if (body == null || body.length == 0) {
552                        this.formParameters = Map.of();
553                        return this.formParameters;
554                }
555
556                String bodyAsString = new String(body, StandardCharsets.ISO_8859_1);
557                Charset charset = getEffectiveCharset();
558                Map<@NonNull String, @NonNull Set<@NonNull String>> parsed =
559                                Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, charset);
560                this.formParameters = Collections.unmodifiableMap(parsed);
561                return this.formParameters;
562        }
563
564        private void markParametersAccessed() {
565                this.parametersAccessed = true;
566        }
567
568        private void markBodyParametersAccessed() {
569                this.bodyParametersAccessed = true;
570        }
571
572        private boolean shouldTrustForwardedHeaders() {
573                if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_ALL)
574                        return true;
575
576                if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_NONE)
577                        return false;
578
579                if (this.trustedProxyPredicate == null)
580                        return false;
581
582                InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null);
583                return remoteAddress != null && this.trustedProxyPredicate.test(remoteAddress);
584        }
585
586        @Nullable
587        private ForwardedClient extractForwardedClientFromHeaders() {
588                Set<@NonNull String> headerValues = getRequest().getHeaders().get("Forwarded");
589
590                if (headerValues == null)
591                        return null;
592
593                for (String headerValue : headerValues) {
594                        ForwardedClient candidate = extractForwardedClientFromHeaderValue(headerValue);
595
596                        if (candidate != null)
597                                return candidate;
598                }
599
600                return null;
601        }
602
603        @Nullable
604        private ForwardedClient extractForwardedClientFromHeaderValue(@Nullable String headerValue) {
605                headerValue = Utilities.trimAggressivelyToNull(headerValue);
606
607                if (headerValue == null)
608                        return null;
609
610                for (String forwardedEntry : splitHeaderValueRespectingQuotes(headerValue, ',')) {
611                        forwardedEntry = Utilities.trimAggressivelyToNull(forwardedEntry);
612
613                        if (forwardedEntry == null)
614                                continue;
615
616                        for (String component : splitHeaderValueRespectingQuotes(forwardedEntry, ';')) {
617                                component = Utilities.trimAggressivelyToNull(component);
618
619                                if (component == null)
620                                        continue;
621
622                                String[] nameValue = component.split("=", 2);
623
624                                if (nameValue.length != 2)
625                                        continue;
626
627                                String name = Utilities.trimAggressivelyToNull(nameValue[0]);
628
629                                if (name == null || !"for".equalsIgnoreCase(name))
630                                        continue;
631
632                                String value = Utilities.trimAggressivelyToNull(nameValue[1]);
633
634                                if (value == null)
635                                        continue;
636
637                                value = stripOptionalQuotes(value);
638                                value = Utilities.trimAggressivelyToNull(value);
639
640                                if (value == null)
641                                        continue;
642
643                                ForwardedClient normalized = parseForwardedForValue(value);
644
645                                if (normalized != null)
646                                        return normalized;
647                        }
648                }
649
650                return null;
651        }
652
653        @Nullable
654        private ForwardedClient parseForwardedForValue(@NonNull String value) {
655                requireNonNull(value);
656
657                String normalized = value.trim();
658
659                if (normalized.isEmpty())
660                        return null;
661
662                if ("unknown".equalsIgnoreCase(normalized) || normalized.startsWith("_"))
663                        return null;
664
665                if (normalized.startsWith("[")) {
666                        int close = normalized.indexOf(']');
667
668                        if (close > 0) {
669                                String host = normalized.substring(1, close);
670
671                                if (host.isEmpty())
672                                        return null;
673
674                                Integer port = null;
675                                String rest = normalized.substring(close + 1).trim();
676
677                                if (!rest.isEmpty()) {
678                                        if (!rest.startsWith(":"))
679                                                return null;
680
681                                        String portToken = Utilities.trimAggressivelyToNull(rest.substring(1));
682
683                                        if (portToken != null) {
684                                                try {
685                                                        port = Integer.parseInt(portToken, 10);
686                                                } catch (Exception ignored) {
687                                                        // Ignore invalid port.
688                                                }
689                                        }
690                                }
691
692                                return new ForwardedClient(host, port);
693                        }
694
695                        return null;
696                }
697
698                int colonCount = 0;
699
700                for (int i = 0; i < normalized.length(); i++) {
701                        if (normalized.charAt(i) == ':')
702                                colonCount++;
703                }
704
705                if (colonCount == 0)
706                        return new ForwardedClient(normalized, null);
707
708                if (colonCount == 1) {
709                        int colon = normalized.indexOf(':');
710                        String host = normalized.substring(0, colon).trim();
711
712                        if (host.isEmpty())
713                                return null;
714
715                        String portToken = Utilities.trimAggressivelyToNull(normalized.substring(colon + 1));
716                        Integer port = null;
717
718                        if (portToken != null) {
719                                try {
720                                        port = Integer.parseInt(portToken, 10);
721                                } catch (Exception ignored) {
722                                        // Ignore invalid port.
723                                }
724                        }
725
726                        return new ForwardedClient(host, port);
727                }
728
729                return new ForwardedClient(normalized, null);
730        }
731
732        @Nullable
733        private ForwardedClient extractXForwardedClientFromHeaders() {
734                Set<@NonNull String> headerValues = getRequest().getHeaders().get("X-Forwarded-For");
735
736                if (headerValues == null)
737                        return null;
738
739                for (String headerValue : headerValues) {
740                        if (headerValue == null)
741                                continue;
742
743                        String[] components = headerValue.split(",");
744
745                        for (String component : components) {
746                                String value = Utilities.trimAggressivelyToNull(component);
747
748                                if (value != null) {
749                                        value = stripOptionalQuotes(value);
750                                        value = Utilities.trimAggressivelyToNull(value);
751
752                                        if (value != null) {
753                                                ForwardedClient normalized = parseForwardedForValue(value);
754
755                                                if (normalized != null)
756                                                        return normalized;
757                                        }
758                                }
759                        }
760                }
761
762                return null;
763        }
764
765        private static final class ForwardedClient {
766                @NonNull
767                private final String host;
768                @Nullable
769                private final Integer port;
770
771                private ForwardedClient(@NonNull String host,
772                                                                                                                @Nullable Integer port) {
773                        this.host = requireNonNull(host);
774                        this.port = port;
775                }
776
777                @NonNull
778                private String getHost() {
779                        return this.host;
780                }
781
782                @Nullable
783                private Integer getPort() {
784                        return this.port;
785                }
786        }
787
788        private static final class SokletServletConnection implements ServletConnection {
789                @NonNull
790                private final String connectionId;
791                @NonNull
792                private final String protocol;
793                @NonNull
794                private final String protocolConnectionId;
795                private final boolean secure;
796
797                private SokletServletConnection(@NonNull String connectionId,
798                                                                                                                                                @NonNull String protocol,
799                                                                                                                                                @NonNull String protocolConnectionId,
800                                                                                                                                                boolean secure) {
801                        this.connectionId = requireNonNull(connectionId);
802                        this.protocol = requireNonNull(protocol);
803                        this.protocolConnectionId = requireNonNull(protocolConnectionId);
804                        this.secure = secure;
805                }
806
807                @Override
808                @NonNull
809                public String getConnectionId() {
810                        return this.connectionId;
811                }
812
813                @Override
814                @NonNull
815                public String getProtocol() {
816                        return this.protocol;
817                }
818
819                @Override
820                @NonNull
821                public String getProtocolConnectionId() {
822                        return this.protocolConnectionId;
823                }
824
825                @Override
826                public boolean isSecure() {
827                        return this.secure;
828                }
829        }
830
831        @NonNull
832        private Optional<String> getHost() {
833                return Optional.ofNullable(this.host);
834        }
835
836        @NonNull
837        private Optional<Integer> getPort() {
838                return Optional.ofNullable(this.port);
839        }
840
841        @NonNull
842        private ServletConnection buildServletConnection() {
843                String connectionId = buildConnectionId();
844                String protocol = normalizeConnectionProtocol(getProtocol());
845                boolean secure = "https".equalsIgnoreCase(getScheme());
846                return new SokletServletConnection(connectionId, protocol, "", secure);
847        }
848
849        @NonNull
850        private String buildConnectionId() {
851                InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null);
852
853                if (remoteAddress == null)
854                        return UUID.randomUUID().toString();
855
856                InetAddress address = remoteAddress.getAddress();
857                String host = address == null ? remoteAddress.getHostString() : address.getHostAddress();
858
859                if (host == null || host.isBlank())
860                        return UUID.randomUUID().toString();
861
862                return host + ":" + remoteAddress.getPort();
863        }
864
865        @NonNull
866        private String normalizeConnectionProtocol(@Nullable String protocol) {
867                if (protocol == null || protocol.isBlank())
868                        return "unknown";
869
870                String normalized = protocol.trim().toLowerCase(ROOT);
871
872                if ("http/2".equals(normalized) || "http/2.0".equals(normalized))
873                        return "h2";
874
875                if ("http/3".equals(normalized) || "http/3.0".equals(normalized))
876                        return "h3";
877
878                if (normalized.startsWith("http/"))
879                        return normalized;
880
881                return "unknown";
882        }
883
884        @NonNull
885        private Optional<String> getEffectiveOrigin() {
886                EffectiveOriginResolver resolver = EffectiveOriginResolver.withRequest(
887                                getRequest(),
888                                this.forwardedHeaderTrustPolicy
889                );
890
891                if (this.trustedProxyPredicate != null)
892                        resolver.trustedProxyPredicate(this.trustedProxyPredicate);
893
894                if (this.allowOriginFallback != null)
895                        resolver.allowOriginFallback(this.allowOriginFallback);
896
897                return Utilities.extractEffectiveOrigin(resolver);
898        }
899
900        @NonNull
901        private Optional<URI> getEffectiveOriginUri() {
902                String effectiveOrigin = getEffectiveOrigin().orElse(null);
903
904                if (effectiveOrigin == null)
905                        return Optional.empty();
906
907                try {
908                        return Optional.of(URI.create(effectiveOrigin));
909                } catch (Exception ignored) {
910                        return Optional.empty();
911                }
912        }
913
914        private int defaultPortForScheme(@Nullable String scheme) {
915                if (scheme == null)
916                        return 0;
917
918                if ("https".equalsIgnoreCase(scheme))
919                        return 443;
920
921                if ("http".equalsIgnoreCase(scheme))
922                        return 80;
923
924                return 0;
925        }
926
927        @NonNull
928        private String stripIpv6Brackets(@NonNull String host) {
929                requireNonNull(host);
930
931                if (host.startsWith("[") && host.endsWith("]") && host.length() > 2)
932                        return host.substring(1, host.length() - 1);
933
934                return host;
935        }
936
937        private boolean isIpv4Literal(@NonNull String value) {
938                requireNonNull(value);
939                String[] parts = value.split("\\.", -1);
940
941                if (parts.length != 4)
942                        return false;
943
944                for (String part : parts) {
945                        if (part.isEmpty())
946                                return false;
947
948                        int acc = 0;
949
950                        for (int i = 0; i < part.length(); i++) {
951                                char c = part.charAt(i);
952                                if (c < '0' || c > '9')
953                                        return false;
954                                acc = acc * 10 + (c - '0');
955                                if (acc > 255)
956                                        return false;
957                        }
958                }
959
960                return true;
961        }
962
963        private boolean isIpv6Literal(@NonNull String value) {
964                requireNonNull(value);
965                return value.indexOf(':') >= 0;
966        }
967
968        @Nullable
969        private String hostFromAuthority(@Nullable String authority) {
970                if (authority == null)
971                        return null;
972
973                String normalized = authority.trim();
974
975                if (normalized.isEmpty())
976                        return null;
977
978                int at = normalized.lastIndexOf('@');
979
980                if (at >= 0)
981                        normalized = normalized.substring(at + 1);
982
983                if (normalized.startsWith("[")) {
984                        int close = normalized.indexOf(']');
985
986                        if (close > 0)
987                                return normalized.substring(1, close);
988
989                        return null;
990                }
991
992                int colon = normalized.indexOf(':');
993                return colon > 0 ? normalized.substring(0, colon) : normalized;
994        }
995
996        @Nullable
997        private Integer portFromAuthority(@Nullable String authority) {
998                if (authority == null)
999                        return null;
1000
1001                String normalized = authority.trim();
1002
1003                if (normalized.isEmpty())
1004                        return null;
1005
1006                int at = normalized.lastIndexOf('@');
1007
1008                if (at >= 0)
1009                        normalized = normalized.substring(at + 1);
1010
1011                if (normalized.startsWith("[")) {
1012                        int close = normalized.indexOf(']');
1013
1014                        if (close > 0 && normalized.length() > close + 1 && normalized.charAt(close + 1) == ':') {
1015                                String portString = normalized.substring(close + 2).trim();
1016
1017                                try {
1018                                        return Integer.parseInt(portString, 10);
1019                                } catch (Exception ignored) {
1020                                        return null;
1021                                }
1022                        }
1023
1024                        return null;
1025                }
1026
1027                int colon = normalized.indexOf(':');
1028
1029                if (colon > 0 && normalized.indexOf(':', colon + 1) == -1) {
1030                        String portString = normalized.substring(colon + 1).trim();
1031
1032                        try {
1033                                return Integer.parseInt(portString, 10);
1034                        } catch (Exception ignored) {
1035                                return null;
1036                        }
1037                }
1038
1039                return null;
1040        }
1041
1042        @NonNull
1043        private Optional<SokletServletInputStream> getServletInputStream() {
1044                return Optional.ofNullable(this.servletInputStream);
1045        }
1046
1047        private void setServletInputStream(@Nullable SokletServletInputStream servletInputStream) {
1048                this.servletInputStream = servletInputStream;
1049        }
1050
1051        @NonNull
1052        private Optional<BufferedReader> getBufferedReader() {
1053                return Optional.ofNullable(this.reader);
1054        }
1055
1056        private void setBufferedReader(@Nullable BufferedReader reader) {
1057                this.reader = reader;
1058        }
1059
1060        @NonNull
1061        private RequestReadMethod getRequestReadMethod() {
1062                return this.requestReadMethod;
1063        }
1064
1065        private void setRequestReadMethod(@NonNull RequestReadMethod requestReadMethod) {
1066                requireNonNull(requestReadMethod);
1067                this.requestReadMethod = requestReadMethod;
1068        }
1069
1070        private enum RequestReadMethod {
1071                UNSPECIFIED,
1072                INPUT_STREAM,
1073                READER
1074        }
1075
1076        /**
1077         * Builder used to construct instances of {@link SokletHttpServletRequest}.
1078         * <p>
1079         * This class is intended for use by a single thread.
1080         *
1081         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1082         */
1083        @NotThreadSafe
1084        public static class Builder {
1085                @NonNull
1086                private Request request;
1087                @Nullable
1088                private Integer port;
1089                @Nullable
1090                private String host;
1091                @Nullable
1092                private ServletContext servletContext;
1093                @Nullable
1094                private HttpSession httpSession;
1095                @NonNull
1096                private TrustPolicy forwardedHeaderTrustPolicy;
1097                @Nullable
1098                private Predicate<@NonNull InetSocketAddress> trustedProxyPredicate;
1099                @Nullable
1100                private Boolean allowOriginFallback;
1101
1102                @NonNull
1103                private Builder(@NonNull Request request) {
1104                        requireNonNull(request);
1105                        this.request = request;
1106                        this.forwardedHeaderTrustPolicy = TrustPolicy.TRUST_NONE;
1107                }
1108
1109                @NonNull
1110                public Builder request(@NonNull Request request) {
1111                        requireNonNull(request);
1112                        this.request = request;
1113                        return this;
1114                }
1115
1116                @NonNull
1117                public Builder host(@Nullable String host) {
1118                        this.host = host;
1119                        return this;
1120                }
1121
1122                @NonNull
1123                public Builder port(@Nullable Integer port) {
1124                        this.port = port;
1125                        return this;
1126                }
1127
1128                @NonNull
1129                public Builder servletContext(@Nullable ServletContext servletContext) {
1130                        this.servletContext = servletContext;
1131                        return this;
1132                }
1133
1134                @NonNull
1135                public Builder httpSession(@Nullable HttpSession httpSession) {
1136                        this.httpSession = httpSession;
1137                        return this;
1138                }
1139
1140                @NonNull
1141                public Builder forwardedHeaderTrustPolicy(@NonNull TrustPolicy forwardedHeaderTrustPolicy) {
1142                        requireNonNull(forwardedHeaderTrustPolicy);
1143                        this.forwardedHeaderTrustPolicy = forwardedHeaderTrustPolicy;
1144                        return this;
1145                }
1146
1147                @NonNull
1148                public Builder trustedProxyPredicate(@Nullable Predicate<@NonNull InetSocketAddress> trustedProxyPredicate) {
1149                        this.trustedProxyPredicate = trustedProxyPredicate;
1150                        return this;
1151                }
1152
1153                @NonNull
1154                public Builder trustedProxyAddresses(@NonNull Set<@NonNull InetAddress> trustedProxyAddresses) {
1155                        requireNonNull(trustedProxyAddresses);
1156                        Set<@NonNull InetAddress> normalizedAddresses = Set.copyOf(trustedProxyAddresses);
1157                        this.trustedProxyPredicate = remoteAddress -> {
1158                                if (remoteAddress == null)
1159                                        return false;
1160
1161                                InetAddress address = remoteAddress.getAddress();
1162                                return address != null && normalizedAddresses.contains(address);
1163                        };
1164                        return this;
1165                }
1166
1167                @NonNull
1168                public Builder allowOriginFallback(@Nullable Boolean allowOriginFallback) {
1169                        this.allowOriginFallback = allowOriginFallback;
1170                        return this;
1171                }
1172
1173                @NonNull
1174                public SokletHttpServletRequest build() {
1175                        if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_PROXY_ALLOWLIST
1176                                        && this.trustedProxyPredicate == null) {
1177                                throw new IllegalStateException(format("%s policy requires a trusted proxy predicate or allowlist.",
1178                                                TrustPolicy.TRUST_PROXY_ALLOWLIST));
1179                        }
1180
1181                        return new SokletHttpServletRequest(this);
1182                }
1183        }
1184
1185        // Implementation of HttpServletRequest methods below:
1186
1187        // Helpful reference at https://stackoverflow.com/a/21046620 by Victor Stafusa - BozoNaCadeia
1188        //
1189        // Method              URL-Decoded Result
1190        // ----------------------------------------------------
1191        // getContextPath()        no      /app
1192        // getLocalAddr()                  127.0.0.1
1193        // getLocalName()                  30thh.loc
1194        // getLocalPort()                  8480
1195        // getMethod()                     GET
1196        // getPathInfo()           yes     /a?+b
1197        // getProtocol()                   HTTP/1.1
1198        // getQueryString()        no      p+1=c+d&p+2=e+f
1199        // getRequestedSessionId() no      S%3F+ID
1200        // getRequestURI()         no      /app/test%3F/a%3F+b;jsessionid=S+ID
1201        // getRequestURL()         no      http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID
1202        // getScheme()                     http
1203        // getServerName()                 30thh.loc
1204        // getServerPort()                 8480
1205        // getServletPath()        yes     /test?
1206        // getParameterNames()     yes     [p 2, p 1]
1207        // getParameter("p 1")     yes     c d
1208
1209        @Override
1210        @Nullable
1211        public String getAuthType() {
1212                // This is legal according to spec
1213                return null;
1214        }
1215
1216        @Override
1217        public @NonNull Cookie @Nullable [] getCookies() {
1218                return this.cookies.isEmpty() ? null : this.cookies.toArray(new Cookie[0]);
1219        }
1220
1221        @Override
1222        public long getDateHeader(@Nullable String name) {
1223                if (name == null)
1224                        return -1;
1225
1226                String value = getHeader(name);
1227
1228                if (value == null)
1229                        return -1;
1230
1231                // Try HTTP-date formats (RFC 1123 → RFC 1036 → asctime)
1232                for (DateTimeFormatter fmt : List.of(RFC_1123_PARSER, RFC_1036_PARSER, ASCTIME_PARSER)) {
1233                        try {
1234                                return Instant.from(fmt.parse(value)).toEpochMilli();
1235                        } catch (Exception ignored) {
1236                                // try next
1237                        }
1238                }
1239
1240                // Fallback: epoch millis
1241                try {
1242                        return Long.parseLong(value);
1243                } catch (NumberFormatException e) {
1244                        throw new IllegalArgumentException(
1245                                        String.format("Header with name '%s' and value '%s' cannot be converted to a date", name, value),
1246                                        e
1247                        );
1248                }
1249        }
1250
1251        @Override
1252        @Nullable
1253        public String getHeader(@Nullable String name) {
1254                if (name == null)
1255                        return null;
1256
1257                Set<@NonNull String> values = getRequest().getHeaders().get(name);
1258
1259                if (values == null || values.isEmpty())
1260                        return null;
1261
1262                return values.iterator().next();
1263        }
1264
1265        @Override
1266        @NonNull
1267        public Enumeration<@NonNull String> getHeaders(@Nullable String name) {
1268                if (name == null)
1269                        return Collections.emptyEnumeration();
1270
1271                Set<@NonNull String> values = request.getHeaders().get(name);
1272                return values == null ? Collections.emptyEnumeration() : Collections.enumeration(values);
1273        }
1274
1275        @Override
1276        @NonNull
1277        public Enumeration<@NonNull String> getHeaderNames() {
1278                return Collections.enumeration(getRequest().getHeaders().keySet());
1279        }
1280
1281        @Override
1282        public int getIntHeader(@Nullable String name) {
1283                if (name == null)
1284                        return -1;
1285
1286                String value = getHeader(name);
1287
1288                if (value == null)
1289                        return -1;
1290
1291                // Throws NumberFormatException if parsing fails, per spec
1292                return Integer.valueOf(value, 10);
1293        }
1294
1295        @Override
1296        @NonNull
1297        public String getMethod() {
1298                return getRequest().getHttpMethod().name();
1299        }
1300
1301        @Override
1302        @Nullable
1303        public String getPathInfo() {
1304                return getRequest().getPath();
1305        }
1306
1307        @Override
1308        @Nullable
1309        public String getPathTranslated() {
1310                return null;
1311        }
1312
1313        @Override
1314        @NonNull
1315        public String getContextPath() {
1316                return "";
1317        }
1318
1319        @Override
1320        @Nullable
1321        public String getQueryString() {
1322                return getRequest().getRawQuery().orElse(null);
1323        }
1324
1325        @Override
1326        @Nullable
1327        public String getRemoteUser() {
1328                // This is legal according to spec
1329                return null;
1330        }
1331
1332        @Override
1333        public boolean isUserInRole(@Nullable String role) {
1334                // This is legal according to spec
1335                return false;
1336        }
1337
1338        @Override
1339        @Nullable
1340        public Principal getUserPrincipal() {
1341                // This is legal according to spec
1342                return null;
1343        }
1344
1345        @Nullable
1346        private String extractRequestedSessionIdFromCookie() {
1347                for (Cookie cookie : this.cookies) {
1348                        String name = cookie.getName();
1349
1350                        if (name != null && SESSION_COOKIE_NAME.equalsIgnoreCase(name)) {
1351                                String value = cookie.getValue();
1352
1353                                if (value != null && !value.isEmpty())
1354                                        return value;
1355                        }
1356                }
1357
1358                return null;
1359        }
1360
1361        @Nullable
1362        private String extractRequestedSessionIdFromUrl() {
1363                String rawPath = getRequest().getRawPath();
1364                int length = rawPath.length();
1365                int index = 0;
1366
1367                while (index < length) {
1368                        int semicolon = rawPath.indexOf(';', index);
1369
1370                        if (semicolon < 0)
1371                                break;
1372
1373                        int nameStart = semicolon + 1;
1374
1375                        if (nameStart >= length)
1376                                break;
1377
1378                        int nameEnd = nameStart;
1379
1380                        while (nameEnd < length) {
1381                                char ch = rawPath.charAt(nameEnd);
1382
1383                                if (ch == '=' || ch == ';' || ch == '/')
1384                                        break;
1385
1386                                nameEnd++;
1387                        }
1388
1389                        if (nameEnd == nameStart) {
1390                                index = nameEnd + 1;
1391                                continue;
1392                        }
1393
1394                        String name = rawPath.substring(nameStart, nameEnd);
1395
1396                        if (!SESSION_URL_PARAM.equalsIgnoreCase(name)) {
1397                                index = nameEnd + 1;
1398                                continue;
1399                        }
1400
1401                        if (nameEnd >= length || rawPath.charAt(nameEnd) != '=') {
1402                                index = nameEnd + 1;
1403                                continue;
1404                        }
1405
1406                        int valueStart = nameEnd + 1;
1407                        int valueEnd = valueStart;
1408
1409                        while (valueEnd < length) {
1410                                char ch = rawPath.charAt(valueEnd);
1411
1412                                if (ch == ';' || ch == '/')
1413                                        break;
1414
1415                                valueEnd++;
1416                        }
1417
1418                        if (valueEnd == valueStart) {
1419                                index = valueEnd + 1;
1420                                continue;
1421                        }
1422
1423                        String value = rawPath.substring(valueStart, valueEnd);
1424
1425                        if (!value.isEmpty())
1426                                return value;
1427
1428                        index = valueEnd + 1;
1429                }
1430
1431                return null;
1432        }
1433
1434        @Override
1435        @Nullable
1436        public String getRequestedSessionId() {
1437                String cookieSessionId = extractRequestedSessionIdFromCookie();
1438
1439                if (cookieSessionId != null)
1440                        return cookieSessionId;
1441
1442                return extractRequestedSessionIdFromUrl();
1443        }
1444
1445        @Override
1446        @NonNull
1447        public String getRequestURI() {
1448                return getRequest().getRawPath();
1449        }
1450
1451        @Override
1452        @NonNull
1453        public StringBuffer getRequestURL() {
1454                String rawPath = getRequest().getRawPath();
1455
1456                if ("*".equals(rawPath))
1457                        return new StringBuffer(rawPath);
1458
1459                // Try forwarded/synthesized absolute prefix first
1460                String effectiveOrigin = getEffectiveOrigin().orElse(null);
1461
1462                if (effectiveOrigin != null)
1463                        return new StringBuffer(format("%s%s", effectiveOrigin, rawPath));
1464
1465                // Fall back to builder-provided host/port when available
1466                String scheme = getScheme(); // Soklet returns "http" by design
1467                String host = getServerName();
1468                int port = getServerPort();
1469                boolean defaultPort = port <= 0 || ("https".equalsIgnoreCase(scheme) && port == 443) || ("http".equalsIgnoreCase(scheme) && port == 80);
1470                String authorityHost = host;
1471
1472                if (host != null && host.indexOf(':') >= 0 && !host.startsWith("[") && !host.endsWith("]"))
1473                        authorityHost = "[" + host + "]";
1474
1475                String authority = defaultPort ? authorityHost : format("%s:%d", authorityHost, port);
1476                return new StringBuffer(format("%s://%s%s", scheme, authority, rawPath));
1477        }
1478
1479        @Override
1480        @NonNull
1481        public String getServletPath() {
1482                // This is legal according to spec
1483                return "";
1484        }
1485
1486        @Override
1487        @Nullable
1488        public HttpSession getSession(boolean create) {
1489                HttpSession currentHttpSession = getHttpSession().orElse(null);
1490                boolean createdNow = false;
1491
1492                if (create && currentHttpSession == null) {
1493                        currentHttpSession = SokletHttpSession.fromServletContext(getServletContext());
1494                        setHttpSession(currentHttpSession);
1495                        this.sessionCreated = true;
1496                        createdNow = true;
1497                }
1498
1499                if (currentHttpSession != null)
1500                        touchSession(currentHttpSession, createdNow);
1501
1502                return currentHttpSession;
1503        }
1504
1505        @Override
1506        @NonNull
1507        public HttpSession getSession() {
1508                HttpSession currentHttpSession = getHttpSession().orElse(null);
1509                boolean createdNow = false;
1510
1511                if (currentHttpSession == null) {
1512                        currentHttpSession = SokletHttpSession.fromServletContext(getServletContext());
1513                        setHttpSession(currentHttpSession);
1514                        this.sessionCreated = true;
1515                        createdNow = true;
1516                }
1517
1518                touchSession(currentHttpSession, createdNow);
1519
1520                return currentHttpSession;
1521        }
1522
1523        @Override
1524        @NonNull
1525        public String changeSessionId() {
1526                HttpSession currentHttpSession = getHttpSession().orElse(null);
1527
1528                if (currentHttpSession == null)
1529                        throw new IllegalStateException("No session is present");
1530
1531                if (!(currentHttpSession instanceof SokletHttpSession))
1532                        throw new IllegalStateException(format("Cannot change session IDs. Session must be of type %s; instead it is of type %s",
1533                                        SokletHttpSession.class.getSimpleName(), currentHttpSession.getClass().getSimpleName()));
1534
1535                UUID newSessionId = UUID.randomUUID();
1536                ((SokletHttpSession) currentHttpSession).setSessionId(newSessionId);
1537                return String.valueOf(newSessionId);
1538        }
1539
1540        @Override
1541        public boolean isRequestedSessionIdValid() {
1542                String requestedSessionId = getRequestedSessionId();
1543
1544                if (requestedSessionId == null)
1545                        return false;
1546
1547                HttpSession currentSession = getHttpSession().orElse(null);
1548
1549                if (currentSession == null)
1550                        return false;
1551
1552                return requestedSessionId.equals(currentSession.getId());
1553        }
1554
1555        @Override
1556        public boolean isRequestedSessionIdFromCookie() {
1557                return extractRequestedSessionIdFromCookie() != null;
1558        }
1559
1560        @Override
1561        public boolean isRequestedSessionIdFromURL() {
1562                if (extractRequestedSessionIdFromCookie() != null)
1563                        return false;
1564
1565                return extractRequestedSessionIdFromUrl() != null;
1566        }
1567
1568        @Deprecated
1569        public boolean isRequestedSessionIdFromUrl() {
1570                return isRequestedSessionIdFromURL();
1571        }
1572
1573        @Override
1574        public boolean authenticate(@NonNull HttpServletResponse httpServletResponse) throws IOException, ServletException {
1575                requireNonNull(httpServletResponse);
1576                // TODO: perhaps revisit this in the future
1577                throw new ServletException("Authentication is not supported");
1578        }
1579
1580        @Override
1581        public void login(@Nullable String username,
1582                                                                                @Nullable String password) throws ServletException {
1583                // This is legal according to spec
1584                throw new ServletException("Authentication login is not supported");
1585        }
1586
1587        @Override
1588        public void logout() throws ServletException {
1589                // This is legal according to spec
1590                throw new ServletException("Authentication logout is not supported");
1591        }
1592
1593        @Override
1594        @NonNull
1595        public Collection<@NonNull Part> getParts() throws IOException, ServletException {
1596                // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize,
1597                // or there is no @MultipartConfig or multipart-config in deployment descriptors
1598                throw new ServletException("Servlet multipart configuration is not supported");
1599        }
1600
1601        @Override
1602        @Nullable
1603        public Part getPart(@Nullable String name) throws IOException, ServletException {
1604                // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize,
1605                // or there is no @MultipartConfig or multipart-config in deployment descriptors
1606                throw new ServletException("Servlet multipart configuration is not supported");
1607        }
1608
1609        @Override
1610        @NonNull
1611        public <T extends HttpUpgradeHandler> T upgrade(@Nullable Class<T> handlerClass) throws IOException, ServletException {
1612                // Legal if the given handlerClass fails to be instantiated
1613                throw new ServletException("HTTP upgrade is not supported");
1614        }
1615
1616        @Override
1617        @Nullable
1618        public Object getAttribute(@Nullable String name) {
1619                if (name == null)
1620                        return null;
1621
1622                return getAttributes().get(name);
1623        }
1624
1625        @Override
1626        @NonNull
1627        public Enumeration<@NonNull String> getAttributeNames() {
1628                return Collections.enumeration(getAttributes().keySet());
1629        }
1630
1631        @Override
1632        @Nullable
1633        public String getCharacterEncoding() {
1634                Charset explicit = getCharset().orElse(null);
1635
1636                if (explicit != null)
1637                        return explicit.name();
1638
1639                Charset context = getContextRequestCharset();
1640                return context == null ? null : context.name();
1641        }
1642
1643        @Override
1644        public void setCharacterEncoding(@Nullable String env) throws UnsupportedEncodingException {
1645                // Note that spec says: "This method must be called prior to reading request parameters or
1646                // reading input using getReader(). Otherwise, it has no effect."
1647                if (this.parametersAccessed || getRequestReadMethod() != RequestReadMethod.UNSPECIFIED)
1648                        return;
1649
1650                if (env == null) {
1651                        setCharset(null);
1652                } else {
1653                        try {
1654                                setCharset(Charset.forName(env));
1655                        } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
1656                                throw new UnsupportedEncodingException(format("Not sure how to handle character encoding '%s'", env));
1657                        }
1658                }
1659
1660                this.queryParameters = null;
1661                this.formParameters = null;
1662        }
1663
1664        @Override
1665        public int getContentLength() {
1666                Long length = getContentLengthHeaderValue();
1667
1668                if (length != null) {
1669                        if (length > Integer.MAX_VALUE)
1670                                return -1;
1671
1672                        return length.intValue();
1673                }
1674
1675                if (hasContentLengthHeader())
1676                        return -1;
1677
1678                byte[] body = getRequest().getBody().orElse(null);
1679
1680                if (body == null || body.length > Integer.MAX_VALUE)
1681                        return -1;
1682
1683                return body.length;
1684        }
1685
1686        @Override
1687        public long getContentLengthLong() {
1688                Long length = getContentLengthHeaderValue();
1689
1690                if (length != null)
1691                        return length;
1692
1693                if (hasContentLengthHeader())
1694                        return -1;
1695
1696                byte[] body = getRequest().getBody().orElse(null);
1697                return body == null ? -1 : body.length;
1698        }
1699
1700        @Override
1701        @Nullable
1702        public String getContentType() {
1703                String headerValue = getHeader("Content-Type");
1704                return headerValue != null ? headerValue : this.contentType;
1705        }
1706
1707        @Override
1708        @NonNull
1709        public ServletInputStream getInputStream() throws IOException {
1710                RequestReadMethod currentReadMethod = getRequestReadMethod();
1711
1712                if (currentReadMethod == RequestReadMethod.UNSPECIFIED) {
1713                        setRequestReadMethod(RequestReadMethod.INPUT_STREAM);
1714                        byte[] body = this.bodyParametersAccessed ? new byte[]{} : getRequest().getBody().orElse(new byte[]{});
1715                        setServletInputStream(SokletServletInputStream.fromInputStream(new ByteArrayInputStream(body)));
1716                        return getServletInputStream().get();
1717                } else if (currentReadMethod == RequestReadMethod.INPUT_STREAM) {
1718                        return getServletInputStream().get();
1719                } else {
1720                        throw new IllegalStateException("getReader() has already been called for this request");
1721                }
1722        }
1723
1724        @Override
1725        @Nullable
1726        public String getParameter(@Nullable String name) {
1727                if (name == null)
1728                        return null;
1729
1730                markParametersAccessed();
1731
1732                Set<@NonNull String> queryValues = getQueryParameters().get(name);
1733
1734                if (queryValues != null && !queryValues.isEmpty())
1735                        return queryValues.iterator().next();
1736
1737                Set<@NonNull String> formValues = getFormParameters().get(name);
1738
1739                if (formValues != null && !formValues.isEmpty())
1740                        return formValues.iterator().next();
1741
1742                return null;
1743        }
1744
1745        @Override
1746        @NonNull
1747        public Enumeration<@NonNull String> getParameterNames() {
1748                markParametersAccessed();
1749
1750                Set<@NonNull String> queryParameterNames = getQueryParameters().keySet();
1751                Set<@NonNull String> formParameterNames = getFormParameters().keySet();
1752
1753                Set<@NonNull String> parameterNames = new LinkedHashSet<>(queryParameterNames.size() + formParameterNames.size());
1754                parameterNames.addAll(queryParameterNames);
1755                parameterNames.addAll(formParameterNames);
1756
1757                return Collections.enumeration(parameterNames);
1758        }
1759
1760        @Override
1761        public @NonNull String @Nullable [] getParameterValues(@Nullable String name) {
1762                if (name == null)
1763                        return null;
1764
1765                markParametersAccessed();
1766
1767                List<@NonNull String> parameterValues = new ArrayList<>();
1768
1769                Set<@NonNull String> queryValues = getQueryParameters().get(name);
1770
1771                if (queryValues != null)
1772                        parameterValues.addAll(queryValues);
1773
1774                Set<@NonNull String> formValues = getFormParameters().get(name);
1775
1776                if (formValues != null)
1777                        parameterValues.addAll(formValues);
1778
1779                return parameterValues.isEmpty() ? null : parameterValues.toArray(new String[0]);
1780        }
1781
1782        @Override
1783        @NonNull
1784        public Map<@NonNull String, @NonNull String @NonNull []> getParameterMap() {
1785                markParametersAccessed();
1786
1787                Map<@NonNull String, @NonNull Set<@NonNull String>> parameterMap = new LinkedHashMap<>();
1788
1789                // Mutable copy of entries
1790                for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : getQueryParameters().entrySet())
1791                        parameterMap.put(entry.getKey(), new LinkedHashSet<>(entry.getValue()));
1792
1793                // Add form parameters to entries
1794                for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : getFormParameters().entrySet()) {
1795                        Set<@NonNull String> existingEntries = parameterMap.get(entry.getKey());
1796
1797                        if (existingEntries != null)
1798                                existingEntries.addAll(entry.getValue());
1799                        else
1800                                parameterMap.put(entry.getKey(), new LinkedHashSet<>(entry.getValue()));
1801                }
1802
1803                Map<@NonNull String, @NonNull String @NonNull []> finalParameterMap = new LinkedHashMap<>();
1804
1805                for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : parameterMap.entrySet())
1806                        finalParameterMap.put(entry.getKey(), entry.getValue().toArray(new String[0]));
1807
1808                return Collections.unmodifiableMap(finalParameterMap);
1809        }
1810
1811        @Override
1812        @NonNull
1813        public String getProtocol() {
1814                return "HTTP/1.1";
1815        }
1816
1817        @Override
1818        @NonNull
1819        public String getScheme() {
1820                URI effectiveOriginUri = getEffectiveOriginUri().orElse(null);
1821
1822                if (effectiveOriginUri != null && effectiveOriginUri.getScheme() != null)
1823                        return effectiveOriginUri.getScheme().trim().toLowerCase(ROOT);
1824
1825                // Honor common reverse-proxy header only when trusted; fall back to http
1826                if (shouldTrustForwardedHeaders()) {
1827                        String proto = getRequest().getHeader("X-Forwarded-Proto").orElse(null);
1828
1829                        if (proto != null) {
1830                                proto = proto.trim().toLowerCase(ROOT);
1831                                if (proto.equals("https") || proto.equals("http"))
1832                                        return proto;
1833                        }
1834                }
1835
1836                return "http";
1837        }
1838
1839        @Override
1840        @NonNull
1841        public String getServerName() {
1842                URI effectiveOriginUri = getEffectiveOriginUri().orElse(null);
1843
1844                if (effectiveOriginUri != null) {
1845                        String host = effectiveOriginUri.getHost();
1846
1847                        if (host == null)
1848                                host = hostFromAuthority(effectiveOriginUri.getAuthority());
1849
1850                        if (host != null) {
1851                                if (host.startsWith("[") && host.endsWith("]") && host.length() > 2)
1852                                        host = host.substring(1, host.length() - 1);
1853
1854                                return host;
1855                        }
1856                }
1857
1858                String hostHeader = getRequest().getHeader("Host").orElse(null);
1859
1860                if (hostHeader != null) {
1861                        String host = hostFromAuthority(hostHeader);
1862
1863                        if (host != null && !host.isBlank())
1864                                return host;
1865                }
1866
1867                return getLocalName();
1868        }
1869
1870        @Override
1871        public int getServerPort() {
1872                URI effectiveOriginUri = getEffectiveOriginUri().orElse(null);
1873
1874                if (effectiveOriginUri != null) {
1875                        int port = effectiveOriginUri.getPort();
1876                        if (port >= 0)
1877                                return port;
1878
1879                        Integer authorityPort = portFromAuthority(effectiveOriginUri.getAuthority());
1880
1881                        if (authorityPort != null)
1882                                return authorityPort;
1883
1884                        return defaultPortForScheme(effectiveOriginUri.getScheme());
1885                }
1886
1887                String hostHeader = getRequest().getHeader("Host").orElse(null);
1888
1889                if (hostHeader != null) {
1890                        Integer hostPort = portFromAuthority(hostHeader);
1891
1892                        if (hostPort != null)
1893                                return hostPort;
1894                }
1895
1896                Integer port = getPort().orElse(null);
1897
1898                if (port != null)
1899                        return port;
1900
1901                int defaultPort = defaultPortForScheme(getScheme());
1902                return defaultPort > 0 ? defaultPort : 0;
1903        }
1904
1905        @Override
1906        @NonNull
1907        public BufferedReader getReader() throws IOException {
1908                RequestReadMethod currentReadMethod = getRequestReadMethod();
1909
1910                if (currentReadMethod == RequestReadMethod.UNSPECIFIED) {
1911                        setRequestReadMethod(RequestReadMethod.READER);
1912                        Charset charset = getEffectiveCharset();
1913                        byte[] body = this.bodyParametersAccessed ? new byte[]{} : getRequest().getBody().orElse(new byte[0]);
1914                        InputStream inputStream = new ByteArrayInputStream(body);
1915                        setBufferedReader(new BufferedReader(new InputStreamReader(inputStream, charset)));
1916                        return getBufferedReader().get();
1917                } else if (currentReadMethod == RequestReadMethod.READER) {
1918                        return getBufferedReader().get();
1919                } else {
1920                        throw new IllegalStateException("getInputStream() has already been called for this request");
1921                }
1922        }
1923
1924        @Override
1925        @Nullable
1926        public String getRemoteAddr() {
1927                if (shouldTrustForwardedHeaders()) {
1928                        ForwardedClient forwardedFor = extractForwardedClientFromHeaders();
1929
1930                        if (forwardedFor != null)
1931                                return forwardedFor.getHost();
1932
1933                        ForwardedClient xForwardedFor = extractXForwardedClientFromHeaders();
1934
1935                        if (xForwardedFor != null)
1936                                return xForwardedFor.getHost();
1937                }
1938
1939                InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null);
1940
1941                if (remoteAddress != null) {
1942                        InetAddress address = remoteAddress.getAddress();
1943                        String host = address != null ? address.getHostAddress() : remoteAddress.getHostString();
1944
1945                        if (host != null && !host.isBlank())
1946                                return host;
1947                }
1948
1949                return null;
1950        }
1951
1952        @Override
1953        @Nullable
1954        public String getRemoteHost() {
1955                // "If the engine cannot or chooses not to resolve the hostname (to improve performance),
1956                // this method returns the dotted-string form of the IP address."
1957                return getRemoteAddr();
1958        }
1959
1960        @Override
1961        public void setAttribute(@Nullable String name,
1962                                                                                                         @Nullable Object o) {
1963                if (name == null)
1964                        return;
1965
1966                if (o == null)
1967                        removeAttribute(name);
1968                else
1969                        getAttributes().put(name, o);
1970        }
1971
1972        @Override
1973        public void removeAttribute(@Nullable String name) {
1974                if (name == null)
1975                        return;
1976
1977                getAttributes().remove(name);
1978        }
1979
1980        @Override
1981        @NonNull
1982        public Locale getLocale() {
1983                List<@NonNull Locale> locales = getRequest().getLocales();
1984                return locales.size() == 0 ? getDefault() : locales.get(0);
1985        }
1986
1987        @Override
1988        @NonNull
1989        public Enumeration<@NonNull Locale> getLocales() {
1990                List<@NonNull Locale> locales = getRequest().getLocales();
1991                return Collections.enumeration(locales.size() == 0 ? List.of(getDefault()) : locales);
1992        }
1993
1994        @Override
1995        public boolean isSecure() {
1996                return getScheme().equals("https");
1997        }
1998
1999        @Override
2000        @Nullable
2001        public RequestDispatcher getRequestDispatcher(@Nullable String path) {
2002                // "This method returns null if the servlet container cannot return a RequestDispatcher."
2003                return null;
2004        }
2005
2006        @Deprecated
2007        @Nullable
2008        public String getRealPath(String path) {
2009                // "As of Version 2.1 of the Java Servlet API, use ServletContext.getRealPath(java.lang.String) instead."
2010                return getServletContext().getRealPath(path);
2011        }
2012
2013        @Override
2014        public int getRemotePort() {
2015                if (shouldTrustForwardedHeaders()) {
2016                        ForwardedClient forwardedFor = extractForwardedClientFromHeaders();
2017
2018                        if (forwardedFor != null) {
2019                                Integer port = forwardedFor.getPort();
2020                                return port == null ? 0 : port;
2021                        }
2022
2023                        ForwardedClient xForwardedFor = extractXForwardedClientFromHeaders();
2024
2025                        if (xForwardedFor != null) {
2026                                Integer port = xForwardedFor.getPort();
2027                                return port == null ? 0 : port;
2028                        }
2029                }
2030
2031                InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null);
2032                return remoteAddress == null ? 0 : remoteAddress.getPort();
2033        }
2034
2035        @Override
2036        @NonNull
2037        public String getLocalName() {
2038                String host = getHost().orElse(null);
2039
2040                if (host != null && !host.isBlank())
2041                        return stripIpv6Brackets(host);
2042
2043                return "localhost";
2044        }
2045
2046        @Override
2047        @NonNull
2048        public String getLocalAddr() {
2049                String host = getHost().orElse(null);
2050
2051                if (host != null) {
2052                        String normalized = stripIpv6Brackets(host).trim();
2053
2054                        if (!normalized.isEmpty() && (isIpv4Literal(normalized) || isIpv6Literal(normalized)))
2055                                return normalized;
2056                }
2057
2058                return "127.0.0.1";
2059        }
2060
2061        @Override
2062        public int getLocalPort() {
2063                Integer port = getPort().orElse(null);
2064                return port == null ? 0 : port;
2065        }
2066
2067        @Override
2068        @NonNull
2069        public ServletContext getServletContext() {
2070                return this.servletContext;
2071        }
2072
2073        @Override
2074        @NonNull
2075        public AsyncContext startAsync() throws IllegalStateException {
2076                throw new IllegalStateException("Soklet does not support async servlet operations");
2077        }
2078
2079        @Override
2080        @NonNull
2081        public AsyncContext startAsync(@NonNull ServletRequest servletRequest,
2082                                                                                                                                 @NonNull ServletResponse servletResponse) throws IllegalStateException {
2083                requireNonNull(servletRequest);
2084                requireNonNull(servletResponse);
2085
2086                throw new IllegalStateException("Soklet does not support async servlet operations");
2087        }
2088
2089        @Override
2090        public boolean isAsyncStarted() {
2091                return false;
2092        }
2093
2094        @Override
2095        public boolean isAsyncSupported() {
2096                return false;
2097        }
2098
2099        @Override
2100        @NonNull
2101        public AsyncContext getAsyncContext() {
2102                throw new IllegalStateException("Soklet does not support async servlet operations");
2103        }
2104
2105        @Override
2106        @NonNull
2107        public DispatcherType getDispatcherType() {
2108                // Currently Soklet does not support RequestDispatcher, so this is safe to hardcode
2109                return DispatcherType.REQUEST;
2110        }
2111
2112        @Override
2113        @NonNull
2114        public String getRequestId() {
2115                return this.requestId;
2116        }
2117
2118        @Override
2119        @NonNull
2120        public String getProtocolRequestId() {
2121                return "";
2122        }
2123
2124        @Override
2125        @NonNull
2126        public ServletConnection getServletConnection() {
2127                return this.servletConnection;
2128        }
2129}