지금까지 우리가 짠 코드에는 심각한 보안 결함이 있다.
사용자가 직접 서버에 읽고 쓸 수있는 임의의 경로를 제공하는 것이다. 
이를 방지하기 위해 정규표현식으로 제목을 검증하는 함수를 작성해보자.

먼저, regexp를 import 해서 validPath 라는 전역변수를 하나 선언한다.

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

regexp.MustCompile 함수는 정규 표현식을 구문 분석하고 컴파일해서 regexp.Regexp를 반환한다.
MustCompile은 표현식 컴파일이 실패할 경우 프로그램이 종료된다는 점에서 일반 Compile 과 다르다.
일반 Compile의 경우 두번째 매개 변수로 오류를 반환한다.

이제 validPath 표현식을 사용해서 경로의 유효성을 검사하고 페이지 제목을 추출하는 함수를 작성해보자.

func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return "", errors.New("invalid Page Title")
    }
    return m[2], nil // The title is the second subexpression.
}

위 함수는 만약 제목이 유효하면 nil 오류 값과 함께 반환한다. 반면 제목이 유효하지 않은 경우 함수는 HTTP 연결에 "404 찾을 수 없음"오류를 기록하고 핸들러에 오류를 반환한다.
새 오류를 생성하려면 errors 패키지도  import 해야한다.

이제 각각의 핸들러 안에 getTitle 이라는 메서드를 추가해보자.
결과적으로 아래와 같은 코드가 완성될 것이다.

package main

import (
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"errors"
	"regexp"
)

type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
	filename := title + ".txt"
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return &Page{Title: title, Body: body}, nil
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	t, _ := template.ParseFiles(tmpl + ".html")
	t.Execute(w, p)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}


func editHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

func main() {
	http.HandleFunc("/view/", viewHandler)
	http.HandleFunc("/edit/", editHandler)
	http.HandleFunc("/save/", saveHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

 

각 핸들러에서 오류조건을 잡게되면 반복되는 코드가 많이 발생하게 된다.
유효성 검사 및 오류 검사를 수행하는 함수에서 각 핸들러를 감싸게 할 수 있다.
 Literals 및 Closures 함수를 사용해보자.
literal 함수는 해당 기능을 추상화 할 수 있는 수단을 제공한다.

먼저, 각 핸들러의 함수를 아래와 같이 다시 정의한다.

func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

이제 위 유형의 함수를 가져와서, 반환하는 래퍼 함수를 정의해보자.

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Here we will extract the page title from the Request,
		// and call the provided handler 'fn'
	}
}

이렇게 반환 된 함수는 외부에서 정의된 값을 포함하므로 클로저라고 한다.
이 경우 변수 fn은 클로저로 둘러쌓여 있다.
이제 getTitle 코드를 다시 가져와서 수정하면 아래와 같이 사용할 수 있다.

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        fn(w, r, m[2])
    }
}

makeHandler에 반환된 클로저는 http.ResponseWriter와 http.Request(즉!! http.HandlerFunc)을 받는 함수다.
클로저는 요청 경로에서 제목을 추출하고 validPath regexp로 유효성을 검사한다.
title이 유효하지 않은 경우 http.NotFound 함수를 사용해서 ResponseWriter에 오류 로그를 작성한다.
title이 유효한 경우 fn은 ResposeWriter, Request 및 제목을 인수로 사용해서 해당 handler를 호출한다.
이렇게 하면 이제 핸들러 함수를 http 패키지에 등록하기 전에 main에서 makeHandler로 래핑할 수 있다.

func main() {
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

이제 우리는 각 핸들러 함수에서 getTitle에 대한 호출을 제거해서 훨씬 간단한 코드를 정의할 수 있다.
최종 wiki.go 의 코드는 아래와 같다.

package main

import (
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"regexp"
)

type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
	filename := title + ".txt"
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return &Page{Title: title, Body: body}, nil
}

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	err := templates.ExecuteTemplate(w, tmpl+".html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		m := validPath.FindStringSubmatch(r.URL.Path)
		if m == nil {
			http.NotFound(w, r)
			return
		}
		fn(w, r, m[2])
	}
}

func main() {
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))

	log.Fatal(http.ListenAndServe(":8080", nil))
}

 

이제 해당코드를 빌드해서 실행해보자.

$ go build wiki.go
$ ./wiki

http://localhost:8080/view/ANewPage  로 접근해보자. 아래와 같은 화면이 뜰 것이다.

특정 Text를 입력 후 save 버튼을 누르면 아래처럼 생성된다. edit 버튼을 눌러서 yunjiTest2로 수정해보자.

정상적으로  수정되는 것을 확인할 수 있다.

 

+ Recent posts