Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/gribbit/auth/Cookie.java
### 代码内容：
package gribbit.auth;

import gribbit.server.config.GribbitProperties;
import gribbit.util.RandomTokenGenerator;
import gribbit.util.WebUtils;
import io.netty.handler.codec.http.DefaultCookie;
import io.netty.handler.codec.http.ServerCookieEncoder;

/**
 * Cookies!
 */
public class Cookie {

    private final String name;

    private final String path;

    /** The unencoded cookie value. */
    private String value;

    private long maxAgeSeconds;

    private boolean discardAtEndOfBrowserSession;

    // -----------------------------------------------------------------------------------------------------

    /** The name of the email address cookie. Used to notify the Persona client as to who is logged in. */
    public static final String EMAIL_COOKIE_NAME = "_email";

    /** The name of the flash cookie. */
    public static final String FLASH_COOKIE_NAME = "_flash";

    /** The name of the cookie that indicates the auth-required URI the user was trying to visit before logging in. */
    public static final String REDIRECT_AFTER_LOGIN_COOKIE_NAME = "_redir";

    // -----------------------------------------------------------------------------------------------------

    /** The name of the session cookie. */
    public static final String SESSION_COOKIE_NAME = "_session";

    /** How long a session cookie lasts for. */
    public static final int SESSION_COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;

    /** Session cookie length (number of random bytes generated before base 64 encoding) */
    public static final int SESSION_COOKIE_LENGTH = 20;

    public static String generateRandomSessionToken() {
        return RandomTokenGenerator.generateRandomTokenBase64(Cookie.SESSION_COOKIE_LENGTH);
    }

    // ------------------------------------------------------------------------------------------------------

    // Valid characters for cookie fields and values
    private static final boolean[] VALID_CHAR = new boolean[256];
    static {
        for (int i = 33; i <= 126; i++)
            VALID_CHAR[i] = true;
        for (char c : new char[] { '\'', '"', ',', ';', '\\' })
            VALID_CHAR[c] = false;
    }

    // Check the chars in a cookie's name and path are valid
    private static void checkValidCookieFieldStr(String str) {
        if (str.length() > 3500) {
            throw new RuntimeException("Cookie value too long: " + str);
        }
        for (int i = 0, n = str.length(); i < n; i++) {
            char c = str.charAt(i);
            if (c > 255 || !VALID_CHAR[c]) {
                throw new RuntimeException("Invalid cookie field: " + str);
            }
        }
    }

    // -----------------------------------------------------------------------------------------------------

    /**
     * Create a cookie.
     */
    public Cookie(String name, String path, String cookieValue, long maxAgeSeconds, boolean discardAtEndOfBrowserSession) {
        this.name = name;
        checkValidCookieFieldStr(name);
        this.path = path;
        if (path != null) {
            checkValidCookieFieldStr(path);
        }
        this.value = cookieValue;
        this.maxAgeSeconds = maxAgeSeconds;
        this.discardAtEndOfBrowserSession = discardAtEndOfBrowserSession;

        if (this.maxAgeSeconds <= 0 && this.maxAgeSeconds != Long.MIN_VALUE) {
            // If maxAge <= 0, cookie is expired immediately (so there is nothing to encode)
            this.value = "";
            this.maxAgeSeconds = 0;
        } else {
            // if maxAge == Long.MIN_VALUE or discardAtEndOfBrowserSession is true, cookie expires at end of session
            if (maxAgeSeconds == Long.MIN_VALUE) {
                this.discardAtEndOfBrowserSession = true;
            } else if (this.discardAtEndOfBrowserSession) {
                this.maxAgeSeconds = Long.MIN_VALUE;
            }
        }
    }

    /**
     * Create a cookie with the discard flag set to false (cookie is not discarded when browser session closes).
     */
    public Cookie(String name, String path, String cookieValue, long maxAgeInSeconds) {
        this(name, path, cookieValue, maxAgeInSeconds, false);
    }

    /**
     * Create a cookie with path unset (meaning, according to the HTTP spec, it will default to the path of the object
     * currently being requested), and the discard flag set to false (cookie is not discarded when browser session
     * closes).
     */
    public Cookie(String name, String cookieValue, long maxAgeInSeconds) {
        this(name, null, cookieValue, maxAgeInSeconds, false);
    }

    // -----------------------------------------------------------------------------------------------------

    /**
     * Parse a cookie from a Netty Cookie. Will throw an exception if cookie decoding failed for some reason (in this
     * case, ignore the cookie).
     */
    public Cookie(io.netty.handler.codec.http.Cookie nettyCookie) {
        this.name = nettyCookie.name();
        this.path = nettyCookie.path();
        this.value = WebUtils.unescapeCookieValue(nettyCookie.value());
        this.maxAgeSeconds = nettyCookie.maxAge();
        this.discardAtEndOfBrowserSession = nettyCookie.isDiscard();
    }

    /** Create a Netty cookie from this Cookie object. */
    public io.netty.handler.codec.http.Cookie toNettyCookie() {
        io.netty.handler.codec.http.Cookie nettyCookie = new DefaultCookie(name, WebUtils.escapeCookieValue(value));
        if (path != null && !path.isEmpty()) {
            nettyCookie.setPath(path);
        }
        nettyCookie.setMaxAge(maxAgeSeconds);
        nettyCookie.setDiscard(discardAtEndOfBrowserSession);
        nettyCookie.setHttpOnly(true);  // TODO
        if (GribbitProperties.SSL) {
            nettyCookie.setSecure(true);  // TODO
        }
        return nettyCookie;
    }

    // -----------------------------------------------------------------------------------------------------

    /**
     * Create a cookie that, if set in response, overwrites and deletes the named cookie (by setting maxAgeSeconds to
     * zero). Have to specify the path since there can be multiple cookies with the same name but with different paths;
     * this will only delete the cookie with the matching path.
     */
    public static Cookie deleteCookie(String name, String path) {
        return new Cookie(name, path, "", 0, false);
    }

    /**
     * Create a cookie that, if set in response, overwrites and deletes the cookie with the same name and path (by
     * setting maxAgeSeconds to zero).
     */
    public static Cookie deleteCookie(Cookie cookie) {
        return new Cookie(cookie.getName(), cookie.getPath(), "", 0, false);
    }

    // -----------------------------------------------------------------------------------------------------

    /**
     * Get the cookie as an HTTP header string, including all cookie headers, with the value escaped or base64-encoded.
     */
    @Override
    public String toString() {
        return ServerCookieEncoder.encode(toNettyCookie());
    }

    /** Get the name of the cookie. */
    public String getName() {
        return name;
    }

    /** Get the cookie path, or "" if the cookie path is not set. */
    public String getPath() {
        return path == null ? "" : path;
    }

    /** Get unencoded value of cookie. */
    public String getValue() {
        return value;
    }

    /** Return true if the cookie has expired. */
    public boolean hasExpired() {
        return maxAgeSeconds <= 0 && maxAgeSeconds != Long.MIN_VALUE;
    }
}

## 内容2
### 文件路径：
src/gribbit/auth/User.java
### 代码内容：
package gribbit.auth;

import gribbit.auth.User.Token.TokenType;
import gribbit.exception.UnauthorizedException;
import gribbit.model.DBModelStringKey;
import gribbit.request.Request;
import gribbit.response.Response;
import gribbit.server.GribbitServer;
import gribbit.server.siteresources.Database;
import gribbit.util.AppException;
import gribbit.util.Hash;
import gribbit.util.WebUtils;

import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;

import org.mongojack.MongoCollection;

/**
 * Used to store user identity and authentication information in the database.
 */
@MongoCollection(name = "users")
public class User extends DBModelStringKey {

    /** Key/value data for user */
    public HashMap<String, String> data;

    public String passwordHash;

    public String createdDate;

    public Boolean emailValidated;

    /**
     * Auth token, stored in both encrypted session-in-client cookie and server. Allows for browser cookie to be
     * revoked.
     */
    public Token sessionTok;

    /** Store CSRF token in User object to avoid re-calculating it where possible */
    public String csrfTok;

    /** Token used for validating email address. */
    public Token emailValidationTok;

    /** Token used for resetting password. */
    public Token passwordResetTok;

    // -----------------------------------------------------------------------------------------------------------------

    // Placeholder in password field for federated logins
    public static final String FEDERATED_LOGIN_PASSWORD_HASH_PLACEHOLDER = "FEDERATED LOGIN";

    // -----------------------------------------------------------------------------------------------------------------

    public User() {
    }

