All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sam.server.http.web.HttpRequest Maven / Gradle / Ivy

package org.sam.server.http.web;

import org.sam.server.constant.ContentType;
import org.sam.server.constant.HttpMethod;
import org.sam.server.http.Cookie;
import org.sam.server.http.CookieStore;
import org.sam.server.http.SessionManager;
import org.sam.server.http.Session;
import org.sam.server.util.StringUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Request 인터페이스의 구현체입니다. 일반적인 HTTP 요청에 대한 정보를 저장합니다.
 *
 * @author hypernova1
 * @see Request
 */
public class HttpRequest implements Request {

    private final String protocol;

    private final String path;

    private final HttpMethod method;

    private final Map headers;

    private final Map parameterMap;

    private final String json;

    private final Set cookies;

    protected HttpRequest(RequestParser requestParser) {
        this.protocol = requestParser.protocol;
        this.path = requestParser.url;
        this.method = requestParser.httpMethod;
        this.headers = requestParser.headers;
        this.parameterMap = requestParser.parameters;
        this.json = requestParser.json;
        this.cookies = requestParser.cookies;
    }

    /**
     * Http 요청을 분석하여 Request 인스턴스를 반환한다.
     *
     * @param in HTTP 요청을 담은 InputStream
     * @return Request 인스턴스
     * */
    public static Request from(InputStream in) {
        RequestParser requestParser = new RequestParser();
        requestParser.parse(in);
        return requestParser.createRequest();
    }

    @Override
    public String getProtocol() {
        return this.protocol;
    }

    @Override
    public String getUrl() {
        return this.path;
    }

    @Override
    public HttpMethod getMethod() {
        return this.method;
    }

    @Override
    public String getParameter(String key) {
        return this.parameterMap.get(key);
    }

    @Override
    public Map getParameters() {
        return this.parameterMap;
    }

    @Override
    public Set getParameterNames() {
        return this.parameterMap.keySet();
    }

    @Override
    public Set getHeaderNames() {
        return headers.keySet();
    }

    @Override
    public String getHeader(String key) {
        return headers.get(key);
    }

    @Override
    public String getJson() {
        return json;
    }

    @Override
    public Set getCookies() {
        return this.cookies;
    }

    @Override
    public Session getSession() {
        for (Cookie cookie : cookies) {
            if (!cookie.getName().equals("sessionId")) continue;
            return SessionManager.getSession(cookie.getValue());
        }
        return new Session();
    }

    /**
     * 소켓으로 부터 받은 InputStream을 읽어 Request 인스턴스를 생성하는 클래스입니다.
     *
     * @author hypernova1
     * @see Request
     * @see HttpRequest
     * @see HttpMultipartRequest
     * */
    protected static class RequestParser {

        protected String protocol;

        protected String url;

        protected HttpMethod httpMethod;

        protected Map headers = new HashMap<>();

        protected ContentType contentType;

        protected String boundary;

        protected Map parameters = new HashMap<>();

        protected String json;

        protected Set cookies = new HashSet<>();

        protected Map files = new HashMap<>();

        /**
         * InputStream에서 HTTP 본문을 읽은 후 파싱합니다.
         *
         * @param in 소켓의 InputStream
         * */
        private void parse(InputStream in) {
            BufferedInputStream inputStream = new BufferedInputStream(in);
            String headersPart = parseHeaderPart(inputStream);

            if (isNonHttpRequest(headersPart)) return;

            String[] headers = headersPart.split("\r\n");
            StringTokenizer tokenizer = new StringTokenizer(headers[0]);
            String httpMethodPart = tokenizer.nextToken().toUpperCase();
            String requestUrl = tokenizer.nextToken().toLowerCase();

            this.protocol = tokenizer.nextToken().toUpperCase();
            this.headers = parseHeaders(headers);
            this.httpMethod = HttpMethod.valueOf(httpMethodPart);
            this.contentType = parseContentType();

            String query = parseRequestUrl(requestUrl);

            if (StringUtils.isNotEmpty(query)) {
                this.parameters = parseQuery(query);
            }

            if (isExistsHttpBody()) {
                parseBody(inputStream);
            }
        }

