001/*
002 * Copyright 2022-2025 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.core;
018
019import javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.ThreadSafe;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.Set;
027
028import static com.soklet.core.Utilities.trimAggressively;
029import static com.soklet.core.Utilities.trimAggressivelyToEmpty;
030import static com.soklet.core.Utilities.trimAggressivelyToNull;
031import static java.lang.String.format;
032import static java.util.Objects.requireNonNull;
033
034/**
035 * Encapsulates <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related HTTP request data.
036 * <p>
037 * Data for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">non-preflight CORS</a> requests is represented by {@link Cors}.
038 * <p>
039 * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for detailed documentation.
040 *
041 * @author <a href="https://www.revetkn.com">Mark Allen</a>
042 */
043@ThreadSafe
044public class CorsPreflight {
045        @Nonnull
046        private final String origin;
047        @Nullable
048        private final HttpMethod accessControlRequestMethod;
049        @Nonnull
050        private final Set<String> accessControlRequestHeaders;
051
052        /**
053         * Constructs a CORS <strong>preflight</strong> request representation for the given HTTP request data.
054         * <p>
055         * CORS preflight requests always have method {@code OPTIONS} and specify their target method via
056         * the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method">{@code Access-Control-Request-Method}</a> header value.
057         *
058         * @param origin                     HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin">{@code Origin}</a> request header value
059         * @param accessControlRequestMethod HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method">{@code Access-Control-Request-Method}</a> request header value
060         */
061        public CorsPreflight(@Nonnull String origin,
062                                                                                         @Nonnull HttpMethod accessControlRequestMethod) {
063                this(requireNonNull(origin), requireNonNull(accessControlRequestMethod), null);
064        }
065
066        /**
067         * Constructs a CORS <strong>preflight</strong> request representation for the given HTTP request data.
068         * <p>
069         * CORS preflight requests always have method {@code OPTIONS} and specify their target method via
070         * the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method">{@code Access-Control-Request-Method}</a> request value.
071         *
072         * @param origin                      HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin">{@code Origin}</a> request header value
073         * @param accessControlRequestMethod  HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method">{@code Access-Control-Request-Method}</a> request header value
074         * @param accessControlRequestHeaders the optional set of HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers">{@code Access-Control-Request-Headers}</a> request header values
075         */
076        public CorsPreflight(@Nonnull String origin,
077                                                                                         @Nonnull HttpMethod accessControlRequestMethod,
078                                                                                         @Nullable Set<String> accessControlRequestHeaders) {
079                requireNonNull(origin);
080                requireNonNull(accessControlRequestMethod);
081
082                this.origin = origin;
083                this.accessControlRequestMethod = accessControlRequestMethod;
084                this.accessControlRequestHeaders = accessControlRequestHeaders == null ?
085                                Set.of() : Set.copyOf(accessControlRequestHeaders);
086        }
087
088        /**
089         * Extracts a <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight request</a> representation from the given HTTP request data.
090         * <p>
091         * Note that only HTTP {@code OPTIONS} requests qualify to be CORS preflight requests.
092         *
093         * @param headers the request headers
094         * @return the CORS preflight data for this request, or {@link Optional#empty()} if insufficent data is present
095         */
096        @Nonnull
097        public static Optional<CorsPreflight> fromHeaders(@Nonnull Map<String, Set<String>> headers) {
098                requireNonNull(headers);
099
100                Set<String> originHeaderValues = headers.get("Origin");
101
102                if (originHeaderValues == null || originHeaderValues.size() == 0)
103                        return Optional.empty();
104
105                String originHeaderValue = trimAggressivelyToNull(originHeaderValues.stream().findFirst().orElse(null));
106
107                if (originHeaderValue == null)
108                        return Optional.empty();
109
110                Set<String> accessControlRequestMethodHeaderValues = headers.get("Access-Control-Request-Method");
111
112                if (accessControlRequestMethodHeaderValues == null)
113                        accessControlRequestMethodHeaderValues = Set.of();
114
115                List<HttpMethod> accessControlRequestMethods = accessControlRequestMethodHeaderValues.stream()
116                                .filter(headerValue -> {
117                                        headerValue = trimAggressivelyToEmpty(headerValue);
118
119                                        try {
120                                                HttpMethod.valueOf(headerValue);
121                                                return true;
122                                        } catch (Exception ignored) {
123                                                return false;
124                                        }
125                                })
126                                .map((headerValue -> HttpMethod.valueOf(trimAggressively(headerValue))))
127                                .toList();
128
129                Set<String> accessControlRequestHeaderValues = headers.get("Access-Control-Request-Header");
130
131                if (accessControlRequestHeaderValues == null)
132                        accessControlRequestHeaderValues = Set.of();
133
134                return Optional.of(new CorsPreflight(originHeaderValue,
135                                accessControlRequestMethods.size() > 0 ? accessControlRequestMethods.get(0) : null,
136                                accessControlRequestHeaderValues));
137        }
138
139        @Override
140        @Nonnull
141        public String toString() {
142                return format("%s{origin=%s, accessControlRequestMethod=%s, accessControlRequestHeaders=%s}",
143                                getClass().getSimpleName(), getOrigin(), getAccessControlRequestMethod(), getAccessControlRequestHeaders());
144        }
145
146        @Override
147        public boolean equals(@Nullable Object object) {
148                if (this == object)
149                        return true;
150
151                if (!(object instanceof CorsPreflight cors))
152                        return false;
153
154                return Objects.equals(getOrigin(), cors.getOrigin())
155                                && Objects.equals(getAccessControlRequestMethod(), cors.getAccessControlRequestMethod())
156                                && Objects.equals(getAccessControlRequestHeaders(), cors.getAccessControlRequestHeaders());
157        }
158
159        @Override
160        public int hashCode() {
161                return Objects.hash(getOrigin(), getAccessControlRequestMethod(), getAccessControlRequestHeaders());
162        }
163
164        /**
165         * Returns the HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin">{@code Origin}</a> request header value.
166         *
167         * @return the header value
168         */
169        @Nonnull
170        public String getOrigin() {
171                return this.origin;
172        }
173
174        /**
175         * The HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method">{@code Access-Control-Request-Method}</a> request header value.
176         *
177         * @return the header value
178         */
179        @Nonnull
180        public HttpMethod getAccessControlRequestMethod() {
181                return this.accessControlRequestMethod;
182        }
183
184        /**
185         * Returns the set of values for the HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers">{@code Access-Control-Request-Headers}</a> request header.
186         *
187         * @return the set of header values, or the empty set if not present
188         */
189        @Nonnull
190        public Set<String> getAccessControlRequestHeaders() {
191                return this.accessControlRequestHeaders;
192        }
193}