    public User(String email) {
        super(email);
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** An authentication token, a password reset token or an email address verification token */
    public static class Token {
        public String token;
        public Long expires;
        public Token.TokenType tokType;

        public enum TokenType {
            SESSION, PW_RESET, EMAIL_VERIF
        };

        public Token() {
        }

        public Token(Token.TokenType tokType, Instant expires) {
            if (expires.isBefore(Instant.now())) {
                throw new IllegalArgumentException("Token already expired");
            }
            this.tokType = tokType;
            // Generate token as random base64-encoded number
            this.token = Cookie.generateRandomSessionToken();
            this.expires = expires.toEpochMilli();
        }

        public Token(Token.TokenType tokType, int numSecondsValid) {
            this(tokType, Instant.now().plus(numSecondsValid, ChronoUnit.SECONDS));
        }

        public boolean hasExpired() {
            return expires == null || expires < Instant.now().toEpochMilli();
        }

        @Override
        public String toString() {
            return tokType + "\t" + token + "\t" + expires;
        }
    }

    /**
     * Take an encrypted token, decrypt it, extract the username and token, look up the user, and make sure that the
     * copy of the auth token in the user matches the copy in the encrypted token, and that the token has not expired.
     * Throws an exception if any of this fails. Returns the user if it all succeeds.
     */
    // FIXME: remove this, and store tokens in user
    public static User validateTok(String email, String suppliedToken, TokenType tokType) throws Exception {
        if (email.isEmpty() || email.equals("null") || email.indexOf('@') < 0) {
            throw new AppException("Invalid email address");
        }
        if (suppliedToken == null || suppliedToken.isEmpty()) {
            throw new AppException("Invalid token");
        }

        // Look up user with this email addr
        User user = User.findByEmail(email);
        if (user == null) {
            throw new AppException("User account does not exist");
        }

        switch (tokType) {

        case SESSION:
            if (user.sessionTok == null || user.sessionTok.hasExpired() || !user.sessionTok.token.equals(suppliedToken)) {
                // Clear token if there is a mismatch, this will prevent users that manage to crack the cookie
                // encryption key from doing much, because they would also have to guess the token to log in.
                // Each attempt to guess the auth token will log them out and require them to log in successfully
                // to generate a new token. The account cannot be accessed using any old session-in-client cookie,
                // because there is no auth token on the server until the next successful login.
                user.clearSessionTok();
            } else {
                // User exists and token is valid; return user
                return user;
            }
            break;

        case EMAIL_VERIF:
            if (user.emailValidationTok == null || user.emailValidationTok.hasExpired()
                    || !user.emailValidationTok.token.equals(suppliedToken)) {
                // Clear token if there is a mismatch, this means that if a user uses an old email validation link,
                // it will invalidate the most recent link.
                user.clearEmailValidationTok();
            } else {
                // User exists and token is valid; return user
                return user;
            }
            break;

        case PW_RESET:
            if (user.passwordResetTok == null || user.passwordResetTok.hasExpired()
                    || !user.passwordResetTok.token.equals(suppliedToken)) {
                // Clear token if there is a mismatch, this means that if a user uses an old password reset link,
                // it will invalidate the most recent link.
                user.clearPasswordResetTok();
            } else {
                // User exists and token is valid; return user
                return user;
            }
            break;

        default:
            break;
        }
        throw new AppException("Token has expired or does not match");
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Puts a key-value pair into the user data HashMap. N.B. does not save user, you need to do that manually once all
     * key-value pairs have been put.
     */
    public void putData(String key, String val) {
        if (this.data == null) {
            this.data = new HashMap<>();
        }
        this.data.put(key, val);
    }

    /** Gets the value corresponding to a given key in the user data HashMap, or null if not found. */
    public String getData(String key) {
        return this.data == null ? null : this.data.get(key);
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Return true if auth token is absent for user or has expired */
    public boolean sessionTokHasExpired() {
        return sessionTok == null || sessionTok.hasExpired();
    }

    /**
     * Create a new email validation token, store it in the user's account, and return the token. Expires after 1 day.
     */
    public String generateNewEmailValidationTok() {
        emailValidationTok = new Token(TokenType.EMAIL_VERIF, 1);
        emailValidated = false;
        save();
        return emailValidationTok.token;
    }

    /**
     * Create a new password reset token, store it in the user's account, and return the token. Expires after 1 day.
     */
    public String generateNewPasswordResetTok() {
        passwordResetTok = new Token(TokenType.PW_RESET, 1);
        save();
        return passwordResetTok.token;
    }

    /** Clear the auth token, meaning the user will have to log in again to get another token. */
    public void clearSessionTok() {
        sessionTok = null;
        csrfTok = null;
        save();
    }

    /** Clear the password reset token, and save user account */
    public void clearPasswordResetTok() {
        passwordResetTok = null;
        save();
    }

    /** Clear the email validation token, and save user account */
    public void clearEmailValidationTok() {
        emailValidationTok = null;
        save();
    }

    public void markEmailValidated() {
        emailValidated = true;
        save();
    }

    public boolean emailIsValidated() {
        return emailValidated != null && emailValidated;
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Change a user's password, forcing other clients to be logged out, then log this client in with a new
     * authentication token.
     *
     * @throws AppException
     *             if new password is too short or invalid, or if user is not logged in, or session has expired.
     * @throws UnauthorizedException
     *             if user is not whitelisted for login
     */
    public void changePassword(String newPassword, Response response) throws UnauthorizedException, AppException {
        if (sessionTokHasExpired()) {
            throw new UnauthorizedException("Session has expired");
        }
        // Re-hash user password
        passwordHash = Hash.hashPassword(newPassword);
        // Generate a new session token and save user.
        // Invalidates current session, forcing other clients to be logged out.
        logIn(response);
    }

    // -----------------------------------------------------------------------------------------------------------------

    public static User findByEmail(String email) {
        if (email == null || email.isEmpty())
            return null;
        String emailNormalized = WebUtils.validateAndNormalizeEmailAddr(email);
        if (emailNormalized == null)
            return null;
        return Database.findOneById(User.class, email);
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Authenticate a user based on values of POST params "email" and "password".
     *
     * @return User if successfully authenticated, null otherwise
     */
    public static User authenticate(String email, String password, Response response) throws UnauthorizedException {
        // FIXME: Allow only one login attempt per email address every 5 seconds. Add email addrs to a ConcurrentTreeSet
        // or something (*if* the email addr is already in the database, to prevent DoS), and every 5s, purge old
        // entries from the tree. If an attempt is made in less than 5s, then return an error rather than blocking for
        // up to 5s, again to prevent DoS.
        User user = findByEmail(email);
        if (user != null) {
            // Get the hash password from the salt + clear password (N.B. It takes 80ms to run BCrypt)
            if (!FEDERATED_LOGIN_PASSWORD_HASH_PLACEHOLDER.equals(user.passwordHash)
                    && Hash.checkPassword(password, user.passwordHash)) {
                // User successfully authenticated.
                user.logIn(response);
                return user;
            }
        }
        // If user was not successfully logged in for some reason, delete cookie to be extra paranoid
        user.logOut(response);
        return null;
    }

    /**
     * Decrypt the session cookie in the HttpRequest, look up the user, and return the user if the user's auth token is
     * valid (i.e. if their session has not expired or been revoked).
     *
     * @return Returns null if session is invalid or user is no longer allowed to log in.
     */
    public static User getLoggedInUser(Request req) {
        // Get email address from cookie
        Cookie emailCookie = req.getCookie(Cookie.EMAIL_COOKIE_NAME);
        if (emailCookie != null && !emailCookie.hasExpired()) {
            String email = emailCookie.getValue();

            // Check user against login whitelist, if it exists (in case whitelist has changed)
            if (GribbitServer.loginWhitelistChecker == null
                    || GribbitServer.loginWhitelistChecker.allowUserToLogin(email)) {

                // Get session cookie
                Cookie sessionCookie = req.getCookie(Cookie.SESSION_COOKIE_NAME);
                if (sessionCookie != null && !sessionCookie.hasExpired()) {
                    try {
                        // Look up email address in database, and check session cookie against session token stored in
                        // the database for that email address
                        User user = validateTok(email, sessionCookie.getValue(), TokenType.SESSION);
                        // If no exception thrown, user is logged in and auth token is valid
                        return user;

                    } catch (Exception e) {
                    }
                }
            }
        }
        return null;
    }

    /** Delete session cookies */
    public static void removeLoginCookies(Response response) {
        response.deleteCookie(Cookie.SESSION_COOKIE_NAME);
        response.deleteCookie(Cookie.EMAIL_COOKIE_NAME);
    }

    /**
     * Invalidate all current login sessions for this user.
     */
    public void logOut(Response response) {
        clearSessionTok();
        removeLoginCookies(response);
    }

    /**
     * See if there is a logged in user, and log them out if they are logged in.
     */
    public static void logOutUser(Request request, Response response) {
        User user = getLoggedInUser(request);
        if (user != null) {
            user.logOut(response);
        } else {
            // If no logged in user, just delete the session cookies
            removeLoginCookies(response);
        }
    }

    /**
     * Create a new authentication token for user and save session cookie.
     *
     * @throws UnauthorizedException
     *             if the user is not whitelisted for login, or their login session has expired.
     */
    public void logIn(Response response) throws UnauthorizedException {
        // Check user against login whitelist, if it exists
        if (GribbitServer.loginWhitelistChecker == null || GribbitServer.loginWhitelistChecker.allowUserToLogin(id)) {

            // Create new session token
            sessionTok = new Token(TokenType.SESSION, Cookie.SESSION_COOKIE_MAX_AGE_SECONDS);
            csrfTok = CSRF.generateRandomCSRFToken();
            save();
            if (sessionTokHasExpired()) {
                // Shouldn't happen, since we just created session tok, but just in case
                clearSessionTok();
                throw new UnauthorizedException("Couldn't create auth session");
            }

            // Save login cookies in result
            response.setCookie(new Cookie(Cookie.SESSION_COOKIE_NAME, "/", sessionTok.token,
                    Cookie.SESSION_COOKIE_MAX_AGE_SECONDS));
            response.setCookie(new Cookie(Cookie.EMAIL_COOKIE_NAME, "/", id, Cookie.SESSION_COOKIE_MAX_AGE_SECONDS));

        } else {
            // User is not authorized
            throw new UnauthorizedException("User is not whitelisted for login: " + id);
        }
    }

    // -----------------------------------------------------------------------------------------------------

    /**
     * Create a user and log them in.
     *
     * @throws UnauthorizedException
     *             if a user with this email addr already exists.
     */
    private static User create(String email, String passwordHash, boolean validateEmail, Response response)
            throws UnauthorizedException {
        // Check user against login whitelist, if it exists
        if (GribbitServer.loginWhitelistChecker == null || GribbitServer.loginWhitelistChecker.allowUserToLogin(email)) {

            // Check if a user of this name already exists, and if not, create user record in database.
            // Should probably be a transaction, although if the record is created twice within a
            // short period of time, one of the two account creation operations will simply have its
            // authorization cookie overwritten by the other, so the first session will be logged out.
            // Either way, users creating an account with the given email address must be in control
            // of that email account, so this is still secure.
            if (findByEmail(email) != null) {
                throw new UnauthorizedException("Could not create new user: user \"" + email + "\" already exists");
            }

            User user = new User(email);

            user.passwordHash = passwordHash;

            user.createdDate = ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
            user.emailValidated = validateEmail;

            // Log in and save user
            user.logIn(response);

            return user;

        } else {
            // User is not authorized
            throw new UnauthorizedException("User is not whitelisted for account creation: " + email);
        }
    }

    /**
     * Create a user from email and password hash, and log them in.
     *
     * @throws UnauthorizedException
     *             if a user with this email addr already exists.
     */
    public static User create(String email, String passwordHash, Response response) throws UnauthorizedException {
        return create(email, passwordHash, /* validateEmail = */false, response);
    }

    /**
     * Create a user from a Persona login, and log them in.
     *
     * @throws UnauthorizedException
     *             if a user with this email addr already exists.
     */
    public static User createFederatedLoginUser(String email, Response response) throws UnauthorizedException {
        return create(email, FEDERATED_LOGIN_PASSWORD_HASH_PLACEHOLDER, /* validateEmail = */true, response);
    }
}

## 内容3
### 文件路径：
src/gribbit/request/HttpRequestHandler.java
### 代码内容：
package gribbit.request;

import static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;
import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.SERVER;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderNames.DATE;
import static io.netty.handler.codec.http.HttpHeaderNames.ETAG;
import static io.netty.handler.codec.http.HttpHeaderNames.EXPECT;
import static io.netty.handler.codec.http.HttpHeaderNames.EXPIRES;
import static io.netty.handler.codec.http.HttpHeaderNames.LAST_MODIFIED;
import static io.netty.handler.codec.http.HttpHeaderNames.PRAGMA;
import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import gribbit.auth.Cookie;
import gribbit.auth.User;
import gribbit.response.ErrorResponse;
import gribbit.response.HTMLPageResponse;
import gribbit.response.HTMLResponse;
import gribbit.response.NotModifiedResponse;
import gribbit.response.Response;
import gribbit.response.flashmsg.FlashMessage;
import gribbit.route.RouteHandler;
import gribbit.route.RouteHandlerAuthAndValidatedEmailRequired;
import gribbit.route.RouteHandlerAuthRequired;
import gribbit.route.RouteInfo;
import gribbit.server.GribbitServer;
import gribbit.server.config.GribbitProperties;
import gribbit.server.siteresources.CacheExtension;
import gribbit.server.siteresources.CacheExtension.HashInfo;
import gribbit.util.Log;
import gribbit.util.WebUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.ServerCookieEncoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.DiskAttribute;
import io.netty.handler.codec.http.multipart.DiskFileUpload;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.ssl.NotSslRecordException;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.zip.GZIPOutputStream;

public class HttpRequestHandler extends SimpleChannelInboundHandler<Object> {

    private Request request;

    private boolean closeAfterWrite = false;
    private boolean addKeepAliveHeader = false;

    // -----------------------------------------------------------------------------------------------------------------

    // Use disk for HTTP data if size >16kB
    private static final HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);

    static {
        // Delete temporary files on exit
        DiskFileUpload.deleteOnExitTemporaryFile = true;
        DiskAttribute.deleteOnExitTemporaryFile = true;
        // Use system temp directory
        DiskFileUpload.baseDirectory = null;
        DiskAttribute.baseDirectory = null;
    }

    // -----------------------------------------------------------------------------------------------------------------

    private HttpPostRequestDecoder decoder;

    private void destroyDecoder() {
        if (request != null) {
            try {
                // Release any temporary files
                request.releasePostFileUploadParams();
            } catch (Exception e) {
            }
        }
        if (decoder != null) {
            try {
                decoder.cleanFiles();
                decoder.destroy();
                decoder = null;
            } catch (Exception e) {
            }
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Set Date and cache-related headers.
     *
     * TODO: read http://www.mobify.com/blog/beginners-guide-to-http-cache-headers/ TODO: read
     * https://www.mnot.net/cache_docs/
     */
    private void setDateAndCacheHeaders(HttpHeaders httpHeaders, ZonedDateTime timeNow, long lastModifiedEpochSeconds,
            long hashKeyMaxRemainingAgeSeconds, String hashKey) {
        // Date header uses server time, and should use the same clock as Expires and Last-Modified
        httpHeaders.set(DATE, timeNow.format(DateTimeFormatter.RFC_1123_DATE_TIME));

        // Last-Modified is used to determine whether a Not Modified response should be returned on the next request
        if (lastModifiedEpochSeconds > 0) {
            // Add last modified header to cacheable resources. This is needed because Chrome sends
            // "Cache-Control: max-age=0" when the user types in a URL and hits enter, or hits refresh.
            // In these circumstances, sending back "Cache-Control: public, max-age=31536000" does
            // no good, because the browser has already requested the resource rather than relying on
            // its cache. By setting the last modified header for all cacheable resources, we can
            // at least send "Not Modified" as a response if the resource has not been modified,
            // which doesn't save on roundtrips, but at least saves on re-transferring the resources
            // to the browser when they're already in the browser's cache.
            httpHeaders.set(
                    LAST_MODIFIED,
                    ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastModifiedEpochSeconds), ZoneId.of("UTC")).format(
                            DateTimeFormatter.RFC_1123_DATE_TIME));
        }

        // Cache hashed URIs forever (or for the specified amount of time)
        if (hashKey != null) {
            // Negative max age => cache indefinitely (although the spec only allows for one year, or 31536000 seconds)
            long maxAgeSeconds = hashKeyMaxRemainingAgeSeconds < 0 ? 31536000 : hashKeyMaxRemainingAgeSeconds;

            // Cache hash URIs for one year
            httpHeaders.set(CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
            httpHeaders.set(EXPIRES, timeNow.plusSeconds(maxAgeSeconds).format(DateTimeFormatter.RFC_1123_DATE_TIME));
            httpHeaders.set(ETAG, hashKey);
        }

        if (hashKey == null && lastModifiedEpochSeconds == 0) {
            // Disable caching for all resources that don't have a last modified time (all static file
            // resources have a last modified time, so these headers will never be set for files, and
            // they won't be set for dynamic resources that don't manually set a last modified time).
            // Without these headers, the server will not have a last modified timestamp to check
            // against its own timestamp on subsequent requests, so cannot return Not Modified.
            // This is the minimum necessary set of headers for disabling caching, see http://goo.gl/yXGd2x
            httpHeaders.add(CACHE_CONTROL, "no-cache, no-store, must-revalidate"); // HTTP 1.1
            httpHeaders.add(PRAGMA, "no-cache"); // HTTP 1.0
            httpHeaders.add(EXPIRES, "0"); // Proxies
        }

    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Call the route handler for the given route. */
    private static Response getResponseForRoute(RouteInfo route, Request req, User user) throws Exception {
        Response response;
        try {
            // Call the RestHandler for the route
            response = route.callHandler(req, user);

        } catch (Exception e) {
            Log.exception("Exception while handling URI " + req.getURI(), e);
            try {
                // Call Internal Server Error handler on exception
                response = GribbitServer.siteResources.getInternalServerErrorRoute().callHandler(req, user);
            } catch (Exception e1) {
                // Fallback in case there's an exception in the Internal Server Error handler
                Log.exception("Error in internal server error handler while handling URI " + req.getURI(), e1);
                response = new ErrorResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Internal Server Error");
            }
        }
        return response;
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Call the given error handler route. */
    private static Response getResponseForErrorHandlerRoute(RouteInfo route, Request req, User user) throws Exception {
        // Temporarily replace the method on the original route with a GET request on the error handler's route
        String origURI = req.getURI();
        HttpMethod origMethod = req.getMethod();
        req.setURI(route.getRoutePath());
        req.setMethod(HttpMethod.GET);
        // Call the error handler
        Response response = getResponseForRoute(route, req, user);
        req.setURI(origURI);
        req.setMethod(origMethod);
        return response;
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Serve a static file. */
    private void serveStaticFile(String reqURI, String hashKey, File staticResourceFile, long lastModifiedEpochSeconds,
            ChannelHandlerContext ctx) throws Exception {

        RandomAccessFile fileToServe = null;
        try {
            // Create new RandomAccessFile (which allows us to find file length etc.)
            fileToServe = new RandomAccessFile(staticResourceFile, "r");

            // -----------------------------------------
            // Serve a static file (not authenticated)
            // -----------------------------------------

            DefaultHttpResponse httpRes = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            httpRes.headers().add("Server", GribbitServer.SERVER_IDENTIFIER);

            long fileLength = fileToServe.length();
            httpRes.headers().set(CONTENT_LENGTH, Long.toString(fileLength));
            WebUtils.setContentTypeHeaders(httpRes.headers(), staticResourceFile.getPath());

            // If the file contents have changed since the last time the file was hashed,
            // schedule the file to be hashed in the background so that future references to the
            // file's URI in a src/href attribute of served HTML templates will include a hash
            // URI rather than the original URI for the file, allowing the browser to cache the
            // file indefinitely until it changes.
            CacheExtension.updateHashURI(reqURI, staticResourceFile);

            // If file was already cached, and the request URI included the hash key, then this is
            // the first time this client has fetched this file since the browser cache was last
            // cleared. Mark this resource as indefinitely cached. If the file is not being served
            // on a hash URI, then at least set the Last-Modified header, so that if the client
            // requests the same unmodified resource again on the same non-hash URI, the server can
            // return Not Modified instead of serving the contents of the file.
            setDateAndCacheHeaders(httpRes.headers(), ZonedDateTime.now(), lastModifiedEpochSeconds, //
                    hashKey != null ? /* cache indefinitely: */-1 : /* ignored if hashKey == null: */0, hashKey);

            if (addKeepAliveHeader) {
                httpRes.headers().add(CONNECTION, KEEP_ALIVE);
            }

            // Write HTTP headers to channel
            ctx.write(httpRes);

            // Write file content to channel.
            // Can add ChannelProgressiveFutureListener to sendFileFuture if we need to track
            // progress (e.g. to update user's UI over a web socket to show download progress.)
            ChannelFuture sendFileFuture;
            ChannelFuture lastContentFuture;
            if (ctx.pipeline().get(SslHandler.class) == null) {
                // Use FileRegions if possible, which supports zero-copy / mmio
                sendFileFuture =
                        ctx.write(new DefaultFileRegion(fileToServe.getChannel(), 0, fileLength),
                                ctx.newProgressivePromise());
                // Write the end marker
                lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            } else {
                // Can't use FileRegions / zero-copy with SSL
                sendFileFuture =
                        ctx.write(new HttpChunkedInput(new ChunkedFile(fileToServe, 0, fileLength, 1)),
                                ctx.newProgressivePromise());
                // HttpChunkedInput will write the end marker (LastHttpContent) for us.
                // See https://github.com/netty/netty/commit/4ba2ce3cbbc55391520cfc98a7d4227630fbf978
                lastContentFuture = sendFileFuture;
            }

            //            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            //                @Override
            //                public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
            //                    if (total < 0) { // total unknown
            //                        System.err.println(future.channel() + " Transfer progress: " + progress);
            //                    } else {
            //                        System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
            //                    }
            //                }
            //
            //                @Override
            //                public void operationComplete(ChannelProgressiveFuture future) {
            //                    System.err.println(future.channel() + " Transfer complete.");
            //                }
            //            });

            // Close connection after flush if needed, and close file after flush
            final RandomAccessFile fileToClose = fileToServe;
            lastContentFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    if (closeAfterWrite) {
                        future.channel().close();
                    }
                    try {
                        fileToClose.close();
                    } catch (IOException e) {
                    }
                }
            });

        } catch (Exception e) {
            if (fileToServe != null) {
                try {
                    fileToServe.close();
                } catch (IOException e1) {
                }
            }
            throw new RuntimeException("Exception serving static file", e);
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Serve an HTTP response (anything other than a static file). */
    private void serveHttpResponse(String reqURI, Response response, boolean isHEAD, boolean acceptEncodingGzip,
            ZonedDateTime timeNow, boolean hashTheResponse, long hashKeyMaxRemainingAgeSeconds, String hashKey,
            ChannelHandlerContext ctx) throws Exception {

        // Add any pending flash messages to the response, if the response is an HTML page
        if (response instanceof HTMLPageResponse) {
            // Add flash messages to response template, if any
            ArrayList<FlashMessage> flashMessages = request.getFlashMessages();
            if (flashMessages != null) {
                // Render pending flash messages into the HTML page
                ((HTMLPageResponse) response).setFlashMessages(flashMessages);
                // Clear the flash message cookie
                response.deleteCookie(Cookie.FLASH_COOKIE_NAME);
            }
        } else {
            // Store any un-displayed flash messages back in the cookie
            ArrayList<FlashMessage> flashMessages = request.getFlashMessages();
            if (flashMessages != null) {
                response.setCookie(new Cookie(Cookie.FLASH_COOKIE_NAME, "/",
                        FlashMessage.toCookieString(flashMessages), 60));
            }
        }

        // Get the content of the response as a byte buffer.
        ByteBuf content;
        String contentType;
        if (response instanceof HTMLResponse) {
            // If "?_getmodel=1" is appended to the request URL of a request that returns an HTML response,
            // then the data model backing the response will be returned as JSON instead of rendered HTML.
            boolean isGetModelRequest = GribbitProperties.ALLOW_GET_MODEL && request.isGetModelRequest();
            content = ((HTMLResponse) response).getContent(isGetModelRequest);
            contentType = isGetModelRequest ? "application/json;charset=utf-8" : response.getContentType();
        } else {
            // Not a "getmodel" request, just get the content from the response
            content = response.getContent();
            contentType = response.getContentType();
        }
        byte[] contentBytes = content.array();

        // Gzip content if the configuration property is set to allow gzip, and the client supports gzip encoding,
        // and the content size is larger than 1kb, and the content type is compressible
        ByteBuf gzippedContent = null;
        if (GribbitProperties.CONTENT_GZIP && //
                acceptEncodingGzip //
                && content.readableBytes() > 1024 //
                && WebUtils.isCompressibleContentType(contentType)) {
            gzippedContent = Unpooled.buffer(/* initialCapacity = */content.readableBytes());
            // TODO: compare speed to using JZlib.GZIPOutputStream
            GZIPOutputStream gzipStream = new GZIPOutputStream(new ByteBufOutputStream(gzippedContent));
            gzipStream.write(contentBytes);
            gzipStream.close();
        }

        // Create a FullHttpResponse object that wraps the response status and content
        HttpResponseStatus status = response.getStatus();
        DefaultFullHttpResponse httpRes = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, //
                gzippedContent != null ? gzippedContent : content);
        httpRes.headers().set(CONTENT_TYPE, contentType);
        httpRes.headers().set(CONTENT_LENGTH,
                Integer.toString(gzippedContent != null ? gzippedContent.readableBytes() : content.readableBytes()));
        if (gzippedContent != null) {
            httpRes.headers().set(CONTENT_ENCODING, GZIP);
        }

        httpRes.headers().add(SERVER, GribbitServer.SERVER_IDENTIFIER);

        // Add an Accept-Encoding: gzip header to the response to let the client know that in future
        // it can send compressed requests. (This header is probably ignored by most clients, because
        // on initial request they don't know yet if the server can accept compressed content, but
        // there must be clients out there that look for this header and compress content on the
        // second and subsequent requests? See http://stackoverflow.com/a/1450163/3950982 )
        httpRes.headers().add(ACCEPT_ENCODING, "gzip");

        // Set date and cache headers
        setDateAndCacheHeaders(httpRes.headers(), timeNow, response.getLastModifiedEpochSeconds(),
                hashKeyMaxRemainingAgeSeconds, hashKey);

        // If the response needs hashing, and the response does not have an error status, then schedule the
        // content of the response for hashing, and store a mapping from the original request URI to the
        // hash URI so that future HTML responses that have src/href attributes that contain this request
        // URI will replace this request URI with the hash URI instead. This will mean the client will
        // fetch that hash URI only once until it expires in the cache, so that on subsequent requests,
        // the linked resource won't even be requested from the server.
        if (hashTheResponse && status == HttpResponseStatus.OK) {
            CacheExtension.updateHashURI(reqURI, content, response.getLastModifiedEpochSeconds());
        }

        // Release the content ByteBuf after last usage if gzippedContent is being used instead
        if (gzippedContent != null) {
            content.release();
        }

        if (isHEAD) {
            // Don't return a body for HEAD requests (but still return the content length,
            // set in the header above)
            httpRes.content().clear();
        }

        // Delete requested cookies in the response
        HashSet<String> cookiesToDelete = response.getCookiesToDelete();
        if (cookiesToDelete != null) {
            for (String cookieName : cookiesToDelete) {
                // Log.fine("Cookie to delete for req " + reqURI + " : " + cookieName);
                ArrayList<Cookie> allCookiesWithName = request.getAllCookiesWithName(cookieName);
                if (allCookiesWithName != null) {
                    for (Cookie cookie : allCookiesWithName) {
                        // Delete all cookies with the requested name (there may be multiple cookies
                        // with this name but with different paths)
                        String deleteCookieStr =
                                ServerCookieEncoder.encode(Cookie.deleteCookie(cookie).toNettyCookie());
                        httpRes.headers().add(SET_COOKIE, deleteCookieStr);
                    }
                }
            }
        }

        // Set requested cookies in the response
        ArrayList<Cookie> cookiesToSet = response.getCookiesToSet();
        if (cookiesToSet != null) {
            for (Cookie cookie : cookiesToSet) {
                if (cookiesToDelete != null && cookiesToDelete.contains(cookie.getName())) {
                    Log.warning("Tried to delete and set the cookie \"" + cookie.getName()
                            + "\" in the same response -- ignoring the set request");
                } else {
                    String setCookieStr = ServerCookieEncoder.encode(cookie.toNettyCookie());
                    httpRes.headers().add(SET_COOKIE, setCookieStr);
                }
            }
        }

        // Add any custom headers from the Response object
        if (response.getCustomHeaders() != null) {
            for (Entry<String, String> header : response.getCustomHeaders().entrySet()) {
                httpRes.headers().add(header.getKey(), header.getValue());
            }
        }

        // Close the connection after serving the response if the response status is anything other than OK
        if (addKeepAliveHeader && status == HttpResponseStatus.OK) {
            httpRes.headers().add(CONNECTION, KEEP_ALIVE);
        }

        // closeAfterWrite = true;  // FIXME: test this, it doesn't seem to work ====================================================================

        if (ctx.channel().isOpen()) {
            // Check Channel.isWritable() to prevent OutOfMemoryError,
            // see http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#10.0
            long startTime = System.currentTimeMillis();
            while (!ctx.channel().isWritable() && (System.currentTimeMillis() - startTime < 5000)) {
                // TODO: replace this spinlock (usually channel is immediately writeable;
                // is this even needed?)
            }
            if (ctx.channel().isWritable()) {
                // Write the ByteBuffer returned by httpRes.content() back into the pipeline
                // See http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#4.0
                ChannelFuture future = ctx.channel().writeAndFlush(httpRes);

                // Close the connection after the write operation is done if necessary.
                // TODO: Apache closes KeepAlive connections after a few seconds,
                // see http://en.wikipedia.org/wiki/HTTP_persistent_connection
                // TODO: implement a stale connection tracker
                if (closeAfterWrite || status != HttpResponseStatus.OK) { // FIXME: should I close the channel for redirects? (probably not...)
                    future.addListener(ChannelFutureListener.CLOSE);
                }

            } else {
                // Tried for a period of time but could not send response; close channel
                ctx.channel().close();
                throw new RuntimeException("Could not send response after repeated attempts");
            }
        } else {
            // Client already closed the connection, nothing can be sent
            // Log.info("Channel closed by client before response sent");
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Websocket handshaker. */
    private WebSocketServerHandshaker handshaker;

    /** The user that was authenticated when the websocket upgrade was requested. */
    private User wsAuthenticatedUser;

    /** Handle a websocket frame. */
    private void handleWebsocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // Check for closing frame
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass()
                    .getName()));
        }

        // TODO: placeholder
        String requestText = ((TextWebSocketFrame) frame).text();
        String responseText =
                requestText.toUpperCase() + " -- "
                        + (wsAuthenticatedUser == null ? "not logged in" : wsAuthenticatedUser.id);

        ctx.channel().writeAndFlush(new TextWebSocketFrame(responseText));
    }

    // -----------------------------------------------------------------------------------------------------------------

    private static void sendHttpErrorResponse(ChannelHandlerContext ctx, HttpRequest req, FullHttpResponse res) {
        // Generate an error page if response getStatus code is not OK (200).
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            res.headers().set(CONTENT_TYPE, "text/plain;charset=utf-8");
            HttpHeaderUtil.setContentLength(res, res.content().readableBytes());
        }
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (req == null || !HttpHeaderUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Decode an HTTP message. */
    @Override
    public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {

        // ------------------------------------------------------------------------------
        // Handle WebSocket frames
        // ------------------------------------------------------------------------------

        if (msg instanceof WebSocketFrame) {
            handleWebsocketFrame(ctx, (WebSocketFrame) msg);
            return;
        }

        // ------------------------------------------------------------------------------
        // Decode HTTP headers
        // ------------------------------------------------------------------------------

        boolean requestComplete = false;
        try {
            if (msg instanceof HttpRequest) {
                HttpRequest httpReq = (HttpRequest) msg;

                // System.out.println("REQUEST: " + httpReq.getUri());

                // Start a new request
                request = new Request(httpReq);

                // Handle expect-100-continue
                boolean expect100Continue = false;
                List<CharSequence> allExpectHeaders = httpReq.headers().getAll(EXPECT);
                for (int i = 0; i < allExpectHeaders.size(); i++) {
                    String h = allExpectHeaders.get(i).toString();
                    if (h.equalsIgnoreCase("100-continue")) {
                        expect100Continue = true;
                        break;
                    }
                }
                if (expect100Continue) {
                    ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE,
                            Unpooled.EMPTY_BUFFER));
                    requestComplete = true;
                    return;
                }

                closeAfterWrite = !HttpHeaderUtil.isKeepAlive(httpReq);
                addKeepAliveHeader = !closeAfterWrite && httpReq.protocolVersion().equals(HttpVersion.HTTP_1_0);

                if (httpReq.method() == HttpMethod.POST) {
                    // Start decoding HttpContent chunks
                    destroyDecoder();
                    decoder = new HttpPostRequestDecoder(factory, httpReq);

                } else {
                    // Non-POST (probably GET) -- start handling the request
                    requestComplete = true;
                }

                // TODO: will this return failure before all POST chunks have been received?
                if (!httpReq.decoderResult().isSuccess()) {
                    sendHttpErrorResponse(ctx, httpReq, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                            HttpResponseStatus.BAD_REQUEST));
                    requestComplete = true;
                    return;
                }
            }

            // ------------------------------------------------------------------------------
            // Decode HTTP POST body
            // ------------------------------------------------------------------------------

            if (msg instanceof HttpContent && decoder != null) {
                HttpContent chunk = (HttpContent) msg;
                // Offer chunk to decoder (this decreases refcount of chunk, so it doesn't have to
                // be separately released). Decoder is released after message has been handled.
                decoder.offer(chunk);

                try {
                    while (decoder.hasNext()) {
                        InterfaceHttpData data = decoder.next();
                        if (data != null) {
                            HttpDataType httpDataType = data.getHttpDataType();
                            if (httpDataType == HttpDataType.Attribute) {
                                try {
                                    Attribute attribute = (Attribute) data;
                                    request.setPostParam(attribute.getName(), attribute.getString(attribute
                                            .getCharset() == null ? Charset.forName("UTF-8") : attribute.getCharset()));
                                } finally {
                                    // Decrease refcount, freeing data
                                    data.release();
                                }

                            } else if (httpDataType == HttpDataType.FileUpload) {
                                FileUpload fileUpload = (FileUpload) data;
                                // TODO consider imposing size limit and returning 413 (Request Entity Too
                                // Large) once the amount of data that has been sent hits the limit
                                if (fileUpload.isCompleted()) {
                                    // Save the FileUpload object (which wraps a DiskFileUpload in /tmp).
                                    // Need to release this resource later.
                                    request.setPostFileUploadParam(fileUpload.getName(), fileUpload);
                                }
                            } else {
                                Log.warning("Got unknown data chunk type: " + httpDataType);
                            }
                        }
                    }
                } catch (EndOfDataDecoderException e) {
                    // Apparently decoder.hasNext() doesn't actually work
                }

                if (chunk instanceof LastHttpContent) {
                    requestComplete = true;
                }
            }

            if (!requestComplete) {
                // Wait for more chunks.
                // (Since requestComplete is false, calling return here will not call destroyDecoder()
                // in the finally block, so it will still exist when the next chunk is received.)
                return;
            }

            // ------------------------------------------------------------------------------
            // Figure out how to handle HTTP request
            // ------------------------------------------------------------------------------

            // All POST chunks have been received (or there are no chunks); ready to start handling the request

            String origReqURI = request.getURI();

            // If this is a hash URI, look up original URI whose served resource was hashed to give this hash URI.
            // We only need to serve the resource at a hash URI once per resource per client, since resources served
            // from hash URIs are indefinitely cached in the browser.
            String hashKey = CacheExtension.getHashKey(origReqURI);
            boolean isHashURI = hashKey != null;
            String reqURI = isHashURI ? CacheExtension.getOrigURI(origReqURI) : origReqURI;

            InetSocketAddress requestor = (InetSocketAddress) ctx.channel().remoteAddress();
            if (requestor != null) {
                InetAddress address = requestor.getAddress();
                if (address != null) {
                    request.setRequestor(address.getHostAddress());
                }
            }

            boolean isHEAD = request.getMethod() == HttpMethod.HEAD;

            // Run the GET method if HEAD is requested, just don't return a body.
            HttpMethod origReqMethod = request.getMethod();
            if (isHEAD) {
                request.setMethod(HttpMethod.GET);
            }

            // ------------------------------------------------------------------------------
            // Authenticate user
            // ------------------------------------------------------------------------------

            // The response object generated by a RestHandler
            Response response = null;

            // Call route handlers until one is able to handle the route,
            // or until we run out of handlers
            User user = null;
            RouteInfo authorizedRoute = null;
            ArrayList<RouteInfo> allRoutes = GribbitServer.siteResources.getAllRoutes();
            for (int i = 0, n = allRoutes.size(); i < n; i++) {
                RouteInfo route = allRoutes.get(i);
                // If the request URI matches this route path
                if (route.matches(reqURI)) {
                    Class<? extends RouteHandler> handler = route.getHandler();

                    if (!(request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.POST)) {

                        // We only support GET and POST at this point
                        Log.error("Unsupported HTTP method " + request.getMethod().name() + " for path " + reqURI);
                        response = new ErrorResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, "HTTP method not allowed");

                    } else if ((request.getMethod() == HttpMethod.GET && !route.hasGetMethod())
                            || (request.getMethod() == HttpMethod.POST && !route.hasPostMethod())) {

                        // Tried to call an HTTP method that is not defined for this route
                        Log.error("HTTP method " + request.getMethod().name() + " not implemented in handler "
                                + handler.getName());
                        response = new ErrorResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, "HTTP method not allowed");

                    } else if (RouteHandlerAuthRequired.class.isAssignableFrom(handler)) {

                        // This handler requires authentication -- check if user is logged in
                        user = User.getLoggedInUser(request);
                        if (user == null) {

                            // User is not logged in: handle request with OnUnauthorized handler instead
                            response =
                                    getResponseForErrorHandlerRoute(GribbitServer.siteResources.getUnauthorizedRoute(),
                                            request, user)
                                    // Redirect the user back to the page they were trying to get to once they
                                    // do manage to log in successfully
                                            .setCookie(
                                                    new Cookie(Cookie.REDIRECT_AFTER_LOGIN_COOKIE_NAME, "/", reqURI,
                                                            300));

                        } else if (RouteHandlerAuthAndValidatedEmailRequired.class.isAssignableFrom(handler)
                                && !user.emailIsValidated()) {

                            // User is logged in, but their email address has not been validated:
                            // handle request with EmailNotValidated handler instead
                            response =
                                    getResponseForErrorHandlerRoute(
                                            GribbitServer.siteResources.getEmailNotValidatedRoute(), request, user);

                        } else {

                            // Authorization required and user logged in: OK to handle request
                            // with this route
                            authorizedRoute = route;
                        }
                    } else {

                        // Authorization not required -- OK to handle request with this route
                        authorizedRoute = route;
                    }

                    // URI matches, so don't need to search further URIs
                    break;
                }
            }

            // ------------------------------------------------------------------------------
            // Complete websocket handshake if requested
            // ------------------------------------------------------------------------------

            if (response == null && authorizedRoute == null && msg instanceof HttpRequest
            // TODO: Read WS routes from class annotations
                    && reqURI.endsWith("/websocket")) {
                HttpRequest httpReq = (HttpRequest) msg;

                // Record which user was authenticated (if any) when websocket upgrade request was made.
                // TODO: Reject WS upgrade request for websockets that require authentication.
                // TODO: Also provide a means for revoking WS login.
                wsAuthenticatedUser = User.getLoggedInUser(request);

                WebSocketServerHandshakerFactory wsFactory =
                        new WebSocketServerHandshakerFactory(GribbitServer.wsUri.toString(), null, true);
                handshaker = wsFactory.newHandshaker(httpReq);
                if (handshaker == null) {
                    WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                } else {
                    // Attempt websocket handshake, and if it succeeds, upgrade connection to websocket
                    // TODO: filed bug report, handshaker.handshake should take HttpRequest, not FullHttpRequest
                    DefaultFullHttpRequest fullReq =
                            new DefaultFullHttpRequest(httpReq.protocolVersion(), httpReq.method(), httpReq.uri());
                    fullReq.headers().add(httpReq.headers());
                    handshaker.handshake(ctx.channel(), (FullHttpRequest) fullReq);
                }
                return;
            }

            // ------------------------------------------------------------------------------
            // Handle static file requests
            // ------------------------------------------------------------------------------

            // If no error has occurred so far, and no route handler matched the request URI, and this is a
            // GET request, then see if the URI points to a static file resource, and if so, serve the file.
            if (response == null && authorizedRoute == null) {
                // Static file requests can only use GET method
                if (request.getMethod() != HttpMethod.GET) {
                    sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                            HttpResponseStatus.FORBIDDEN));
                    return;
                }

                File staticResourceFile = GribbitServer.siteResources.getStaticResource(reqURI);
                if (staticResourceFile == null) {

                    // Neither a route handler nor a static resource matched the request URI.
                    // Return 404 Not Found.
                    response =
                            getResponseForErrorHandlerRoute(GribbitServer.siteResources.getNotFoundRoute(), request,
                                    user);

                } else {

                    // A static resource matched the request URI, check last-modified timestamp
                    // against the If-Modified-Since header timestamp in the request.
                    long lastModifiedEpochSeconds = staticResourceFile.lastModified() / 1000;
                    if (!request.cachedVersionIsOlderThan(lastModifiedEpochSeconds)) {
                        // File has not been modified since it was last cached -- return Not Modified
                        response = new NotModifiedResponse(lastModifiedEpochSeconds);

                    } else {
                        // If file is newer than what is in the browser cache, or is not in cache, serve the file
                        serveStaticFile(reqURI, hashKey, staticResourceFile, lastModifiedEpochSeconds, ctx);

                        Log.fine(request.getRequestor() + "\t" + origReqMethod + "\t" + reqURI + "\tfile://"
                                + staticResourceFile.getPath() + "\t" + HttpResponseStatus.OK + "\t"
                                + (System.currentTimeMillis() - request.getReqReceivedTimeEpochMillis()) + " msec");

                        // Finished request
                        return;
                    }
                }
            }

