0
0
mirror of https://github.com/rls-moe/nyx synced 2024-11-21 22:12:24 +00:00

MVP, no mod tools or anything but it works

This commit is contained in:
Tim Schuster 2017-03-12 20:37:53 +01:00
parent 70b12c516a
commit 69b0d20825
No known key found for this signature in database
GPG Key ID: F9E27097EFB77F61
186 changed files with 44200 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
config.yml
nyx-testing.db
nyx
run.sh

93
config/config.go Normal file
View File

@ -0,0 +1,93 @@
package config
import (
"flag"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
)
var configFileName = "./config.yml"
func init() {
flag.StringVar(&configFileName, "config", "./config.yml", "Config File Location")
}
type Config struct {
Site SiteConfig `yaml:"site"` // Site/HTML Configuration
DB DBConfig `yaml:"db"` // Database Configuration
HostnameWhiteList []string `yaml:"hosts"` // List of valid hostnames, ignored if empty
ListenOn string `yaml:"listen_on"` // Address & Port to use
MasterSecret string `yaml:"secret"` // Master Secret for keychain
DisableSecurity bool `yaml:"disable_security"` // Disables various flags to ensure non-HTTPS requests work
Captcha CaptchaConfig `yaml:"captcha"`
}
const (
CaptchaRecaptcha = "recaptcha"
CaptchaDisabled = "disabled"
)
type CaptchaConfig struct {
Mode string `yaml:"mode"` // Captcha Mode
Settings map[string]string `yaml:"settings,inline"`
}
type SiteConfig struct {
Title string `yaml:"title"` // Site Title
Description string `yaml:"description"` // Site Description
PrimaryColor string `yaml:"color"` // Primary Color for Size
}
type DBConfig struct {
File string `yaml:"file"`
ReadOnly bool `yaml:"read_only"`
}
func Load() (*Config, error) {
var config = &Config{
Site: SiteConfig{
Title: "NyxChan",
PrimaryColor: "#78909c",
Description: "NyxChan Default Configuration",
},
DB: DBConfig{
File: ":memory:",
ReadOnly: false,
},
HostnameWhiteList: []string{},
ListenOn: ":8080",
MasterSecret: "changeme",
DisableSecurity: false,
Captcha: CaptchaConfig{
Mode: CaptchaDisabled,
},
}
if _, err := os.Stat(configFileName); os.IsNotExist(err) {
return config, nil
}
dat, err := ioutil.ReadFile(configFileName)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(dat, config)
if err != nil {
return nil, err
}
return config, nil
}
func (c Config) IsHostNameValid(hostname string) bool {
if c.HostnameWhiteList == nil {
return true
}
if len(c.HostnameWhiteList) == 0 {
return true
}
for _, v := range c.HostnameWhiteList {
if v == hostname {
return true
}
}
return false
}

119
http/admin/handler.go Normal file
View File

@ -0,0 +1,119 @@
package admin
import (
"bytes"
"github.com/GeertJohan/go.rice"
"github.com/icza/session"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"html/template"
"net/http"
"time"
)
var riceConf = rice.Config{
LocateOrder: []rice.LocateMethod{
rice.LocateWorkingDirectory,
rice.LocateEmbedded,
rice.LocateAppended,
},
}
var box = riceConf.MustFindBox("http/admin/res/")
var (
panelTmpl = template.New("admin/panel")
loginTmpl = template.New("admin/login")
)
func init() {
var err error
panelTmpl, err = panelTmpl.Parse(box.MustString("panel.html"))
if err != nil {
panic(err)
}
loginTmpl, err = loginTmpl.Parse(box.MustString("index.html"))
if err != nil {
panic(err)
}
}
// Router sets up the Administration Panel
// It **must** be setup on the /admin/ basepath
func Router(r chi.Router) {
r.Get("/", serveLogin)
r.Get("/index.html", serveLogin)
r.Get("/panel.html", servePanel)
r.Post("/new_board.sh", handleNewBoard)
r.Post("/login.sh", handleLogin)
r.Post("/logout.sh", handleLogout)
}
func serveLogin(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "index.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}
func servePanel(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
dat := bytes.NewBuffer([]byte{})
err := panelTmpl.Execute(dat, middle.GetBaseCtx(r))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "panel.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
session.Remove(sess, w)
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
err = db.View(func(tx *buntdb.Tx) error {
var err error
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
return err
})
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
err = admin.VerifyLogin(r.FormValue("pass"))
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"mode": "admin"},
})
session.Add(sess, w)
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

58
http/admin/newboard.go Normal file
View File

@ -0,0 +1,58 @@
package admin
import (
"errors"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleNewBoard(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var board = &resources.Board{}
board.ShortName = r.FormValue("shortname")
board.LongName = r.FormValue("longname")
if board.ShortName == "" {
errw.ErrorWriter(errors.New("Need shortname"), w, r)
return
}
if board.ShortName == "admin" && board.ShortName == "@" {
errw.ErrorWriter(errors.New("No"), w, r)
}
if board.LongName == "" && len(board.LongName) < 5 {
errw.ErrorWriter(errors.New("Need 5 characters for long name"), w, r)
return
}
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.NewBoard(tx, r.Host, board)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

41
http/admin/res/index.html Normal file
View File

@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Admin Login</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
<link rel="stylesheet" href="/@/admin.css">
</head>
<body>
<div class="admin login">
<form action="/admin/login.sh" method="POST">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<div class="admin form row">
<input
class="admin form input"
type="text"
name="id"
placeholder="admin id"
minlength="3"/>
</div>
<div class="admin form row">
<input
class="admin form input"
type="password"
name="pass"
placeholder="password"
minlength="3"/>
</div>
<div class="admin form row">
<input class="admin form input halfsize" type="submit" value="Login"/>
<input class="admin form input halfsize" type="reset" value="Reset"/>
</div>
</form>
</div>
</body>
</html>

57
http/admin/res/panel.html Normal file
View File

@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Admin Panel</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
<link rel="stylesheet" href="/@/admin.css">
</head>
<body>
<div class="welcome">
Welcome {{.Admin.Id}}<br>
<form class="form logout" method="POST" action="/admin/logout.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="submit" value="Logout" />
</form>
</div>
<div class="panel boardmgr">
<form method="POST" action="/admin/new_board.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="text" placeholder="shortname" name="shortname"/>
<input type="text" placeholder="longname" name="longname"/>
<input type="submit" value="Create Board" />
<input type="reset" value="Reset" />
</form>
</div>
<div class="panel remover post">
<form method="POST" action="/admin/rem_post.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="text" placeholder="post id" name="post id"/>
<input type="submit" value="Remove Post" />
<input type="reset" value="Reset" />
</form>
</div>
<div class="panel remover thread">
<form method="POST" action="/admin/rem_thread.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="text" placeholder="thread id" name="thread id"/>
<input type="submit" value="Remove thread" />
<input type="reset" value="Reset" />
</form>
</div>
</body>
</html>

52
http/board/board.go Normal file
View File

@ -0,0 +1,52 @@
package board
import (
"bytes"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"log"
"net/http"
"time"
)
func serveBoard(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
db := middle.GetDB(r)
ctx := middle.GetBaseCtx(r)
err := db.View(func(tx *buntdb.Tx) error {
bName := chi.URLParam(r, "board")
b, err := resources.GetBoard(tx, r.Host, bName)
if err != nil {
return err
}
ctx["Board"] = b
threads, err := resources.ListThreads(tx, r.Host, bName)
if err != nil {
return err
}
log.Println("Number of Thread on board: ", len(threads))
for k := range threads {
err := resources.FillReplies(tx, r.Host, threads[k])
if err != nil {
return err
}
}
ctx["Threads"] = threads
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
err = boardTmpl.Execute(dat, ctx)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "board.html", time.Now(), bytes.NewReader(dat.Bytes()))
}

88
http/board/handler.go Normal file
View File

@ -0,0 +1,88 @@
package board
import (
"bytes"
"github.com/GeertJohan/go.rice"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"html/template"
"net/http"
"time"
)
var riceConf = rice.Config{
LocateOrder: []rice.LocateMethod{
rice.LocateWorkingDirectory,
rice.LocateEmbedded,
rice.LocateAppended,
},
}
var box = riceConf.MustFindBox("http/board/res/")
var (
dirTmpl = template.New("board/dir")
boardTmpl = template.New("board/board")
threadTmpl = template.New("board/thread")
hdlFMap = template.FuncMap{
"renderText": resources.OperateReplyText,
}
)
func init() {
var err error
dirTmpl, err = dirTmpl.Parse(box.MustString("dir.html"))
if err != nil {
panic(err)
}
boardTmpl, err = boardTmpl.Funcs(hdlFMap).Parse(box.MustString("board.html"))
if err != nil {
panic(err)
}
threadTmpl, err = threadTmpl.Funcs(hdlFMap).Parse(box.MustString("thread.html"))
if err != nil {
panic(err)
}
}
func Router(r chi.Router) {
r.Get("/", serveDir)
r.Get("/dir.html", serveDir)
r.Get("/:board/board.html", serveBoard)
r.Post("/:board/new_thread.sh", handleNewThread)
r.Get("/:board/:thread/thread.html", serveThread)
r.Get("/:board/:thread/:post/post.html", servePost)
r.Post("/:board/:thread/reply.sh", handleNewReply)
}
func servePost(w http.ResponseWriter, r *http.Request) {
return
}
func serveDir(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
db := middle.GetDB(r)
ctx := middle.GetBaseCtx(r)
err := db.View(func(tx *buntdb.Tx) error {
bList, err := resources.ListBoards(tx, r.Host)
if err != nil {
return err
}
ctx["Boards"] = bList
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
err = dirTmpl.Execute(dat, ctx)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "dir.html", time.Now(), bytes.NewReader(dat.Bytes()))
}

59
http/board/newreply.go Normal file
View File

@ -0,0 +1,59 @@
package board
import (
"fmt"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
"strconv"
)
func handleNewReply(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
var reply = &resources.Reply{}
reply.Board = chi.URLParam(r, "board")
tid, err := strconv.Atoi(chi.URLParam(r, "thread"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
reply.Thread = int64(tid)
reply.Text = r.FormValue("text")
if len(reply.Text) > 1000 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return
}
if len(reply.Text) < 10 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return
}
reply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" {
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
} else {
reply.Metadata["trip"] = "Anonymous"
}
db := middle.GetDB(r)
if err = db.Update(func(tx *buntdb.Tx) error {
thread, err := resources.GetThread(tx, r.Host, reply.Board, reply.Thread)
if err != nil {
return err
}
return resources.NewReply(tx, r.Host, reply.Board, thread, reply, false)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", chi.URLParam(r, "board"), reply.Thread), http.StatusSeeOther)
}

48
http/board/newthread.go Normal file
View File

@ -0,0 +1,48 @@
package board
import (
"fmt"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleNewThread(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
var thread = &resources.Thread{}
var mainReply = &resources.Reply{}
mainReply.Board = chi.URLParam(r, "board")
thread.Board = chi.URLParam(r, "board")
mainReply.Text = r.FormValue("text")
if len(mainReply.Text) > 1000 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return
}
if len(mainReply.Text) < 10 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return
}
mainReply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" {
mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
}
db := middle.GetDB(r)
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.NewThread(tx, r.Host, mainReply.Board, thread, mainReply)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", chi.URLParam(r, "board"), thread.ID), http.StatusSeeOther)
}

119
http/board/res/board.html Normal file
View File

@ -0,0 +1,119 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} - /{{.Board.ShortName}}/</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
</head>
<body>
<div class="banner logo">
<div class="site title"><h1><span class="reflink"><a href="/{{.Board.ShortName}}/board.html">/{{.Board.ShortName}}/</a></span></h1></div>
<div class="site description"><h2>{{.Board.LongName}}</h2></div>
</div>
{{ $boardlink := .Board.ShortName }}
<div class="postarea">
<form id="postform" action="/{{$boardlink}}/new_thread.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
TripCode
</td>
<td>
<input type="text" name="tripcode" size=48 placeholder="Anonymous"/>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Comment
</td>
<td>
<textarea
name="text"
placeholder="your comment"
rows="4"
cols="48"
minlength="10"
required
></textarea>
</td>
</tr>
<tr>
<td class="postblock">
Image File
</td>
<td>
<input type="file" name="image" />
</td>
</tr>
{{ if ne .Config.Captcha.Mode "disabled" }}
<tr>
<td class="postblock">
Captcha
</td>
<td>
<input type="text" name="captcha" size=48 />
<input type="hidden"
value="{{.CaptchaToken}}"/>
<img alt=""
src="{{.CaptchaImage}}" />
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
</td>
<td>
<input type="submit" value="Post" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<hr />
<div class="postlists">
{{range .Threads}}
{{ $threadrid := .GetReply.ID }}
<label><span class="postertrip">
{{ if .GetReply.Metadata.trip }}
{{.GetReply.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.ID}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .GetReply.Text}}
</blockquote></blockquote>
{{range .GetReplies}}
{{ if ne .ID $threadrid }}
<table><tbody><tr><td class="doubledash">&gt;&gt;</td>
<td class="reply" id="reply{{.ID}}">
<label><span class="postertrip">
{{ if .Metadata.trip }}
{{.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.Thread}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .Text}}
</blockquote></blockquote>
</td>
</tr></tbody></table>
{{end}}
{{end}}
<br clear="left" /><hr />
{{end}}
</div>
</body>
</html>

30
http/board/res/dir.html Normal file
View File

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Boards</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
</head>
<body>
<div class="banner logo">
<div class="site title"><h1>{{.Config.Site.Title}}</h1></div>
<div class="site description"><h2>{{.Config.Site.Description}}</h2></div>
</div>
<div class="boardlist">
<div class="boardtitle">
<h3>Boards</h3>
</div>
<div class="boardlist">
<ul>
{{range .Boards}}
<li>
<a class="boardlink" href="/{{ .ShortName}}/board.html">{{.ShortName}}: {{.LongName}}</a>
</li>
{{end}}
</ul>
</div>
</div>
</body>
</html>

129
http/board/res/thread.html Normal file
View File

@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} - /{{.Board.ShortName}}/</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
</head>
<body>
<div class="banner logo">
<div class="site title"><h1><span class="reflink"><a href="/{{.Board.ShortName}}/board.html">/{{.Board.ShortName}}/</a></span></h1></div>
<div class="site description"><h2>{{.Board.LongName}}</h2></div>
</div>
{{ $boardlink := .Board.ShortName }}
<hr />
<div class="postarea">
<form id="postform" action="/{{.Board.ShortName}}/{{.Thread.ID}}/reply.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
TripCode
</td>
<td>
<input type="text" name="tripcode" size=48 placeholder="Anonymous"/>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Comment
</td>
<td>
<textarea
name="text"
placeholder="your comment"
rows="4"
cols="48"
minlength="10"
required
></textarea>
</td>
</tr>
<tr>
<td class="postblock">
Image File
</td>
<td>
<input type="file" name="image" />
</td>
</tr>
{{ if ne .Config.Captcha.Mode "disabled" }}
<tr>
<td class="postblock">
Captcha
</td>
<td>
<input type="text" name="captcha" size=48 />
<input type="hidden"
value="{{.CaptchaToken}}"/>
<img alt=""
src="{{.CaptchaImage}}" />
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
</td>
<td>
<input type="submit" value="Post" />
</td>
</tr>
{{ if .Board.Metadata.rules }}
<tr>
<td class="postblock">
Rules
</td>
<td>
{{ .Board.Metadata.rules }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</form>
</div>
<div class="postlists">
{{with .Thread }}
{{ $threadrid := .GetReply.ID }}
<label><span class="postertrip">
{{ if .GetReply.Metadata.trip }}
{{.GetReply.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.ID}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .GetReply.Text}}
</blockquote></blockquote>
{{range .GetReplies}}
{{ if ne .ID $threadrid }}
<table><tbody><tr><td class="doubledash">&gt;&gt;</td>
<td class="reply" id="reply{{.ID}}">
<label><span class="postertrip">
{{ if .Metadata.trip }}
{{.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.Thread}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .Text}}
</blockquote></blockquote>
</td>
</tr></tbody></table>
{{end}}
{{end}}
<br clear="left" /><hr />
{{end}}
</div>
</body>
</html>

57
http/board/thread.go Normal file
View File

@ -0,0 +1,57 @@
package board
import (
"bytes"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
"strconv"
"time"
)
func serveThread(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
db := middle.GetDB(r)
ctx := middle.GetBaseCtx(r)
err := db.View(func(tx *buntdb.Tx) error {
bName := chi.URLParam(r, "board")
b, err := resources.GetBoard(tx, r.Host, bName)
if err != nil {
return err
}
ctx["Board"] = b
id, err := strconv.Atoi(chi.URLParam(r, "thread"))
if err != nil {
return err
}
thread, err := resources.GetThread(tx, r.Host, bName, int64(id))
if err != nil {
return err
}
err = resources.FillReplies(tx, r.Host, thread)
if err != nil {
return err
}
if err != nil {
return err
}
ctx["Thread"] = thread
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
err = threadTmpl.Execute(dat, ctx)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "board.html", time.Now(), bytes.NewReader(dat.Bytes()))
}

77
http/errw/handler.go Normal file
View File

@ -0,0 +1,77 @@
package errw
import (
"errors"
"github.com/GeertJohan/go.rice"
"github.com/pressly/chi/middleware"
"go.rls.moe/nyx/http/middle"
"html/template"
"net/http"
)
var riceConf = rice.Config{
LocateOrder: []rice.LocateMethod{
rice.LocateWorkingDirectory,
rice.LocateEmbedded,
rice.LocateAppended,
},
}
var box = riceConf.MustFindBox("http/errw/res/")
var (
errorTmpl = template.New("errw/error")
)
func init() {
var err error
errorTmpl, err = errorTmpl.Parse(box.MustString("error.html"))
if err != nil {
panic(err)
}
}
type ErrorWithTitle interface {
error
ErrorTitle() string
}
type errorWTInt struct {
message, title string
}
func (e errorWTInt) Error() string {
return e.message
}
func (e errorWTInt) ErrorTitle() string {
return e.title
}
func MakeErrorWithTitle(title, message string) ErrorWithTitle {
return errorWTInt{message, title}
}
func ErrorWriter(err error, w http.ResponseWriter, r *http.Request) {
ctx := middle.GetBaseCtx(r)
if err == nil {
ErrorWriter(errors.New("Unknonw Error"), w, r)
}
if errWT, ok := err.(ErrorWithTitle); ok {
ctx["Error"] = map[string]string{
"Code": middleware.GetReqID(r.Context()),
"Description": errWT.Error(),
"Title": errWT.ErrorTitle(),
}
} else {
ctx["Error"] = map[string]string{
"Code": middleware.GetReqID(r.Context()),
"Description": err.Error(),
"Title": "Error",
}
}
errorTmpl.Execute(w, ctx)
return
}

35
http/errw/res/error.html Normal file
View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Admin Login</title>
<style>
div.error {
border: 1px solid black;
width: 500px;
margin: auto;
margin-top: 100px;
}
div.error h1 {
margin-bottom: 0px;
text-align: center;
}
div.error h2 {
text-align: center;
}
div.error h3 {
margin-top: 0px;
text-align: center;
color: #888;
}
</style>
</head>
<body>
<div class="error">
<h1>{{.Error.Title}}</h1><br/>
<h3>{{.Error.Code}}</h3><br/>
<h2>{{.Error.Description}}</h2>
</div>
</body>
</html>

21
http/middle/base.go Normal file
View File

@ -0,0 +1,21 @@
package middle
import (
"github.com/justinas/nosurf"
"github.com/pressly/chi/middleware"
"net/http"
)
func GetBaseCtx(r *http.Request) map[string]interface{} {
val := map[string]interface{}{
"Config": GetConfig(r),
"ReqID": middleware.GetReqID(r.Context()),
"CSRFToken": nosurf.Token(r),
}
return val
}
func CSRFProtect(next http.Handler) http.Handler {
return nosurf.New(next)
}

24
http/middle/config.go Normal file
View File

@ -0,0 +1,24 @@
package middle
import (
"context"
"go.rls.moe/nyx/config"
"net/http"
)
func ConfigCtx(config *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), configKey, config))
next.ServeHTTP(w, r)
})
}
}
func GetConfig(r *http.Request) *config.Config {
val := r.Context().Value(configKey)
if val == nil {
panic("Config Middleware not configured")
}
return val.(*config.Config)
}

9
http/middle/ctx.go Normal file
View File

@ -0,0 +1,9 @@
package middle
type ctxKey int64
const (
configKey ctxKey = iota
dbCtxKey
sessionKey
)

34
http/middle/db.go Normal file
View File

@ -0,0 +1,34 @@
package middle
import (
"context"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/config"
"go.rls.moe/nyx/resources"
"net/http"
)
func Database(c *config.Config) (func(http.Handler) http.Handler, error) {
db, err := buntdb.Open(c.DB.File)
if err != nil {
return nil, err
}
if err = resources.InitialSetup(db); err != nil {
return nil, err
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(),
dbCtxKey, db))
next.ServeHTTP(w, r)
})
}, nil
}
func GetDB(r *http.Request) *buntdb.DB {
val := r.Context().Value(dbCtxKey)
if val == nil {
panic("DB Middleware not configured")
}
return val.(*buntdb.DB)
}

15
http/middle/session.go Normal file
View File

@ -0,0 +1,15 @@
package middle
import (
"github.com/icza/session"
"net/http"
)
func init() {
session.Global.Close()
session.Global = session.NewCookieManager(session.NewInMemStore())
}
func GetSession(r *http.Request) session.Session {
return session.Get(r)
}

26
http/res/admin.css Normal file
View File

@ -0,0 +1,26 @@
/* CUSTOM CSS */
div.admin.login {
border: 1px solid black;
width: 500px;
margin: auto;
margin-top: 100px;
}
.admin.form.row {
margin: auto;
padding: 5px;
width: 90%;
height: 22px;
left: 0;
right: 0;
display: flex;
}
.admin.form.input {
font-family: "monospace";
width: 100%;
height: 100%;
padding: 2px;
display: inline;
}
.admin.form.input.halfsize {
width: 50%;
}

21
http/res/custom.css Normal file
View File

@ -0,0 +1,21 @@
h1 {
font-size: 32px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 16px;
}
div {
display: block;
margin: 0;
padding: 0;
}
blockquote blockquote { max-width: 80%; word-wrap: break-word; white-space: normal; }
.reply blockquote, blockquote :last-child { max-width: 80%; word-wrap: break-word; white-space: normal; }

157
http/res/style.css Normal file
View File

