mirror of
https://github.com/rls-moe/nyx
synced 2024-11-22 22:12:24 +00:00
MVP, no mod tools or anything but it works
This commit is contained in:
parent
70b12c516a
commit
69b0d20825
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
config.yml
|
||||||
|
nyx-testing.db
|
||||||
|
nyx
|
||||||
|
run.sh
|
93
config/config.go
Normal file
93
config/config.go
Normal 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
119
http/admin/handler.go
Normal 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
58
http/admin/newboard.go
Normal 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
41
http/admin/res/index.html
Normal 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
57
http/admin/res/panel.html
Normal 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
52
http/board/board.go
Normal 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
88
http/board/handler.go
Normal 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
59
http/board/newreply.go
Normal 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
48
http/board/newthread.go
Normal 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
119
http/board/res/board.html
Normal 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">>></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
30
http/board/res/dir.html
Normal 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
129
http/board/res/thread.html
Normal 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">>></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
57
http/board/thread.go
Normal 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
77
http/errw/handler.go
Normal 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
35
http/errw/res/error.html
Normal 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
21
http/middle/base.go
Normal 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
24
http/middle/config.go
Normal 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
9
http/middle/ctx.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package middle
|
||||||
|
|
||||||
|
type ctxKey int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
configKey ctxKey = iota
|
||||||
|
dbCtxKey
|
||||||
|
sessionKey
|
||||||
|
)
|
34
http/middle/db.go
Normal file
34
http/middle/db.go
Normal 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
15
http/middle/session.go
Normal 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
26
http/res/admin.css
Normal 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
21
http/res/custom.css
Normal 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
157
http/res/style.css
Normal 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
53
http/server.go
Normal 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
18
main.go
Normal 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
67
resources/adminpass.go
Normal 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
78
resources/board.go
Normal 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
114
resources/db.go
Normal 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
17
resources/ids.go
Normal 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
3
resources/metadata.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
type Metadata map[string]string
|
72
resources/modpass.go
Normal file
72
resources/modpass.go
Normal 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
113
resources/reply.go
Normal 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])
|
||||||
|
}
|
20
resources/snowflakes/LICENSE
Normal file
20
resources/snowflakes/LICENSE
Normal 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.
|
1
resources/snowflakes/NOTICE
Normal file
1
resources/snowflakes/NOTICE
Normal file
@ -0,0 +1 @@
|
|||||||
|
You can find the original generator at https://github.com/arke-works/arke/blob/master/snowflakes/generator.go
|
75
resources/snowflakes/generator.go
Normal file
75
resources/snowflakes/generator.go
Normal 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
12
resources/text.go
Normal 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
145
resources/thread.go
Normal 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
4
vendor/github.com/GeertJohan/go.rice/AUTHORS
generated
vendored
Normal 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
22
vendor/github.com/GeertJohan/go.rice/LICENSE
generated
vendored
Normal 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
151
vendor/github.com/GeertJohan/go.rice/README.md
generated
vendored
Normal 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
138
vendor/github.com/GeertJohan/go.rice/appended.go
generated
vendored
Normal 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
337
vendor/github.com/GeertJohan/go.rice/box.go
generated
vendored
Normal 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
39
vendor/github.com/GeertJohan/go.rice/config.go
generated
vendored
Normal 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
4
vendor/github.com/GeertJohan/go.rice/debug.go
generated
vendored
Normal 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
90
vendor/github.com/GeertJohan/go.rice/embedded.go
generated
vendored
Normal 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
|
||||||
|
}
|
80
vendor/github.com/GeertJohan/go.rice/embedded/embedded.go
generated
vendored
Normal file
80
vendor/github.com/GeertJohan/go.rice/embedded/embedded.go
generated
vendored
Normal 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
144
vendor/github.com/GeertJohan/go.rice/file.go
generated
vendored
Normal 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
21
vendor/github.com/GeertJohan/go.rice/http.go
generated
vendored
Normal 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
19
vendor/github.com/GeertJohan/go.rice/sort.go
generated
vendored
Normal 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
252
vendor/github.com/GeertJohan/go.rice/virtual.go
generated
vendored
Normal 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
122
vendor/github.com/GeertJohan/go.rice/walk.go
generated
vendored
Normal 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
31
vendor/github.com/GeertJohan/go.rice/wercker.yml
generated
vendored
Normal 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
21
vendor/github.com/daaku/go.zipexe/license
generated
vendored
Normal 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
5
vendor/github.com/daaku/go.zipexe/readme.md
generated
vendored
Normal 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
142
vendor/github.com/daaku/go.zipexe/zipexe.go
generated
vendored
Normal 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
39
vendor/github.com/hlandau/passlib/COPYING
generated
vendored
Normal 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
95
vendor/github.com/hlandau/passlib/README.md
generated
vendored
Normal 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
177
vendor/github.com/hlandau/passlib/passlib.go
generated
vendored
Normal 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
201
vendor/github.com/icza/session/LICENSE
generated
vendored
Normal 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
114
vendor/github.com/icza/session/README.md
generated
vendored
Normal 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
123
vendor/github.com/icza/session/cookie_manager.go
generated
vendored
Normal 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
117
vendor/github.com/icza/session/doc.go
generated
vendored
Normal 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
375
vendor/github.com/icza/session/gae_memcache_store.go
generated
vendored
Normal 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
39
vendor/github.com/icza/session/global.go
generated
vendored
Normal 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
145
vendor/github.com/icza/session/inmem_store.go
generated
vendored
Normal 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
31
vendor/github.com/icza/session/manager.go
generated
vendored
Normal 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
231
vendor/github.com/icza/session/session.go
generated
vendored
Normal 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
25
vendor/github.com/icza/session/store.go
generated
vendored
Normal 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
20
vendor/github.com/justinas/nosurf/LICENSE
generated
vendored
Normal 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
125
vendor/github.com/justinas/nosurf/README.md
generated
vendored
Normal 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
60
vendor/github.com/justinas/nosurf/context.go
generated
vendored
Normal 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
101
vendor/github.com/justinas/nosurf/context_legacy.go
generated
vendored
Normal 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
54
vendor/github.com/justinas/nosurf/crypto.go
generated
vendored
Normal 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
108
vendor/github.com/justinas/nosurf/exempt.go
generated
vendored
Normal 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
220
vendor/github.com/justinas/nosurf/handler.go
generated
vendored
Normal 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
12
vendor/github.com/justinas/nosurf/handler_go17.go
generated
vendored
Normal 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
9
vendor/github.com/justinas/nosurf/handler_legacy.go
generated
vendored
Normal 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
105
vendor/github.com/justinas/nosurf/token.go
generated
vendored
Normal 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
25
vendor/github.com/justinas/nosurf/utils.go
generated
vendored
Normal 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
27
vendor/github.com/kardianos/osext/LICENSE
generated
vendored
Normal 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
21
vendor/github.com/kardianos/osext/README.md
generated
vendored
Normal 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
33
vendor/github.com/kardianos/osext/osext.go
generated
vendored
Normal 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
9
vendor/github.com/kardianos/osext/osext_go18.go
generated
vendored
Normal 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
22
vendor/github.com/kardianos/osext/osext_plan9.go
generated
vendored
Normal 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
36
vendor/github.com/kardianos/osext/osext_procfs.go
generated
vendored
Normal 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
126
vendor/github.com/kardianos/osext/osext_sysctl.go
generated
vendored
Normal 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
36
vendor/github.com/kardianos/osext/osext_windows.go
generated
vendored
Normal 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
40
vendor/github.com/pressly/chi/CHANGELOG.md
generated
vendored
Normal 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
31
vendor/github.com/pressly/chi/CONTRIBUTING.md
generated
vendored
Normal 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
20
vendor/github.com/pressly/chi/LICENSE
generated
vendored
Normal 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
408
vendor/github.com/pressly/chi/README.md
generated
vendored
Normal 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
47
vendor/github.com/pressly/chi/chain.go
generated
vendored
Normal 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
98
vendor/github.com/pressly/chi/chi.go
generated
vendored
Normal 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
138
vendor/github.com/pressly/chi/context.go
generated
vendored
Normal 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
|
||||||
|
}
|
42
vendor/github.com/pressly/chi/middleware/closenotify17.go
generated
vendored
Normal file
42
vendor/github.com/pressly/chi/middleware/closenotify17.go
generated
vendored
Normal 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)
|
||||||
|
}
|
17
vendor/github.com/pressly/chi/middleware/closenotify18.go
generated
vendored
Normal file
17
vendor/github.com/pressly/chi/middleware/closenotify18.go
generated
vendored
Normal 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
212
vendor/github.com/pressly/chi/middleware/compress.go
generated
vendored
Normal 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
15
vendor/github.com/pressly/chi/middleware/compress18.go
generated
vendored
Normal 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
26
vendor/github.com/pressly/chi/middleware/heartbeat.go
generated
vendored
Normal 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
Loading…
Reference in New Issue
Block a user