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