@ -0,0 +1,157 @@
/* The following CSS is mostly taken from Wakaba, big thanks for the devs there! <3 */
html, body {
background:#FFFFEE;
color:#800000;
}
a {
color:#0000EE;
}
a:hover {
color:#DD0000;
}
.adminbar {
text-align:right;
clear:both;
float:right;
}
.logo {
clear:both;
text-align:center;
font-size:2em;
color:#800000;
width:100%;
}
.theader {
background:#E04000;
text-align:center;
padding:2px;
color:#FFFFFF;
width:100%;
}
.postarea {
}
.rules {
font-size:0.7em;
}
.postblock {
background:#EEAA88;
color:#800000;
font-weight:800;
}
.footer {
text-align:center;
font-size:12px;
font-family:serif;
}
.passvalid {
background:#EEAA88;
text-align:center;
width:100%;
color:#ffffff;
}
.dellist {
font-weight: bold;
text-align:center;
}
.delbuttons {
text-align:center;
padding-bottom:4px;
}
.managehead {
background:#AAAA66;
color:#400000;
padding:0px;
}
.postlists {
background:#FFFFFF;
width:100%;
padding:0px;
color:#800000;
}
.row1 {
background:#EEEECC;
color:#800000;
}
.row2 {
background:#DDDDAA;
color:#800000;
}
.unkfunc {
background:inert;
color:#789922;
}
.filesize {
text-decoration:none;
}
.filetitle {
background:inherit;
font-size:1.2em;
color:#CC1105;
font-weight:800;
}
.postername {
color:#117743;
font-weight:bold;
}
.postertrip {
color:#228854;
}
.oldpost {
color:#CC1105;
font-weight:800;
}
.omittedposts {
color:#707070;
}
.reply {
background:#F0E0D6;
color:#800000;
}
.doubledash {
vertical-align:top;
clear:both;
float:left;
}
.replytitle {
font-size: 1.2em;
color:#CC1105;
font-weight:800;
}
.commentpostername {
color:#117743;
font-weight:800;
}
.thumbnailmsg {
font-size: small;
color:#800000;
}
.abbrev {
color:#707070;
}
.highlight {
background:#F0E0D6;
color:#800000;
border: 2px dashed #EEAA88;
}
/* From pl files */
/* futaba_style.pl */
blockquote blockquote { margin-left: 0em; }
form { margin-bottom: 0px }
form .trap { display:none }
.postarea { text-align: center }
.postarea table { margin: 0px auto; text-align: left }
.thumb { border: none; float: left; margin: 2px 20px }
.nothumb { float: left; background: #eee; border: 2px dashed #aaa; text-align: center; margin: 2px 20px; padding: 1em 0.5em 1em 0.5em; }
.reply blockquote, blockquote :last-child { margin-bottom: 0em; }
.reflink a { color: inherit; text-decoration: none }
.reply .filesize { margin-left: 20px }
.userdelete { float: right; text-align: center; white-space: nowrap }
.replypage .replylink { display: none }

53
http/server.go Normal file
View File

@ -0,0 +1,53 @@
package http
import (
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/pressly/chi"
"github.com/pressly/chi/middleware"
"go.rls.moe/nyx/config"
"go.rls.moe/nyx/http/admin"
"go.rls.moe/nyx/http/board"
"go.rls.moe/nyx/http/middle"
"net/http"
)
var riceConf = rice.Config{
LocateOrder: []rice.LocateMethod{
rice.LocateWorkingDirectory,
rice.LocateEmbedded,
rice.LocateAppended,
},
}
func Start(config *config.Config) {
r := chi.NewRouter()
fmt.Println("Setting up Router")
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.CloseNotify)
r.Use(middleware.DefaultCompress)
r.Use(middle.ConfigCtx(config))
r.Use(middle.CSRFProtect)
{
mw, err := middle.Database(config)
if err != nil {
panic(err)
}
r.Use(mw)
}
r.Route("/admin/", admin.Router)
{
box := riceConf.MustFindBox("http/res")
atFileServer := http.StripPrefix("/@/", http.FileServer(box.HTTPBox()))
r.Mount("/@/", atFileServer)
}
r.Group(board.Router)
fmt.Println("Setup Complete, Starting Web Server...")
http.ListenAndServe(config.ListenOn, r)
}

18
main.go Normal file
View File

@ -0,0 +1,18 @@
package main
import (
"fmt"
"go.rls.moe/nyx/config"
"go.rls.moe/nyx/http"
)
func main() {
c, err := config.Load()
if err != nil {
fmt.Printf("Could not read configuration: %s\n", err)
return
}
fmt.Println("Starting Server")
http.Start(c)
}

67
resources/adminpass.go Normal file
View File

@ -0,0 +1,67 @@
package resources
import (
"encoding/json"
"errors"
"fmt"
"github.com/hlandau/passlib"
"github.com/tidwall/buntdb"
)
type AdminPass struct {
ID string `json:"id"`
Password string `json:"password"`
}
func (a *AdminPass) HashLogin(pass string) error {
var err error
a.Password, err = passlib.Hash(pass)
return err
}
func (a *AdminPass) VerifyLogin(pass string) error {
var err error
err = passlib.VerifyNoUpgrade(pass, a.Password)
return err
}
func NewAdmin(tx *buntdb.Tx, in *AdminPass) error {
dat, err := json.Marshal(in)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(adminPassPath, escapeString(in.ID)),
string(dat),
nil)
if err != nil {
return err
}
if replaced {
return errors.New("Admin already exists")
}
return nil
}
func GetAdmin(tx *buntdb.Tx, id string) (*AdminPass, error) {
var ret = &AdminPass{}
dat, err := tx.Get(
fmt.Sprintf(adminPassPath, escapeString(id)),
)
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(dat), ret); err != nil {
return nil, err
}
return ret, nil
}
func DelAdmin(tx *buntdb.Tx, id string) error {
if _, err := tx.Delete(
fmt.Sprintf(adminPassPath, escapeString(id)),
); err != nil {
return err
}
return nil
}

78
resources/board.go Normal file
View File

@ -0,0 +1,78 @@
package resources
import (
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/buntdb"
)
type Board struct {
ShortName string `json:"short"`
LongName string `json:"long"`
Metadata Metadata `json:"meta"`
}
func NewBoard(tx *buntdb.Tx, hostname string, in *Board) error {
dat, err := json.Marshal(in)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(boardPath, escapeString(hostname), escapeString(in.ShortName)),
string(dat),
nil)
if err != nil {
return err
}
if replaced {
return errors.New("Board " + escapeString(in.ShortName) + " already exists")
}
return nil
}
func TestBoard(tx *buntdb.Tx, hostname, shortname string) (error) {
_, err := tx.Get(
fmt.Sprintf(boardPath, escapeString(hostname), escapeString(shortname)),
)
return err
}
func GetBoard(tx *buntdb.Tx, hostname, shortname string) (*Board, error) {
var ret = &Board{}
dat, err := tx.Get(
fmt.Sprintf(boardPath, escapeString(hostname), escapeString(shortname)),
)
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(dat), ret); err != nil {
return nil, err
}
return ret, nil
}
func DelBoard(tx *buntdb.Tx, hostname, shortname string) error {
if _, err := tx.Delete(
fmt.Sprintf(boardPath, escapeString(hostname), escapeString(shortname)),
); err != nil {
return err
}
return nil
}
func ListBoards(tx *buntdb.Tx, hostname string) ([]*Board, error) {
var boardList = []*Board{}
var err error
tx.AscendKeys(fmt.Sprintf(boardPath, escapeString(hostname), "*"),
func(key, value string) bool {
var board = &Board{}
err = json.Unmarshal([]byte(value), board)
if err != nil {
return false
}
boardList = append(boardList, board)
return true
})
return boardList, err
}

114
resources/db.go Normal file
View File

@ -0,0 +1,114 @@
package resources
import (
"errors"
"fmt"
"github.com/tidwall/buntdb"
"strings"
)
const (
setup = "/jack/setup"
hostEnable = "/jack/%s/enabled"
boardPath = "/jack/%s/board/%s/board-data"
threadPath = "/jack/%s/board/%s/thread/%032d/thread-data"
threadSPath = "/jack/%s/board/%s/thread/*/thread-data"
replyPath = "/jack/%s/board/%s/thread/%032d/reply/%032d/reply-data"
replySPath = "/jack/%s/board/%s/thread/%032d/reply/*/reply-data"
modPassPath = "/jack/%s/pass/mod/%s/mod-data"
adminPassPath = "/jack/./pass/admin/%s/admin-data"
)
func InitialSetup(db *buntdb.DB) error {
return db.Update(func(tx *buntdb.Tx) error {
if _, err := tx.Get(setup); err != nil {
fmt.Println("")
if err != buntdb.ErrNotFound {
fmt.Println("DB setup not known.")
return err
}
fmt.Println("DB not setup.")
tx.Set(setup, "yes", nil)
} else {
fmt.Println("DB setup.")
return nil
}
fmt.Println("Creating Indices")
err := tx.CreateIndex("board/short", "/jack/*/board/*/board-data", buntdb.IndexJSON("short"))
if err != nil {
return err
}
err = tx.CreateIndex("replies", "/jack/*/board/*/thread/*/reply/*/reply-data", buntdb.IndexJSON("thread"))
if err != nil {
return err
}
err = tx.CreateIndex("board/thread", "/jack/*/board/*/thread/*/thread-data", buntdb.IndexJSON("board"))
if err != nil {
return err
}
fmt.Println("Creating default admin")
admin := &AdminPass{
ID: "admin",
}
err = admin.HashLogin("admin")
if err != nil {
return err
}
fmt.Println("Saving default admin to DB")
err = NewAdmin(tx, admin)
if err != nil {
return err
}
fmt.Println("Committing setup...")
return nil
})
}
func CreateHost(db *buntdb.DB, hostname string) error {
return db.Update(func(tx *buntdb.Tx) error {
hostname = escapeString(hostname)
_, replaced, err := tx.Set(fmt.Sprintf(hostEnable, "hostname"), "", nil)
if err != nil {
tx.Rollback()
return err
}
if replaced {
tx.Rollback()
return errors.New("Hostname already enabled")
}
board := &Board{
ShortName: "d",
LongName: "default",
Metadata: map[string]string{
"locked": "true",
"description": "Default Board",
},
}
err = NewBoard(tx, hostname, board)
if err != nil {
tx.Rollback()
return err
}
return nil
})
}
func escapeString(in string) string {
in = strings.Replace(in, ".", ".dot.", -1)
in = strings.Replace(in, "-", ".minus.", -1)
in = strings.Replace(in, "\\", ".backslash.", -1)
in = strings.Replace(in, "*", ".star.", -1)
in = strings.Replace(in, "?", ".ask.", -1)
in = strings.Replace(in, "/", ".slash.", -1)
in = strings.Replace(in, "@", ".at.", -1)
in = strings.Replace(in, ">>", ".quote.", -1)
in = strings.Replace(in, ">", ".arrow-left.", -1)
in = strings.Replace(in, "<", ".arrow-right.", -1)
return in
}

17
resources/ids.go Normal file
View File

@ -0,0 +1,17 @@
package resources
import (
"go.rls.moe/nyx/resources/snowflakes"
"time"
)
var fountain = snowflakes.Generator{
StartTime: time.Date(
2017, 03, 11,
11, 12, 29,
0, time.UTC).Unix(),
}
func getID() (int64, error) {
return fountain.NewID()
}

3
resources/metadata.go Normal file
View File

@ -0,0 +1,3 @@
package resources
type Metadata map[string]string

72
resources/modpass.go Normal file
View File

@ -0,0 +1,72 @@
package resources
import (
"encoding/json"
"errors"
"fmt"
"github.com/hlandau/passlib"
"github.com/tidwall/buntdb"
)
type ModPass struct {
ID string `json:"id"`
Password string `json:"password"`
Board string `json:"board"`
}
func (m *ModPass) HashLogin(pass string) error {
var err error
m.Password, err = passlib.Hash(pass)
return err
}
func (m *ModPass) VerifyLogin(pass string) error {
var err error
err = passlib.VerifyNoUpgrade(pass, m.Password)
return err
}
func NewMod(tx *buntdb.Tx, host string, in *ModPass) error {
dat, err := json.Marshal(in)
if err != nil {
tx.Rollback()
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(modPassPath, escapeString(host), escapeString(in.ID)),
string(dat),
nil)
if err != nil {
tx.Rollback()
return err
}
if replaced {
tx.Rollback()
return errors.New("Admin already exists")
}
return nil
}
func GetMod(tx *buntdb.Tx, host, id string) (*ModPass, error) {
var ret = &ModPass{}
dat, err := tx.Get(
fmt.Sprintf(modPassPath, escapeString(host), escapeString(id)),
)
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(dat), ret); err != nil {
return nil, err
}
return ret, nil
}
func DelMod(tx *buntdb.Tx, host, id string) error {
if _, err := tx.Delete(
fmt.Sprintf(modPassPath, escapeString(host), escapeString(id)),
); err != nil {
tx.Rollback()
return err
}
return nil
}

113
resources/reply.go Normal file
View File

@ -0,0 +1,113 @@
package resources
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/buntdb"
"golang.org/x/crypto/blake2b"
)
type Reply struct {
ID int64 `json:"id"`
Text string `json:"text"`
Image []byte `json:"image"`
Thread int64 `json:"thread"`
Board string `json:"board"`
Metadata Metadata `json:"meta"`
}
func NewReply(tx *buntdb.Tx, host, board string, thread *Thread, in *Reply, noId bool) error {
var err error
if !noId {
in.ID, err = getID()
if err != nil {
return err
}
} else {
}
dat, err := json.Marshal(in)
if err != nil {
return err
}
err = TestThread(tx, host, in.Board, in.Thread)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread.ID, in.ID),
string(dat),
nil)
if err != nil {
return err
}
if replaced {
return errors.New("Admin already exists")
}
return nil
}
func GetReply(tx *buntdb.Tx, host, board string, thread, id int64) (*Reply, error) {
var ret = &Reply{}
dat, err := tx.Get(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id),
)
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(dat), ret); err != nil {
return nil, err
}
return ret, nil
}
func DelReply(tx *buntdb.Tx, host, board string, thread, id int64) error {
if _, err := tx.Delete(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id),
); err != nil {
return err
}
return nil
}
func ListReplies(tx *buntdb.Tx, host, board string, thread int64) ([]*Reply, error) {
var replyList = []*Reply{}
var err error
err = TestThread(tx, host, board, thread)
if err != nil {
return nil, err
}
tx.DescendKeys(
fmt.Sprintf(
replySPath,
escapeString(host),
escapeString(board),
thread,
),
func(key, value string) bool {
var reply = &Reply{}
err = json.Unmarshal([]byte(value), reply)
if err != nil {
return false
}
replyList = append(replyList, reply)
if len(replyList) >= 100 {
return false
}
return true
})
return replyList, err
}
func CalcTripCode(trip string) string {
fullTrip := blake2b.Sum256([]byte(trip))
return base64.RawStdEncoding.EncodeToString(fullTrip[:8])
}

View File

@ -0,0 +1,20 @@
MIT License
Copyright (c) 2017 Arke Works
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1 @@
You can find the original generator at https://github.com/arke-works/arke/blob/master/snowflakes/generator.go

View File

@ -0,0 +1,75 @@
package snowflakes
import (
"errors"
"sync"
"time"
)
const (
counterLen = 10
counterMask = -1 ^ (-1 << counterLen)
)
var (
errNoFuture = errors.New("Start Time cannot be set in the future")
)
// Generator is a fountain for new snowflakes. StartTime must be
// initialized to a past point in time and Instance ID can be any
// positive value or 0.
//
// If any value is not correctly set, new IDs cannot be produced.
type Generator struct {
StartTime int64
mutex *sync.Mutex
sequence int32
now int64
}
// NewID generates a new, unique snowflake value
//
// Up to 8192 snowflakes per second can be requested
// If exhausted, it blocks and sleeps until a new second
// of unix time starts.
//
// The return value is signed but always positive.
//
// Additionally, the return value is monotonic for a single
// instance and weakly monotonic for many instances.
func (g *Generator) NewID() (int64, error) {
if g.mutex == nil {
g.mutex = new(sync.Mutex)
}
if g.StartTime > time.Now().Unix() {
return 0, errNoFuture
}
g.mutex.Lock()
defer g.mutex.Unlock()
var (
now int64
flake int64
)
now = int64(time.Now().Unix())
if now == g.now {
g.sequence = (g.sequence + 1) & counterMask
if g.sequence == 0 {
for now <= g.now {
now = int64(time.Now().Unix())
time.Sleep(time.Microsecond * 100)
}
}
} else {
g.sequence = 0
}
g.now = now
flake = int64(
((now - g.StartTime) << counterLen) |
int64(g.sequence))
return flake, nil
}

12
resources/text.go Normal file
View File

@ -0,0 +1,12 @@
package resources
import (
"html/template"
"strings"
)
func OperateReplyText(unsafe string) template.HTML {
unsafe = template.HTMLEscapeString(unsafe)
unsafe = strings.Replace(unsafe, "\n", "<br />", -1)
return template.HTML(unsafe)
}

145
resources/thread.go Normal file
View File

@ -0,0 +1,145 @@
package resources
import (
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/buntdb"
)
type Thread struct {
ID int64 `json:"id"`
StartReply int64 `json:"start"`
Board string `json:"board"`
Metadata Metadata `json:"-"`
intReply *Reply
intReplies []*Reply
}
func (t *Thread) GetReplies() []*Reply {
return t.intReplies
}
func (t *Thread) GetReply() *Reply {
return t.intReply
}
func NewThread(tx *buntdb.Tx, host, board string, in *Thread, in2 *Reply) error {
var err error
err = TestBoard(tx, host, in.Board)
if err != nil {
return err
}
in.ID, err = getID()
if err != nil {
return err
}
in2.Thread = in.ID
in2.ID, err = getID()
if err != nil {
return err
}
in.StartReply = in2.ID
dat, err := json.Marshal(in)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), in.ID),
string(dat),
nil)
if err != nil {
return err
}
if replaced {
return errors.New("Thread already exists")
}
return NewReply(tx, host, board, in, in2, true)
}
func TestThread(tx *buntdb.Tx, host, board string, id int64) error {
err := TestBoard(tx, host, board)
if err != nil {
return err
}
_, err = tx.Get(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id),
)
return err
}
func GetThread(tx *buntdb.Tx, host, board string, id int64) (*Thread, error) {
var ret = &Thread{}
dat, err := tx.Get(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id),
)
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(dat), ret); err != nil {
return nil, err
}
ret.intReply, err = GetReply(tx, host, board, id, ret.StartReply)
return ret, nil
}
func DelThread(tx *buntdb.Tx, host, board string, id int64) error {
if _, err := tx.Delete(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id),
); err != nil {
tx.Rollback()
return err
}
return nil
}
func FillReplies(tx *buntdb.Tx, host string, thread *Thread) (err error) {
thread.intReplies, err = ListReplies(tx, host, thread.Board, thread.ID)
return
}
func ListThreads(tx *buntdb.Tx, host, board string) ([]*Thread, error) {
var threadList = []*Thread{}
var err error
err = TestBoard(tx, host, board)
if err != nil {
return nil, err
}
tx.DescendKeys(
fmt.Sprintf(
threadSPath,
escapeString(host),
escapeString(board),
),
func(key, value string) bool {
var thread = &Thread{}
err = json.Unmarshal([]byte(value), thread)
if err != nil {
return false
}
thread.intReply, err = GetReply(tx, host, board, thread.ID, thread.StartReply)
if err != nil {
return false
}
threadList = append(threadList, thread)
if len(threadList) >= 25 {
return false
}
return true
})
return threadList, err
}

4
vendor/github.com/GeertJohan/go.rice/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,4 @@
Geert-Johan Riemer <geertjohan@geertjohan.net>
Paul Maddox <paul.maddox@gmail.com>
Vincent Petithory <vincent.petithory@gmail.com>

22
vendor/github.com/GeertJohan/go.rice/LICENSE generated vendored Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2013, Geert-Johan Riemer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

151
vendor/github.com/GeertJohan/go.rice/README.md generated vendored Normal file
View File