        private ContentType parseContentType() {
            String contentType = this.headers.getOrDefault("content-type", "text/plain");
            ContentType result = ContentType.get(contentType);
            if (contentType.startsWith(ContentType.MULTIPART_FORM_DATA.getValue())) {
                this.boundary = "--" + contentType.split("; ")[1].split("=")[1];
                this.contentType = ContentType.get(contentType.split("; ")[0]);
            }
            return result;
        }

        /**
         * HTTP 바디에 있는 데이터를 파싱합니다.
         *
         * @param inputStream 인풋 스트림
         * */
        private void parseBody(BufferedInputStream inputStream) {
            if (this.boundary != null) {
                parseMultipartBody(inputStream);
                return;
            }
            parseRequestBody(inputStream);
        }

        /**
         * HTTP 헤더를 읽어 반환합니다.
         *
         * @param inputStream 인풋 스트림
         * @return HTTP 헤더 내용
         * */
        private String parseHeaderPart(BufferedInputStream inputStream) {
            int i;
            String headersPart = "";
            StringBuilder sb = new StringBuilder();
            try {
                while ((i = inputStream.read()) != -1) {
                    char c = (char) i;
                    sb.append(c);
                    if (isEndOfHeader(sb.toString())) {
                        headersPart = sb.toString().replace("\r\n\r\n", "");
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return headersPart;
        }

        /**
         * HTTP 바디를 파싱합니다.
         *
         * @param inputStream 소켓의 InputSteam
         * */
        private void parseRequestBody(InputStream inputStream) {
            StringBuilder sb = new StringBuilder();
            try {
                int binary;
                int inputStreamLength = inputStream.available();
                byte[] data = new byte[inputStreamLength];
                int i = 0;
                while ((binary = inputStream.read()) != -1) {
                    data[i] = (byte) binary;
                    if (isEndOfLine(data, i) || inputStream.available() == 0) {
                        data = Arrays.copyOfRange(data, 0, i + 1);
                        String line = new String(data, StandardCharsets.UTF_8);
                        sb.append(line);
                        data = new byte[inputStreamLength];
                        i = 0;
                    }
                    if (inputStream.available() == 0) break;
                    i++;
                }
                if (isJsonRequest()) {
                    this.json = sb.toString();
                    return;
                }
                this.parameters = parseQuery(sb.toString());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        /**
         * HTTP 헤더를 파싱합니다.
         *  @param headers 헤더 본문
         *  @return 헤더 목록
         * */
        private Map parseHeaders(String[] headers) {
            Map result = new HashMap<>();
            for (int i = 1; i < headers.length; i++) {
                int index = headers[i].indexOf(": ");
                String key = headers[i].substring(0, index).toLowerCase();
                String value = headers[i].substring(index + 2);
                if ("cookie".equals(key)) {
                    this.cookies = CookieStore.parseCookie(value);
                    continue;
                }
                result.put(key, value);
            }
            return result;
        }

        /**
         * 요청 URL을 파싱하여 저장하고 쿼리 스트링을 반환합니다.
         *
         * @param url 요청 URL
         * @return 쿼리 스트링
         * */
        private String parseRequestUrl(String url) {
            int index = url.indexOf("?");
            if (index == -1) {
                this.url = url;
                return "";
            }
            this.url = url.substring(0, index);
            return url.substring(index + 1);
        }

        /**
         * 쿼리 스트링을 파싱합니다.
         *
         * @param  parameters 쿼리 스트링
         * @return 파라미터 목록
         * */
        private Map parseQuery(String parameters) {
            Map map = new HashMap<>();
            String[] rawParameters = parameters.split("&");
            for (String rawParameter : rawParameters) {
                String[] parameterPair = rawParameter.split("=");
                String name = parameterPair[0];
                String value = "";
                if (isExistsParameterValue(parameterPair)) {
                    value = parameterPair[1];
                }
                map.put(name, value);
            }
            return map;
        }

        /**
         * multipart/form-data 요청을 파싱합니다.
         *
         * @param inputStream 소켓의 InputStream
         * */
        private void parseMultipartBody(InputStream inputStream) {
            try {
                StringBuilder sb = new StringBuilder();
                int i;
                while ((i = inputStream.read()) != -1) {
                    sb.append((char) i);
                    if (isBoundaryLine(sb.toString())) {
                        parseMultipartLine(inputStream);
                        return;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * multipart/form-data 본문을 한 파트씩 파싱합니다.
         *
         * @param inputStream 소켓의 InputStream
         * @throws IOException InputStream을 읽다가 오류 발생시
         * */
        private void parseMultipartLine(InputStream inputStream) throws IOException {
            int i = 0;
            int loopCnt = 0;
            String name = "";
            String value = "";
            String filename = "";
            String mimeType = "";
            byte[] fileData = null;
            boolean isFile = false;
            int inputStreamLength = inputStream.available();
            byte[] data = new byte[inputStreamLength];
            int binary;
            while ((binary = inputStream.read()) != -1) {
                data[i] = (byte) binary;
                if (isEndOfLine(data, i)) {
                    data = Arrays.copyOfRange(data, 0, i);
                    String line = new String(data, StandardCharsets.UTF_8);
                    data = new byte[inputStreamLength];
                    i = 0;
                    if (loopCnt == 0) {
                        loopCnt++;
                        int index = line.indexOf("\"");
                        if (index == -1) continue;
                        String[] split = line.split("\"");
                        name = split[1];
                        if (split.length == 5) {
                            filename = split[3];
                            isFile = true;
                        }
                        continue;
                    } else if (loopCnt == 1 && isFile) {
                        int index = line.indexOf(": ");
                        mimeType = line.substring(index + 2);
                        fileData = parseFile(inputStream);
                        loopCnt = 0;
                        if (fileData == null) continue;
                        line = boundary;
                    } else if (loopCnt == 1 && !line.contains(boundary)) {
                        value = line;
                        loopCnt = 0;
                        continue;
                    }

                    if (line.contains(boundary)) {
                        if (!filename.isEmpty()) {
                            createMultipartFile(name, filename, mimeType, fileData);
                        } else {
                            this.parameters.put(name, value);
                        }

                        name = "";
                        value = "";
                        filename = "";
                        mimeType = "";
                        fileData = null;
                        loopCnt = 0;
                    }
                    if (inputStream.available() == 0) return;
                }
                i++;
            }
        }

        /**
         * multipart/form-data로 받은 파일을 인스턴스로 만듭니다.
         *
         * @param name 파일 이름
         * @param filename 파일 전체 이름
         * @param mimeType 미디어 타입
         * @param fileData 파일의 데이터
         * @see MultipartFile
         * */
        private void createMultipartFile(String name, String filename, String mimeType, byte[] fileData) {
            MultipartFile multipartFile = new MultipartFile(filename, mimeType, fileData);
            if (this.files.get(name) == null) {
                this.files.put(name, multipartFile);
                return;
            }
            Object file = this.files.get(name);
            addMultipartFile(name, multipartFile, file);
        }

        /**
         * MultipartFile을 추가합니다.
         *
         * @param name MultipartFile의 이름
         * @param multipartFile MultipartFile 인스턴스
         * @param file MultipartFile 목록 또는 MultipartFile
         * */
        @SuppressWarnings("unchecked")
        private void addMultipartFile(String name, MultipartFile multipartFile, Object file) {
            if (file.getClass().equals(ArrayList.class)) {
                ((ArrayList) file).add(multipartFile);
                return;
            }
            List files = new ArrayList<>();
            MultipartFile preFile = (MultipartFile) file;
            files.add(preFile);
            files.add(multipartFile);
            this.files.put(name, files);
        }

        /**
         * Multipart boundary를 기준으로 파일을 읽어 들이고 바이트 배열을 반환합니다.
         *
         * @param inputStream 소켓의 InputStream
         * @return 파일의 바이트 배열
         * */
        private byte[] parseFile(InputStream inputStream) {
            byte[] data = new byte[1024 * 8];
            int fileLength = 0;
            try {
                int i;
                while ((i = inputStream.read()) != -1) {
                    if (isFullCapacity(data, fileLength)) {
                        data = getDoubleArray(data);
                    }
                    data[fileLength] = (byte) i;
                    if (isEndOfLine(data, fileLength)) {
                        String content = new String(data, StandardCharsets.UTF_8);
                        if (isEmptyBoundaryContent(content)) return null;
                        if (isEndOfBoundaryLine(content)) break;
                    }
                    fileLength++;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return Arrays.copyOfRange(data, 2, fileLength - boundary.getBytes(StandardCharsets.UTF_8).length);
        }

        /**
         * HttpRequest 혹은 HttpMultipartRequest 인스턴스를 생성합니다.
         *
         * @return 요청 인스턴스
         * @see org.sam.server.http.web.HttpRequest
         * @see org.sam.server.http.web.HttpMultipartRequest
         * */
        public Request createRequest() {
            if (headers.isEmpty()) return null;
            if (contentType == ContentType.MULTIPART_FORM_DATA) {
                return new HttpMultipartRequest(this);
            }
            return new HttpRequest(this);
        }

        /**
         *  한 줄의 마지막인지 확인합니다.
         *
         * @param data 데이터
         * @param index 인덱스
         * @return 한 줄의 마지막인지 여부
         * */
        private boolean isEndOfLine(byte[] data, int index) {
            return index != 0 && data[index - 1] == '\r' && data[index] == '\n';
        }

        /**
         *  헤더의 끝 부분인지 확인합니다.
         *
         * @param data 데이터
         * @return 헤더의 끝인지 여부
         * */
        private static boolean isEndOfHeader(String data) {
            String CR = "\r";
            return data.endsWith(CR + "\n\r\n");
        }

        /**
         * 파라미터에 값이 있는지 확인합니다.
         *
         * @param parameterPair 파라미터 쌍
         * @return 파라미터 값 존재 여부
         * */
        private boolean isExistsParameterValue(String[] parameterPair) {
            return parameterPair.length == 2;
        }

        /**
         * HTTP 요청이 아닌지 확인합니다.
         *
         * @param headersPart 헤더
         * @return HTTP 요청이 아닌지에 대한 여부
         * */
        private boolean isNonHttpRequest(String headersPart) {
            return headersPart.trim().isEmpty();
        }

        /**
         * HTTP 바디에 메시지가 존재하는 지 확인합니다.
         *
         * @return HTTP 바디에 메시지가 존재하는지 여부
         * */
        private boolean isExistsHttpBody() {
            return this.httpMethod == HttpMethod.POST ||
                    this.httpMethod == HttpMethod.PUT ||
                    this.contentType == ContentType.APPLICATION_JSON;
        }

        /**
         * HTTP 요청 본문이 JSON인지 확인합니다.
         * */
        private boolean isJsonRequest() {
            return this.contentType == ContentType.APPLICATION_JSON && this.parameters.isEmpty();
        }

        /**
         * boundary 라인인지 확인합니다.
         *
         * @param line multipart 본문 라인
         * @return boundary 라인 여부
         * */
        private boolean isBoundaryLine(String line) {
            return line.contains(this.boundary + "\r\n");
        }

        /**
         * boundary 라인이 끝났는지 확인합니다.
         *
         * @param content multipart 본문 라인
         * @return boundary 라인이 끝났는지 여부
         */
        private boolean isEndOfBoundaryLine(String content) {
            String boundary = new String(this.boundary.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            return content.contains(boundary);
        }

        /**
         * boundary 라인이 존재하는 지 확인합니다.
         *
         * @param content multipart 본문 라인
         * @return boundary 라인 존재 여부
         * */
        private boolean isEmptyBoundaryContent(String content) {
            return content.trim().equals(this.boundary);
        }

        /**
         * 입력받은 배열의 길이의 두배인 배열을 생성하고 카피 후 반환합니다.
         *
         * @param data 배열
         * @return 2배 길이의 배열
         * */
        private byte[] getDoubleArray(byte[] data) {
            byte[] arr = new byte[data.length * 2];
            System.arraycopy(data, 0, arr, 0, data.length);
            return arr;
        }

        /**
         * 배열의 길이가 최대인지 확인합니다.
         *
         * @param data 확인할 배열
         * @param fileLength 파일 길이
         * @return 배열의 길이가 최대인지 여부
         * */
        private boolean isFullCapacity(byte[] data, int fileLength) {
            return data.length == fileLength;
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy