Skip to content

Instantly share code, notes, and snippets.

@mackee
Created December 2, 2018 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mackee/2247f081ad860ed36c7b27bb6886b862 to your computer and use it in GitHub Desktop.
Save mackee/2247f081ad860ed36c7b27bb6886b862 to your computer and use it in GitHub Desktop.
Go de CGI
<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>
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