@ -0,0 +1,151 @@
## go.rice
[![Wercker](https://img.shields.io/wercker/ci/54c7af4dcc09f9963725bb25.svg?style=flat-square)](https://app.wercker.com/#applications/54c7af4dcc09f9963725bb25)
[![Godoc](https://img.shields.io/badge/godoc-go.rice-blue.svg?style=flat-square)](https://godoc.org/github.com/GeertJohan/go.rice)
go.rice is a [Go](http://golang.org) package that makes working with resources such as html,js,css,images and templates very easy. During development `go.rice` will load required files directly from disk. Upon deployment it is easy to add all resource files to a executable using the `rice` tool, without changing the source code for your package. go.rice provides several methods to add resources to a binary.
### What does it do?
The first thing go.rice does is finding the correct absolute path for your resource files. Say you are executing go binary in your home directory, but your `html-files` are located in `$GOPATH/src/yourApplication/html-files`. `go.rice` will lookup the correct path for that directory (relative to the location of yourApplication). The only thing you have to do is include the resources using `rice.FindBox("html-files")`.
This only works when the source is available to the machine executing the binary. Which is always the case when the binary was installed with `go get` or `go install`. It might occur that you wish to simply provide a binary, without source. The `rice` tool analyses source code and finds call's to `rice.FindBox(..)` and adds the required directories to the executable binary. There are several methods to add these resources. You can 'embed' by generating go source code, or append the resource to the executable as zip file. In both cases `go.rice` will detect the embedded or appended resources and load those, instead of looking up files from disk.
### Installation
Use `go get` to install the package the `rice` tool.
```
go get github.com/GeertJohan/go.rice
go get github.com/GeertJohan/go.rice/rice
```
### Package usage
Import the package: `import "github.com/GeertJohan/go.rice"`
**Serving a static content folder over HTTP with a rice Box**
```go
http.Handle("/", http.FileServer(rice.MustFindBox("http-files").HTTPBox()))
http.ListenAndServe(":8080", nil)
```
**Service a static content folder over HTTP at a non-root location**
```go
box := rice.MustFindBox("cssfiles")
cssFileServer := http.StripPrefix("/css/", http.FileServer(box.HTTPBox()))
http.Handle("/css/", cssFileServer)
http.ListenAndServe(":8080", nil)
```
Note the *trailing slash* in `/css/` in both the call to
`http.StripPrefix` and `http.Handle`.
**Loading a template**
```go
// find a rice.Box
templateBox, err := rice.FindBox("example-templates")
if err != nil {
log.Fatal(err)
}
// get file contents as string
templateString, err := templateBox.String("message.tmpl")
if err != nil {
log.Fatal(err)
}
// parse and execute the template
tmplMessage, err := template.New("message").Parse(templateString)
if err != nil {
log.Fatal(err)
}
tmplMessage.Execute(os.Stdout, map[string]string{"Message": "Hello, world!"})
```
Never call `FindBox()` or `MustFindBox()` from an `init()` function, as the boxes might have not been loaded at that time.
### Tool usage
The `rice` tool lets you add the resources to a binary executable so the files are not loaded from the filesystem anymore. This creates a 'standalone' executable. There are several ways to add the resources to a binary, each has pro's and con's but all will work without requiring changes to the way you load the resources.
#### embed-go
**Embed resources by generating Go source code**
This method must be executed before building. It generates a single Go source file called *rice-box.go* for each package, that is compiled by the go compiler into the binary.
The downside with this option is that the generated go source files can become very large, which will slow down compilation and require lots of memory to compile.
Execute the following commands:
```
rice embed-go
go build
```
*A Note on Symbolic Links*: `embed-go` uses the `os.Walk` function
from the standard library. The `os.Walk` function does **not** follow
symbolic links. So, when creating a box, be aware that any symbolic
links inside your box's directory will not be followed. **However**,
if the box itself is a symbolic link, its actual location will be
resolved first and then walked. In summary, if your box location is a
symbolic link, it will be followed but none of the symbolic links in
the box will be followed.
#### embed-syso
**Embed resources by generating a coff .syso file and some .go source code**
** This method is experimental and should not be used for production systems just yet **
This method must be executed before building. It generates a COFF .syso file and Go source file that are compiled by the go compiler into the binary.
Execute the following commands:
```
rice embed-syso
go build
```
#### append
**Append resources to executable as zip file**
This method changes an already built executable. It appends the resources as zip file to the binary. It makes compilation a lot faster and can be used with large resource files.
Downsides for appending are that it requires `zip` to be installed and does not provide a working Seek method.
Run the following commands to create a standalone executable.
```
go build -o example
rice append --exec example
```
**Note: requires zip command to be installed**
On windows, install zip from http://gnuwin32.sourceforge.net/packages/zip.htm or cygwin/msys toolsets.
#### Help information
Run `rice -h` for information about all options.
You can run the -h option for each sub-command, e.g. `rice append -h`.
### Order of precedence
When opening a new box, the rice package tries to locate the resources in the following order:
- embedded in generated go source
- appended as zip
- 'live' from filesystem
### License
This project is licensed under a Simplified BSD license. Please read the [LICENSE file][license].
### TODO & Development
This package is not completed yet. Though it already provides working embedding, some important featuers are still missing.
- implement Readdir() correctly on virtualDir
- in-code TODO's
- find boxes in imported packages
Less important stuff:
- idea, os/arch dependent embeds. rice checks if embedding file has _os_arch or build flags. If box is not requested by file without buildflags, then the buildflags are applied to the embed file.
### Package documentation
You will find package documentation at [godoc.org/github.com/GeertJohan/go.rice][godoc].
[license]: https://github.com/GeertJohan/go.rice/blob/master/LICENSE
[godoc]: http://godoc.org/github.com/GeertJohan/go.rice

138
vendor/github.com/GeertJohan/go.rice/appended.go generated vendored Normal file
View File

@ -0,0 +1,138 @@
package rice
import (
"archive/zip"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/daaku/go.zipexe"
"github.com/kardianos/osext"
)
// appendedBox defines an appended box
type appendedBox struct {
Name string // box name
Files map[string]*appendedFile // appended files (*zip.File) by full path
}
type appendedFile struct {
zipFile *zip.File
dir bool
dirInfo *appendedDirInfo
children []*appendedFile
content []byte
}
// appendedBoxes is a public register of appendes boxes
var appendedBoxes = make(map[string]*appendedBox)
func init() {
// find if exec is appended
thisFile, err := osext.Executable()
if err != nil {
return // not appended or cant find self executable
}
closer, rd, err := zipexe.OpenCloser(thisFile)
if err != nil {
return // not appended
}
defer closer.Close()
for _, f := range rd.File {
// get box and file name from f.Name
fileParts := strings.SplitN(strings.TrimLeft(filepath.ToSlash(f.Name), "/"), "/", 2)
boxName := fileParts[0]
var fileName string
if len(fileParts) > 1 {
fileName = fileParts[1]
}
// find box or create new one if doesn't exist
box := appendedBoxes[boxName]
if box == nil {
box = &appendedBox{
Name: boxName,
Files: make(map[string]*appendedFile),
}
appendedBoxes[boxName] = box
}
// create and add file to box
af := &appendedFile{
zipFile: f,
}
if f.Comment == "dir" {
af.dir = true
af.dirInfo = &appendedDirInfo{
name: filepath.Base(af.zipFile.Name),
//++ TODO: use zip modtime when that is set correctly: af.zipFile.ModTime()
time: time.Now(),
}
} else {
// this is a file, we need it's contents so we can create a bytes.Reader when the file is opened
// make a new byteslice
af.content = make([]byte, af.zipFile.FileInfo().Size())
// ignore reading empty files from zip (empty file still is a valid file to be read though!)
if len(af.content) > 0 {
// open io.ReadCloser
rc, err := af.zipFile.Open()
if err != nil {
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
log.Printf("error opening appended file %s: %v", af.zipFile.Name, err)
} else {
_, err = rc.Read(af.content)
rc.Close()
if err != nil {
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
log.Printf("error reading data for appended file %s: %v", af.zipFile.Name, err)
}
}
}
}
// add appendedFile to box file list
box.Files[fileName] = af
// add to parent dir (if any)
dirName := filepath.Dir(fileName)
if dirName == "." {
dirName = ""
}
if fileName != "" { // don't make box root dir a child of itself
if dir := box.Files[dirName]; dir != nil {
dir.children = append(dir.children, af)
}
}
}
}
// implements os.FileInfo.
// used for Readdir()
type appendedDirInfo struct {
name string
time time.Time
}
func (adi *appendedDirInfo) Name() string {
return adi.name
}
func (adi *appendedDirInfo) Size() int64 {
return 0
}
func (adi *appendedDirInfo) Mode() os.FileMode {
return os.ModeDir
}
func (adi *appendedDirInfo) ModTime() time.Time {
return adi.time
}
func (adi *appendedDirInfo) IsDir() bool {
return true
}
func (adi *appendedDirInfo) Sys() interface{} {
return nil
}

337
vendor/github.com/GeertJohan/go.rice/box.go generated vendored Normal file
View File

@ -0,0 +1,337 @@
package rice
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/GeertJohan/go.rice/embedded"
)
// Box abstracts a directory for resources/files.
// It can either load files from disk, or from embedded code (when `rice --embed` was ran).
type Box struct {
name string
absolutePath string
embed *embedded.EmbeddedBox
appendd *appendedBox
}
var defaultLocateOrder = []LocateMethod{LocateEmbedded, LocateAppended, LocateFS}
func findBox(name string, order []LocateMethod) (*Box, error) {
b := &Box{name: name}
// no support for absolute paths since gopath can be different on different machines.
// therefore, required box must be located relative to package requiring it.
if filepath.IsAbs(name) {
return nil, errors.New("given name/path is absolute")
}
var err error
for _, method := range order {
switch method {
case LocateEmbedded:
if embed := embedded.EmbeddedBoxes[name]; embed != nil {
b.embed = embed
return b, nil
}
case LocateAppended:
appendedBoxName := strings.Replace(name, `/`, `-`, -1)
if appendd := appendedBoxes[appendedBoxName]; appendd != nil {
b.appendd = appendd
return b, nil
}
case LocateFS:
// resolve absolute directory path
err := b.resolveAbsolutePathFromCaller()
if err != nil {
continue
}
// check if absolutePath exists on filesystem
info, err := os.Stat(b.absolutePath)
if err != nil {
continue
}
// check if absolutePath is actually a directory
if !info.IsDir() {
err = errors.New("given name/path is not a directory")
continue
}
return b, nil
case LocateWorkingDirectory:
// resolve absolute directory path
err := b.resolveAbsolutePathFromWorkingDirectory()
if err != nil {
continue
}
// check if absolutePath exists on filesystem
info, err := os.Stat(b.absolutePath)
if err != nil {
continue
}
// check if absolutePath is actually a directory
if !info.IsDir() {
err = errors.New("given name/path is not a directory")
continue
}
return b, nil
}
}
if err == nil {
err = fmt.Errorf("could not locate box %q", name)
}
return nil, err
}
// FindBox returns a Box instance for given name.
// When the given name is a relative path, it's base path will be the calling pkg/cmd's source root.
// When the given name is absolute, it's absolute. derp.
// Make sure the path doesn't contain any sensitive information as it might be placed into generated go source (embedded).
func FindBox(name string) (*Box, error) {
return findBox(name, defaultLocateOrder)
}
// MustFindBox returns a Box instance for given name, like FindBox does.
// It does not return an error, instead it panics when an error occurs.
func MustFindBox(name string) *Box {
box, err := findBox(name, defaultLocateOrder)
if err != nil {
panic(err)
}
return box
}
// This is injected as a mutable function literal so that we can mock it out in
// tests and return a fixed test file.
var resolveAbsolutePathFromCaller = func(name string, nStackFrames int) (string, error) {
_, callingGoFile, _, ok := runtime.Caller(nStackFrames)
if !ok {
return "", errors.New("couldn't find caller on stack")
}
// resolve to proper path
pkgDir := filepath.Dir(callingGoFile)
// fix for go cover
const coverPath = "_test/_obj_test"
if !filepath.IsAbs(pkgDir) {
if i := strings.Index(pkgDir, coverPath); i >= 0 {
pkgDir = pkgDir[:i] + pkgDir[i+len(coverPath):] // remove coverPath
pkgDir = filepath.Join(os.Getenv("GOPATH"), "src", pkgDir) // make absolute
}
}
return filepath.Join(pkgDir, name), nil
}
func (b *Box) resolveAbsolutePathFromCaller() error {
path, err := resolveAbsolutePathFromCaller(b.name, 4)
if err != nil {
return err
}
b.absolutePath = path
return nil
}
func (b *Box) resolveAbsolutePathFromWorkingDirectory() error {
path, err := os.Getwd()
if err != nil {
return err
}
b.absolutePath = filepath.Join(path, b.name)
return nil
}
// IsEmbedded indicates wether this box was embedded into the application
func (b *Box) IsEmbedded() bool {
return b.embed != nil
}
// IsAppended indicates wether this box was appended to the application
func (b *Box) IsAppended() bool {
return b.appendd != nil
}
// Time returns how actual the box is.
// When the box is embedded, it's value is saved in the embedding code.
// When the box is live, this methods returns time.Now()
func (b *Box) Time() time.Time {
if b.IsEmbedded() {
return b.embed.Time
}
//++ TODO: return time for appended box
return time.Now()
}
// Open opens a File from the box
// If there is an error, it will be of type *os.PathError.
func (b *Box) Open(name string) (*File, error) {
if Debug {
fmt.Printf("Open(%s)\n", name)
}
if b.IsEmbedded() {
if Debug {
fmt.Println("Box is embedded")
}
// trim prefix (paths are relative to box)
name = strings.TrimLeft(name, "/")
if Debug {
fmt.Printf("Trying %s\n", name)
}
// search for file
ef := b.embed.Files[name]
if ef == nil {
if Debug {
fmt.Println("Didn't find file in embed")
}
// file not found, try dir
ed := b.embed.Dirs[name]
if ed == nil {
if Debug {
fmt.Println("Didn't find dir in embed")
}
// dir not found, error out
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
if Debug {
fmt.Println("Found dir. Returning virtual dir")
}
vd := newVirtualDir(ed)
return &File{virtualD: vd}, nil
}
// box is embedded
if Debug {
fmt.Println("Found file. Returning virtual file")
}
vf := newVirtualFile(ef)
return &File{virtualF: vf}, nil
}
if b.IsAppended() {
// trim prefix (paths are relative to box)
name = strings.TrimLeft(name, "/")
// search for file
appendedFile := b.appendd.Files[name]
if appendedFile == nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
// create new file
f := &File{
appendedF: appendedFile,
}
// if this file is a directory, we want to be able to read and seek
if !appendedFile.dir {
// looks like malformed data in zip, error now
if appendedFile.content == nil {
return nil, &os.PathError{
Op: "open",
Path: "name",
Err: errors.New("error reading data from zip file"),
}
}
// create new bytes.Reader
f.appendedFileReader = bytes.NewReader(appendedFile.content)
}
// all done
return f, nil
}
// perform os open
if Debug {
fmt.Printf("Using os.Open(%s)", filepath.Join(b.absolutePath, name))
}
file, err := os.Open(filepath.Join(b.absolutePath, name))
if err != nil {
return nil, err
}
return &File{realF: file}, nil
}
// Bytes returns the content of the file with given name as []byte.
func (b *Box) Bytes(name string) ([]byte, error) {
file, err := b.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return content, nil
}
// MustBytes returns the content of the file with given name as []byte.
// panic's on error.
func (b *Box) MustBytes(name string) []byte {
bts, err := b.Bytes(name)
if err != nil {
panic(err)
}
return bts
}
// String returns the content of the file with given name as string.
func (b *Box) String(name string) (string, error) {
// check if box is embedded, optimized fast path
if b.IsEmbedded() {
// find file in embed
ef := b.embed.Files[name]
if ef == nil {
return "", os.ErrNotExist
}
// return as string
return ef.Content, nil
}
bts, err := b.Bytes(name)
if err != nil {
return "", err
}
return string(bts), nil
}
// MustString returns the content of the file with given name as string.
// panic's on error.
func (b *Box) MustString(name string) string {
str, err := b.String(name)
if err != nil {
panic(err)
}
return str
}
// Name returns the name of the box
func (b *Box) Name() string {
return b.name
}

39
vendor/github.com/GeertJohan/go.rice/config.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
package rice
// LocateMethod defines how a box is located.
type LocateMethod int
const (
LocateFS = LocateMethod(iota) // Locate on the filesystem according to package path.
LocateAppended // Locate boxes appended to the executable.
LocateEmbedded // Locate embedded boxes.
LocateWorkingDirectory // Locate on the binary working directory
)
// Config allows customizing the box lookup behavior.
type Config struct {
// LocateOrder defines the priority order that boxes are searched for. By
// default, the package global FindBox searches for embedded boxes first,
// then appended boxes, and then finally boxes on the filesystem. That
// search order may be customized by provided the ordered list here. Leaving
// out a particular method will omit that from the search space. For
// example, []LocateMethod{LocateEmbedded, LocateAppended} will never search
// the filesystem for boxes.
LocateOrder []LocateMethod
}
// FindBox searches for boxes using the LocateOrder of the config.
func (c *Config) FindBox(boxName string) (*Box, error) {
return findBox(boxName, c.LocateOrder)
}
// MustFindBox searches for boxes using the LocateOrder of the config, like
// FindBox does. It does not return an error, instead it panics when an error
// occurs.
func (c *Config) MustFindBox(boxName string) *Box {
box, err := findBox(boxName, c.LocateOrder)
if err != nil {
panic(err)
}
return box
}

4
vendor/github.com/GeertJohan/go.rice/debug.go generated vendored Normal file
View File

@ -0,0 +1,4 @@
package rice
// Debug can be set to true to enable debugging.
var Debug = false

90
vendor/github.com/GeertJohan/go.rice/embedded.go generated vendored Normal file
View File

@ -0,0 +1,90 @@
package rice
import (
"os"
"time"
"github.com/GeertJohan/go.rice/embedded"
)
// re-type to make exported methods invisible to user (godoc)
// they're not required for the user
// embeddedDirInfo implements os.FileInfo
type embeddedDirInfo embedded.EmbeddedDir
// Name returns the base name of the directory
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Name() string {
return ed.Filename
}
// Size always returns 0
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Size() int64 {
return 0
}
// Mode returns the file mode bits
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Mode() os.FileMode {
return os.FileMode(0555 | os.ModeDir) // dr-xr-xr-x
}
// ModTime returns the modification time
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) ModTime() time.Time {
return ed.DirModTime
}
// IsDir returns the abbreviation for Mode().IsDir() (always true)
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) IsDir() bool {
return true
}
// Sys returns the underlying data source (always nil)
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Sys() interface{} {
return nil
}
// re-type to make exported methods invisible to user (godoc)
// they're not required for the user
// embeddedFileInfo implements os.FileInfo
type embeddedFileInfo embedded.EmbeddedFile
// Name returns the base name of the file
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Name() string {
return ef.Filename
}
// Size returns the length in bytes for regular files; system-dependent for others
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Size() int64 {
return int64(len(ef.Content))
}
// Mode returns the file mode bits
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Mode() os.FileMode {
return os.FileMode(0555) // r-xr-xr-x
}
// ModTime returns the modification time
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) ModTime() time.Time {
return ef.FileModTime
}
// IsDir returns the abbreviation for Mode().IsDir() (always false)
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) IsDir() bool {
return false
}
// Sys returns the underlying data source (always nil)
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Sys() interface{} {
return nil
}

View File

@ -0,0 +1,80 @@
// Package embedded defines embedded data types that are shared between the go.rice package and generated code.
package embedded
import (
"fmt"
"path/filepath"
"strings"
"time"
)
const (
EmbedTypeGo = 0
EmbedTypeSyso = 1
)
// EmbeddedBox defines an embedded box
type EmbeddedBox struct {
Name string // box name
Time time.Time // embed time
EmbedType int // kind of embedding
Files map[string]*EmbeddedFile // ALL embedded files by full path
Dirs map[string]*EmbeddedDir // ALL embedded dirs by full path
}
// Link creates the ChildDirs and ChildFiles links in all EmbeddedDir's
func (e *EmbeddedBox) Link() {
for path, ed := range e.Dirs {
fmt.Println(path)
ed.ChildDirs = make([]*EmbeddedDir, 0)
ed.ChildFiles = make([]*EmbeddedFile, 0)
}
for path, ed := range e.Dirs {
parentDirpath, _ := filepath.Split(path)
if strings.HasSuffix(parentDirpath, "/") {
parentDirpath = parentDirpath[:len(parentDirpath)-1]
}
parentDir := e.Dirs[parentDirpath]
if parentDir == nil {
panic("parentDir `" + parentDirpath + "` is missing in embedded box")
}
parentDir.ChildDirs = append(parentDir.ChildDirs, ed)
}
for path, ef := range e.Files {
dirpath, _ := filepath.Split(path)
if strings.HasSuffix(dirpath, "/") {
dirpath = dirpath[:len(dirpath)-1]
}
dir := e.Dirs[dirpath]
if dir == nil {
panic("dir `" + dirpath + "` is missing in embedded box")
}
dir.ChildFiles = append(dir.ChildFiles, ef)
}
}
// EmbeddedDir is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
type EmbeddedDir struct {
Filename string
DirModTime time.Time
ChildDirs []*EmbeddedDir // direct childs, as returned by virtualDir.Readdir()
ChildFiles []*EmbeddedFile // direct childs, as returned by virtualDir.Readdir()
}
// EmbeddedFile is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
type EmbeddedFile struct {
Filename string // filename
FileModTime time.Time
Content string
}
// EmbeddedBoxes is a public register of embedded boxes
var EmbeddedBoxes = make(map[string]*EmbeddedBox)
// RegisterEmbeddedBox registers an EmbeddedBox
func RegisterEmbeddedBox(name string, box *EmbeddedBox) {
if _, exists := EmbeddedBoxes[name]; exists {
panic(fmt.Sprintf("EmbeddedBox with name `%s` exists already", name))
}
EmbeddedBoxes[name] = box
}

144
vendor/github.com/GeertJohan/go.rice/file.go generated vendored Normal file
View File

@ -0,0 +1,144 @@
package rice
import (
"bytes"
"errors"
"os"
"path/filepath"
)
// File implements the io.Reader, io.Seeker, io.Closer and http.File interfaces
type File struct {
// File abstracts file methods so the user doesn't see the difference between rice.virtualFile, rice.virtualDir and os.File
// TODO: maybe use internal File interface and four implementations: *os.File, appendedFile, virtualFile, virtualDir
// real file on disk
realF *os.File
// when embedded (go)
virtualF *virtualFile
virtualD *virtualDir
// when appended (zip)
appendedF *appendedFile
appendedFileReader *bytes.Reader
// TODO: is appendedFileReader subject of races? Might need a lock here..
}
// Close is like (*os.File).Close()
// Visit http://golang.org/pkg/os/#File.Close for more information
func (f *File) Close() error {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return errors.New("already closed")
}
f.appendedFileReader = nil
return nil
}
if f.virtualF != nil {
return f.virtualF.close()
}
if f.virtualD != nil {
return f.virtualD.close()
}
return f.realF.Close()
}
// Stat is like (*os.File).Stat()
// Visit http://golang.org/pkg/os/#File.Stat for more information
func (f *File) Stat() (os.FileInfo, error) {
if f.appendedF != nil {
if f.appendedF.dir {
return f.appendedF.dirInfo, nil
}
if f.appendedFileReader == nil {
return nil, errors.New("file is closed")
}
return f.appendedF.zipFile.FileInfo(), nil
}
if f.virtualF != nil {
return f.virtualF.stat()
}
if f.virtualD != nil {
return f.virtualD.stat()
}
return f.realF.Stat()
}
// Readdir is like (*os.File).Readdir()
// Visit http://golang.org/pkg/os/#File.Readdir for more information
func (f *File) Readdir(count int) ([]os.FileInfo, error) {
if f.appendedF != nil {
if f.appendedF.dir {
fi := make([]os.FileInfo, 0, len(f.appendedF.children))
for _, childAppendedFile := range f.appendedF.children {
if childAppendedFile.dir {
fi = append(fi, childAppendedFile.dirInfo)
} else {
fi = append(fi, childAppendedFile.zipFile.FileInfo())
}
}
return fi, nil
}
//++ TODO: is os.ErrInvalid the correct error for Readdir on file?
return nil, os.ErrInvalid
}
if f.virtualF != nil {
return f.virtualF.readdir(count)
}
if f.virtualD != nil {
return f.virtualD.readdir(count)
}
return f.realF.Readdir(count)
}
// Read is like (*os.File).Read()
// Visit http://golang.org/pkg/os/#File.Read for more information
func (f *File) Read(bts []byte) (int, error) {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return 0, &os.PathError{
Op: "read",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("file is closed"),
}
}
if f.appendedF.dir {
return 0, &os.PathError{
Op: "read",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("is a directory"),
}
}
return f.appendedFileReader.Read(bts)
}
if f.virtualF != nil {
return f.virtualF.read(bts)
}
if f.virtualD != nil {
return f.virtualD.read(bts)
}
return f.realF.Read(bts)
}
// Seek is like (*os.File).Seek()
// Visit http://golang.org/pkg/os/#File.Seek for more information
func (f *File) Seek(offset int64, whence int) (int64, error) {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return 0, &os.PathError{
Op: "seek",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("file is closed"),
}
}
return f.appendedFileReader.Seek(offset, whence)
}
if f.virtualF != nil {
return f.virtualF.seek(offset, whence)
}
if f.virtualD != nil {
return f.virtualD.seek(offset, whence)
}
return f.realF.Seek(offset, whence)
}

