Created
December 2, 2018 14:55
-
-
Save mackee/2247f081ad860ed36c7b27bb6886b862 to your computer and use it in GitHub Desktop.
Go de CGI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>ようこそ! GoでCGIのホームページへ!</title> | |
</head> | |
<body style="text-align: center; margin: 0 auto; width: 800px; background-color: lightseagreen;"> | |
<h1>ようこそ!GoでCGIのホームページへ!<h1> | |
<hr> | |
<p>あなたは{{ .Counter }}番目の訪問者です!<p> | |
<hr> | |
<h2>コメント</h2> | |
<div> | |
<form action="/" method="POST"> | |
<p> | |
<label for="hitokoto">ひとこと</label> | |
<input type="text" name="hitokoto" id="hoitokoto" size=50> | |
<button type="submit">送信</button> | |
</p> | |
{{ if ne .ErrorMessage "" }} | |
<p style="color: red;">{{ .ErrorMessage }}</p> | |
{{ end }} | |
</form> | |
<div style="width: 100%;"> | |
<hr> | |
{{ range .Comments }} | |
<p style="text-align: left; padding: 0 100px;">{{ . }}</p> | |
<hr> | |
{{ end }} | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"crypto/sha256" | |
"encoding/base64" | |
"fmt" | |
"html/template" | |
"io" | |
"log" | |
"net/http" | |
"net/http/cgi" | |
"os" | |
"strconv" | |
"strings" | |
"time" | |
) | |
const ( | |
viewFilename = "views/bbs.tmpl" | |
logFilename = "log/bbserror.log" | |
csrfSalt = "XXXXXXXXXX" | |
csrfTokenExpireSeconds = 60 * 30 | |
counterFilename = "data/counter.dat" | |
hitokotoFilename = "data/hitokoto.dat" | |
commentLimit = 10 | |
) | |
type formError int | |
func (f formError) Error() string { | |
switch f { | |
case formErrorEmptyHitokoto: | |
return "空白もしくは空白文字のみの投稿はできません" | |
case formErrorInvalidCSRFToken: | |
return "不正なフォーム入力を検出しました" | |
case formErrorInternalServerError: | |
return "内部エラーが発生しました" | |
case formErrorCSRFTokenExpired: | |
return "フォームの有効期限が切れました。もう一度お試しください" | |
default: | |
return "不明なエラーが発生しました" | |
} | |
} | |
const ( | |
formErrorNone formError = iota | |
formErrorEmptyHitokoto | |
formErrorInvalidCSRFToken | |
formErrorInternalServerError | |
formErrorCSRFTokenExpired | |
) | |
func main() { | |
logFile, err := os.OpenFile(logFilename, os.O_WRONLY|os.O_APPEND, os.ModeAppend) | |
if err != nil { | |
panic(err) | |
} | |
defer logFile.Close() | |
log.SetOutput(logFile) | |
err = cgi.Serve(http.HandlerFunc(handler)) | |
//err = http.ListenAndServe(":8080", http.HandlerFunc(handler)) | |
if err != nil { | |
log.Fatalf("[ERROR] ListenAndServe error: %s", err) | |
} | |
} | |
func handler(w http.ResponseWriter, r *http.Request) { | |
switch r.Method { | |
case http.MethodGet: | |
handlerGET(w, r, formErrorNone) | |
case http.MethodPost: | |
handlerPOST(w, r) | |
default: | |
w.WriteHeader(http.StatusMethodNotAllowed) | |
} | |
} | |
func handlerGET(w http.ResponseWriter, r *http.Request, fe error) { | |
tmpl, err := template.ParseFiles(viewFilename) | |
if err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
log.Println("[ERROR] parse template error: %s", err) | |
return | |
} | |
var errorMessage string | |
if fe != formErrorNone { | |
errorMessage = fe.Error() | |
} else { | |
err := incrCounter() | |
if err != nil { | |
log.Printf("[ERROR] fail incrCounter: %s", err) | |
w.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
} | |
c, err := counter() | |
if err != nil { | |
log.Printf("[ERROR] fail counter(): %s", err) | |
w.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
csrfToken, err := generateCSRFToken() | |
if err != nil { | |
log.Printf("[ERROR] fail generateCSRFToken: %s", err) | |
w.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
cookie := &http.Cookie{ | |
Name: "_csrftoken", | |
Value: csrfToken, | |
Expires: time.Now().Add(30 * time.Minute), | |
HttpOnly: true, | |
} | |
http.SetCookie(w, cookie) | |
comments, err := comments() | |
if err != nil { | |
log.Printf("[ERROR] fail read comment: %s", err) | |
w.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
args := struct { | |
Counter int | |
Comments []string | |
ErrorMessage string | |
}{ | |
Counter: c, | |
Comments: comments, | |
ErrorMessage: errorMessage, | |
} | |
err = tmpl.ExecuteTemplate(w, "bbs.tmpl", args) | |
if err != nil { | |
log.Printf("[ERROR] execute template error: %s", err) | |
return | |
} | |
return | |
} | |
func handlerPOST(w http.ResponseWriter, r *http.Request) { | |
c, err := r.Cookie("_csrftoken") | |
if err != nil { | |
log.Printf("[WARN] cannot retrieve CSRFToken on Cookie: %s", err) | |
handlerGET(w, r, formErrorInvalidCSRFToken) | |
return | |
} | |
err = checkValidCSRFToken(c.Value) | |
if err != nil { | |
handlerGET(w, r, err) | |
return | |
} | |
hitokoto := r.PostFormValue("hitokoto") | |
hitokoto = strings.TrimSpace(hitokoto) | |
if hitokoto == "" { | |
handlerGET(w, r, formErrorEmptyHitokoto) | |
return | |
} | |
err = appendComment(hitokoto) | |
if err != nil { | |
handlerGET(w, r, err) | |
return | |
} | |
handlerGET(w, r, formErrorNone) | |
return | |
} | |
func generateCSRFToken() (string, error) { | |
t := time.Now().Unix() | |
hs, err := generateHash(t) | |
if err != nil { | |
return "", err | |
} | |
ts := strconv.FormatInt(t, 10) | |
o := strings.Join([]string{hs, ts}, ":") | |
return o, nil | |
} | |
func generateHash(t int64) (string, error) { | |
ts := strconv.FormatInt(t, 10) | |
s := sha256.New() | |
key := strings.Join([]string{ts, csrfSalt}, ":") | |
_, err := io.WriteString(s, key) | |
if err != nil { | |
return "", err | |
} | |
h := s.Sum(nil) | |
hs := base64.StdEncoding.EncodeToString(h) | |
return hs, nil | |
} | |
func checkValidCSRFToken(token string) error { | |
ss := strings.Split(token, ":") | |
if len(ss) != 2 { | |
log.Println("[WARN] invalid csrf token: token=%s", token) | |
return formErrorInvalidCSRFToken | |
} | |
hs := ss[0] | |
ts := ss[1] | |
t, err := strconv.ParseInt(ts, 10, 64) | |
if err != nil { | |
log.Println("[WARN] cannot parse time: %s token=%s", err, token) | |
return formErrorInvalidCSRFToken | |
} | |
now := time.Now().Unix() | |
if t < now-csrfTokenExpireSeconds { | |
} | |
expectedHash, err := generateHash(t) | |
if err != nil { | |
log.Println("[WARN] fail generate hash: %s", err) | |
return formErrorInternalServerError | |
} | |
if expectedHash != hs { | |
log.Printf("[WARN] invalid hash: token=%s, expected=%s", token, expectedHash) | |
return formErrorInvalidCSRFToken | |
} | |
return nil | |
} | |
func counter() (int, error) { | |
fi, err := os.Stat(counterFilename) | |
if err != nil { | |
return 0, err | |
} | |
s := fi.Size() | |
return int(s / 2), nil | |
} | |
func incrCounter() error { | |
f, err := os.OpenFile(counterFilename, os.O_WRONLY|os.O_APPEND, os.ModeAppend) | |
if err != nil { | |
return err | |
} | |
defer f.Close() | |
_, err = f.WriteString(".") | |
if err != nil { | |
return err | |
} | |
return nil | |
} | |
func appendComment(comment string) error { | |
comment = fmt.Sprintf("%.100s", comment) | |
replacer := strings.NewReplacer("\t", " ", "\n", " ") | |
comment = replacer.Replace(comment) | |
now := time.Now() | |
ns := now.Format("2006-01-02 15:04:05") | |
newLine := strings.Join([]string{ns, comment}, "\t") | |
f, err := os.OpenFile(hitokotoFilename, os.O_RDWR, os.ModeExclusive) | |
if err != nil { | |
return err | |
} | |
defer f.Close() | |
scanner := bufio.NewScanner(f) | |
comments := make([]string, 0, commentLimit+1) | |
for scanner.Scan() { | |
c := scanner.Text() | |
comments = append(comments, c) | |
} | |
err = f.Truncate(0) | |
if err != nil { | |
return err | |
} | |
_, err = f.Seek(0, 0) | |
if err != nil { | |
return err | |
} | |
comments = append(comments, newLine) | |
if len(comments) > commentLimit { | |
comments = comments[len(comments)-commentLimit:] | |
} | |
for _, c := range comments { | |
_, err := f.WriteString(c + "\n") | |
if err != nil { | |
return err | |
} | |
} | |
return nil | |
} | |
func comments() ([]string, error) { | |
f, err := os.Open(hitokotoFilename) | |
if err != nil { | |
return nil, err | |
} | |
defer f.Close() | |
scanner := bufio.NewScanner(f) | |
comments := make([]string, 0, commentLimit) | |
for scanner.Scan() { | |
log.Printf("[INFO] %s", scanner.Text()) | |
comments = append(comments, scanner.Text()) | |
} | |
return comments, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment