cos.jar 의 FileRenamePolicy 만들기.

Published on: 2011. 2. 15. 09:52 by louis.dev

MultipartRequest를 통해 업로드를 하면, 생성자에 FileRenamePolicy 인스턴스를 파라미터로 전달합니다.

일반적으로 DefaultFileRenamePolicy를 많이 사용하게 되는데요.

이 DefaultFileRenamePolicy는 간단한 역할을 합니다.

public class DefaultFileRenamePolicy implements FileRenamePolicy {
  
  // This method does not need to be synchronized because createNewFile()
  // is atomic and used here to mark when a file name is chosen
  public File rename(File f) {
    if (createNewFile(f)) {
      return f;
    }
    String name = f.getName();
    String body = null;
    String ext = null;

    int dot = name.lastIndexOf(".");
    if (dot != -1) {
      body = name.substring(0, dot);
      ext = name.substring(dot);  // includes "."
    }
    else {
      body = name;
      ext = "";
    }

    // Increase the count until an empty spot is found.
    // Max out at 9999 to avoid an infinite loop caused by a persistent
    // IOException, like when the destination dir becomes non-writable.
    // We don't pass the exception up because our job is just to rename,
    // and the caller will hit any IOException in normal processing.
    int count = 0;
    while (!createNewFile(f) && count < 9999) {
      count++;
      String newName = body + count + ext;
      f = new File(f.getParent(), newName);
    }

    return f;
  }

  private boolean createNewFile(File f) {
    try {
      return f.createNewFile();
    }
    catch (IOException ignored) {
      return false;
    }
  }
}


코드에서 보시다 시피 중복된 파일이 있으면 파일이름 뒤에 1~9999까지의 숫자를 붙여서 파일의 rename을 진행하고 있는것을 확인 할 수 있습니다.

만약 서버의 업로드 서버의 인코딩 설정 문제로 한글이 깨지거나 할수 있습니다. 그렇기 때문에 저는 Timestamp를 통해 파일이름을 변경하고 있습니다. 이렇듯 MultipartRequest에서 File Rename Policy를 Timestamp로 변경하는 방법을 진행해 보겠습니다.

package net.tutorial.util;

import java.io.File;

import com.oreilly.servlet.multipart.FileRenamePolicy;

public class TimestampFileRenamePolicy implements FileRenamePolicy {
	@Override
	public File rename(File f) {
		String name = f.getName();
		String body = null;
		String ext = null;

		int dot = name.lastIndexOf(".");
		if (dot != -1) {
			ext = name.substring(dot); // includes "."
		} else {
			ext = "";
		}
		body = Long.toString( System.currentTimeMillis() );

		File renameFile = new File( f.getParent(), body + ext );
		
		if( renameFile.exists() ){
			int count = 0;
			do {
				count++;
				String newName = body + count + ext;
				renameFile = new File(f.getParent(), newName);
			}while( renameFile.exists() && count < 9999 );
			
		}
		f.renameTo( renameFile );
		return renameFile;
		
	}
}


System.currentTimeMillis()를 통해 현재의 시간을 ms 단위로 구한뒤에 이를 파일이름에 붙입니다. 만약 그럴일은 없겠지만..^^ 동시에 파일 업로드를 진행해서 파일이름이 똑같다면, Timestamp뒤에 1~9999를 붙여서 처리하는 TimeStampRenamePolicy를 만들어 보았습니다.

cos.jar 의 MultipartRequest를 확장한 확장자 체크 만들기.

Published on: 2011. 2. 15. 09:36 by louis.dev

그동안 파일 업로드를 할때 apache의 commons-fileupload를 사용하거나, oreilly의 MultipartRequest를 통해 업로드를 해왔습니다. 사실은 commons-fileupload 보다 MultipartRequest를 사용하는 것을 더 선호 했습니다.

왜냐!!

더 쉽기 때문이죠..^^;;

MultipartRequest를 통해 파일 업로드를 하려면, 단순히 MultipartRequest 인스턴스를 생성하는 것만으로도 파일업로드가 진행이 되었습니다.