21
vendor/github.com/GeertJohan/go.rice/http.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package rice
import (
"net/http"
)
// HTTPBox implements http.FileSystem which allows the use of Box with a http.FileServer.
// e.g.: http.Handle("/", http.FileServer(rice.MustFindBox("http-files").HTTPBox()))
type HTTPBox struct {
*Box
}
// HTTPBox creates a new HTTPBox from an existing Box
func (b *Box) HTTPBox() *HTTPBox {
return &HTTPBox{b}
}
// Open returns a File using the http.File interface
func (hb *HTTPBox) Open(name string) (http.File, error) {
return hb.Box.Open(name)
}

19
vendor/github.com/GeertJohan/go.rice/sort.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
package rice
import "os"
// SortByName allows an array of os.FileInfo objects
// to be easily sorted by filename using sort.Sort(SortByName(array))
type SortByName []os.FileInfo
func (f SortByName) Len() int { return len(f) }
func (f SortByName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
func (f SortByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
// SortByModified allows an array of os.FileInfo objects
// to be easily sorted by modified date using sort.Sort(SortByModified(array))
type SortByModified []os.FileInfo
func (f SortByModified) Len() int { return len(f) }
func (f SortByModified) Less(i, j int) bool { return f[i].ModTime().Unix() > f[j].ModTime().Unix() }
func (f SortByModified) Swap(i, j int) { f[i], f[j] = f[j], f[i] }

252
vendor/github.com/GeertJohan/go.rice/virtual.go generated vendored Normal file
View File

@ -0,0 +1,252 @@
package rice
import (
"errors"
"io"
"os"
"path/filepath"
"sort"
"github.com/GeertJohan/go.rice/embedded"
)
//++ TODO: IDEA: merge virtualFile and virtualDir, this decreases work done by rice.File
// Error indicating some function is not implemented yet (but available to satisfy an interface)
var ErrNotImplemented = errors.New("not implemented yet")
// virtualFile is a 'stateful' virtual file.
// virtualFile wraps an *EmbeddedFile for a call to Box.Open() and virtualizes 'read cursor' (offset) and 'closing'.
// virtualFile is only internally visible and should be exposed through rice.File
type virtualFile struct {
*embedded.EmbeddedFile // the actual embedded file, embedded to obtain methods
offset int64 // read position on the virtual file
closed bool // closed when true
}
// create a new virtualFile for given EmbeddedFile
func newVirtualFile(ef *embedded.EmbeddedFile) *virtualFile {
vf := &virtualFile{
EmbeddedFile: ef,
offset: 0,
closed: false,
}
return vf
}
//++ TODO check for nil pointers in all these methods. When so: return os.PathError with Err: os.ErrInvalid
func (vf *virtualFile) close() error {
if vf.closed {
return &os.PathError{
Op: "close",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("already closed"),
}
}
vf.EmbeddedFile = nil
vf.closed = true
return nil
}
func (vf *virtualFile) stat() (os.FileInfo, error) {
if vf.closed {
return nil, &os.PathError{
Op: "stat",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
return (*embeddedFileInfo)(vf.EmbeddedFile), nil
}
func (vf *virtualFile) readdir(count int) ([]os.FileInfo, error) {
if vf.closed {
return nil, &os.PathError{
Op: "readdir",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
//TODO: return proper error for a readdir() call on a file
return nil, ErrNotImplemented
}
func (vf *virtualFile) read(bts []byte) (int, error) {
if vf.closed {
return 0, &os.PathError{
Op: "read",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
end := vf.offset + int64(len(bts))
if end >= int64(len(vf.Content)) {
// end of file, so return what we have + EOF
n := copy(bts, vf.Content[vf.offset:])
vf.offset = 0
return n, io.EOF
}
n := copy(bts, vf.Content[vf.offset:end])
vf.offset += int64(n)
return n, nil
}
func (vf *virtualFile) seek(offset int64, whence int) (int64, error) {
if vf.closed {
return 0, &os.PathError{
Op: "seek",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
var e error
//++ TODO: check if this is correct implementation for seek
switch whence {
case os.SEEK_SET:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset = offset
case os.SEEK_CUR:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset += offset
case os.SEEK_END:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset = int64(len(vf.EmbeddedFile.Content)) - offset
}
if e != nil {
return 0, &os.PathError{
Op: "seek",
Path: vf.Filename,
Err: e,
}
}
return vf.offset, nil
}
// virtualDir is a 'stateful' virtual directory.
// virtualDir wraps an *EmbeddedDir for a call to Box.Open() and virtualizes 'closing'.
// virtualDir is only internally visible and should be exposed through rice.File
type virtualDir struct {
*embedded.EmbeddedDir
offset int // readdir position on the directory
closed bool
}
// create a new virtualDir for given EmbeddedDir
func newVirtualDir(ed *embedded.EmbeddedDir) *virtualDir {
vd := &virtualDir{
EmbeddedDir: ed,
offset: 0,
closed: false,
}
return vd
}
func (vd *virtualDir) close() error {
//++ TODO: needs sync mutex?
if vd.closed {
return &os.PathError{
Op: "close",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("already closed"),
}
}
vd.closed = true
return nil
}
func (vd *virtualDir) stat() (os.FileInfo, error) {
if vd.closed {
return nil, &os.PathError{
Op: "stat",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return (*embeddedDirInfo)(vd.EmbeddedDir), nil
}
func (vd *virtualDir) readdir(n int) (fi []os.FileInfo, err error) {
if vd.closed {
return nil, &os.PathError{
Op: "readdir",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
// Build up the array of our contents
var files []os.FileInfo
// Add the child directories
for _, child := range vd.ChildDirs {
child.Filename = filepath.Base(child.Filename)
files = append(files, (*embeddedDirInfo)(child))
}
// Add the child files
for _, child := range vd.ChildFiles {
child.Filename = filepath.Base(child.Filename)
files = append(files, (*embeddedFileInfo)(child))
}
// Sort it by filename (lexical order)
sort.Sort(SortByName(files))
// Return all contents if that's what is requested
if n <= 0 {
vd.offset = 0
return files, nil
}
// If user has requested past the end of our list
// return what we can and send an EOF
if vd.offset+n >= len(files) {
offset := vd.offset
vd.offset = 0
return files[offset:], io.EOF
}
offset := vd.offset
vd.offset += n
return files[offset : offset+n], nil
}
func (vd *virtualDir) read(bts []byte) (int, error) {
if vd.closed {
return 0, &os.PathError{
Op: "read",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return 0, &os.PathError{
Op: "read",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("is a directory"),
}
}
func (vd *virtualDir) seek(offset int64, whence int) (int64, error) {
if vd.closed {
return 0, &os.PathError{
Op: "seek",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return 0, &os.PathError{
Op: "seek",
Path: vd.Filename,
Err: errors.New("is a directory"),
}
}

122
vendor/github.com/GeertJohan/go.rice/walk.go generated vendored Normal file
View File

@ -0,0 +1,122 @@
package rice
import (
"os"
"path/filepath"
"sort"
"strings"
)
// Walk is like filepath.Walk()
// Visit http://golang.org/pkg/path/filepath/#Walk for more information
func (b *Box) Walk(path string, walkFn filepath.WalkFunc) error {
pathFile, err := b.Open(path)
if err != nil {
return err
}
defer pathFile.Close()
pathInfo, err := pathFile.Stat()
if err != nil {
return err
}
if b.IsAppended() || b.IsEmbedded() {
return b.walk(path, pathInfo, walkFn)
}
// We don't have any embedded or appended box so use live filesystem mode
return filepath.Walk(b.absolutePath+string(os.PathSeparator)+path, func(path string, info os.FileInfo, err error) error {
// Strip out the box name from the returned paths
path = strings.TrimPrefix(path, b.absolutePath+string(os.PathSeparator))
return walkFn(path, info, err)
})
}
// walk recursively descends path.
// See walk() in $GOROOT/src/pkg/path/filepath/path.go
func (b *Box) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := b.readDirNames(path)
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := filepath.Join(path, name)
fileObject, err := b.Open(filename)
if err != nil {
return err
}
defer fileObject.Close()
fileInfo, err := fileObject.Stat()
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = b.walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// readDirNames reads the directory named by path and returns a sorted list of directory entries.
// See readDirNames() in $GOROOT/pkg/path/filepath/path.go
func (b *Box) readDirNames(path string) ([]string, error) {
f, err := b.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, err
}
if !stat.IsDir() {
return nil, nil
}
infos, err := f.Readdir(0)
if err != nil {
return nil, err
}
var names []string
for _, info := range infos {
names = append(names, info.Name())
}
sort.Strings(names)
return names, nil
}

31
vendor/github.com/GeertJohan/go.rice/wercker.yml generated vendored Normal file
View File

@ -0,0 +1,31 @@
box: wercker/golang
build:
steps:
- setup-go-workspace
- script:
name: get dependencies
code: |
go get -d -t ./...
- script:
name: build
code: |
go build -x ./...
- script:
name: test
code: |
go test -cover ./...
- script:
name: vet
code: |
go vet ./...
- script:
name: lint
code: |
go get github.com/golang/lint/golint
golint .

21
vendor/github.com/daaku/go.zipexe/license generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2012-2015 Carlos Castillo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
vendor/github.com/daaku/go.zipexe/readme.md generated vendored Normal file
View File

@ -0,0 +1,5 @@
go.zipexe
=========
This module was taken as-is from https://github.com/cookieo9/resources-go.
Documentation: https://godoc.org/github.com/daaku/go.zipexe

142
vendor/github.com/daaku/go.zipexe/zipexe.go generated vendored Normal file
View File

@ -0,0 +1,142 @@
// Package zipexe attempts to open an executable binary file as a zip file.
package zipexe
import (
"archive/zip"
"debug/elf"
"debug/macho"
"debug/pe"
"errors"
"io"
"os"
)
// Opens a zip file by path.
func Open(path string) (*zip.Reader, error) {
_, rd, err := OpenCloser(path)
return rd, err
}
// OpenCloser is like Open but returns an additional Closer to avoid leaking open files.
func OpenCloser(path string) (io.Closer, *zip.Reader, error) {
file, err := os.Open(path)
if err != nil {
return nil, nil, err
}
finfo, err := file.Stat()
if err != nil {
return nil, nil, err
}
zr, err := NewReader(file, finfo.Size())
if err != nil {
return nil, nil, err
}
return file, zr, nil
}
// Open a zip file, specially handling various binaries that may have been
// augmented with zip data.
func NewReader(rda io.ReaderAt, size int64) (*zip.Reader, error) {
handlers := []func(io.ReaderAt, int64) (*zip.Reader, error){
zip.NewReader,
zipExeReaderMacho,
zipExeReaderElf,
zipExeReaderPe,
}
for _, handler := range handlers {
zfile, err := handler(rda, size)
if err == nil {
return zfile, nil
}
}
return nil, errors.New("Couldn't Open As Executable")
}
// zipExeReaderMacho treats the file as a Mach-O binary
// (Mac OS X / Darwin executable) and attempts to find a zip archive.
func zipExeReaderMacho(rda io.ReaderAt, size int64) (*zip.Reader, error) {
file, err := macho.NewFile(rda)
if err != nil {
return nil, err
}
var max int64
for _, load := range file.Loads {
seg, ok := load.(*macho.Segment)
if ok {
// Check if the segment contains a zip file
if zfile, err := zip.NewReader(seg, int64(seg.Filesz)); err == nil {
return zfile, nil
}
// Otherwise move end of file pointer
end := int64(seg.Offset + seg.Filesz)
if end > max {
max = end
}
}
}
// No zip file within binary, try appended to end
section := io.NewSectionReader(rda, max, size-max)
return zip.NewReader(section, section.Size())
}
// zipExeReaderPe treats the file as a Portable Exectuable binary
// (Windows executable) and attempts to find a zip archive.
func zipExeReaderPe(rda io.ReaderAt, size int64) (*zip.Reader, error) {
file, err := pe.NewFile(rda)
if err != nil {
return nil, err
}
var max int64
for _, sec := range file.Sections {
// Check if this section has a zip file
if zfile, err := zip.NewReader(sec, int64(sec.Size)); err == nil {
return zfile, nil
}
// Otherwise move end of file pointer
end := int64(sec.Offset + sec.Size)
if end > max {
max = end
}
}
// No zip file within binary, try appended to end
section := io.NewSectionReader(rda, max, size-max)
return zip.NewReader(section, section.Size())
}
// zipExeReaderElf treats the file as a ELF binary
// (linux/BSD/etc... executable) and attempts to find a zip archive.
func zipExeReaderElf(rda io.ReaderAt, size int64) (*zip.Reader, error) {
file, err := elf.NewFile(rda)
if err != nil {
return nil, err
}
var max int64
for _, sect := range file.Sections {
if sect.Type == elf.SHT_NOBITS {
continue
}
// Check if this section has a zip file
if zfile, err := zip.NewReader(sect, int64(sect.Size)); err == nil {
return zfile, nil
}
// Otherwise move end of file pointer
end := int64(sect.Offset + sect.Size)
if end > max {
max = end
}
}
// No zip file within binary, try appended to end
section := io.NewSectionReader(rda, max, size-max)
return zip.NewReader(section, section.Size())
}

39
vendor/github.com/hlandau/passlib/COPYING generated vendored Normal file
View File

@ -0,0 +1,39 @@
passlib is a Golang password verification library strongly inspired by and
derived from Python passlib (<https://pypi.python.org/pypi/passlib>). The BSD
license is preserved and extended to all new code.
License for Passlib
===================
Passlib is (c) `Assurance Technologies <http://www.assurancetechnologies.com>`_,
and is released under the `BSD license <http://www.opensource.org/licenses/bsd-license.php>`_::
Passlib
Copyright (c) 2008-2012 Assurance Technologies, LLC.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Assurance Technologies, nor the names of the
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

95
vendor/github.com/hlandau/passlib/README.md generated vendored Normal file
View File

@ -0,0 +1,95 @@
passlib for go
==============
[![GoDoc](https://godoc.org/gopkg.in/hlandau/passlib.v1?status.svg)](https://godoc.org/gopkg.in/hlandau/passlib.v1) [![Build Status](https://travis-ci.org/hlandau/passlib.svg?branch=master)](https://travis-ci.org/hlandau/passlib)
[Python's passlib](https://pythonhosted.org/passlib/) is quite an amazing
library. I'm not sure there's a password library in existence with more thought
put into it, or with more support for obscure password formats.
This is a skeleton of a port of passlib to Go. It dogmatically adopts the
modular crypt format, which [passlib has excellent documentation for](https://pythonhosted.org/passlib/modular_crypt_format.html#modular-crypt-format).
Currently, it supports sha256-crypt, sha512-crypt, scrypt-sha256, bcrypt and
passlib's bcrypt-sha256 variant. By default, it will hash using scrypt-sha256
and verify existing hashes using any of these schemes.
Example Usage
-------------
There's a default context for ease of use. Most people need only concern
themselves with the functions `Hash` and `Verify`:
```go
// Hash a plaintext, UTF-8 password.
func Hash(password string) (hash string, err error)
// Verifies a plaintext, UTF-8 password using a previously derived hash.
// Returns non-nil err if verification fails.
//
// Also returns an upgraded password hash if the hash provided is
// deprecated.
func Verify(password, hash string) (newHash string, err error)
```
Here's a rough skeleton of typical usage.
```go
import "gopkg.in/hlandau/passlib.v1"
func RegisterUser() {
(...)
password := get a (UTF-8, plaintext) password from somewhere
hash, err := passlib.Hash(password)
if err != nil {
// couldn't hash password for some reason
return
}
(store hash in database, etc.)
}
func CheckPassword() bool {
password := get the password the user entered
hash := the hash you stored from the call to Hash()
newHash, err := passlib.Verify(password, hash)
if err != nil {
// incorrect password, malformed hash, etc.
// either way, reject
return false
}
// The context has decided, as per its policy, that
// the hash which was used to validate the password
// should be changed. It has upgraded the hash using
// the verified password.
if newHash != "" {
(store newHash in database, replacing old hash)
}
return true
}
```
scrypt Modular Crypt Format
---------------------------
Since scrypt does not have a pre-existing modular crypt format standard, I made one. It's as follows:
$s2$N$r$p$salt$hash
...where `N`, `r` and `p` are the respective difficulty parameters to scrypt as positive decimal integers without leading zeroes, and `salt` and `hash` are base64-encoded binary strings. Note that the RFC 4648 base64 encoding is used (not the one used by sha256-crypt and sha512-crypt).
TODO
----
- PBKDF2
Licence
-------
passlib is partially derived from Python's passlib and so maintains its BSD license.
© 2008-2012 Assurance Technologies LLC. (Python passlib) BSD License
© 2014 Hugo Landau <hlandau@devever.net> BSD License

177
vendor/github.com/hlandau/passlib/passlib.go generated vendored Normal file
View File

@ -0,0 +1,177 @@
// Package passlib provides a simple password hashing and verification
// interface abstracting multiple password hashing schemes.
//
// Most people need concern themselves only with the functions Hash
// and Verify, which uses the default context and sensible defaults.
package passlib // import "gopkg.in/hlandau/passlib.v1"
import "gopkg.in/hlandau/passlib.v1/abstract"
import "gopkg.in/hlandau/passlib.v1/hash/scrypt"
import "gopkg.in/hlandau/passlib.v1/hash/sha2crypt"
import "gopkg.in/hlandau/passlib.v1/hash/bcryptsha256"
import "gopkg.in/hlandau/passlib.v1/hash/bcrypt"
import "gopkg.in/hlandau/easymetric.v1/cexp"
var cHashCalls = cexp.NewCounter("passlib.ctx.hashCalls")
var cVerifyCalls = cexp.NewCounter("passlib.ctx.verifyCalls")
var cSuccessfulVerifyCalls = cexp.NewCounter("passlib.ctx.successfulVerifyCalls")
var cFailedVerifyCalls = cexp.NewCounter("passlib.ctx.failedVerifyCalls")
var cSuccessfulVerifyCallsWithUpgrade = cexp.NewCounter("passlib.ctx.successfulVerifyCallsWithUpgrade")
var cSuccessfulVerifyCallsDeferringUpgrade = cexp.NewCounter("passlib.ctx.successfulVerifyCallsDeferringUpgrade")
// The default schemes, most preferred first. The first scheme will be used to
// hash passwords, and any of the schemes may be used to verify existing
// passwords. The contents of this value may change with subsequent releases.
var DefaultSchemes = []abstract.Scheme{
scrypt.SHA256Crypter,
sha2crypt.Crypter256,
sha2crypt.Crypter512,
bcryptsha256.Crypter,
bcrypt.Crypter,
}
// A password hashing context, that uses a given set of schemes to hash and
// verify passwords.
type Context struct {
// Slice of schemes to use, most preferred first.
//
// If left uninitialized, a sensible default set of schemes will be used.
//
// An upgrade hash (see the newHash return value of the Verify method of the
// abstract.Scheme interface) will be issued whenever a password is validated
// using a scheme which is not the first scheme in this slice.
Schemes []abstract.Scheme
}
func (ctx *Context) schemes() []abstract.Scheme {
if ctx.Schemes == nil {
return DefaultSchemes
}
return ctx.Schemes
}
// Hashes a UTF-8 plaintext password using the context and produces a password hash.
//
// If stub is "", one is generated automaticaly for the preferred password hashing
// scheme; you should specify stub as "" in almost all cases.
//
// The provided or randomly generated stub is used to deterministically hash
// the password. The returned hash is in modular crypt format.
//
// If the context has not been specifically configured, a sensible default policy
// is used. See the fields of Context.
func (ctx *Context) Hash(password string) (hash string, err error) {
cHashCalls.Add(1)
return ctx.schemes()[0].Hash(password)
}
// Verifies a UTF-8 plaintext password using a previously derived password hash
// and the default context. Returns nil err only if the password is valid.
//
// If the hash is determined to be deprecated based on the context policy, and
// the password is valid, the password is hashed using the preferred password
// hashing scheme and returned in newHash. You should use this to upgrade any
// stored password hash in your database.
//
// newHash is empty if the password was not valid or if no upgrade is required.
//
// You should treat any non-nil err as a password verification error.
func (ctx *Context) Verify(password, hash string) (newHash string, err error) {
return ctx.verify(password, hash, true)
}
// Like Verify, but does not hash an upgrade password when upgrade is required.
func (ctx *Context) VerifyNoUpgrade(password, hash string) error {
_, err := ctx.verify(password, hash, false)
return err
}
func (ctx *Context) verify(password, hash string, canUpgrade bool) (newHash string, err error) {
cVerifyCalls.Add(1)
for i, scheme := range ctx.schemes() {
if !scheme.SupportsStub(hash) {
continue
}
err = scheme.Verify(password, hash)
if err != nil {
cFailedVerifyCalls.Add(1)
return "", err
}
cSuccessfulVerifyCalls.Add(1)
if i != 0 || scheme.NeedsUpdate(hash) {
if canUpgrade {
cSuccessfulVerifyCallsWithUpgrade.Add(1)
// If the scheme is not the first scheme, try and rehash with the
// preferred scheme.
if newHash, err2 := ctx.Hash(password); err2 == nil {
return newHash, nil
}
} else {
cSuccessfulVerifyCallsDeferringUpgrade.Add(1)
}
}
return "", nil
}
return "", abstract.ErrUnsupportedScheme
}
// Determines whether a stub or hash needs updating according to the policy of
// the context.
func (ctx *Context) NeedsUpdate(stub string) bool {
for i, scheme := range ctx.schemes() {
if scheme.SupportsStub(stub) {
return i != 0 || scheme.NeedsUpdate(stub)
}
}
return false
}
// The default context, which uses sensible defaults. Most users should not
// reconfigure this. The defaults may change over time, so you may wish
// to reconfigure the context or use a custom context if you want precise
// control over the hashes used.
var DefaultContext Context
// Hashes a UTF-8 plaintext password using the default context and produces a
// password hash. Chooses the preferred password hashing scheme based on the
// configured policy. The default policy is sensible.
func Hash(password string) (hash string, err error) {
return DefaultContext.Hash(password)
}
// Verifies a UTF-8 plaintext password using a previously derived password hash
// and the default context. Returns nil err only if the password is valid.
//
// If the hash is determined to be deprecated based on policy, and the password
// is valid, the password is hashed using the preferred password hashing scheme
// and returned in newHash. You should use this to upgrade any stored password
// hash in your database.
//
// newHash is empty if the password was invalid or no upgrade is required.
//
// You should treat any non-nil err as a password verification error.
func Verify(password, hash string) (newHash string, err error) {
return DefaultContext.Verify(password, hash)
}
// Like Verify, but never upgrades.
func VerifyNoUpgrade(password, hash string) error {
return DefaultContext.VerifyNoUpgrade(password, hash)
}
// Uses the default context to determine whether a stub or hash needs updating.
func NeedsUpdate(stub string) bool {
return DefaultContext.NeedsUpdate(stub)
}
// © 2008-2012 Assurance Technologies LLC. (Python passlib) BSD License
// © 2014 Hugo Landau <hlandau@devever.net> BSD License

201
vendor/github.com/icza/session/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

114
vendor/github.com/icza/session/README.md generated vendored Normal file
View File

@ -0,0 +1,114 @@
# Session
[![Build Status](https://travis-ci.org/icza/session.svg?branch=master)](https://travis-ci.org/icza/session)
[![GoDoc](https://godoc.org/github.com/icza/session?status.svg)](https://godoc.org/github.com/icza/session)
[![Go Report Card](https://goreportcard.com/badge/github.com/icza/session)](https://goreportcard.com/report/github.com/icza/session)
[![codecov](https://codecov.io/gh/icza/session/branch/master/graph/badge.svg)](https://codecov.io/gh/icza/session)
The [Go](https://golang.org/) standard library includes a nice [http server](https://golang.org/pkg/net/http/), but unfortunately it lacks a very basic and important feature: _HTTP session management_.
This package provides an easy-to-use, extensible and secure session implementation and management. Package documentation can be found and godoc.org:
https://godoc.org/github.com/icza/session
This is "just" an HTTP session implementation and management, you can use it as-is, or with any existing Go web toolkits and frameworks.
## Overview
There are 3 key _players_ in the package:
- **`Session`** is the (HTTP) session interface. We can use it to store and retrieve constant and variable attributes from it.
- **`Store`** is a session store interface which is responsible to store sessions and make them retrievable by their IDs at the server side.
- **`Manager`** is a session manager interface which is responsible to acquire a `Session` from an (incoming) HTTP request, and to add a `Session` to an HTTP response to let the client know about the session. A `Manager` has a backing `Store` which is responsible to manage `Session` values at server side.
_Players_ of this package are represented by interfaces, and various implementations are provided for all these players.
You are not bound by the provided implementations, feel free to provide your own implementations for any of the players.
## Usage
Usage can't be simpler than this. To get the current session associated with the [http.Request](https://golang.org/pkg/net/http/#Request):
sess := session.Get(r)
if sess == nil {
// No session (yet)
} else {
// We have a session, use it
}
To create a new session (e.g. on a successful login) and add it to an [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter) (to let the client know about the session):
sess := session.NewSession()
session.Add(sess, w)
Let's see a more advanced session creation: let's provide a constant attribute (for the lifetime of the session) and an initial, variable attribute:
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"UserName": userName},
Attrs: map[string]interface{}{"Count": 1},
})
And to access these attributes and change value of `"Count"`:
userName := sess.CAttr("UserName")
count := sess.Attr("Count").(int) // Type assertion, you might wanna check if it succeeds
sess.SetAttr("Count", count+1) // Increment count
(Of course variable attributes can be added later on too with `Session.SetAttr()`, not just at session creation.)
To remove a session (e.g. on logout):
session.Remove(sess, w)
Check out the [session demo application](https://github.com/icza/session/blob/master/session_demo/session_demo.go) which shows all these in action.
## Google App Engine support
The package provides support for Google App Engine (GAE) platform.
The documentation doesn't include it (due to the `+build appengine` build constraint), but here it is: [gae_memcache_store.go](https://github.com/icza/session/blob/master/gae_memcache_store.go)
The implementation stores sessions in the Memcache and also saves sessions in the Datastore as a backup
in case data would be removed from the Memcache. This behaviour is optional, Datastore can be disabled completely.
You can also choose whether saving to Datastore happens synchronously (in the same goroutine)
or asynchronously (in another goroutine), resulting in faster response times.
We can use `NewMemcacheStore()` and `NewMemcacheStoreOptions()` functions to create a session Store implementation
which stores sessions in GAE's Memcache. Important to note that since accessing the Memcache relies on
Appengine Context which is bound to an `http.Request`, the returned Store can only be used for the lifetime of a request!
Note that the Store will automatically "flush" sessions accessed from it when the Store is closed,
so it is very important to close the Store at the end of your request; this is usually done by closing
the session manager to which you passed the store (preferably with the defer statement).
So in each request handling we have to create a new session manager using a new Store, and we can use the session manager
to do session-related tasks, something like this:
ctx := appengine.NewContext(r)
sessmgr := session.NewCookieManager(session.NewMemcacheStore(ctx))
defer sessmgr.Close() // This will ensure changes made to the session are auto-saved
// in Memcache (and optionally in the Datastore).
sess := sessmgr.Get(r) // Get current session
if sess != nil {
// Session exists, do something with it.
ctx.Infof("Count: %v", sess.Attr("Count"))
} else {
// No session yet, let's create one and add it:
sess = session.NewSession()
sess.SetAttr("Count", 1)
sessmgr.Add(sess, w)
}
Expired sessions are not automatically removed from the Datastore. To remove expired sessions, the package
provides a `PurgeExpiredSessFromDSFunc()` function which returns an [http.HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc).
It is recommended to register the returned handler function to a path which then can be defined
as a cron job to be called periodically, e.g. in every 30 minutes or so (your choice).
As cron handlers may run up to 10 minutes, the returned handler will stop at 8 minutes
to complete safely even if there are more expired, undeleted sessions.
It can be registered like this:
http.HandleFunc("/demo/purge", session.PurgeExpiredSessFromDSFunc(""))
Check out the GAE session demo application which shows how it can be used.
[cron.yaml](https://github.com/icza/session/blob/master/gae_session_demo/cron.yaml) file of the demo shows how a cron job can be defined to purge expired sessions.
Check out the [GAE session demo application](https://github.com/icza/session/blob/master/gae_session_demo/gae_session_demo.go) which shows how to use this in action.

123
vendor/github.com/icza/session/cookie_manager.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
/*
A secure, cookie based session Manager implementation.
*/
package session
import (
"net/http"
"time"
)
// CookieManager is a secure, cookie based session Manager implementation.
// Only the session ID is transmitted / stored at the clients, and it is managed using cookies.
type CookieManager struct {
store Store // Backing Store
sessIDCookieName string // Name of the cookie used for storing the session id
cookieSecure bool // Tells if session ID cookies are to be sent only over HTTPS
cookieMaxAgeSec int // Max age for session ID cookies in seconds
cookiePath string // Cookie path to use
}
// CookieMngrOptions defines options that may be passed when creating a new CookieManager.
// All fields are optional; default value will be used for any field that has the zero value.
type CookieMngrOptions struct {
// Name of the cookie used for storing the session id; default value is "sessid"
SessIDCookieName string
// Tells if session ID cookies are allowed to be sent over unsecure HTTP too (else only HTTPS);
// default value is false (only HTTPS)
AllowHTTP bool
// Max age for session ID cookies; default value is 30 days
CookieMaxAge time.Duration
// Cookie path to use; default value is the root: "/"
CookiePath string
}
// Pointer to zero value of CookieMngrOptions to be reused for efficiency.
var zeroCookieMngrOptions = new(CookieMngrOptions)
// NewCookieManager creates a new, cookie based session Manager with default options.
// Default values of options are listed in the CookieMngrOptions type.
func NewCookieManager(store Store) Manager {
return NewCookieManagerOptions(store, zeroCookieMngrOptions)
}
// NewCookieManagerOptions creates a new, cookie based session Manager with the specified options.
func NewCookieManagerOptions(store Store, o *CookieMngrOptions) Manager {
m := &CookieManager{
store: store,
cookieSecure: !o.AllowHTTP,
sessIDCookieName: o.SessIDCookieName,
cookiePath: o.CookiePath,
}
if m.sessIDCookieName == "" {
m.sessIDCookieName = "sessid"
}
if o.CookieMaxAge == 0 {
m.cookieMaxAgeSec = 30 * 24 * 60 * 60 // 30 days max age
} else {
m.cookieMaxAgeSec = int(o.CookieMaxAge.Seconds())
}
if m.cookiePath == "" {
m.cookiePath = "/"
}
return m
}
// Get is to implement Manager.Get().
func (m *CookieManager) Get(r *http.Request) Session {
c, err := r.Cookie(m.sessIDCookieName)
if err != nil {
return nil
}
return m.store.Get(c.Value)
}
// Add is to implement Manager.Add().
func (m *CookieManager) Add(sess Session, w http.ResponseWriter) {
// HttpOnly: do not allow non-HTTP access to it (like javascript) to prevent stealing it...
// Secure: only send it over HTTPS
// MaxAge: to specify the max age of the cookie in seconds, else it's a session cookie and gets deleted after the browser is closed.
c := http.Cookie{
Name: m.sessIDCookieName,
Value: sess.ID(),
Path: m.cookiePath,
HttpOnly: true,
Secure: m.cookieSecure,
MaxAge: m.cookieMaxAgeSec,
}
http.SetCookie(w, &c)
m.store.Add(sess)
}
// Remove is to implement Manager.Remove().
func (m *CookieManager) Remove(sess Session, w http.ResponseWriter) {
// Set the cookie with empty value and 0 max age
c := http.Cookie{
Name: m.sessIDCookieName,
Value: "",
Path: m.cookiePath,
HttpOnly: true,
Secure: m.cookieSecure,
MaxAge: -1, // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
}
http.SetCookie(w, &c)
m.store.Remove(sess)
}
// Close is to implement Manager.Close().
func (m *CookieManager) Close() {
m.store.Close()
}

117
vendor/github.com/icza/session/doc.go generated vendored Normal file
View File

@ -0,0 +1,117 @@
/*
Package session provides an easy-to-use, extensible and secure HTTP session implementation and management.
This is "just" an HTTP session implementation and management, you can use it as-is, or with any existing Go web toolkits and frameworks.
Package documentation can be found and godoc.org:
https://godoc.org/github.com/icza/session
Overview
There are 3 key players in the package:
- Session is the (HTTP) session interface. We can use it to store and retrieve constant and variable attributes from it.
- Store is a session store interface which is responsible to store sessions and make them retrievable by their IDs at the server side.
- Manager is a session manager interface which is responsible to acquire a Session from an (incoming) HTTP request, and to add a Session to an HTTP response to let the client know about the session. A Manager has a backing Store which is responsible to manage Session values at server side.
Players of this package are represented by interfaces, and various implementations are provided for all these players.
You are not bound by the provided implementations, feel free to provide your own implementations for any of the players.
Usage
Usage can't be simpler than this. To get the current session associated with the http.Request:
sess := session.Get(r)
if sess == nil {
// No session (yet)
} else {
// We have a session, use it
}
To create a new session (e.g. on a successful login) and add it to an http.ResponseWriter (to let the client know about the session):
sess := session.NewSession()
session.Add(sess, w)
Let's see a more advanced session creation: let's provide a constant attribute (for the lifetime of the session) and an initial, variable attribute:
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"UserName": userName},
Attrs: map[string]interface{}{"Count": 1},
})
And to access these attributes and change value of "Count":
userName := sess.CAttr("UserName")
count := sess.Attr("Count").(int) // Type assertion, you might wanna check if it succeeds
sess.SetAttr("Count", count+1) // Increment count
(Of course variable attributes can be added later on too with Session.SetAttr(), not just at session creation.)
To remove a session (e.g. on logout):
session.Remove(sess, w)
Check out the session demo application which shows all these in action:
https://github.com/icza/session/blob/master/session_demo/session_demo.go
Google App Engine support
The package provides support for Google App Engine (GAE) platform.
The documentation doesn't include it (due to the '+build appengine' build constraint), but here it is:
https://github.com/icza/session/blob/master/gae_memcache_store.go
The implementation stores sessions in the Memcache and also saves sessions in the Datastore as a backup
in case data would be removed from the Memcache. This behaviour is optional, Datastore can be disabled completely.
You can also choose whether saving to Datastore happens synchronously (in the same goroutine)
or asynchronously (in another goroutine), resulting in faster response times.
We can use NewMemcacheStore() and NewMemcacheStoreOptions() functions to create a session Store implementation
which stores sessions in GAE's Memcache. Important to note that since accessing the Memcache relies on
Appengine Context which is bound to an http.Request, the returned Store can only be used for the lifetime of a request!
Note that the Store will automatically "flush" sessions accessed from it when the Store is closed,
so it is very important to close the Store at the end of your request; this is usually done by closing
the session manager to which you passed the store (preferably with the defer statement).
So in each request handling we have to create a new session manager using a new Store, and we can use the session manager
to do session-related tasks, something like this:
ctx := appengine.NewContext(r)
sessmgr := session.NewCookieManager(session.NewMemcacheStore(ctx))
defer sessmgr.Close() // This will ensure changes made to the session are auto-saved
// in Memcache (and optionally in the Datastore).
sess := sessmgr.Get(r) // Get current session
if sess != nil {
// Session exists, do something with it.
ctx.Infof("Count: %v", sess.Attr("Count"))
} else {
// No session yet, let's create one and add it:
sess = session.NewSession()
sess.SetAttr("Count", 1)
sessmgr.Add(sess, w)
}
Expired sessions are not automatically removed from the Datastore. To remove expired sessions, the package
provides a PurgeExpiredSessFromDSFunc() function which returns an http.HandlerFunc.
It is recommended to register the returned handler function to a path which then can be defined
as a cron job to be called periodically, e.g. in every 30 minutes or so (your choice).
As cron handlers may run up to 10 minutes, the returned handler will stop at 8 minutes
to complete safely even if there are more expired, undeleted sessions.
It can be registered like this:
http.HandleFunc("/demo/purge", session.PurgeExpiredSessFromDSFunc(""))
Check out the GAE session demo application which shows how it can be used.
cron.yaml file of the demo shows how a cron job can be defined to purge expired sessions.
https://github.com/icza/session/blob/master/gae_session_demo/gae_session_demo.go
*/
package session

375
vendor/github.com/icza/session/gae_memcache_store.go generated vendored Normal file
View File

@ -0,0 +1,375 @@
// +build appengine
/*
A Google App Engine Memcache session store implementation.
The implementation stores sessions in the Memcache and also saves sessions to the Datastore as a backup
in case data would be removed from the Memcache. This behaviour is optional, Datastore can be disabled completely.
You can also choose whether saving to Datastore happens synchronously (in the same goroutine)
or asynchronously (in another goroutine).
Limitations based on GAE Memcache:
- Since session ids are used in the Memcache keys, session ids can't be longer than 250 chars (bytes, but with Base64 charset it's the same).
If you also specify a key prefix (in MemcacheStoreOptions), that also counts into it.
- The size of a Session cannot be larger than 1 MB (marshalled into a byte slice).
Note that the Store will automatically "flush" sessions accessed from it when the Store is closed,
so it is very important to close the Store at the end of your request; this is usually done by closing
the session manager to which you passed the store (preferably with the defer statement).
Check out the GAE session demo application which shows how to use it properly:
https://github.com/icza/session/blob/master/gae_session_demo/session_demo.go
*/
package session
import (
"net/http"
"sync"
"time"
"appengine"
"appengine/datastore"
"appengine/memcache"
)
// A Google App Engine Memcache session store implementation.
type memcacheStore struct {
ctx appengine.Context // Appengine context used when accessing the Memcache
keyPrefix string // Prefix to use in front of session ids to construct Memcache key
retries int // Number of retries to perform in case of general Memcache failures
codec memcache.Codec // Codec used to marshal and unmarshal a Session to a byte slice
onlyMemcache bool // Tells if sessions are not to be saved in Datastore
asyncDatastoreSave bool // Tells if saving in Datastore should happen asynchronously, in a new goroutine
dsEntityName string // Name of the datastore entity to use to save sessions
// Map of sessions (mapped from ID) that were accessed using this store; usually it will only be 1.
// It is also used as a cache, should the user call Get() with the same id multiple times.
sessions map[string]Session
mux *sync.RWMutex // mutex to synchronize access to sessions
}
// MemcacheStoreOptions defines options that may be passed when creating a new Memcache session store.
// All fields are optional; default value will be used for any field that has the zero value.
type MemcacheStoreOptions struct {
// Prefix to use when storing sessions in the Memcache, cannot contain a null byte
// and cannot be longer than 250 chars (bytes) when concatenated with the session id; default value is the empty string
// The Memcache key will be this prefix and the session id concatenated.
KeyPrefix string
// Number of retries to perform if Memcache operations fail due to general service error;
// default value is 3
Retries int
// Codec used to marshal and unmarshal a Session to a byte slice;
// Default value is &memcache.Gob (which uses the gob package).
Codec *memcache.Codec
// Tells if sessions are only to be stored in Memcache, and do not store them in Datastore as backup;
// as Memcache has no guarantees, it may lose content from time to time, but if Datastore is
// also used, the session will automatically be retrieved from the Datastore if not found in Memcache;
// default value is false (which means to also save sessions in the Datastore)
OnlyMemcache bool
// Tells if saving in Datastore should happen asynchronously (in a new goroutine, possibly after returning),
// if false, session saving in Datastore will happen in the same goroutine, before returning from the request.
// Asynchronous saving gives smaller latency (and is enough most of the time as Memcache is always checked first);
// default value is false which means to save sessions in the Datastore in the same goroutine, synchronously
// Not used if OnlyMemcache=true.
// FIXME: See https://github.com/icza/session/issues/3
AsyncDatastoreSave bool
// Name of the entity to use for saving sessions;
// default value is "sess_"
// Not used if OnlyMemcache=true.
DSEntityName string
}
// SessEntity models the session entity saved to Datastore.
// The Key is the session id.
type SessEntity struct {
Expires time.Time `datastore:"exp"`
Value []byte `datastore:"val"`
}
// Pointer to zero value of MemcacheStoreOptions to be reused for efficiency.
var zeroMemcacheStoreOptions = new(MemcacheStoreOptions)
// NewMemcacheStore returns a new, GAE Memcache session Store with default options.
// Default values of options are listed in the MemcacheStoreOptions type.
//
// Important! Since accessing the Memcache relies on Appengine Context
// which is bound to an http.Request, the returned Store can only be used for the lifetime of a request!
func NewMemcacheStore(ctx appengine.Context) Store {
return NewMemcacheStoreOptions(ctx, zeroMemcacheStoreOptions)
}
const defaultDSEntityName = "sess_" // Default value of DSEntityName.
// NewMemcacheStoreOptions returns a new, GAE Memcache session Store with the specified options.
//
// Important! Since accessing the Memcache relies on Appengine Context
// which is bound to an http.Request, the returned Store can only be used for the lifetime of a request!
func NewMemcacheStoreOptions(ctx appengine.Context, o *MemcacheStoreOptions) Store {
s := &memcacheStore{
ctx: ctx,
keyPrefix: o.KeyPrefix,
retries: o.Retries,
onlyMemcache: o.OnlyMemcache,
asyncDatastoreSave: o.AsyncDatastoreSave,
dsEntityName: o.DSEntityName,
sessions: make(map[string]Session, 2),
mux: &sync.RWMutex{},
}
if s.retries <= 0 {
s.retries = 3
}
if o.Codec != nil {
s.codec = *o.Codec
} else {
s.codec = memcache.Gob
}
if s.dsEntityName == "" {
s.dsEntityName = defaultDSEntityName
}
return s
}
// Get is to implement Store.Get().
// Important! Since sessions are marshalled and stored in the Memcache,
// the mutex of the Session (Session.RWMutex()) will be different for each
// Session value (even though they might have the same session id)!
func (s *memcacheStore) Get(id string) Session {
s.mux.RLock()
defer s.mux.RUnlock()
// First check our "cache"
if sess := s.sessions[id]; sess != nil {
return sess
}
// Next check in Memcache
var err error
var sess *sessionImpl
for i := 0; i < s.retries; i++ {
var sess_ sessionImpl
_, err = s.codec.Get(s.ctx, s.keyPrefix+id, &sess_)
if err == memcache.ErrCacheMiss {
break // It's not in the Memcache (e.g. invalid sess id or was removed from Memcache by AppEngine)
}
if err == nil {
sess = &sess_
break
}
// Service error? Retry..
}
if sess == nil {
if err != nil && err != memcache.ErrCacheMiss {
s.ctx.Errorf("Failed to get session from memcache, id: %s, error: %v", id, err)
}
// Ok, we didn't get it from Memcace (either was not there or Memcache service is unavailable).
// Now it's time to check in the Datastore.
key := datastore.NewKey(s.ctx, s.dsEntityName, id, 0, nil)
for i := 0; i < s.retries; i++ {
e := SessEntity{}
err = datastore.Get(s.ctx, key, &e)
if err == datastore.ErrNoSuchEntity {
return nil // It's not in the Datastore either
}
if err != nil {
// Service error? Retry..
continue
}
if e.Expires.Before(time.Now()) {
// Session expired.
datastore.Delete(s.ctx, key) // Omitting error check...
return nil
}
var sess_ sessionImpl
if err = s.codec.Unmarshal(e.Value, &sess_); err != nil {
break // Invalid data in stored session entity...
}
sess = &sess_
break
}
}
if sess == nil {
s.ctx.Errorf("Failed to get session from datastore, id: %s, error: %v", id, err)
return nil
}
// Yes! We have it! "Actualize" it.
sess.Access()
// Mutex is not marshalled, so create a new one:
sess.mux = &sync.RWMutex{}
s.sessions[id] = sess
return sess
}
// Add is to implement Store.Add().
func (s *memcacheStore) Add(sess Session) {
s.mux.Lock()
defer s.mux.Unlock()
if s.setMemcacheSession(sess) {
s.ctx.Infof("Session added: %s", sess.ID())
s.sessions[sess.ID()] = sess
return
}
}
// setMemcacheSession sets the specified session in the Memcache.
func (s *memcacheStore) setMemcacheSession(sess Session) (success bool) {
item := &memcache.Item{
Key: s.keyPrefix + sess.ID(),
Object: sess,
Expiration: sess.Timeout(),
}
var err error
for i := 0; i < s.retries; i++ {
if err = s.codec.Set(s.ctx, item); err == nil {
return true
}
}
s.ctx.Errorf("Failed to add session to memcache, id: %s, error: %v", sess.ID(), err)
return false
}
// Remove is to implement Store.Remove().
func (s *memcacheStore) Remove(sess Session) {
s.mux.Lock()
defer s.mux.Unlock()
var err error
for i := 0; i < s.retries; i++ {
if err = memcache.Delete(s.ctx, s.keyPrefix+sess.ID()); err == nil || err == memcache.ErrCacheMiss {
s.ctx.Infof("Session removed: %s", sess.ID())
delete(s.sessions, sess.ID())
if !s.onlyMemcache {
// Also from the Datastore:
key := datastore.NewKey(s.ctx, s.dsEntityName, sess.ID(), 0, nil)
datastore.Delete(s.ctx, key) // Omitting error check...
}
return
}
}
s.ctx.Errorf("Failed to remove session from memcache, id: %s, error: %v", sess.ID(), err)
}
// Close is to implement Store.Close().
func (s *memcacheStore) Close() {
// Flush out sessions that were accessed from this store. No need locking, we're closing...
// We could use Cocec.SetMulti(), but sessions will contain at most 1 session like all the times.
for _, sess := range s.sessions {
s.setMemcacheSession(sess)
}
if s.onlyMemcache {
return // Don't save to Datastore
}
if s.asyncDatastoreSave {
go s.saveToDatastore()
} else {
s.saveToDatastore()
}
}
// saveToDatastore saves the sessions of the Store to the Datastore
// in the caller's goroutine.
func (s *memcacheStore) saveToDatastore() {
// Save sessions that were accessed from this store. No need locking, we're closing...
// We could use datastore.PutMulti(), but sessions will contain at most 1 session like all the times.
for _, sess := range s.sessions {
value, err := s.codec.Marshal(sess)
if err != nil {
s.ctx.Errorf("Failed to marshal session: %s, error: %v", sess.ID(), err)
continue
}
e := SessEntity{
Expires: sess.Accessed().Add(sess.Timeout()),
Value: value,
}
key := datastore.NewKey(s.ctx, s.dsEntityName, sess.ID(), 0, nil)
for i := 0; i < s.retries; i++ {
if _, err = datastore.Put(s.ctx, key, &e); err == nil {
break
}
}
if err != nil {
s.ctx.Errorf("Failed to save session to datastore: %s, error: %v", sess.ID(), err)
}
}
}
// PurgeExpiredSessFromDSFunc returns a request handler function which deletes expired sessions
// from the Datastore.
// dsEntityName is the name of the entity used for saving sessions; pass an empty string
// to use the default value (which is "sess_").
//
// It is recommended to register the returned handler function to a path which then can be defined
// as a cron job to be called periodically, e.g. in every 30 minutes or so (your choice).
// As cron handlers may run up to 10 minutes, the returned handler will stop at 8 minutes
// to complete safely even if there are more expired, undeleted sessions.
//
// The response of the handler func is a JSON text telling if the handler was able to delete all expired sessions,
// or that it was finished early due to the time. Examle of a respone where all expired sessions were deleted:
//
// {"completed":true}
func PurgeExpiredSessFromDSFunc(dsEntityName string) http.HandlerFunc {
if dsEntityName == "" {
dsEntityName = defaultDSEntityName
}
return func(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
// Delete in batches of 100
q := datastore.NewQuery(dsEntityName).Filter("exp<", time.Now()).KeysOnly().Limit(100)
deadline := time.Now().Add(time.Minute * 8)
for {
var err error
var keys []*datastore.Key
if keys, err = q.GetAll(c, nil); err != nil {
// Datastore error.
c.Errorf("Failed to query expired sessions: %v", err)
http.Error(w, "Failed to query expired sessions!", http.StatusInternalServerError)
}
if len(keys) == 0 {
// We're done, no more expired sessions
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"completed":true}`))
return
}
if err = datastore.DeleteMulti(c, keys); err != nil {
c.Errorf("Error while deleting expired sessions: %v", err)
}
if time.Now().After(deadline) {
// Our time is up, return
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"completed":false}`))
return
}
// We have time to continue
}
}
}

39
vendor/github.com/icza/session/global.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
/*
A global session Manager and delegator functions - for easy to use.
*/
package session
import (
"net/http"
)
// Global is the default session Manager to which the top-level functions such as Get, Add, Remove and Close
// are wrappers of Manager.
// You may replace this and keep using the top-level functions, but if you intend to do so,
// you should close it first with Global.Close().
var Global = NewCookieManager(NewInMemStore())
// Get delegates to Global.Get(); returns the session specified by the HTTP request.
// nil is returned if the request does not contain a session, or the contained session is not know by this manager.
func Get(r *http.Request) Session {
return Global.Get(r)
}
// Add delegates to Global.Add(); adds the session to the HTTP response.
// This means to let the client know about the specified session by including the sesison id in the response somehow.
func Add(sess Session, w http.ResponseWriter) {
Global.Add(sess, w)
}
// Remove delegates to Global.Remove(); removes the session from the HTTP response.
func Remove(sess Session, w http.ResponseWriter) {
Global.Remove(sess, w)
}
// Close delegates to Global.Close(); closes the session manager, releasing any resources that were allocated.
func Close() {
Global.Close()
}

145
vendor/github.com/icza/session/inmem_store.go generated vendored Normal file
View File

@ -0,0 +1,145 @@
/*
An in-memory session store implementation.
*/
package session
import (
"log"
"sync"
"time"
)
// In-memory session Store implementation.
type inMemStore struct {
sessions map[string]Session // Map of sessions (mapped from ID)
mux *sync.RWMutex // mutex to synchronize access to sessions
ticker *time.Ticker // Ticker for the session cleaner
closeTicker chan struct{} // Channel to signal close for the session cleaner
}
// InMemStoreOptions defines options that may be passed when creating a new in-memory Store.
// All fields are optional; default value will be used for any field that has the zero value.
type InMemStoreOptions struct {
// Session cleaner check interval, default is 10 seconds.
SessCleanerInterval time.Duration
}
// Pointer to zero value of InMemStoreOptions to be reused for efficiency.
var zeroInMemStoreOptions = new(InMemStoreOptions)
// NewInMemStore returns a new, in-memory session Store with the default options.
// Default values of options are listed in the InMemStoreOptions type.
// The returned Store has an automatic session cleaner which runs
// in its own goroutine.
func NewInMemStore() Store {
return NewInMemStoreOptions(zeroInMemStoreOptions)
}
// NewInMemStoreOptions returns a new, in-memory session Store with the specified options.
// The returned Store has an automatic session cleaner which runs
// in its own goroutine.
func NewInMemStoreOptions(o *InMemStoreOptions) Store {
s := &inMemStore{
sessions: make(map[string]Session),
mux: &sync.RWMutex{},
closeTicker: make(chan struct{}),
}
interval := o.SessCleanerInterval
if interval == 0 {
interval = 10 * time.Second
}
go s.sessCleaner(interval)
return s
}
// sessCleaner periodically checks whether sessions have timed out
// in an endless loop. If a session has timed out, removes it.
// This method is to be started as a new goroutine.
func (s *inMemStore) sessCleaner(interval time.Duration) {
ticker := time.NewTicker(interval)
for {
select {
case <-s.closeTicker:
// We are being shut down...
ticker.Stop()
return
case now := <-ticker.C:
// Do a sweep.
// Remove is very rare compared to the number of checks, so:
// "Quick" check with read-lock to see if there's anything to remove:
// Note: Session.Access() is called with s.mux, the same mutex we use
// when looking for timed-out sessions, so we're good.
needRemove := func() bool {
s.mux.RLock() // Read lock is enough
defer s.mux.RUnlock()
for _, sess := range s.sessions {
if now.Sub(sess.Accessed()) > sess.Timeout() {
return true
}
}
return false
}()
if !needRemove {
continue
}
// Remove required:
func() {
s.mux.Lock() // Read-write lock required
defer s.mux.Unlock()
for _, sess := range s.sessions {
if now.Sub(sess.Accessed()) > sess.Timeout() {
log.Println("Session timed out:", sess.ID())
delete(s.sessions, sess.ID())
}
}
}()
}
}
}
// Get is to implement Store.Get().
func (s *inMemStore) Get(id string) Session {
s.mux.RLock()
defer s.mux.RUnlock()
sess := s.sessions[id]
if sess == nil {
return nil
}
sess.Access()
return sess
}
// Add is to implement Store.Add().
func (s *inMemStore) Add(sess Session) {
s.mux.Lock()
defer s.mux.Unlock()
log.Println("Session added:", sess.ID())
s.sessions[sess.ID()] = sess
}
// Remove is to implement Store.Remove().
func (s *inMemStore) Remove(sess Session) {
s.mux.Lock()
defer s.mux.Unlock()
log.Println("Session removed:", sess.ID())
delete(s.sessions, sess.ID())
}
// Close is to implement Store.Close().
func (s *inMemStore) Close() {
close(s.closeTicker)
}

31
vendor/github.com/icza/session/manager.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
/*
Session Manager interface.
*/
package session
import (
"net/http"
)
// Manager is a session manager interface.
// A session manager is responsible to acquire a Session from an (incoming) HTTP request,
// and to add a Session to an HTTP response to let the client know about the session.
// A Manager has a backing Store which is responsible to manage Session values at server side.
type Manager interface {
// Get returns the session specified by the HTTP request.
// nil is returned if the request does not contain a session, or the contained session is not know by this manager.
Get(r *http.Request) Session
// Add adds the session to the HTTP response.
// This means to let the client know about the specified session by including the sesison id in the response somehow.
Add(sess Session, w http.ResponseWriter)
// Remove removes the session from the HTTP response.
Remove(sess Session, w http.ResponseWriter)
// Close closes the session manager, releasing any resources that were allocated.
Close()
}

231
vendor/github.com/icza/session/session.go generated vendored Normal file
View File

@ -0,0 +1,231 @@
/*
Session interface and its implementation.
*/
package session
import (
"crypto/rand"
"encoding/base64"
"io"
"sync"
"time"
)
// Session is the (HTTP) session interface.
// We can use it to store and retrieve constant and variable attributes from it.
type Session interface {
// ID returns the id of the session.
ID() string
// New tells if the session is new.
// Implementation is based on whether created and access times are equal.
New() bool
// CAttr returns the value of an attribute provided at session creation.
// These attributes cannot be changes during the lifetime of a session,
// so they can be accessed safely without synchronization. Exampe is storing the
// authenticated user.
CAttr(name string) interface{}
// Attr returns the value of an attribute stored in the session.
// Safe for concurrent use.
Attr(name string) interface{}
// SetAttr sets the value of an attribute stored in the session.
// Pass the nil value to delete the attribute.
// Safe for concurrent use.
SetAttr(name string, value interface{})
// Attrs returns a copy of all the attribute values stored in the session.
// Safe for concurrent use.
Attrs() map[string]interface{}
// Created returns the session creation time.
Created() time.Time
// Accessed returns the time when the session was last accessed.
Accessed() time.Time
// Timeout returns the session timeout.
// A session may be removed automatically if it is not accessed for this duration.
Timeout() time.Duration
// Mutex returns the RW mutex of the session.
// It is used to synchronize access/modification of the state stored in the session.
// It can be used if session-level synchronization is required.
// Important! If Session values are marshalled / unmarshalled
// (e.g. multi server instance environment such as Google AppEngine),
// this mutex may be different for each Session value and thus
// it can only be used to session-value level synchronization!
Mutex() *sync.RWMutex
// Access registers an access to the session,
// updates its last accessed time to the current time.
// Users do not need to call this as the session store is responsible for that.
Access()
}
// Session implementation.
// Fields are exported so a session may be marshalled / unmarshalled.
type sessionImpl struct {
IDF string // ID of the session
CreatedF time.Time // Creation time
AccessedF time.Time // Last accessed time
CAttrsF map[string]interface{} // Constant attributes specified at session creation
AttrsF map[string]interface{} // Attributes stored in the session
TimeoutF time.Duration // Session timeout
mux *sync.RWMutex // RW mutex to synchronize session state access
}
// SessOptions defines options that may be passed when creating a new Session.
// All fields are optional; default value will be used for any field that has the zero value.
type SessOptions struct {
// Constant attributes of the session. These be will available via the Session.CAttr() method, without synchronization.
// Values from the map will be copied, and will be available via Session.CAttr().
CAttrs map[string]interface{}
// Initial, non-constant attributes to be stored in the session.
// Values from the map will be copied, and will be available via Session.Attr() and Session.Attrs,
// and may be changed with Session.SetAttr().
Attrs map[string]interface{}
// Session timeout, default is 30 minutes.
Timeout time.Duration
// Byte-length of the information that builds up the session ids.
// Using Base-64 encoding, id length will be this multiplied by 4/3 chars.
// Default value is 18 (which means length of ID will be 24 chars).
IDLength int
}
// Pointer to zero value of SessOptions to be reused for efficiency.
var zeroSessOptions = new(SessOptions)
// NewSession creates a new Session with the default options.
// Default values of options are listed in the SessOptions type.
func NewSession() Session {
return NewSessionOptions(zeroSessOptions)
}
// NewSessionOptions creates a new Session with the specified options.
func NewSessionOptions(o *SessOptions) Session {
now := time.Now()
idLength := o.IDLength
if idLength <= 0 {
idLength = 18
}
timeout := o.Timeout
if timeout == 0 {
timeout = 30 * time.Minute
}
sess := sessionImpl{
IDF: genID(idLength),
CreatedF: now,
AccessedF: now,
AttrsF: make(map[string]interface{}),
TimeoutF: timeout,
mux: &sync.RWMutex{},
}
if len(o.CAttrs) > 0 {
sess.CAttrsF = make(map[string]interface{}, len(o.CAttrs))
for k, v := range o.CAttrs {
sess.CAttrsF[k] = v
}
}
for k, v := range o.Attrs {
sess.AttrsF[k] = v
}
return &sess
}
// genID generates a secure, random session id using the crypto/rand package.
func genID(length int) string {
r := make([]byte, length)
io.ReadFull(rand.Reader, r)
return base64.URLEncoding.EncodeToString(r)
}
// ID is to implement Session.ID().
func (s *sessionImpl) ID() string {
return s.IDF
}
// New is to implement Session.New().
func (s *sessionImpl) New() bool {
return s.CreatedF == s.AccessedF
}
// CAttr is to implement Session.CAttr().
func (s *sessionImpl) CAttr(name string) interface{} {
return s.CAttrsF[name]
}
// Attr is to implement Session.Attr().
func (s *sessionImpl) Attr(name string) interface{} {
s.mux.RLock()
defer s.mux.RUnlock()
return s.AttrsF[name]
}
// SetAttr is to implement Session.SetAttr().
func (s *sessionImpl) SetAttr(name string, value interface{}) {
s.mux.Lock()
defer s.mux.Unlock()
if value == nil {
delete(s.AttrsF, name)
} else {
s.AttrsF[name] = value
}
}
// Attrs is to implement Session.Attrs().
func (s *sessionImpl) Attrs() map[string]interface{} {
s.mux.RLock()
defer s.mux.RUnlock()
m := make(map[string]interface{}, len(s.AttrsF))
for k, v := range s.AttrsF {
m[k] = v
}
return m
}
// Created is to implement Session.Created().
func (s *sessionImpl) Created() time.Time {
return s.CreatedF
}
// Accessed is to implement Session.Accessed().
func (s *sessionImpl) Accessed() time.Time {
s.mux.RLock()
defer s.mux.RUnlock()
return s.AccessedF
}
// Timeout is to implement Session.Timeout().
func (s *sessionImpl) Timeout() time.Duration {
return s.TimeoutF
}
// Mutex is to implement Session.Mutex().
func (s *sessionImpl) Mutex() *sync.RWMutex {
return s.mux
}
// Access is to implement Session.Access().
func (s *sessionImpl) Access() {
s.mux.Lock()
defer s.mux.Unlock()
s.AccessedF = time.Now()
}

25
vendor/github.com/icza/session/store.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
/*
Session Store interface.
*/
package session
// Store is a session store interface.
// A session store is responsible to store sessions and make them retrievable by their IDs at the server side.
type Store interface {
// Get returns the session specified by its id.
// The returned session will have an updated access time (set to the current time).
// nil is returned if this store does not contain a session with the specified id.
Get(id string) Session
// Add adds a new session to the store.
Add(sess Session)
// Remove removes a session from the store.
Remove(sess Session)
// Close closes the session store, releasing any resources that were allocated.
Close()
}

20
vendor/github.com/justinas/nosurf/LICENSE generated vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Justinas Stankevicius
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

125
vendor/github.com/justinas/nosurf/README.md generated vendored Normal file
View File

@ -0,0 +1,125 @@
# nosurf
[![Build Status](https://travis-ci.org/justinas/nosurf.svg?branch=master)](https://travis-ci.org/justinas/nosurf)
[![GoDoc](http://godoc.org/github.com/justinas/nosurf?status.png)](http://godoc.org/github.com/justinas/nosurf)
`nosurf` is an HTTP package for Go
that helps you prevent Cross-Site Request Forgery attacks.
It acts like a middleware and therefore
is compatible with basically any Go HTTP application.
### Why?
Even though CSRF is a prominent vulnerability,
Go's web-related package infrastructure mostly consists of
micro-frameworks that neither do implement CSRF checks,
nor should they.
`nosurf` solves this problem by providing a `CSRFHandler`
that wraps your `http.Handler` and checks for CSRF attacks
on every non-safe (non-GET/HEAD/OPTIONS/TRACE) method.
`nosurf` requires Go 1.1 or later.
### Features
* Supports any `http.Handler` (frameworks, your own handlers, etc.)
and acts like one itself.
* Allows exempting specific endpoints from CSRF checks by
an exact URL, a glob, or a regular expression.
* Allows specifying your own failure handler.
Want to present the hacker with an ASCII middle finger
instead of the plain old `HTTP 400`? No problem.
* Uses masked tokens to mitigate the BREACH attack.
* Has no dependencies outside the Go standard library.
### Example
```go
package main
import (
"fmt"
"github.com/justinas/nosurf"
"html/template"
"net/http"
)
var templateString string = `
<!doctype html>
<html>
<body>
{{ if .name }}
<p>Your name: {{ .name }}</p>
{{ end }}
<form action="/" method="POST">
<input type="text" name="name">
<!-- Try removing this or changing its value
and see what happens -->
<input type="hidden" name="csrf_token" value="{{ .token }}">
<input type="submit" value="Send">
</form>
</body>
</html>
`
var templ = template.Must(template.New("t1").Parse(templateString))
func myFunc(w http.ResponseWriter, r *http.Request) {
context := make(map[string]string)
context["token"] = nosurf.Token(r)
if r.Method == "POST" {
context["name"] = r.FormValue("name")
}
templ.Execute(w, context)
}
func main() {
myHandler := http.HandlerFunc(myFunc)
fmt.Println("Listening on http://127.0.0.1:8000/")
http.ListenAndServe(":8000", nosurf.New(myHandler))
}
```
### Manual token verification
In some cases the CSRF token may be send through a non standard way,
e.g. a body or request is a JSON encoded message with one of the fields
being a token.
In such case the handler(path) should be excluded from an automatic
verification by using one of the exemption methods:
```go
func (h *CSRFHandler) ExemptFunc(fn func(r *http.Request) bool)
func (h *CSRFHandler) ExemptGlob(pattern string)
func (h *CSRFHandler) ExemptGlobs(patterns ...string)
func (h *CSRFHandler) ExemptPath(path string)
func (h *CSRFHandler) ExemptPaths(paths ...string)
func (h *CSRFHandler) ExemptRegexp(re interface{})
func (h *CSRFHandler) ExemptRegexps(res ...interface{})
```
Later on, the token **must** be verified by manually getting the token from the cookie
and providing the token sent in body through: `VerifyToken(tkn, tkn2 string) bool`.
Example:
```go
func HandleJson(w http.ResponseWriter, r *http.Request) {
d := struct{
X,Y int
Tkn string
}{}
json.Unmarshal(ioutil.ReadAll(r.Body), &d)
if !nosurf.VerifyToken(Token(r), d.Tkn) {
http.Errorf(w, "CSRF token incorrect", http.StatusBadRequest)
return
}
// do smth cool
}
```
### Contributing
0. Find an issue that bugs you / open a new one.
1. Discuss.
2. Branch off, commit, test.
3. Make a pull request / attach the commits to the issue.

60
vendor/github.com/justinas/nosurf/context.go generated vendored Normal file
View File

@ -0,0 +1,60 @@
// +build go1.7
package nosurf
import "net/http"
type ctxKey int
const (
nosurfKey ctxKey = iota
)
type csrfContext struct {
// The masked, base64 encoded token
// That's suitable for use in form fields, etc.
token string
// reason for the failure of CSRF check
reason error
}
// Token takes an HTTP request and returns
// the CSRF token for that request
// or an empty string if the token does not exist.
//
// Note that the token won't be available after
// CSRFHandler finishes
// (that is, in another handler that wraps it,
// or after the request has been served)
func Token(req *http.Request) string {
ctx := req.Context().Value(nosurfKey).(*csrfContext)
return ctx.token
}
// Reason takes an HTTP request and returns
// the reason of failure of the CSRF check for that request
//
// Note that the same availability restrictions apply for Reason() as for Token().
func Reason(req *http.Request) error {
ctx := req.Context().Value(nosurfKey).(*csrfContext)
return ctx.reason
}
func ctxClear(_ *http.Request) {
}
func ctxSetToken(req *http.Request, token []byte) {
ctx := req.Context().Value(nosurfKey).(*csrfContext)
ctx.token = b64encode(maskToken(token))
}
func ctxSetReason(req *http.Request, reason error) {
ctx := req.Context().Value(nosurfKey).(*csrfContext)
if ctx.token == "" {
panic("Reason should never be set when there's no token in the context yet.")
}
ctx.reason = reason
}

101
vendor/github.com/justinas/nosurf/context_legacy.go generated vendored Normal file
View File

@ -0,0 +1,101 @@
// +build !go1.7
package nosurf
import (
"net/http"
"sync"
)
// This file implements a context similar to one found
// in gorilla/context, but tailored specifically for our use case
// and not using gorilla's package just because.
type csrfContext struct {
// The masked, base64 encoded token
// That's suitable for use in form fields, etc.
token string
// reason for the failure of CSRF check
reason error
}
var (
contextMap = make(map[*http.Request]*csrfContext)
cmMutex = new(sync.RWMutex)
)
// Token() takes an HTTP request and returns
// the CSRF token for that request
// or an empty string if the token does not exist.
//
// Note that the token won't be available after
// CSRFHandler finishes
// (that is, in another handler that wraps it,
// or after the request has been served)
func Token(req *http.Request) string {
cmMutex.RLock()
defer cmMutex.RUnlock()
ctx, ok := contextMap[req]
if !ok {
return ""
}
return ctx.token
}
// Reason() takes an HTTP request and returns
// the reason of failure of the CSRF check for that request
//
// Note that the same availability restrictions apply for Reason() as for Token().
func Reason(req *http.Request) error {
cmMutex.RLock()
defer cmMutex.RUnlock()
ctx, ok := contextMap[req]
if !ok {
return nil
}
return ctx.reason
}
// Takes a raw token, masks it with a per-request key,
// encodes in base64 and makes it available to the wrapped handler
func ctxSetToken(req *http.Request, token []byte) *http.Request {
cmMutex.Lock()
defer cmMutex.Unlock()
ctx, ok := contextMap[req]
if !ok {
ctx = new(csrfContext)
contextMap[req] = ctx
}
ctx.token = b64encode(maskToken(token))
return req
}
func ctxSetReason(req *http.Request, reason error) *http.Request {
cmMutex.Lock()
defer cmMutex.Unlock()
ctx, ok := contextMap[req]
if !ok {
panic("Reason should never be set when there's no token" +
" (context) yet.")
}
ctx.reason = reason
return req
}
func ctxClear(req *http.Request) {
cmMutex.Lock()
defer cmMutex.Unlock()
delete(contextMap, req)
}

54
vendor/github.com/justinas/nosurf/crypto.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
package nosurf
import (
"crypto/rand"
"io"
)
// Masks/unmasks the given data *in place*
// with the given key
// Slices must be of the same length, or oneTimePad will panic
func oneTimePad(data, key []byte) {
n := len(data)
if n != len(key) {
panic("Lengths of slices are not equal")
}
for i := 0; i < n; i++ {
data[i] ^= key[i]
}
}
func maskToken(data []byte) []byte {
if len(data) != tokenLength {
return nil
}
// tokenLength*2 == len(enckey + token)
result := make([]byte, 2*tokenLength)
// the first half of the result is the OTP
// the second half is the masked token itself
key := result[:tokenLength]
token := result[tokenLength:]
copy(token, data)
// generate the random token
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err)
}
oneTimePad(token, key)
return result
}
func unmaskToken(data []byte) []byte {
if len(data) != tokenLength*2 {
return nil
}
key := data[:tokenLength]
token := data[tokenLength:]
oneTimePad(token, key)
return token
}

108
vendor/github.com/justinas/nosurf/exempt.go generated vendored Normal file
View File

@ -0,0 +1,108 @@
package nosurf
import (
"fmt"
"net/http"
pathModule "path"
"reflect"
"regexp"
)
// Checks if the given request is exempt from CSRF checks.
// It checks the ExemptFunc first, then the exact paths,
// then the globs and finally the regexps.
func (h *CSRFHandler) IsExempt(r *http.Request) bool {
if h.exemptFunc != nil && h.exemptFunc(r) {
return true
}
path := r.URL.Path
if sContains(h.exemptPaths, path) {
return true
}
// then the globs
for _, glob := range h.exemptGlobs {
matched, err := pathModule.Match(glob, path)
if matched && err == nil {
return true
}
}
// finally, the regexps
for _, re := range h.exemptRegexps {
if re.MatchString(path) {
return true
}
}
return false
}
// Exempts an exact path from CSRF checks
// With this (and other Exempt* methods)
// you should take note that Go's paths
// include a leading slash.
func (h *CSRFHandler) ExemptPath(path string) {
h.exemptPaths = append(h.exemptPaths, path)
}
// A variadic argument version of ExemptPath()
func (h *CSRFHandler) ExemptPaths(paths ...string) {
for _, v := range paths {
h.ExemptPath(v)
}
}
// Exempts URLs that match the specified glob pattern
// (as used by filepath.Match()) from CSRF checks
//
// Note that ExemptGlob() is unable to detect syntax errors,
// because it doesn't have a path to check it against
// and filepath.Match() doesn't report an error
// if the path is empty.
// If we find a way to check the syntax, ExemptGlob
// MIGHT PANIC on a syntax error in the future.
// ALWAYS check your globs for syntax errors.
func (h *CSRFHandler) ExemptGlob(pattern string) {
h.exemptGlobs = append(h.exemptGlobs, pattern)
}
// A variadic argument version of ExemptGlob()
func (h *CSRFHandler) ExemptGlobs(patterns ...string) {
for _, v := range patterns {
h.ExemptGlob(v)
}
}
// Accepts a regular expression string or a compiled *regexp.Regexp
// and exempts URLs that match it from CSRF checks.
//
// If the given argument is neither of the accepted values,
// or the given string fails to compile, ExemptRegexp() panics.
func (h *CSRFHandler) ExemptRegexp(re interface{}) {
var compiled *regexp.Regexp
switch re.(type) {
case string:
compiled = regexp.MustCompile(re.(string))
case *regexp.Regexp:
compiled = re.(*regexp.Regexp)
default:
err := fmt.Sprintf("%v isn't a valid type for ExemptRegexp()", reflect.TypeOf(re))
panic(err)
}
h.exemptRegexps = append(h.exemptRegexps, compiled)
}
// A variadic argument version of ExemptRegexp()
func (h *CSRFHandler) ExemptRegexps(res ...interface{}) {
for _, v := range res {
h.ExemptRegexp(v)
}
}
func (h *CSRFHandler) ExemptFunc(fn func(r *http.Request) bool) {
h.exemptFunc = fn
}

220
vendor/github.com/justinas/nosurf/handler.go generated vendored Normal file
View File

@ -0,0 +1,220 @@
// Package nosurf implements an HTTP handler that
// mitigates Cross-Site Request Forgery Attacks.
package nosurf
import (
"errors"
"net/http"
"net/url"
"regexp"
)
const (
// the name of CSRF cookie
CookieName = "csrf_token"
// the name of the form field
FormFieldName = "csrf_token"
// the name of CSRF header
HeaderName = "X-CSRF-Token"
// the HTTP status code for the default failure handler
FailureCode = 400
// Max-Age in seconds for the default base cookie. 365 days.
MaxAge = 365 * 24 * 60 * 60
)
var safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
// reasons for CSRF check failures
var (
ErrNoReferer = errors.New("A secure request contained no Referer or its value was malformed")
ErrBadReferer = errors.New("A secure request's Referer comes from a different Origin" +
" from the request's URL")
ErrBadToken = errors.New("The CSRF token in the cookie doesn't match the one" +
" received in a form/header.")
)
type CSRFHandler struct {
// Handlers that CSRFHandler wraps.
successHandler http.Handler
failureHandler http.Handler
// The base cookie that CSRF cookies will be built upon.
// This should be a better solution of customizing the options
// than a bunch of methods SetCookieExpiration(), etc.
baseCookie http.Cookie
// Slices of paths that are exempt from CSRF checks.
// They can be specified by...
// ...an exact path,
exemptPaths []string
// ...a regexp,
exemptRegexps []*regexp.Regexp
// ...or a glob (as used by path.Match()).
exemptGlobs []string
// ...or a custom matcher function
exemptFunc func(r *http.Request) bool
// All of those will be matched against Request.URL.Path,
// So they should take the leading slash into account
}
func defaultFailureHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", FailureCode)
}
// Extracts the "sent" token from the request
// and returns an unmasked version of it
func extractToken(r *http.Request) []byte {
var sentToken string
// Prefer the header over form value
sentToken = r.Header.Get(HeaderName)
// Then POST values
if len(sentToken) == 0 {
sentToken = r.PostFormValue(FormFieldName)
}
// If all else fails, try a multipart value.
// PostFormValue() will already have called ParseMultipartForm()
if len(sentToken) == 0 && r.MultipartForm != nil {
vals := r.MultipartForm.Value[FormFieldName]
if len(vals) != 0 {
sentToken = vals[0]
}
}
return b64decode(sentToken)
}
// Constructs a new CSRFHandler that calls
// the specified handler if the CSRF check succeeds.
func New(handler http.Handler) *CSRFHandler {
baseCookie := http.Cookie{}
baseCookie.MaxAge = MaxAge
csrf := &CSRFHandler{successHandler: handler,
failureHandler: http.HandlerFunc(defaultFailureHandler),
baseCookie: baseCookie,
}
return csrf
}
// The same as New(), but has an interface return type.
func NewPure(handler http.Handler) http.Handler {
return New(handler)
}
func (h *CSRFHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = addNosurfContext(r)
defer ctxClear(r)
w.Header().Add("Vary", "Cookie")
var realToken []byte
tokenCookie, err := r.Cookie(CookieName)
if err == nil {
realToken = b64decode(tokenCookie.Value)
}
// If the length of the real token isn't what it should be,
// it has either been tampered with,
// or we're migrating onto a new algorithm for generating tokens,
// or it hasn't ever been set so far.
// In any case of those, we should regenerate it.
//
// As a consequence, CSRF check will fail when comparing the tokens later on,
// so we don't have to fail it just yet.
if len(realToken) != tokenLength {
h.RegenerateToken(w, r)
} else {
ctxSetToken(r, realToken)
}
if sContains(safeMethods, r.Method) || h.IsExempt(r) {
// short-circuit with a success for safe methods
h.handleSuccess(w, r)
return
}
// if the request is secure, we enforce origin check
// for referer to prevent MITM of http->https requests
if r.URL.Scheme == "https" {
referer, err := url.Parse(r.Header.Get("Referer"))
// if we can't parse the referer or it's empty,
// we assume it's not specified
if err != nil || referer.String() == "" {
ctxSetReason(r, ErrNoReferer)
h.handleFailure(w, r)
return
}
// if the referer doesn't share origin with the request URL,
// we have another error for that
if !sameOrigin(referer, r.URL) {
ctxSetReason(r, ErrBadReferer)
h.handleFailure(w, r)
return
}
}
// Finally, we check the token itself.
sentToken := extractToken(r)
if !verifyToken(realToken, sentToken) {
ctxSetReason(r, ErrBadToken)
h.handleFailure(w, r)
return
}
// Everything else passed, handle the success.
h.handleSuccess(w, r)
}
// handleSuccess simply calls the successHandler.
// Everything else, like setting a token in the context
// is taken care of by h.ServeHTTP()
func (h *CSRFHandler) handleSuccess(w http.ResponseWriter, r *http.Request) {
h.successHandler.ServeHTTP(w, r)
}
// Same applies here: h.ServeHTTP() sets the failure reason, the token,
// and only then calls handleFailure()
func (h *CSRFHandler) handleFailure(w http.ResponseWriter, r *http.Request) {
h.failureHandler.ServeHTTP(w, r)
}
// Generates a new token, sets it on the given request and returns it
func (h *CSRFHandler) RegenerateToken(w http.ResponseWriter, r *http.Request) string {
token := generateToken()
h.setTokenCookie(w, r, token)
return Token(r)
}
func (h *CSRFHandler) setTokenCookie(w http.ResponseWriter, r *http.Request, token []byte) {
// ctxSetToken() does the masking for us
ctxSetToken(r, token)
cookie := h.baseCookie
cookie.Name = CookieName
cookie.Value = b64encode(token)
http.SetCookie(w, &cookie)
}
// Sets the handler to call in case the CSRF check
// fails. By default it's defaultFailureHandler.
func (h *CSRFHandler) SetFailureHandler(handler http.Handler) {
h.failureHandler = handler
}
// Sets the base cookie to use when building a CSRF token cookie
// This way you can specify the Domain, Path, HttpOnly, Secure, etc.
func (h *CSRFHandler) SetBaseCookie(cookie http.Cookie) {
h.baseCookie = cookie
}

12
vendor/github.com/justinas/nosurf/handler_go17.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// +build go1.7
package nosurf
import (
"context"
"net/http"
)
func addNosurfContext(r *http.Request) *http.Request {
return r.WithContext(context.WithValue(r.Context(), nosurfKey, &csrfContext{}))
}

9
vendor/github.com/justinas/nosurf/handler_legacy.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
// +build !go1.7
package nosurf
import "net/http"
func addNosurfContext(r *http.Request) *http.Request {
return r
}

105
vendor/github.com/justinas/nosurf/token.go generated vendored Normal file
View File

@ -0,0 +1,105 @@
package nosurf
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"io"
)
const (
tokenLength = 32
)
/*
There are two types of tokens.
* The unmasked "real" token consists of 32 random bytes.
It is stored in a cookie (base64-encoded) and it's the
"reference" value that sent tokens get compared to.
* The masked "sent" token consists of 64 bytes:
32 byte key used for one-time pad masking and
32 byte "real" token masked with the said key.
It is used as a value (base64-encoded as well)
in forms and/or headers.
Upon processing, both tokens are base64-decoded
and then treated as 32/64 byte slices.
*/
// A token is generated by returning tokenLength bytes
// from crypto/rand
func generateToken() []byte {
bytes := make([]byte, tokenLength)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
panic(err)
}
return bytes
}
func b64encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func b64decode(data string) []byte {
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil
}
return decoded
}
// VerifyToken verifies the sent token equals the real one
// and returns a bool value indicating if tokens are equal.
// Supports masked tokens. realToken comes from Token(r) and
// sentToken is token sent unusual way.
func VerifyToken(realToken, sentToken string) bool {
r := b64decode(realToken)
if len(r) == 2*tokenLength {
r = unmaskToken(r)
}
s := b64decode(sentToken)
if len(s) == 2*tokenLength {
s = unmaskToken(s)
}
return subtle.ConstantTimeCompare(r, s) == 1
}
func verifyToken(realToken, sentToken []byte) bool {
realN := len(realToken)
sentN := len(sentToken)
// sentN == tokenLength means the token is unmasked
// sentN == 2*tokenLength means the token is masked.
if realN == tokenLength && sentN == 2*tokenLength {
return verifyMasked(realToken, sentToken)
} else {
return false
}
}
// Verifies the masked token
func verifyMasked(realToken, sentToken []byte) bool {
sentPlain := unmaskToken(sentToken)
return subtle.ConstantTimeCompare(realToken, sentPlain) == 1
}
func checkForPRNG() {
// Check that cryptographically secure PRNG is available
// In case it's not, panic.
buf := make([]byte, 1)
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
}
}
func init() {
checkForPRNG()
}

25
vendor/github.com/justinas/nosurf/utils.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
package nosurf
import (
"net/url"
)
func sContains(slice []string, s string) bool {
// checks if the given slice contains the given string
for _, v := range slice {
if v == s {
return true
}
}
return false
}
// Checks if the given URLs have the same origin
// (that is, they share the host, the port and the scheme)
func sameOrigin(u1, u2 *url.URL) bool {
// we take pointers, as url.Parse() returns a pointer
// and http.Request.URL is a pointer as well
// Host is either host or host:port
return (u1.Scheme == u2.Scheme && u1.Host == u2.Host)
}

27
vendor/github.com/kardianos/osext/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

21
vendor/github.com/kardianos/osext/README.md generated vendored Normal file
View File

@ -0,0 +1,21 @@
### Extensions to the "os" package.
[![GoDoc](https://godoc.org/github.com/kardianos/osext?status.svg)](https://godoc.org/github.com/kardianos/osext)
## Find the current Executable and ExecutableFolder.
As of go1.8 the Executable function may be found in `os`. The Executable function
in the std lib `os` package is used if available.
There is sometimes utility in finding the current executable file
that is running. This can be used for upgrading the current executable
or finding resources located relative to the executable file. Both
working directory and the os.Args[0] value are arbitrary and cannot
be relied on; os.Args[0] can be "faked".
Multi-platform and supports:
* Linux
* OS X
* Windows
* Plan 9
* BSDs.

33
vendor/github.com/kardianos/osext/osext.go generated vendored Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Extensions to the standard "os" package.
package osext // import "github.com/kardianos/osext"
import "path/filepath"
var cx, ce = executableClean()
func executableClean() (string, error) {
p, err := executable()
return filepath.Clean(p), err
}
// Executable returns an absolute path that can be used to
// re-invoke the current program.
// It may not be valid after the current program exits.
func Executable() (string, error) {
return cx, ce
}
// Returns same path as Executable, returns just the folder
// path. Excludes the executable name and any trailing slash.
func ExecutableFolder() (string, error) {
p, err := Executable()
if err != nil {
return "", err
}
return filepath.Dir(p), nil
}

9
vendor/github.com/kardianos/osext/osext_go18.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
//+build go1.8,!openbsd
package osext
import "os"
func executable() (string, error) {
return os.Executable()
}

22
vendor/github.com/kardianos/osext/osext_plan9.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//+build !go1.8
package osext
import (
"os"
"strconv"
"syscall"
)
func executable() (string, error) {
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
if err != nil {
return "", err
}
defer f.Close()
return syscall.Fd2path(int(f.Fd()))
}

36
vendor/github.com/kardianos/osext/osext_procfs.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8,linux !go1.8,netbsd !go1.8,solaris !go1.8,dragonfly
package osext
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
)
func executable() (string, error) {
switch runtime.GOOS {
case "linux":
const deletedTag = " (deleted)"
execpath, err := os.Readlink("/proc/self/exe")
if err != nil {
return execpath, err
}
execpath = strings.TrimSuffix(execpath, deletedTag)
execpath = strings.TrimPrefix(execpath, deletedTag)
return execpath, nil
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "dragonfly":
return os.Readlink("/proc/curproc/file")
case "solaris":
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
}
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
}

126
vendor/github.com/kardianos/osext/osext_sysctl.go generated vendored Normal file
View File

@ -0,0 +1,126 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8,darwin !go1.8,freebsd openbsd
package osext
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
"unsafe"
)
var initCwd, initCwdErr = os.Getwd()
func executable() (string, error) {
var mib [4]int32
switch runtime.GOOS {
case "freebsd":
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
case "darwin":
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
case "openbsd":
mib = [4]int32{1 /* CTL_KERN */, 55 /* KERN_PROC_ARGS */, int32(os.Getpid()), 1 /* KERN_PROC_ARGV */}
}
n := uintptr(0)
// Get length.
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
buf := make([]byte, n)
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
var execPath string
switch runtime.GOOS {
case "openbsd":
// buf now contains **argv, with pointers to each of the C-style
// NULL terminated arguments.
var args []string
argv := uintptr(unsafe.Pointer(&buf[0]))
Loop:
for {
argp := *(**[1 << 20]byte)(unsafe.Pointer(argv))
if argp == nil {
break
}
for i := 0; uintptr(i) < n; i++ {
// we don't want the full arguments list
if string(argp[i]) == " " {
break Loop
}
if argp[i] != 0 {
continue
}
args = append(args, string(argp[:i]))
n -= uintptr(i)
break
}
if n < unsafe.Sizeof(argv) {
break
}
argv += unsafe.Sizeof(argv)
n -= unsafe.Sizeof(argv)
}
execPath = args[0]
// There is no canonical way to get an executable path on
// OpenBSD, so check PATH in case we are called directly
if execPath[0] != '/' && execPath[0] != '.' {
execIsInPath, err := exec.LookPath(execPath)
if err == nil {
execPath = execIsInPath
}
}
default:
for i, v := range buf {
if v == 0 {
buf = buf[:i]
break
}
}
execPath = string(buf)
}
var err error
// execPath will not be empty due to above checks.
// Try to get the absolute path if the execPath is not rooted.
if execPath[0] != '/' {
execPath, err = getAbs(execPath)
if err != nil {
return execPath, err
}
}
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
// actual executable.
if runtime.GOOS == "darwin" {
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
return execPath, err
}
}
return execPath, nil
}
func getAbs(execPath string) (string, error) {
if initCwdErr != nil {
return execPath, initCwdErr
}
// The execPath may begin with a "../" or a "./" so clean it first.
// Join the two paths, trailing and starting slashes undetermined, so use
// the generic Join function.
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
}

36
vendor/github.com/kardianos/osext/osext_windows.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//+build !go1.8
package osext
import (
"syscall"
"unicode/utf16"
"unsafe"
)
var (
kernel = syscall.MustLoadDLL("kernel32.dll")
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
)
// GetModuleFileName() with hModule = NULL
func executable() (exePath string, err error) {
return getModuleFileName()
}
func getModuleFileName() (string, error) {
var n uint32
b := make([]uint16, syscall.MAX_PATH)
size := uint32(len(b))
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
n = uint32(r0)
if n == 0 {
return "", e1
}
return string(utf16.Decode(b[0:n])), nil
}

40
vendor/github.com/pressly/chi/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,40 @@
# Changelog
## v2.0.0 (2017-01-06)
- After many months of v2 being in an RC state with many companies and users running it in
production, the inclusion of some improvements to the middlewares, we are very pleased to
announce v2.0.0 of chi.
## v2.0.0-rc1 (2016-07-26)
- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular
community `"net/context"` package has been included in the standard library as `"context"` and
utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other
request-scoped values. We're very excited about the new context addition and are proud to
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
stdlib HTTP handlers and middlwares.
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
which provides direct access to URL routing parameters, the routing path and the matching
routing patterns.
- Users upgrading from chi v1 to v2, need to:
1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to
the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)`
2. Use `chi.URLParam(r *http.Request, paramKey string) string`
or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value
## v1.0.0 (2016-07-01)
- Released chi v1 stable https://github.com/pressly/chi/tree/v1.0.0 for Go 1.6 and older.
## v0.9.0 (2016-03-31)
- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/pressly/chi/pull/33)
- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters
has changed to: `chi.URLParam(ctx, "id")`

31
vendor/github.com/pressly/chi/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,31 @@
# Contributing
## Prerequisites
1. [Install Go][go-install].
2. Download the sources and switch the working directory:
```bash
go get -u -d github.com/pressly/chi
cd $GOPATH/src/github.com/pressly/chi
```
## Submitting a Pull Request
A typical workflow is:
1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip]
2. [Create a topic branch.][branch]
3. Add tests for your change.
4. Run `go test`. If your tests pass, return to the step 3.
5. Implement the change and ensure the steps from the previous step pass.
6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline.
7. [Add, commit and push your changes.][git-help]
8. [Submit a pull request.][pull-req]
[go-install]: https://golang.org/doc/install
[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html
[fork]: https://help.github.com/articles/fork-a-repo
[branch]: http://learn.github.com/p/branching.html
[git-help]: https://guides.github.com
[pull-req]: https://help.github.com/articles/using-pull-requests

20
vendor/github.com/pressly/chi/LICENSE generated vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka)
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

408
vendor/github.com/pressly/chi/README.md generated vendored Normal file
View File

@ -0,0 +1,408 @@
<img alt="chi" src="https://cdn.rawgit.com/pressly/chi/master/_examples/chi.svg" width="220" />
===
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
`chi` is a lightweight, idiomatic and composable router for building Go 1.7+ HTTP services. It's
especially good at helping you write large REST API services that are kept maintainable as your
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
handle signaling, cancelation and request-scoped values across a handler chain.
The focus of the project has been to seek out an elegant and comfortable design for writing
REST API servers, written during the development of the Pressly API service that powers our
public API service, which in turn powers all of our client-side applications.
The key considerations of chi's design are: project structure, maintainability, standard http
handlers (stdlib-only), developer productivity, and deconstructing a large system into many small
parts. The core router `github.com/pressly/chi` is quite small (less than 1000 LOC), but we've also
included some useful/optional subpackages: `middleware`, `render` and `docgen`. We hope you enjoy it too!
## Install
`go get -u github.com/pressly/chi`
## Features
* **Lightweight** - cloc'd in <1000 LOC for the chi router
* **Fast** - yes, see [benchmarks](#benchmarks)
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compat with `net/http`
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
* **Robust** - tested / used in production at Pressly.com, and many others
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
* **No external dependencies** - plain ol' Go 1.7+ stdlib + net/http
## Examples
* [rest](https://github.com/pressly/chi/blob/master/_examples/rest/main.go) - REST APIs made easy, productive and maintainable
* [logging](https://github.com/pressly/chi/blob/master/_examples/logging/main.go) - Easy structured logging for any backend
* [limits](https://github.com/pressly/chi/blob/master/_examples/limits/main.go) - Timeouts and Throttling
* [todos-resource](https://github.com/pressly/chi/blob/master/_examples/todos-resource/main.go) - Struct routers/handlers, an example of another code layout style
* [versions](https://github.com/pressly/chi/blob/master/_examples/versions/main.go) - Demo of `chi/render` subpkg
* [fileserver](https://github.com/pressly/chi/blob/master/_examples/fileserver/main.go) - Easily serve static files
* [graceful](https://github.com/pressly/chi/blob/master/_examples/graceful/main.go) - Graceful context signaling and server shutdown
**As easy as:**
```go
package main
import (
"net/http"
"github.com/pressly/chi"
)
func main() {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
})
http.ListenAndServe(":3000", r)
}
```
**REST Preview:**
Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs
in JSON ([routes.json](https://github.com/pressly/chi/blob/master/_examples/rest/routes.json)) and in
Markdown ([routes.md](https://github.com/pressly/chi/blob/master/_examples/rest/routes.md)).
I highly recommend reading the source of the [examples](#examples) listed above, they will show you all the features
of chi and serve as a good form of documentation.
```go
import (
//...
"context"
"github.com/pressly/chi"
"github.com/pressly/chi/middleware"
)
func main() {
r := chi.NewRouter()
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// When a client closes their connection midway through a request, the
// http.CloseNotifier will cancel the request context (ctx).
r.Use(middleware.CloseNotify)
// Set a timeout value on the request context (ctx), that will signal
// through ctx.Done() that the request has timed out and further
// processing should be stopped.
r.Use(middleware.Timeout(60 * time.Second))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
// RESTy routes for "articles" resource
r.Route("/articles", func(r chi.Router) {
r.With(paginate).Get("/", listArticles) // GET /articles
r.Post("/", createArticle) // POST /articles
r.Get("/search", searchArticles) // GET /articles/search
r.Route("/:articleID", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", getArticle) // GET /articles/123
r.Put("/", updateArticle) // PUT /articles/123
r.Delete("/", deleteArticle) // DELETE /articles/123
})
})
// Mount the admin sub-router
r.Mount("/admin", adminRouter())
http.ListenAndServe(":3333", r)
}
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "articleID")
article, err := dbGetArticle(articleID)
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getArticle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
article, ok := ctx.Value("article").(*Article)
if !ok {
http.Error(w, http.StatusText(422), 422)
return
}
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}
// A completely separate router for administrator routes
func adminRouter() http.Handler {
r := chi.NewRouter()
r.Use(AdminOnly)
r.Get("/", adminIndex)
r.Get("/accounts", adminListAccounts)
return r
}
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
if !ok || !perm.IsAdmin() {
http.Error(w, http.StatusText(403), 403)
return
}
next.ServeHTTP(w, r)
})
}
```
## Router design
chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree).
The router is fully compatible with `net/http`.
Built on top of the tree is the `Router` interface:
```go
// Router consisting of the core routing methods used by chi's Mux,
// using only the standard net/http.
type Router interface {
http.Handler
Routes
// Use appends one of more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)
// With adds inline middlewares for an endpoint handler.
With(middlewares ...func(http.Handler) http.Handler) Router
// Group adds a new inline-Router along the current routing
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
Mount(pattern string, h http.Handler)
// Handle and HandleFunc adds routes for `pattern` that matches
// all HTTP methods.
Handle(pattern string, h http.Handler)
HandleFunc(pattern string, h http.HandlerFunc)
// HTTP-method routing along `pattern`
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
// not be found.
NotFound(h http.HandlerFunc)
}
// Routes interface adds two methods for router traversal, which is also
// used by the `docgen` subpackage to generation documentation for Routers.
type Routes interface {
// Routes returns the routing tree in an easily traversable structure.
Routes() []Route
// Middlewares returns the list of middlewares in use by the router.
Middlewares() Middlewares
}
```
Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern
supports named params (ie. `/users/:userID`) and wildcards (ie. `/admin/*`).
### Middleware handlers
chi's middlewares are just stdlib net/http middleware handlers. There is nothing special
about them, which means the router and all the tooling is designed to be compatible and
friendly with any middleware in the community. This offers much better extensibility and reuse
of packages and is at the heart of chi's purpose.
Here is an example of a standard net/http middleware handler using the new request context
available in Go 1.7+. This middleware sets a hypothetical user identifier on the request
context and calls the next handler in the chain.
```go
// HTTP middleware setting a value on the request context
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "user", "123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
### Request handlers
chi uses standard net/http request handlers. This little snippet is an example of a http.Handler
func that reads a user identifier from the request context - hypothetically, identifying
the user sending an authenticated request, validated+set by a previous middleware handler.
```go
// HTTP handler accessing data from the request context.
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(string)
w.Write([]byte(fmt.Sprintf("hi %s", user)))
}
```
### URL parameters
chi's router parses and stores URL parameters right onto the request context. Here is
an example of how to access URL params in your net/http handlers. And of course, middlewares
are able to access the same information.
```go
// HTTP handler accessing the url routing parameters.
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID") // from a route like /users/:userID
ctx := r.Context()
key := ctx.Value("key").(string)
w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key)))
}
```
## Middlewares
chi comes equipped with an optional `middleware` package, providing a suite of standard
`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible
with `net/http` can be used with chi's mux.
----------------------------------------------------------------------------------------------------------
| Middleware | Description |
|:---------------------|:---------------------------------------------------------------------------------
| RequestID | Injects a request ID into the context of each request. |
| RealIP | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP. |
| Logger | Logs the start and end of each request with the elapsed processing time. |
| Recoverer | Gracefully absorb panics and prints the stack trace. |
| NoCache | Sets response headers to prevent clients from caching. |
| Timeout | Signals to the request context when the timeout deadline is reached. |
| Throttle | Puts a ceiling on the number of concurrent requests. |
| Compress | Gzip compression for clients that accept compressed responses. |
| Profiler | Easily attach net/http/pprof to your routers. |
| StripSlashes | Strip slashes on routing paths. |
| RedirectSlashes | Redirect slashes on routing paths. |
| WithValue | Short-hand middleware to set a key/value on the request context. |
| Heartbeat | Monitoring endpoint to check the servers pulse. |
----------------------------------------------------------------------------------------------------------
Other cool net/http middlewares:
* [jwtauth](https://github.com/goware/jwtauth) - JWT authenticator
* [cors](https://github.com/goware/cors) - CORS middleware
* [httpcoala](https://github.com/goware/httpcoala) - Request coalescer
please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi middleware
## context?
`context` is a tiny pkg that provides simple interface to signal context across call stacks
and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani)
and is available in stdlib since go1.7.
Learn more at https://blog.golang.org/context
and..
* Docs: https://golang.org/pkg/context
* Source: https://github.com/golang/go/tree/master/src/context
## Benchmarks
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
Comparison with other routers (as of Jan 7/17): https://gist.github.com/pkieltyka/d0814d5396c996cb3ff8076399583d1f
```shell
BenchmarkChi_Param 5000000 398 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Param5 3000000 556 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Param20 1000000 1184 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParamWrite 3000000 443 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubStatic 3000000 427 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubParam 3000000 565 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubAll 10000 122143 ns/op 61716 B/op 406 allocs/op
BenchmarkChi_GPlusStatic 5000000 383 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlusParam 3000000 431 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlus2Params 3000000 500 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlusAll 200000 6410 ns/op 3952 B/op 26 allocs/op
BenchmarkChi_ParseStatic 5000000 384 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParseParam 3000000 415 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Parse2Params 3000000 450 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParseAll 100000 12124 ns/op 7904 B/op 52 allocs/op
BenchmarkChi_StaticAll 20000 78501 ns/op 47731 B/op 314 allocs/op
```
NOTE: the allocs in the benchmark above are from the calls to http.Request's
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
on the duplicated (alloc'd) request and returns it the new request object. This is just
how setting context on a request in Go 1.7+ works.
## Credits
* Carl Jackson for https://github.com/zenazn/goji
* Parts of chi's thinking comes from goji, and chi's middleware package
sources from goji.
* Armon Dadgar for https://github.com/armon/go-radix
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
## Beyond REST
chi is just a http router that lets you decompose request handling into many smaller layers.
Many companies including Pressly.com (of course) use chi to write REST services for their public
APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces
required to write a complete client-server system or network of microservices.
Looking ahead beyond REST, I also recommend some newer works in the field coming from
[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit)
and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their
own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server
communication feel like a single program on a single computer, no need to hand-write a client library
and the request/response payloads are typed contracts. NATS is pretty amazing too as a super
fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery -
an excellent combination with gRPC.
## License
Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
Licensed under [MIT License](./LICENSE)
[GoDoc]: https://godoc.org/github.com/pressly/chi
[GoDoc Widget]: https://godoc.org/github.com/pressly/chi?status.svg
[Travis]: https://travis-ci.org/pressly/chi
[Travis Widget]: https://travis-ci.org/pressly/chi.svg?branch=master

47
vendor/github.com/pressly/chi/chain.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
package chi
import "net/http"
// Chain returns a Middlewares type from a slice of middleware handlers.
func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
return Middlewares(middlewares)
}
// Handler builds and returns a http.Handler from the chain of middlewares,
// with `h http.Handler` as the final handler.
func (mws Middlewares) Handler(h http.Handler) http.Handler {
return &ChainHandler{mws, h, chain(mws, h)}
}
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
// with `h http.Handler` as the final handler.
func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
return &ChainHandler{mws, h, chain(mws, h)}
}
type ChainHandler struct {
Middlewares Middlewares
Endpoint http.Handler
chain http.Handler
}
func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.chain.ServeHTTP(w, r)
}
// chain builds a http.Handler composed of an inline middleware stack and endpoint
// handler in the order they are passed.
func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler {
// Return ahead of time if there aren't any middlewares for the chain
if len(middlewares) == 0 {
return endpoint
}
// Wrap the end handler with the middleware chain
h := middlewares[len(middlewares)-1](endpoint)
for i := len(middlewares) - 2; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}

98
vendor/github.com/pressly/chi/chi.go generated vendored Normal file
View File

@ -0,0 +1,98 @@
//
// Package chi is a small, idiomatic and composable router for building HTTP services.
//
// chi requires Go 1.7 or newer.
//
// Example:
// package main
//
// import (
// "net/http"
//
// "github.com/pressly/chi"
// "github.com/pressly/chi/middleware"
// )
//
// func main() {
// r := chi.NewRouter()
// r.Use(middleware.Logger)
// r.Use(middleware.Recoverer)
//
// r.Get("/", func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte("root."))
// })
//
// http.ListenAndServe(":3333", r)
// }
//
// See github.com/pressly/chi/_examples/ for more in-depth examples.
//
package chi
import "net/http"
// NewRouter returns a new Mux object that implements the Router interface.
func NewRouter() *Mux {
return NewMux()
}
// Router consisting of the core routing methods used by chi's Mux,
// using only the standard net/http.
type Router interface {
http.Handler
Routes
// Use appends one of more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)
// With adds inline middlewares for an endpoint handler.
With(middlewares ...func(http.Handler) http.Handler) Router
// Group adds a new inline-Router along the current routing
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
Mount(pattern string, h http.Handler)
// Handle and HandleFunc adds routes for `pattern` that matches
// all HTTP methods.
Handle(pattern string, h http.Handler)
HandleFunc(pattern string, h http.HandlerFunc)
// HTTP-method routing along `pattern`
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
// not be found.
NotFound(h http.HandlerFunc)
// MethodNotAllowed defines a handler to respond whenever a method is
// not allowed.
MethodNotAllowed(h http.HandlerFunc)
}
// Routes interface adds two methods for router traversal, which is also
// used by the `docgen` subpackage to generation documentation for Routers.
type Routes interface {
// Routes returns the routing tree in an easily traversable structure.
Routes() []Route
// Middlewares returns the list of middlewares in use by the router.
Middlewares() Middlewares
}
// Middlewares type is a slice of standard middleware handlers with methods
// to compose middleware chains and http.Handler's.
type Middlewares []func(http.Handler) http.Handler

138
vendor/github.com/pressly/chi/context.go generated vendored Normal file
View File

@ -0,0 +1,138 @@
package chi
import (
"context"
"net"
"net/http"
)
var (
RouteCtxKey = &contextKey{"RouteContext"}
)
// Context is the default routing context set on the root node of a
// request context to track URL parameters and an optional routing path.
type Context struct {
// URL routing parameter key and values.
URLParams params
// Routing path override used by subrouters.
RoutePath string
// Routing pattern matching the path.
RoutePattern string
// Routing patterns throughout the lifecycle of the request,
// across all connected routers.
RoutePatterns []string
}
// NewRouteContext returns a new routing Context object.
func NewRouteContext() *Context {
return &Context{}
}
// reset a routing context to its initial state.
func (x *Context) reset() {
x.URLParams = x.URLParams[:0]
x.RoutePath = ""
x.RoutePattern = ""
x.RoutePatterns = x.RoutePatterns[:0]
}
// RouteContext returns chi's routing Context object from a
// http.Request Context.
func RouteContext(ctx context.Context) *Context {
return ctx.Value(RouteCtxKey).(*Context)
}
// URLParam returns the url parameter from a http.Request object.
func URLParam(r *http.Request, key string) string {
if rctx := RouteContext(r.Context()); rctx != nil {
return rctx.URLParams.Get(key)
}
return ""
}
// URLParamFromCtx returns the url parameter from a http.Request Context.
func URLParamFromCtx(ctx context.Context, key string) string {
if rctx := RouteContext(ctx); rctx != nil {
return rctx.URLParams.Get(key)
}
return ""
}
type param struct {
Key, Value string
}
type params []param
func (ps *params) Add(key string, value string) {
*ps = append(*ps, param{key, value})
}
func (ps params) Get(key string) string {
for _, p := range ps {
if p.Key == key {
return p.Value
}
}
return ""
}
func (ps *params) Set(key string, value string) {
idx := -1
for i, p := range *ps {
if p.Key == key {
idx = i
break
}
}
if idx < 0 {
(*ps).Add(key, value)
} else {
(*ps)[idx] = param{key, value}
}
}
func (ps *params) Del(key string) string {
for i, p := range *ps {
if p.Key == key {
*ps = append((*ps)[:i], (*ps)[i+1:]...)
return p.Value
}
}
return ""
}
// ServerBaseContext wraps an http.Handler to set the request context to the
// `baseCtx`.
func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler {
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
baseCtx := baseCtx
// Copy over default net/http server context keys
if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok {
baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v)
}
if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok {
baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v)
}
h.ServeHTTP(w, r.WithContext(baseCtx))
})
return fn
}
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "chi context value " + k.name
}

View File

@ -0,0 +1,42 @@
// +build go1.7,!go1.8
package middleware
import (
"context"
"net/http"
)
// CloseNotify is a middleware that cancels ctx when the underlying
// connection has gone away. It can be used to cancel long operations
// on the server when the client disconnects before the response is ready.
//
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
// on 1.8+ and exists just for backwards compatibility.
func CloseNotify(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
cn, ok := w.(http.CloseNotifier)
if !ok {
panic("chi/middleware: CloseNotify expects http.ResponseWriter to implement http.CloseNotifier interface")
}
closeNotifyCh := cn.CloseNotify()
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
go func() {
select {
case <-ctx.Done():
return
case <-closeNotifyCh:
cancel()
return
}
}()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

View File

@ -0,0 +1,17 @@
// +build go1.8
package middleware
import (
"net/http"
)
// CloseNotify is a middleware that cancels ctx when the underlying
// connection has gone away. It can be used to cancel long operations
// on the server when the client disconnects before the response is ready.
//
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
// on 1.8+ and exists just for backwards compatibility.
func CloseNotify(next http.Handler) http.Handler {
return next
}

212
vendor/github.com/pressly/chi/middleware/compress.go generated vendored Normal file
View File

@ -0,0 +1,212 @@
package middleware
import (
"bufio"
"compress/flate"
"compress/gzip"
"errors"
"io"
"net"
"net/http"
"strings"
)
type encoding int
const (
encodingNone encoding = iota
encodingGzip
encodingDeflate
)
var defaultContentTypes = map[string]struct{}{
"text/html": struct{}{},
"text/css": struct{}{},
"text/plain": struct{}{},
"text/javascript": struct{}{},
"application/javascript": struct{}{},
"application/x-javascript": struct{}{},
"application/json": struct{}{},
"application/atom+xml": struct{}{},
"application/rss+xml ": struct{}{},
}
// DefaultCompress is a middleware that compresses response
// body of predefined content types to a data format based
// on Accept-Encoding request header. It uses a default
// compression level.
func DefaultCompress(next http.Handler) http.Handler {
return Compress(flate.DefaultCompression)(next)
}
// Compress is a middleware that compresses response
// body of a given content types to a data format based
// on Accept-Encoding request header. It uses a given
// compression level.
func Compress(level int, types ...string) func(next http.Handler) http.Handler {
contentTypes := defaultContentTypes
if len(types) > 0 {
contentTypes = make(map[string]struct{}, len(types))
for _, t := range types {
contentTypes[t] = struct{}{}
}
}
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
mcw := &maybeCompressResponseWriter{
ResponseWriter: w,
w: w,
contentTypes: contentTypes,
encoding: selectEncoding(r.Header),
level: level,
}
defer mcw.Close()
next.ServeHTTP(mcw, r)
}
return http.HandlerFunc(fn)
}
}
func selectEncoding(h http.Header) encoding {
enc := h.Get("Accept-Encoding")
switch {
// TODO:
// case "br": // Brotli, experimental. Firefox 2016, to-be-in Chromium.
// case "lzma": // Opera.
// case "sdch": // Chrome, Android. Gzip output + dictionary header.
case strings.Contains(enc, "gzip"):
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
return encodingGzip
case strings.Contains(enc, "deflate"):
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
// checksum compared to CRC-32 used in "gzip" and thus is faster.
//
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
// raw DEFLATE data only, without the mentioned zlib wrapper.
// Because of this major confusion, most modern browsers try it
// both ways, first looking for zlib headers.
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
//
// The list of browsers having problems is quite big, see:
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
//
// That's why we prefer gzip over deflate. It's just more reliable
// and not significantly slower than gzip.
return encodingDeflate
// NOTE: Not implemented, intentionally:
// case "compress": // LZW. Deprecated.
// case "bzip2": // Too slow on-the-fly.
// case "zopfli": // Too slow on-the-fly.
// case "xz": // Too slow on-the-fly.
}
return encodingNone
}
type maybeCompressResponseWriter struct {
http.ResponseWriter
w io.Writer
encoding encoding
contentTypes map[string]struct{}
level int
wroteHeader bool
}
func (w *maybeCompressResponseWriter) WriteHeader(code int) {
if w.wroteHeader {
return
}
w.wroteHeader = true
defer w.ResponseWriter.WriteHeader(code)
// Already compressed data?
if w.ResponseWriter.Header().Get("Content-Encoding") != "" {
return
}
// The content-length after compression is unknown
w.ResponseWriter.Header().Del("Content-Length")
// Parse the first part of the Content-Type response header.
contentType := ""
parts := strings.Split(w.ResponseWriter.Header().Get("Content-Type"), ";")
if len(parts) > 0 {
contentType = parts[0]
}
// Is the content type compressable?
if _, ok := w.contentTypes[contentType]; !ok {
return
}
// Select the compress writer.
switch w.encoding {
case encodingGzip:
gw, err := gzip.NewWriterLevel(w.ResponseWriter, w.level)
if err != nil {
w.w = w.ResponseWriter
return
}
w.w = gw
w.ResponseWriter.Header().Set("Content-Encoding", "gzip")
case encodingDeflate:
dw, err := flate.NewWriter(w.ResponseWriter, w.level)
if err != nil {
w.w = w.ResponseWriter
return
}
w.w = dw
w.ResponseWriter.Header().Set("Content-Encoding", "deflate")
}
}
func (w *maybeCompressResponseWriter) Write(p []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
return w.w.Write(p)
}
func (w *maybeCompressResponseWriter) Flush() {
if f, ok := w.w.(http.Flusher); ok {
f.Flush()
}
}
func (w *maybeCompressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := w.w.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
}
func (w *maybeCompressResponseWriter) CloseNotify() <-chan bool {
if cn, ok := w.w.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
// If the underlying writer does not implement http.CloseNotifier, return
// a channel that never receives a value. The semantics here is that the
// client never disconnnects before the request is processed by the
// http.Handler, which is close enough to the default behavior (when
// CloseNotify() is not even called).
return make(chan bool, 1)
}
func (w *maybeCompressResponseWriter) Close() error {
if c, ok := w.w.(io.WriteCloser); ok {
return c.Close()
}
return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
}

15
vendor/github.com/pressly/chi/middleware/compress18.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
// +build go1.8
package middleware
import (
"errors"
"net/http"
)
func (w *maybeCompressResponseWriter) Push(target string, opts *http.PushOptions) error {
if ps, ok := w.w.(http.Pusher); ok {
return ps.Push(target, opts)
}
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
}

26
vendor/github.com/pressly/chi/middleware/heartbeat.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
package middleware
import (
"net/http"
"strings"
)
// Heartbeat endpoint middleware useful to setting up a path like
// `/ping` that load balancers or uptime testing external services
// can make a request before hitting any routes. It's also convenient
// to place this above ACL middlewares as well.
func Heartbeat(endpoint string) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && strings.EqualFold(r.URL.Path, endpoint) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("."))
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
return f
}

Some files were not shown because too many files have changed in this diff Show More