            // ------------------------------------------------------------------------------
            // Handle GET or POST requests
            // ------------------------------------------------------------------------------

            ZonedDateTime timeNow = null;

            // If an error response hasn't yet been generated and this is a (non-static-file) GET or POST request,
            // then call the get() or post() method for the route handler bound to the request URI to obtain the
            // response object.
            boolean hashTheResponse = false;
            long hashKeyRemainingAgeSeconds = 0;
            if (response == null && authorizedRoute != null) {

                // ----------------------------------
                // See if response should be hashed
                // ----------------------------------

                // For hashed *non-file* URIs, the actual last modified timestamp of dynamically-served
                // content can't be read directly, so read the last modified timestamp stored for the
                // previously hashed version in the CacheExtension class, as long as the max age of the
                // cached version hasn't been exceeded, and see if the last modified timestamp is more
                // recent than the version cached in the browser.
                //
                // The important ramification of this is that when the resource identified by the non-file
                // URI changes, the CacheExtension class must be notified of that change (including in cases
                // where the database is modified by another database client) if the modified version should
                // start being served at a new hash URI immediately, otherwise the web client connected to
                // this web server will continue to serve old resources until the max age of the cached
                // content is exceeded.
                if (isHashURI) {
                    HashInfo hashInfo = CacheExtension.getHashInfo(reqURI);
                    if (hashInfo != null) {
                        long lastModifiedEpochSeconds = hashInfo.getLastModifiedEpochSeconds();
                        timeNow = ZonedDateTime.now();
                        long timeNowEpochSeconds = timeNow.toEpochSecond();

                        long maxAgeSeconds = authorizedRoute.getMaxAgeSeconds();
                        hashKeyRemainingAgeSeconds = lastModifiedEpochSeconds + maxAgeSeconds - timeNowEpochSeconds;

                        if (maxAgeSeconds == 0) {
                            // Content is not hash-cached
                            hashKeyRemainingAgeSeconds = 0;
                        }

                        if (maxAgeSeconds > 0 && hashKeyRemainingAgeSeconds <= 0) {
                            // Resource has expired -- call the route handler to generate a new response rather
                            // than serving a Not Modified response, and schedule the response to be hashed or
                            // re-hashed once the response has been generated.
                            hashTheResponse = true;

                            // Reset the expiry time at the requested number of seconds in the future
                            hashKeyRemainingAgeSeconds = maxAgeSeconds;

                        } else if (!request.cachedVersionIsOlderThan(lastModifiedEpochSeconds)) {
                            // Resource has not expired in cache, but client has requested it anyway.
                            // However, resource has not been modified since it was last hashed --
                            // return Not Modified.
                            response = new NotModifiedResponse(lastModifiedEpochSeconds);

                        } else {
                            // Resource has not expired in cache, but client has requested it anyway.
                            // Resource *has* been modified since it was last hashed -- serve it the
                            // normal way using the route handler, but don't hash the response, since
                            // it has not expired yet.
                        }
                    } else {
                        // There is no original URI matching this hash URI, so the hash key was stale
                        // (i.e. a URI whose hashcode has been spoofed, or a very old hashcode from
                        // the previous time the server was run), but we still got a valid request URI
                        // by stripping away the hash code, so that is served below in the normal way.
                    }
                }

                // If the response wasn't just set to "Not Modified" above, serve the request
                if (response == null) {

                    // -----------------------------------------------------------------
                    // Call the route handler for this request, generating the response
                    // -----------------------------------------------------------------

                    response = getResponseForRoute(authorizedRoute, request, user);

                    if (response == null) {
                        // Should not happen
                        throw new RuntimeException("Didn't generate a response");
                    }

                }

            }
            if (response == null) {
                // Should not happen
                throw new RuntimeException("Didn't generate a response");
            }