이렇게 쉽게 파일 업로드를 하는 MultipartRequest에도 문제점은 가지고 있었으니.. 그것은 바로, 파일업로드를 진행하기 전, 파일의 확장자를 체크하지 못한다는 점입니다. 보안상 이슈로 인해 jsp, php, asp와 같은 파일들은 업로드 하지 못하게 막아야 하는데 ( 업로드 한후 업로드 한 파일을 실행하여 서버의 정보나 기타 다른 보안내용을 가져갈 수 있기 때문입니다..). 물론 스크립트 단에서 스크립트를 통해 확장자를 제어 하는 방법이 존재 하긴 하지만, 스크립트는 언제나 우회 가능 하기 때문에, 결국은 Backend에서 확장자를 체크하고, 확장자가 유효한지, 유효하지 않은지에 따라 업로드 가능 유무를 체크 해야 합니다.

이런 점을 들어, MultipartRequest를 개조하여 CheckExtentionMultipartRequest 를 만들어 보았습니다. 파일 Validation을 하는 것과 동시에, 소스 코드 리펙토링을 진행하여 코드를 작성해 보았습니다.

소스코드를 보다 보니 대부분의 Collection을 Vector로 작성을 하였는데, 아마 synchronized 때문에 Vector를 사용한 것으로 보여집니다. 하지만 자바 권고 안은 synchronized 가 걸린 Collection은
 
 Collections.synchronizedMap( new HashMap() )

을 통해 생성하라고 권고 하고 있기 때문에 Vector로 구현된 내용을 위와 같이 수정하였습니다.


CheckExtentionMultipartRequest

package net.tutorial;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;

import net.daum.info.exception.InvalidFileExtensionException;

import com.oreilly.servlet.multipart.FilePart;
import com.oreilly.servlet.multipart.FileRenamePolicy;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.ParamPart;
import com.oreilly.servlet.multipart.Part;

@SuppressWarnings("unchecked")
public class CheckExtentionMultipartRequest {
	//파라미터 저장 Map
	protected Map<Object,Object> parameters = Collections.synchronizedMap( new HashMap() ); 
	//file 저장 Map
	protected Map<Object,Object> files = Collections.synchronizedMap( new HashMap() ); 

	//기존의 파라미터들은 동일하고, String[] extentsion이 추가 됨.
	//.(컴마)를 제외한 제외하려는 확장자를 array 형태로 넘기면 됨(ex - {"php","jsp","asp"} )
	public CheckExtentionMultipartRequest(HttpServletRequest request,String saveDirectory, int maxPostSize, String encoding,FileRenamePolicy policy, String[] extensions) throws Exception {
		// Sanity check values
		if (request == null)
			throw new IllegalArgumentException("request cannot be null");
		if (saveDirectory == null)
			throw new IllegalArgumentException("saveDirectory cannot be null");
		if (maxPostSize <= 0) {
			throw new IllegalArgumentException("maxPostSize must be positive");
		}

		// Save the dir
		File dir = new File(saveDirectory);

		// 디렉토리 인지 체크
		if (!dir.isDirectory())
			throw new IllegalArgumentException("Not a directory: "+ saveDirectory);

		// write 할수 있는 디렉토리인지 체크
		if (!dir.canWrite())
			throw new IllegalArgumentException("Not writable: " + saveDirectory);

		MultipartParser parser = new MultipartParser(request, maxPostSize, true, true, encoding);

		if (request.getQueryString() != null) {
			// HttpUtil이 Deprecated 되었기 때문에 다른방식으로 작성						Map<String,Object> queryParameters = Collections.synchronizedMap( request.getParameterMap() );
			Iterator queryParameterNames = queryParameters.keySet().iterator();
			while( queryParameterNames.hasNext() ) {
				Object paramName = queryParameterNames.next();
				String[] values = ( String[] ) queryParameters.get( paramName );
				List<Object> newValues = Collections.synchronizedList( new ArrayList<Object>() );
				for( String value : values ) {
					newValues.add( value );
				}
				parameters.put(paramName, newValues ); 	
			}
		}

		Part part;
		while ((part = parser.readNextPart()) != null) {
			String name = part.getName();
			if (name == null) {
				throw new IOException("Malformed input: parameter name missing (known Opera 7 bug)");
			}
			if (part.isParam()) {
				// It's a parameter part, add it to the vector of values
				ParamPart paramPart = (ParamPart) part;
				String value = paramPart.getStringValue();
				List<Object> existingValues = (List<Object>) parameters.get(name);
				if (existingValues == null) {
					existingValues = new Vector();
					parameters.put(name, existingValues);
				}
				existingValues.add( value );
			} else if ( part.isFile() ) {
				// It's a file part
				FilePart filePart = (FilePart) part;
				String fileName = filePart.getFileName();
				if (fileName != null) {
					//file 확장자를 validation 합니다.
					//만약 유효하지 않은 확장자라면 InvalidFileExtensionException을 throw 합니다.
					if( !isValidFileExtension(fileName, extensions)) {
						throw new InvalidFileExtensionException( "Invalid File Extension" );
					}
					else {
						filePart.setRenamePolicy(policy); // null policy is OK
						filePart.writeTo(dir);
						files.put(name,
								new UploadedFile(dir.toString(), filePart
										.getFileName(), fileName, filePart
										.getContentType()));	
					}
					
				} else {
					// The field did not contain a file
					files.put(name, new UploadedFile(null, null, null, null));
				}
			}
		}
	}

	public Iterator<Object> getParameterNames() {
		return parameters.keySet().iterator();
	}

	public Iterator<Object> getFileNames() {
		return files.keySet().iterator();
	}

	public String getParameter(String name) {
		try {
			List<Object> values = (List<Object>) parameters.get(name);
			if (values == null || values.size() == 0) {
				return null;
			}
			String value = (String) values.get(values.size() - 1);
			return value;
		} catch (Exception e) {
			return null;
		}
	}

	public Object[] getParameterValues(String name) {
		try {
			List<Object> values = (List<Object>) parameters.get(name);
			if (values == null || values.size() == 0) {
				return null;
			}
			return values.toArray();
		} catch (Exception e) {
			return null;
		}
	}

	public String getFilesystemName(String name) {
		try {
			UploadedFile file = (UploadedFile) files.get(name);
			return file.getFilesystemName(); // may be null
		} catch (Exception e) {
			return null;
		}
	}

	public String getOriginalFileName(String name) {
		try {
			UploadedFile file = (UploadedFile) files.get(name);
			return file.getOriginalFileName(); // may be null
		} catch (Exception e) {
			return null;
		}
	}

	public String getContentType(String name) {
		try {
			UploadedFile file = (UploadedFile) files.get(name);
			return file.getContentType(); // may be null
		} catch (Exception e) {
			return null;
		}
	}

	public File getFile(String name) {
		try {
			UploadedFile file = (UploadedFile) files.get(name);
			return file.getFile(); // may be null
		} catch (Exception e) {
			return null;
		}
	}
	
	public boolean isValidFileExtension( String fileName ,String[] extensions) throws Exception{
		boolean result = false;
		if( fileName != null && !"".equals( fileName ) && !isContainFileExtension(fileName, extensions)) {
			result = true;
		}
		else {
			result = false;
		}
		return result;
	}
	private boolean isContainFileExtension( String fileName, String[] extensions ) throws Exception{
		boolean result = false;
		String fileExtension = getFileExtension( fileName );
		for( String ex : extensions ) {
			if( fileExtension.equals( ex ) ) {
				result = true;
				break;
			}
		}
		return result;
	}
	private String getFileExtension( String fileName ) throws Exception{
		String fileExtension = "";
		if( fileName != null && !"".equals( fileName )) {
			if( fileName.lastIndexOf( "." ) != -1){
				fileExtension = fileName.toLowerCase().substring( fileName.lastIndexOf( "." ) + 1, fileName.length() );
			}
			else {
				fileExtension = "";
			}
		}else{
			fileExtension = "";
		}
		return fileExtension;
	}
}
class UploadedFile {

	private String dir;
	private String filename;
	private String original;
	private String type;

	UploadedFile(String dir, String filename, String original, String type) {
		this.dir = dir;
		this.filename = filename;
		this.original = original;
		this.type = type;
	}

	public String getContentType() {
		return type;
	}

	public String getFilesystemName() {
		return filename;
	}

	public String getOriginalFileName() {
		return original;
	}

	public File getFile() {
		if (dir == null || filename == null) {
			return null;
		} else {
			return new File(dir + File.separator + filename);
		}
	}
}


위와 같이 작성하면 됩니다. InvalidFileExtensionException은 간단하게 다음과 같이 작성하시면 됩니다.

package net.tutorial.exception

@SuppressWarnings("serial")
public class InvalidFileExtensionException extends Exception {

	public InvalidFileExtensionException(String message) {
		super(message);
	}

}


실질 적으로 사용하는 방법은 다음과 같이 사용하시면 됩니다.
ExtensionCheckMultipartRequest multipartReqeust = null;
	try {
		String ex = {"php","jsp","asp"};
		multipartReqeust = new ExtensionCheckMultipartRequest(request, "파일업로드 패스", 5 * 1024 * 1024, "utf-8", new DefaultFileRenamePolicy(), ex);
	}catch ( InvalidFileExtensionException e) {
		e.printStackTrace();
		//TODO
	}

DWR - DWR 실행시 Alert창으로 Session Error란 에러가 발생될때

Published on: 2011. 1. 31. 12:13 by louis.dev

서버에 올린후 테스트하려고 하니 DWR에러가 나더군요. log를 확인해 보니
A request has been denied as a potential CSRF attack.
란 에러를 뿌리며 에러가 발생합니다.