            // ------------------------------------------------------------------------------------
            // Serve an HTTP result (except in the case of static files, they were served already)
            // ------------------------------------------------------------------------------------

            // Turn the Response object into an HttpResponse object and serve it to the user over Netty.
            if (timeNow == null) {
                timeNow = ZonedDateTime.now();
            }

            // Serve the response to the client
            serveHttpResponse(reqURI, response, isHEAD, request.acceptEncodingGzip(),//
                    timeNow, hashTheResponse, hashKeyRemainingAgeSeconds, hashKey, ctx);

            // Log the request and response
            HttpResponseStatus status = response.getStatus();
            String logMsg =
                    request.getRequestor() + "\t" + origReqMethod + "\t" + reqURI
                            + (request.getMethod() == origReqMethod ? "" : "\t" + request.getMethod()) + "\t" + status
                            + "\t" + (System.currentTimeMillis() - request.getReqReceivedTimeEpochMillis()) + " msec";
            if (status == HttpResponseStatus.OK //
                    || status == HttpResponseStatus.NOT_MODIFIED //
                    || status == HttpResponseStatus.FOUND //
                    || (status == HttpResponseStatus.NOT_FOUND //
                    && (reqURI.equals("favicon.ico") || reqURI.endsWith("/favicon.ico")))) {
                // Log at level "fine" for non-errors, or 404 for favicon
                Log.fine(logMsg);
            } else {
                // Log at level "warning" for errors, or 404 for non-favicon
                Log.warningWithoutCallerRef(logMsg);
            }

        } finally {
            if (requestComplete) {
                // Finished request -- destroy the multipart decoder and remove temporary files
                destroyDecoder();
            }
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // Release HTTP decoder resources, including any file uploads that were received in a POST
        // request and stored in /tmp
        destroyDecoder();

        if (cause instanceof NotSslRecordException) {
            ctx.channel().close();
            return;
        }
        if ("Connection reset by peer".equals(cause.getMessage())) {
            // (No need to log the backtrace in this case)
            // Log.info(cause.getMessage());
        } else {
            // Log exception with backtrace
            Log.exception("Uncaught exception", cause);
        }
        if (ctx.channel().isActive()) {
            sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.INTERNAL_SERVER_ERROR));
        }
    }
}