이런에러가 발생할 때는 web.xml의 DWR 설정에 init-param 설정으로 간단하게 처리할 수 있습니다.


 <servlet>
  <servlet-name>dwr-invoker</servlet-name>
  <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
  <init-param>
   <param-name>debug</param-name>
   <param-value>true</param-value>
  </init-param>
  <init-param>
   <param-name>crossDomainSessionSecurity</param-name>
   <param-value>false</param-value>
  </init-param>
  </servlet>
 <servlet-mapping>
  <servlet-name>dwr-invoker</servlet-name>
  <url-pattern>/dwr/*</url-pattern>
 </servlet-mapping>

위와 같이 crossDomainSessionSecrurity를 false로 주면 됩니다. 이 옵션을 false를 줌으로서 어떤 보안상에 문제가 발생하는지는 좀더 알아봐야겠네요.

Linux - Apache server와 Tomcat을 JK Connector(JKModule)로 연결하기

Published on: 2011. 1. 31. 12:08 by louis.dev

1. 여기에 들어가서 JK Connector 파일을 다운로드 받습니다.
저는 여기에서 jk/binaries/linux/jk-1.2.31/x86_64/mod_jk-1.2.31-httpd-2.2.x.so 를 다운 받겠습니다. 작업하시는 분의 서버에 따라 다운받으시면 되겠습니다.

2. 이름이 너무 길기 때문에 다운로드된 파일을 mod_jk.so로 변경합니다.

3. apache 서버 루트에 modules 디렉토리에 복사하여 넣습니다. 권한은 755로 주시면 됩니다.
chmod 755 mod_jk.so

4. apache 서버루트에 conf/httpd.conf 에 다음을 추가합니다.
 
 LoadModule jk_module     modules/mod_jk.so
<IfModule jk_module>
  JkWorkersFile conf/workers.properties
  JkLogFile             logs/mod_jk.log
  JkLogLevel    info
  JkMountFile   conf/uriworkermap.properties
</IfModule>

5. conf 디렉토리 밑( httpd.conf와 같은 위치 입니다.) workers.properties 파일을 생성합니다.
worker.list=worker1
worker.worker1.port=8009
worker.worker1.host=localhost
worker.worker1.type=ajp13
worker.worker1.lbfactor=1

6. conf 디렉토리 밑( httpd.conf와 같은 위치 입니다.) uriworkermap.properties 파일을 생성합니다.
/*.do=worker1
/*.jsp=worker1
이렇게 uriworkermap.properties파일에 추가하면 해당 suffix로 URL을 요청하면 tomcat이 처리하게 넘겨주게 됩니다.
특정 컨텍스트의 모든 요청을 톰켓이 처리 하길 원하신 다면
/contextName/*=worker1
로 설정하시면 됩니다.

7. 설정하신후 ${server_root}/bin/apachectl graceful 을 실행하여 서버 설정을 적용합니다.
만약
apachectl: Configuration syntax error, will not run "graceful":
httpd: Syntax error on line 205 of /httpd/conf/httpd.conf: Cannot load /httpd/modules/mod_jk.so into server: /httpd/modules/mod_jk.so: wrong ELF class: ELFCLASS32
라는 문제가 발생한다면 해당 jk 모듈을 서버에 맞게 32비트용, 혹은 64비트용으로 새로 다운받으신후 적용하시면 문제 없이 적용이 되는것을 볼수가 있습니다.

Maven - maven 과 eclipse WTP 연동하기

Published on: 2011. 1. 10. 13:55 by louis.dev

많은 분들이 자바 웹 프로젝트를 진행할때 이클립스 IDE를 사용합니다. 물론 저도 그렇구요. 이클립스는 WTP라는 아주 훌륭한 플랫폼이 있어서 웹개발시에 작업하고 서버에 deploy하지 않고도 이클립스상에서 서버를 껐다 키고, 서버의 임시 deploy 디렉토리에 배포하여 빠르게 개발할수 있습니다.

하지만 Maven을 통해 프로젝트를 생성하면 바로 WTP를 사용할 수가 없습니다. 인터넷에 검색해보면 여러가지 적용방법이 있는데 그중 가장 간단한 방법인 이클립스의 m2eclipse plugin과 m2eclipse Extras plugin을 통해서 간단하게 WTP용 Maven 프로젝트를 생성해 보겠습니다.

전체적으로 3단계로 나눌수 있겠는데요.
첫째. 이클립스 플러그인인 m2eclipse 를 설치한다.
(maven이 설치 되었다는 가정하에 진행하겠습니다.)
둘째. 이클립스 플러그인인 m2eclipse Extras 를 설치한다.
셋째. maven 프로젝트를 생성한다.

이정도 쯤으로 간단하게 WTP와 maven을 연동할수 있습니다.

그럼 지금부터 시작해 보도록 하겠습니다.

1. m2eclipse 플러그인을 설치합니다.
Help >> Install New Software >> Add버튼을 누르고 다음 url을 입력해 줍니다.
http://m2eclipse.sonatype.org/sites/m2e


위와 같이 Maven Integration for Eclipse를 선택하시고 Next >> Next >> I accept the terms of the license argreement 선택하시고 Finish 누르시면 설치가 시작됩니다.

2. m2eclipse Extras 플러그인을 설치합니다. 위와같이 Help >> Install New Software >> Add버튼을 누르고 다음 url을 입력해 줍니다.
http://m2eclipse.sonatype.org/sites/m2e-extras

위와 같이 Maven Integration for Eclipse Extras와 Maven Integration for Eclipse WTP를 선택하시고 설치하시면 됩니다.

위의 4개를 보두 선택하셔도 상관없지만 좀더 빠른 설치를 위해서 필요한것만 설치했습니다.

3. Maven Project를 생성합니다.
File >> New >> Other >> Maven >> Maven Project 를 선택합니다.
Use default Workspace location 을 선택하고 Next를 눌러줍니다.


다음 창에서 artifact Id가 maven-archetype-webapp를 선택합니다.

Group Id : 이 프로젝트를 구별하기 위한 일종의 ID입니다. 보통 package 와 똑같이 지정합니다.
Artifact Id : Project 이름입니다.
Package : 프로젝트에서 사용할 package이름입니다.

위와 같이 프로젝트를 생성하면 Dynamic Web Project가 생성됩니다.

지금부터 약간의 설정을 해야합니다.

새로 만들어진 프로젝트를 선택한후 오른쪽 마우스 클릭 >> Properties >> Project Facets를 선택합니다.
이곳에서 바꾸어야 할것은 3가지 입니다.

1. Dynamic Web Module의 버전
2. Dynamic Web Project의 Webroot 위치
3. Java 버전 변경
입니다.

먼저 첫번째를 수정하려면 다음과 같이 진행해야 합니다.
Dynamic Web Module의 체크를 풀어준다 >> Apply 버튼을 누른다 >> Dynamic Web Module을 Version을 알맞게 변경한다.(저는 참고로 2.5로 변경하겠습니다.) >> Dynamic WebModule의 체크를 다시 한다

두번째 webroot의 위치를 변경하겠습니다.
위사진의 (3번)을 선택한다. >> 다음창에서 Context root는 프로젝트 이름으로(기본적으로 프로젝트 이름이고 바꾸고 싶은것으로 바꾸어도 됩니다. 하지만 전 default로 두겠습니다.) 설정하고 directory는 /src/main/webapp 로 세팅합니다.

이렇게 세팅하면 기존의 Dynamic Web Project의 webroot가 WebContent에서 /src/main/webapp로 변경됩니다.

세번째 Java Version을 변경합니다. 저는 1.6 버전을 사용할 것이기 때문에 1.6으로 선택하고 OK를 누르겠습니다.


이렇게 세팅하고 OK를 눌러 빠져나옵니다. 만약 위와같이 설정한뒤 프로젝트에 에러가 발생하면 아마 거의 대부분은 위에서 설정한 Java버전과 Build Path의 java버전과 맞지 않았을때 에러가 날것입니다. 당연히 Build Path의 자바 버전도 변경해 주면 에러는 없어집니다.

기본적인 설정은 여기 까지입니다. 하지만 메이븐을 많이 사용하신 분들이라면 뭔가 이상함을 느끼실 겁니다.

패키지 이름이 이상합니다..ㅜㅜ

보통 메이븐은 4가지 정도로 분리가 됩니다.
1) src/main/java --> back end를 구성할 java 파일을 작성합니다.
2) src/main/resources --> 프로젝트의 설정파일을 저장합니다.
3) src/test/java --> 테스트를 하기위한 테스트 케이스를 작성합니다.
4) src/test/resources --> 테스트를 하기위한 설정파일을 작성합니다.

이렇게 4가지가 있어야 하는데 지금은 src/main/resources 밖에 없습니다.
그렇다면 직접 만들어 주는 수밖에 없겠네요..

탐색기를 통해 해당 웹프로젝트 디렉토리로 이동합니다.
다음과 같은 디렉토리를 만듭니다.

1) src/main/java
2) src/test 생성후
               /java
              /resources

이런 구조로 만들어 주시면 됩니다.

그리고 이클립스로 돌아오셔서 상단의 Navigator를 선택하시면 다음과 같은 구조를 보실수 있습니다.
(Navigator가 없으시면 Window >> Show View >> Navigator 를 선택하시면 됩니다)

이중에 수정해야 할 파일은 2가지 입니다.
.settings/org.eclipse.wst.common.component

.classpath 파일입니다.

먼저 .settings/org.eclipse.wst.common.component를 열고 다음을 추가합니다.

빨간 3줄을 추가해 줍니다.

다음으로 .classpath입니다.


위의 3줄을 추가해주고 그 아래 한줄을 수정해 줍니다.

이로서 maven + WTP의 통합 과정이 끝났습니다. 이제 불편하게 maven package를 만들어서 테스트 서버에 배포할 필요없이 바로 사용할 수 있습니다.
Maven
국내도서>컴퓨터/인터넷
저자 : Sonatype Company / 장선진역
출판 : 지앤선(지&선) 2010.09.06
상세보기

자바 프로젝트 필수 유틸리티
국내도서>컴퓨터/인터넷
저자 : 박재성
출판 : 한빛미디어 2009.02.28
상세보기