## 内容4
### 文件路径：
src/gribbit/request/Request.java
### 代码内容：
package gribbit.request;

import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT;
import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_CHARSET;
import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_LANGUAGE;
import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import static io.netty.handler.codec.http.HttpHeaderNames.IF_MODIFIED_SINCE;
import static io.netty.handler.codec.http.HttpHeaderNames.ORIGIN;
import static io.netty.handler.codec.http.HttpHeaderNames.REFERER;
import static io.netty.handler.codec.http.HttpHeaderNames.USER_AGENT;
import gribbit.auth.Cookie;
import gribbit.response.flashmsg.FlashMessage;
import io.netty.handler.codec.http.CookieDecoder;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.multipart.FileUpload;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Request {
    private long reqReceivedTimeEpochMillis;
    private HttpMethod method;
    private String requestor;
    private String host;
    private String path;
    private CharSequence accept;
    private CharSequence acceptCharset;
    private CharSequence acceptLanguage;
    private boolean acceptEncodingGzip;
    private CharSequence referer;
    private CharSequence userAgent;
    private long ifModifiedSinceEpochSecond = 0;

    private HashMap<String, ArrayList<Cookie>> cookieNameToCookies;
    private HashMap<String, String> postParamToValue;
    private HashMap<String, FileUpload> postParamToFileUpload;
    private Map<String, List<String>> queryParamToVals;

    /**
     * Header for CORS.
     *
     * See http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
     **/
    private CharSequence origin;

    /**
     * Header for CSRF protection of AJAX requests (regular GETs and POSTs don't allow for header manipulation.)
     *
     * See https://nealpoole.com/blog/2010/11/preventing-csrf-attacks-with-ajax-and-http-headers/
     */
    private CharSequence xRequestedWith;

    /**
     * If set to true by appending "?_getmodel=1" to the URL, then return the data model backing an HTML page, not the
     * rendered page itself.
     */
    private boolean isGetModelRequest;

    /** Flash messages. */
    private ArrayList<FlashMessage> flashMessages;

    // -----------------------------------------------------------------------------------------------------

    public Request(HttpRequest httpReq) {
        this.reqReceivedTimeEpochMillis = System.currentTimeMillis();
        HttpHeaders headers = httpReq.headers();

        // Parse and decode/decrypt cookies
        for (CharSequence cookieHeader : headers.getAll(COOKIE)) {
            for (io.netty.handler.codec.http.Cookie nettyCookie : CookieDecoder.decode(cookieHeader.toString())) {
                // Log.fine("Cookie in request: " + nettyCookie);
                if (this.cookieNameToCookies == null) {
                    this.cookieNameToCookies = new HashMap<>();
                }
                String cookieName = nettyCookie.name();
                Cookie cookie = new Cookie(nettyCookie);

                // Multiple cookies may be present in the request with the same name but with different paths
                ArrayList<Cookie> cookiesWithThisName = this.cookieNameToCookies.get(cookieName);
                if (cookiesWithThisName == null) {
                    this.cookieNameToCookies.put(cookieName, cookiesWithThisName = new ArrayList<>());
                }
                cookiesWithThisName.add(cookie);
            }
        }

        this.method = httpReq.method();

        CharSequence host = headers.get(HOST);
        this.host = host == null ? null : host.toString();

        this.xRequestedWith = headers.get("X-Requested-With");
        this.accept = headers.get(ACCEPT);
        this.acceptCharset = headers.get(ACCEPT_CHARSET);
        this.acceptLanguage = headers.get(ACCEPT_LANGUAGE);
        this.origin = headers.get(ORIGIN);
        this.referer = headers.get(REFERER);
        this.userAgent = headers.get(USER_AGENT);

        CharSequence acceptEncoding = headers.get(ACCEPT_ENCODING);
        this.acceptEncodingGzip = acceptEncoding != null && acceptEncoding.toString().toLowerCase().contains("gzip");

        CharSequence cacheDateHeader = headers.get(IF_MODIFIED_SINCE);
        if (cacheDateHeader != null && cacheDateHeader.length() > 0) {
            this.ifModifiedSinceEpochSecond =
                    ZonedDateTime.parse(cacheDateHeader, DateTimeFormatter.RFC_1123_DATE_TIME).toEpochSecond();
        }

        // Decode the path.
        QueryStringDecoder decoder = new QueryStringDecoder(httpReq.uri());
        this.path = decoder.path();
        this.queryParamToVals = decoder.parameters();

        // Look for _getmodel=1 query parameter setting
        String getModel = this.getQueryParam("_getmodel");
        if (getModel != null) {
            this.isGetModelRequest = getModel.equals("1");
            // Remove _getmodel param so that user doesn't see it
            this.queryParamToVals.remove("_getmodel");
        }

        // Get flash messages from cookie, if any
        this.flashMessages = FlashMessage.fromCookieString(getCookieValue(Cookie.FLASH_COOKIE_NAME));
    }

    // -----------------------------------------------------------------------------------------------------------------

    public String getPostParam(String paramName) {
        if (postParamToValue == null) {
            return null;
        } else {
            return postParamToValue.get(paramName);
        }
    }

    public void setPostParam(String name, String value) {
        if (postParamToValue == null) {
            postParamToValue = new HashMap<>();
        }
        postParamToValue.put(name, value);
    }

    public Set<String> getPostParamNames() {
        if (postParamToValue == null) {
            return null;
        } else {
            return postParamToValue.keySet();
        }
    }

    public void setPostFileUploadParam(String name, FileUpload fileUpload) {
        if (postParamToFileUpload == null) {
            postParamToFileUpload = new HashMap<>();
        }
        FileUpload old = postParamToFileUpload.put(name, fileUpload);
        if (old != null) {
            // Shouldn't happen, but just in case there are two file upload params with the same
            // param name, free the first, since we're overwriting it
            old.release();
        }
    }

    public FileUpload getPostFileUploadParam(String name) {
        if (postParamToFileUpload == null) {
            return null;
        }
        return postParamToFileUpload.get(name);
    }

    public void releasePostFileUploadParams() {
        if (postParamToFileUpload != null) {
            for (FileUpload fileUpload : postParamToFileUpload.values()) {
                fileUpload.release();
            }
            postParamToFileUpload = null;
        }
    }

    void setPostParams(HashMap<String, String> postParamToValue) {
        this.postParamToValue = postParamToValue;
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Return all URL parameters matching the given name, or null if none. */
    public List<String> getQueryParams(String paramName) {
        if (queryParamToVals == null) {
            return null;
        } else {
            return queryParamToVals.get(paramName);
        }
    }

    /** Return the first URL parameter matching the given name, or null if none. */
    public String getQueryParam(String paramName) {
        List<String> list = getQueryParams(paramName);
        if (list == null || list.isEmpty()) {
            return null;
        } else {
            return list.get(0);
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Used for sorting cookies into decreasing order of path length */
    private static final Comparator<Cookie> cookiePathLengthComparator = new Comparator<Cookie>() {
        @Override
        public int compare(Cookie o1, Cookie o2) {
            return o2.getPath().length() - o1.getPath().length();
        }
    };

    /**
     * Get a collection of lists of cookies -- each list in the collection consists of one or more cookies, where all
     * cookies in a list have the same name but different paths. (It is possible to receive multiple cookies with the
     * same name in a request.) Cookie lists are ordered into decreasing order of path length to conform to a "SHOULD"
     * clause in the HTTP header spec.
     *
     * See http://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
     */
    public Collection<ArrayList<Cookie>> getAllCookies() {
        if (cookieNameToCookies == null) {
            return null;
        } else {
            Collection<ArrayList<Cookie>> cookieLists = cookieNameToCookies.values();
            for (ArrayList<Cookie> cookieList : cookieLists) {
                Collections.sort(cookieList, cookiePathLengthComparator);
            }
            return cookieLists;
        }
    }

    /**
     * Get all cookies with the given name, or null if there are no cookies with this name. (There may be multiple
     * cookies with the same name but with different paths.) The returned list is ordered into decreasing order of path
     * length to conform to a "SHOULD" clause in the HTTP header spec.
     *
     * See http://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
     */
    public ArrayList<Cookie> getAllCookiesWithName(String cookieName) {
        if (cookieNameToCookies == null) {
            return null;
        } else {
            ArrayList<Cookie> cookieList = cookieNameToCookies.get(cookieName);
            if (cookieList != null) {
                Collections.sort(cookieList, cookiePathLengthComparator);
            }
            return cookieList;
        }
    }

    /**
     * Get a cookie by name, or null if there are no cookies with this name. If there is more than one cookie with the
     * same name, return the cookie with the longest path.
     *
     * See http://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
     */
    public Cookie getCookie(String cookieName) {
        ArrayList<Cookie> cookieList = getAllCookiesWithName(cookieName);
        if (cookieList == null) {
            return null;
        } else {
            return cookieList.get(0);
        }
    }

    /**
     * Get the string value of a named cookie, or null if there are no cookies with this name. If there is more than one
     * cookie with the same name, return the value of the one with the longest path.
     *
     * See http://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
     */
    public String getCookieValue(String cookieName) {
        Cookie cookie = getCookie(cookieName);
        if (cookie == null) {
            return null;
        } else {
            return cookie.getValue();
        }
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Compare timestamp in the If-Modified-Since request header, if present, to the given resource timestamp to see if
     * the resource is newer than any cached version. If the If-Modified-Since header is not set, the timestamp of the
     * cached version is assumed to be zero (the beginning of the Epoch), so this method will return true. If the passed
     * resource timestamp is zero, the resource timestamp is assumed to be invalid or unavailable, so we assume the
     * resource is not cached and return true, indicating that the cached version is out of date and should be served
     * (or served again).
     */
    public boolean cachedVersionIsOlderThan(long resourceTimestampEpochSeconds) {
        if (resourceTimestampEpochSeconds == 0) {
            // If the resource timestamp is zero, it's not valid, so don't assume this resource is cached,
            // by returning true indicating that the cached version is out of date.
            return true;
        } else {
            // Otherwise return true if the resource timestamp is later than the cached version timestamp
            // (including when the cached version timestamp is zero.)
            // Note that the HTTP If-Modified-Since header only has single-second granularity.
            return resourceTimestampEpochSeconds > ifModifiedSinceEpochSecond;
        }
    }

    public long getReqReceivedTimeEpochMillis() {
        return reqReceivedTimeEpochMillis;
    }

    // -----------------------------------------------------------------------------------------------------------------

    /** Add a flash message (a message that will be popped up at the top of a webpage the next time a page is served. */
    public void addFlashMessage(FlashMessage flashMessage) {
        if (flashMessages == null) {
            flashMessages = new ArrayList<>();
        }
        flashMessages.add(flashMessage);
    }

    /** Clear the flash messages. */
    public void clearFlashMessage() {
        flashMessages = null;
    }

    public ArrayList<FlashMessage> getFlashMessages() {
        return flashMessages;
    }

    // -----------------------------------------------------------------------------------------------------------------

    public String getURI() {
        return path.toString();
    }

    public String getRequestor() {
        return requestor == null ? "" : requestor;
    }

    public void setRequestor(String requestor) {
        this.requestor = requestor;
    }

    public HttpMethod getMethod() {
        return method;
    }

    public void setMethod(HttpMethod method) {
        this.method = method;
    }

    public void setURI(String uri) {
        this.path = uri;
    }

    public CharSequence getHost() {
        return host;
    }

    public CharSequence getAccept() {
        return accept;
    }

    public CharSequence getAcceptCharset() {
        return acceptCharset;
    }

    public CharSequence getAcceptLanguage() {
        return acceptLanguage;
    }

    public boolean acceptEncodingGzip() {
        return acceptEncodingGzip;
    }

    public CharSequence getReferer() {
        return referer;
    }

    public CharSequence getUserAgent() {
        return userAgent;
    }

    public CharSequence getOrigin() {
        return origin;
    }

    public CharSequence getXRequestedWith() {
        return xRequestedWith;
    }

    /**
     * True if the request URL contained the query parameter "?_getmodel=1", in which case return the DataModel backing
     * an HTML page, and not the rendered page itself.
     */
    public boolean isGetModelRequest() {
        return isGetModelRequest;
    }
}

Output:
【漏洞类型】WebSocket 握手安全漏洞
【严重性】High
【漏洞摘要】原始代码在处理 WebSocket 协议升级请求时，未验证请求的来源和用户的身份，导致潜在的跨站 WebSocket 劫持（Cross-Site WebSocket Hijacking, CSWSH）漏洞。具体表现为，没有检查请求的 Origin 头和 CSRF 令牌，攻击者可以通过跨站请求合法用户登录后的 WebSocket 端点，从而获取用户的 Websocket 连接。
【关键代码模式】`handleWebsocketFrame` 方法，特别是 `msg instanceof HttpRequest` 和 `reqURI.endsWith("/websocket")` 的条件判断，以及 `WebSocketServerHandshakerFactory.newHandshaker` 的调用。
【修复方法说明】通过在处理 WebSocket 握手请求时检查 Origin 头和 CSRF 令牌，修复了潜在的 CSWSH 漏洞。确保仅允许合法来源的请求进行 WebSocket 握手，同时验证用户的身份。