for release
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,7 +7,6 @@ bin/release
|
||||
bin/test.sh
|
||||
bin/tmp
|
||||
bin/test
|
||||
bin/src
|
||||
public/upload
|
||||
app/routes/routes.go
|
||||
app/tmp/main.go
|
||||
|
7
bin/src/github.com/revel/modules/README.md
Normal file
7
bin/src/github.com/revel/modules/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
modules
|
||||
=======
|
||||
|
||||
Set of officially supported modules for Revel applications
|
||||
|
||||
### Caution
|
||||
this is a work in progress
|
6
bin/src/github.com/revel/modules/auth/README.md
Normal file
6
bin/src/github.com/revel/modules/auth/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
modules/auth
|
||||
===============
|
||||
|
||||
Basic user/auth module
|
||||
|
||||
This should be modeled after [flask-security](https://github.com/mattupstate/flask-security)
|
78
bin/src/github.com/revel/modules/auth/auth.go
Normal file
78
bin/src/github.com/revel/modules/auth/auth.go
Normal file
@ -0,0 +1,78 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
// "errors"
|
||||
)
|
||||
|
||||
var (
|
||||
Store StorageDriver
|
||||
)
|
||||
|
||||
// Store = gormauth.NewGormAuthDriver()
|
||||
|
||||
type UserAuth interface {
|
||||
// getters/setters implemented by the app-level model
|
||||
UserId() string
|
||||
Secret() string
|
||||
HashedSecret() string
|
||||
SetHashedSecret(string)
|
||||
|
||||
SecretDriver
|
||||
// // implemented by secret driver
|
||||
// Authenticate() (bool, error)
|
||||
}
|
||||
|
||||
type SecretDriver interface {
|
||||
Authenticate() (bool, error)
|
||||
HashSecret(args ...interface{}) (string, error)
|
||||
|
||||
// stuff for documentation
|
||||
// UserContext is expected in these?
|
||||
|
||||
// Secret expects 0 or non-0 arguments
|
||||
// When no parameter is passed, it acts as a getter
|
||||
// When one or more parameters are passed, it acts as a setter
|
||||
// A driver should specify the expected arguments and their meanings
|
||||
|
||||
// Register()
|
||||
// Login()
|
||||
// Logout()
|
||||
}
|
||||
|
||||
type StorageDriver interface {
|
||||
Save(user interface{}) error
|
||||
// Load should take a partially filled struct
|
||||
// (with values needed to look up)
|
||||
// and fills in the rest
|
||||
Load(user interface{}) error
|
||||
}
|
||||
|
||||
// func init() {
|
||||
// // auth.Store = gorm...
|
||||
// }
|
||||
|
||||
// func (c App) Login(email, password string) {
|
||||
|
||||
// u := User {
|
||||
// Email ...
|
||||
// }
|
||||
|
||||
// good, err := auth.Authenticate(email, password)
|
||||
|
||||
// user, err := user_info.GetUserByEmail(email)
|
||||
// }
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
// func Authenticate(attemptedUser UserAuth) (bool, error) {
|
||||
// // check user in Store
|
||||
// loadedUser, err := Store.Load(attemptedUser.UserId())
|
||||
// if err != nil {
|
||||
// return false, errors.New("User Not Found")
|
||||
// }
|
||||
|
||||
// loadedUser.Authenticate(attemptedUser.Secret())
|
||||
|
||||
// // successfully authenticated
|
||||
// return true, nil
|
||||
// }
|
134
bin/src/github.com/revel/modules/auth/auth_test.go
Normal file
134
bin/src/github.com/revel/modules/auth/auth_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/modules/auth"
|
||||
"github.com/revel/modules/auth/driver/secret"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
email string
|
||||
password string
|
||||
hashpass string
|
||||
|
||||
secret.BcryptAuth // SecurityDriver for testing
|
||||
}
|
||||
|
||||
func NewUser(email, pass string) *User {
|
||||
u := &User{
|
||||
email: email,
|
||||
password: pass,
|
||||
}
|
||||
u.UserContext = u
|
||||
return u
|
||||
}
|
||||
|
||||
func (self *User) UserId() string {
|
||||
return self.email
|
||||
}
|
||||
|
||||
func (self *User) Secret() string {
|
||||
return self.password
|
||||
}
|
||||
|
||||
func (self *User) HashedSecret() string {
|
||||
return self.hashpass
|
||||
}
|
||||
|
||||
func (self *User) SetHashedSecret(hpass string) {
|
||||
self.hashpass = hpass
|
||||
}
|
||||
|
||||
// func (self *User) Load() string
|
||||
|
||||
type TestStore struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (self *TestStore) Save(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Save() expected arg of type User")
|
||||
}
|
||||
|
||||
hPass, err := u.HashSecret(u.Secret())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.data[u.UserId()] = hPass
|
||||
|
||||
return nil
|
||||
}
|
||||
func (self *TestStore) Load(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Load() expected arg of type User")
|
||||
}
|
||||
|
||||
hpass, ok := self.data[u.UserId()]
|
||||
if !ok {
|
||||
return errors.New("Record Not Found")
|
||||
}
|
||||
u.SetHashedSecret(hpass)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "")
|
||||
|
||||
var err error
|
||||
u.hashpass, err = u.HashSecret(u.password)
|
||||
if err != nil {
|
||||
t.Errorf("Should have hashed password, get error: %v\n", err)
|
||||
}
|
||||
fail.hashpass, err = fail.HashSecret(fail.password)
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed hashing\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
|
||||
// user registered a long time ago
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
err := auth.Store.Save(u)
|
||||
if err != nil {
|
||||
t.Errorf("Should have saved user: %v", err)
|
||||
}
|
||||
|
||||
// users now logging in
|
||||
pass := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "invalid")
|
||||
|
||||
// valid user is now trying to login
|
||||
// check user in DB
|
||||
err = auth.Store.Load(pass)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded pass user: %v\n", err)
|
||||
}
|
||||
// check credentials
|
||||
ok, err := pass.Authenticate()
|
||||
if !ok || err != nil {
|
||||
t.Errorf("Should have authenticated user")
|
||||
}
|
||||
|
||||
// invalid user is now trying to login
|
||||
err = auth.Store.Load(fail)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded fail user")
|
||||
}
|
||||
// this should fail
|
||||
ok, err = fail.Authenticate()
|
||||
if ok || err != nil {
|
||||
t.Errorf("Should have failed to authenticate user: %v\n", err)
|
||||
}
|
||||
}
|
27
bin/src/github.com/revel/modules/auth/doc.go
Normal file
27
bin/src/github.com/revel/modules/auth/doc.go
Normal file
@ -0,0 +1,27 @@
|
||||
/* A basic user authentication module for Revel
|
||||
|
||||
list of concerns:
|
||||
- Separating out the interface and driver
|
||||
- Removing DB/Storage dependency
|
||||
- UUID as default identifier?
|
||||
- how to deal with password/secret or generally, method of authorization
|
||||
- default {views,controllers,routes} for register/login/logut ?
|
||||
- reset password in most basic ?
|
||||
- activation (and other features) in a second / more sophisticated driver
|
||||
- filter for checking that user is authenticated
|
||||
|
||||
|
||||
I think a driver is made up of 2 parts
|
||||
data prep and data storage
|
||||
register and password reset are part of data prep
|
||||
as is the auth hash method
|
||||
they don't care how the data is stored
|
||||
|
||||
then there is the data store
|
||||
|
||||
perhaps each auth user model should instantiate 2 drivers instead of 1?
|
||||
one for data prep components and one for storage
|
||||
so the security driver and the storage driver
|
||||
|
||||
*/
|
||||
package auth
|
@ -0,0 +1,73 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"github.com/revel/modules/auth"
|
||||
)
|
||||
|
||||
// example implementation of a Revel auth security driver
|
||||
// This driver should be embedded into your app-level User model
|
||||
// It expects your User model to have `Password` and `HashedPassword` string fields
|
||||
//
|
||||
// Your User model also needs to set itself as the UserContext for the BcryptAuth driver
|
||||
//
|
||||
// func NewUser(email, pass string) *User {
|
||||
// u := &User{
|
||||
// email: email,
|
||||
// password: pass,
|
||||
// }
|
||||
// u.UserContext = u
|
||||
// }
|
||||
//
|
||||
type BcryptAuth struct {
|
||||
UserContext auth.UserAuth
|
||||
}
|
||||
|
||||
// Bcrypt Secret() returns the hashed version of the password.
|
||||
// It expects an argument of type string, which is the plain text password
|
||||
func (self *BcryptAuth) HashSecret(args ...interface{}) (string, error) {
|
||||
if auth.Store == nil {
|
||||
return "", errors.New("Auth module StorageDriver not set")
|
||||
}
|
||||
argLen := len(args)
|
||||
if argLen == 0 {
|
||||
// we are getting
|
||||
return string(self.UserContext.HashedSecret()), nil
|
||||
}
|
||||
|
||||
if argLen == 1 {
|
||||
// we are setting
|
||||
password, ok := args[0].(string)
|
||||
if !ok {
|
||||
return "", errors.New("Wrong argument type provided, expected plaintext password as string")
|
||||
}
|
||||
hPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
self.UserContext.SetHashedSecret(string(hPass))
|
||||
return self.UserContext.HashedSecret(), nil
|
||||
}
|
||||
|
||||
// bad argument count
|
||||
return "", errors.New("Too many arguments provided, expected one")
|
||||
}
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
func (self *BcryptAuth) Authenticate() (bool, error) {
|
||||
// check password
|
||||
err := bcrypt.CompareHashAndPassword([]byte(self.UserContext.HashedSecret()), []byte(self.UserContext.Secret()))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// successfully authenticated
|
||||
return true, nil
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
Mysql Auth driver
|
||||
==================
|
||||
|
@ -0,0 +1,3 @@
|
||||
Postgresql Auth driver
|
||||
==================
|
||||
|
@ -0,0 +1,3 @@
|
||||
Sqlite Auth driver
|
||||
==================
|
||||
|
17
bin/src/github.com/revel/modules/auth/user.go
Normal file
17
bin/src/github.com/revel/modules/auth/user.go
Normal file
@ -0,0 +1,17 @@
|
||||
package auth
|
||||
|
||||
// var storageDriver auth.StorageDriver // postgres in example
|
||||
|
||||
// type AuthUserModel struct {
|
||||
// userId string
|
||||
// security *SecurityDriver // bcrypt in example
|
||||
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) UserId() string {
|
||||
// return self.userId
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) Secret() string {
|
||||
// return self.security.Secret()
|
||||
// }
|
121
bin/src/github.com/revel/modules/csrf/app/csrf.go
Normal file
121
bin/src/github.com/revel/modules/csrf/app/csrf.go
Normal file
@ -0,0 +1,121 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// allowMethods are HTTP methods that do NOT require a token
|
||||
var allowedMethods = map[string]bool{
|
||||
"GET": true,
|
||||
"HEAD": true,
|
||||
"OPTIONS": true,
|
||||
"TRACE": true,
|
||||
}
|
||||
|
||||
func RandomString(length int) (string, error) {
|
||||
buffer := make([]byte, int(math.Ceil(float64(length)/2)))
|
||||
if _, err := io.ReadFull(rand.Reader, buffer); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
str := hex.EncodeToString(buffer)
|
||||
return str[:length], nil
|
||||
}
|
||||
|
||||
func RefreshToken(c *revel.Controller) {
|
||||
token, err := RandomString(64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Session["csrf_token"] = token
|
||||
}
|
||||
|
||||
// CsrfFilter enables CSRF request token creation and verification.
|
||||
//
|
||||
// Usage:
|
||||
// 1) Add `csrf.CsrfFilter` to the app's filters (it must come after the revel.SessionFilter).
|
||||
// 2) Add CSRF fields to a form with the template tag `{{ csrftoken . }}`. The filter adds a function closure to the `RenderArgs` that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the `X-CSRFToken` header.
|
||||
func CsrfFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
token, foundToken := c.Session["csrf_token"]
|
||||
|
||||
if !foundToken {
|
||||
RefreshToken(c)
|
||||
}
|
||||
|
||||
referer, refErr := url.Parse(c.Request.Header.Get("Referer"))
|
||||
isSameOrigin := sameOrigin(c.Request.URL, referer)
|
||||
|
||||
// If the Request method isn't in the white listed methods
|
||||
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
|
||||
// Token wasn't present at all
|
||||
if !foundToken {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Session token missing.")
|
||||
return
|
||||
}
|
||||
|
||||
// Referer header is invalid
|
||||
if refErr != nil {
|
||||
c.Result = c.Forbidden("REVEL CSRF: HTTP Referer malformed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Same origin
|
||||
if !isSameOrigin {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Same origin mismatch.")
|
||||
return
|
||||
}
|
||||
|
||||
var requestToken string
|
||||
// First check for token in post data
|
||||
if c.Request.Method == "POST" {
|
||||
requestToken = c.Request.FormValue("csrftoken")
|
||||
}
|
||||
|
||||
// Then check for token in custom headers, as with AJAX
|
||||
if requestToken == "" {
|
||||
requestToken = c.Request.Header.Get("X-CSRFToken")
|
||||
}
|
||||
|
||||
if requestToken == "" || !compareToken(requestToken, token) {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Invalid token.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fc[0](c, fc[1:])
|
||||
|
||||
// Only add token to RenderArgs if the request is: not AJAX, not missing referer header, and is same origin.
|
||||
if c.Request.Header.Get("X-CSRFToken") == "" && isSameOrigin {
|
||||
c.RenderArgs["_csrftoken"] = token
|
||||
}
|
||||
}
|
||||
|
||||
func compareToken(requestToken, token string) bool {
|
||||
// ConstantTimeCompare will panic if the []byte aren't the same length
|
||||
if len(requestToken) != len(token) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(requestToken), []byte(token)) == 1
|
||||
}
|
||||
|
||||
// Validates same origin policy
|
||||
func sameOrigin(u1, u2 *url.URL) bool {
|
||||
return u1.Scheme == u2.Scheme && u1.Host == u2.Host
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["csrftoken"] = func(renderArgs map[string]interface{}) template.HTML {
|
||||
if tokenFunc, ok := renderArgs["_csrftoken"]; !ok {
|
||||
panic("REVEL CSRF: _csrftoken missing from RenderArgs.")
|
||||
} else {
|
||||
return template.HTML(tokenFunc.(func() string)())
|
||||
}
|
||||
}
|
||||
}
|
169
bin/src/github.com/revel/modules/csrf/app/csrf_test.go
Normal file
169
bin/src/github.com/revel/modules/csrf/app/csrf_test.go
Normal file
@ -0,0 +1,169 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var testFilters = []revel.Filter{
|
||||
CsrfFilter,
|
||||
func(c *revel.Controller, fc []revel.Filter) {
|
||||
c.RenderHtml("{{ csrftoken . }}")
|
||||
},
|
||||
}
|
||||
|
||||
func TestTokenInSession(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(getRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.Session["csrf_token"]; !ok {
|
||||
t.Fatal("token should be present in session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostWithoutToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without token should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoReferrer(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without referer should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefererHttps(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "https://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("posts to https should have an https referer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
formPostRequest.Header.Add("X-CSRFToken", token)
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post with http header token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormPostWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("form post with token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoTokenInArgsWhenCORs(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example1.com/", nil)
|
||||
getRequest.Header.Add("Referer", "http://www.example2.com/")
|
||||
|
||||
c := revel.NewController(revel.NewRequest(getRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.RenderArgs["_csrftoken"]; ok {
|
||||
t.Fatal("RenderArgs should not contain token when not same origin")
|
||||
}
|
||||
}
|
36
bin/src/github.com/revel/modules/csrf/app/exempt.go
Normal file
36
bin/src/github.com/revel/modules/csrf/app/exempt.go
Normal file
@ -0,0 +1,36 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
exemptPath = make(map[string]bool)
|
||||
exemptAction = make(map[string]bool)
|
||||
)
|
||||
|
||||
func MarkExempt(route string) {
|
||||
if strings.HasPrefix(route, "/") {
|
||||
// e.g. "/controller/action"
|
||||
exemptPath[strings.ToLower(route)] = true
|
||||
} else if routeParts := strings.Split(route, "."); len(routeParts) == 2 {
|
||||
// e.g. "ControllerName.ActionName"
|
||||
exemptAction[route] = true
|
||||
} else {
|
||||
err := fmt.Sprintf("csrf.MarkExempt() received invalid argument \"%v\". Either provide a path prefixed with \"/\" or controller action in the form of \"ControllerName.ActionName\".", route)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsExempt(c *revel.Controller) bool {
|
||||
if _, ok := exemptPath[strings.ToLower(c.Request.Request.URL.Path)]; ok {
|
||||
return true
|
||||
} else if _, ok := exemptAction[c.Action]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
55
bin/src/github.com/revel/modules/csrf/app/exempt_test.go
Normal file
55
bin/src/github.com/revel/modules/csrf/app/exempt_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func TestExemptPath(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptPathCaseInsensitive(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/controller/action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptAction(t *testing.T) {
|
||||
MarkExempt("Controller.Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
c.Action = "Controller.Action"
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
86
bin/src/github.com/revel/modules/db/app/db.go
Normal file
86
bin/src/github.com/revel/modules/db/app/db.go
Normal file
@ -0,0 +1,86 @@
|
||||
// This module configures a database connection for the application.
|
||||
//
|
||||
// Developers use this module by importing and calling db.Init().
|
||||
// A "Transactional" controller type is provided as a way to import interceptors
|
||||
// that manage the transaction
|
||||
//
|
||||
// In particular, a transaction is begun before each request and committed on
|
||||
// success. If a panic occurred during the request, the transaction is rolled
|
||||
// back. (The application may also roll the transaction back itself.)
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
Db *sql.DB
|
||||
Driver string
|
||||
Spec string
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// Read configuration.
|
||||
var found bool
|
||||
if Driver, found = revel.Config.String("db.driver"); !found {
|
||||
revel.ERROR.Fatal("No db.driver found.")
|
||||
}
|
||||
if Spec, found = revel.Config.String("db.spec"); !found {
|
||||
revel.ERROR.Fatal("No db.spec found.")
|
||||
}
|
||||
|
||||
// Open a connection.
|
||||
var err error
|
||||
Db, err = sql.Open(Driver, Spec)
|
||||
if err != nil {
|
||||
revel.ERROR.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type Transactional struct {
|
||||
*revel.Controller
|
||||
Txn *sql.Tx
|
||||
}
|
||||
|
||||
// Begin a transaction
|
||||
func (c *Transactional) Begin() revel.Result {
|
||||
txn, err := Db.Begin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Txn = txn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback if it's still going (must have panicked).
|
||||
func (c *Transactional) Rollback() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Rollback(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit the transaction.
|
||||
func (c *Transactional) Commit() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Commit(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.InterceptMethod((*Transactional).Begin, revel.BEFORE)
|
||||
revel.InterceptMethod((*Transactional).Commit, revel.AFTER)
|
||||
revel.InterceptMethod((*Transactional).Rollback, revel.FINALLY)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/jobs/app/jobs"
|
||||
"github.com/robfig/cron"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Jobs struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
func (c Jobs) Status() revel.Result {
|
||||
remoteAddress := c.Request.RemoteAddr
|
||||
if revel.Config.BoolDefault("jobs.acceptproxyaddress", false) {
|
||||
if proxiedAddress, isProxied := c.Request.Header["X-Forwarded-For"]; isProxied {
|
||||
remoteAddress = proxiedAddress[0]
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(remoteAddress, "127.0.0.1") && !strings.HasPrefix(remoteAddress, "::1") {
|
||||
return c.Forbidden("%s is not local", remoteAddress)
|
||||
}
|
||||
entries := jobs.MainCron.Entries()
|
||||
return c.Render(entries)
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["castjob"] = func(job cron.Job) *jobs.Job {
|
||||
return job.(*jobs.Job)
|
||||
}
|
||||
}
|
66
bin/src/github.com/revel/modules/jobs/app/jobs/job.go
Normal file
66
bin/src/github.com/revel/modules/jobs/app/jobs/job.go
Normal file
@ -0,0 +1,66 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Name string
|
||||
inner cron.Job
|
||||
status uint32
|
||||
running sync.Mutex
|
||||
}
|
||||
|
||||
const UNNAMED = "(unnamed)"
|
||||
|
||||
func New(job cron.Job) *Job {
|
||||
name := reflect.TypeOf(job).Name()
|
||||
if name == "Func" {
|
||||
name = UNNAMED
|
||||
}
|
||||
return &Job{
|
||||
Name: name,
|
||||
inner: job,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) Status() string {
|
||||
if atomic.LoadUint32(&j.status) > 0 {
|
||||
return "RUNNING"
|
||||
}
|
||||
return "IDLE"
|
||||
}
|
||||
|
||||
func (j *Job) Run() {
|
||||
// If the job panics, just print a stack trace.
|
||||
// Don't let the whole process die.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if revelError := revel.NewErrorFromPanic(err); revelError != nil {
|
||||
revel.ERROR.Print(err, "\n", revelError.Stack)
|
||||
} else {
|
||||
revel.ERROR.Print(err, "\n", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !selfConcurrent {
|
||||
j.running.Lock()
|
||||
defer j.running.Unlock()
|
||||
}
|
||||
|
||||
if workPermits != nil {
|
||||
workPermits <- struct{}{}
|
||||
defer func() { <-workPermits }()
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&j.status, 1)
|
||||
defer atomic.StoreUint32(&j.status, 0)
|
||||
|
||||
j.inner.Run()
|
||||
}
|
64
bin/src/github.com/revel/modules/jobs/app/jobs/jobrunner.go
Normal file
64
bin/src/github.com/revel/modules/jobs/app/jobs/jobrunner.go
Normal file
@ -0,0 +1,64 @@
|
||||
// A job runner for executing scheduled or ad-hoc tasks asynchronously from HTTP requests.
|
||||
//
|
||||
// It adds a couple of features on top of the cron package to make it play nicely with Revel:
|
||||
// 1. Protection against job panics. (They print to ERROR instead of take down the process)
|
||||
// 2. (Optional) Limit on the number of jobs that may run simulatenously, to
|
||||
// limit resource consumption.
|
||||
// 3. (Optional) Protection against multiple instances of a single job running
|
||||
// concurrently. If one execution runs into the next, the next will be queued.
|
||||
// 4. Cron expressions may be defined in app.conf and are reusable across jobs.
|
||||
// 5. Job status reporting.
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Callers can use jobs.Func to wrap a raw func.
|
||||
// (Copying the type to this package makes it more visible)
|
||||
//
|
||||
// For example:
|
||||
// jobs.Schedule("cron.frequent", jobs.Func(myFunc))
|
||||
type Func func()
|
||||
|
||||
func (r Func) Run() { r() }
|
||||
|
||||
func Schedule(spec string, job cron.Job) error {
|
||||
// Look to see if given spec is a key from the Config.
|
||||
if strings.HasPrefix(spec, "cron.") {
|
||||
confSpec, found := revel.Config.String(spec)
|
||||
if !found {
|
||||
panic("Cron spec not found: " + spec)
|
||||
}
|
||||
spec = confSpec
|
||||
}
|
||||
sched, err := cron.Parse(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainCron.Schedule(sched, New(job))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the given job at a fixed interval.
|
||||
// The interval provided is the time between the job ending and the job being run again.
|
||||
// The time that the job takes to run is not included in the interval.
|
||||
func Every(duration time.Duration, job cron.Job) {
|
||||
MainCron.Schedule(cron.Every(duration), New(job))
|
||||
}
|
||||
|
||||
// Run the given job right now.
|
||||
func Now(job cron.Job) {
|
||||
go New(job).Run()
|
||||
}
|
||||
|
||||
// Run the given job once, after the given delay.
|
||||
func In(duration time.Duration, job cron.Job) {
|
||||
go func() {
|
||||
time.Sleep(duration)
|
||||
New(job).Run()
|
||||
}()
|
||||
}
|
32
bin/src/github.com/revel/modules/jobs/app/jobs/plugin.go
Normal file
32
bin/src/github.com/revel/modules/jobs/app/jobs/plugin.go
Normal file
@ -0,0 +1,32 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
const DEFAULT_JOB_POOL_SIZE = 10
|
||||
|
||||
var (
|
||||
// Singleton instance of the underlying job scheduler.
|
||||
MainCron *cron.Cron
|
||||
|
||||
// This limits the number of jobs allowed to run concurrently.
|
||||
workPermits chan struct{}
|
||||
|
||||
// Is a single job allowed to run concurrently with itself?
|
||||
selfConcurrent bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
MainCron = cron.New()
|
||||
revel.OnAppStart(func() {
|
||||
if size := revel.Config.IntDefault("jobs.pool", DEFAULT_JOB_POOL_SIZE); size > 0 {
|
||||
workPermits = make(chan struct{}, size)
|
||||
}
|
||||
selfConcurrent = revel.Config.BoolDefault("jobs.selfconcurrent", false)
|
||||
MainCron.Start()
|
||||
fmt.Println("Go to /@jobs to see job status.")
|
||||
})
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
}
|
||||
table td, table th {
|
||||
padding: 4 10px;
|
||||
border: none;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Scheduled Jobs</h1>
|
||||
|
||||
<table>
|
||||
<tr><th>Name</th><th>Status</th><th>Last run</th><th>Next run</th></tr>
|
||||
{{range .entries}}
|
||||
{{$job := castjob .Job}}
|
||||
<tr>
|
||||
<td>{{$job.Name}}</td>
|
||||
<td>{{$job.Status}}</td>
|
||||
<td>{{if not .Prev.IsZero}}{{.Prev.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
<td>{{if not .Next.IsZero}}{{.Next.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
1
bin/src/github.com/revel/modules/jobs/conf/routes
Normal file
1
bin/src/github.com/revel/modules/jobs/conf/routes
Normal file
@ -0,0 +1 @@
|
||||
GET /@jobs Jobs.Status
|
19
bin/src/github.com/revel/modules/pprof/README.md
Normal file
19
bin/src/github.com/revel/modules/pprof/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
Revel pprof module
|
||||
============
|
||||
|
||||
#### How to use:
|
||||
|
||||
1. Open your app.conf file and add the following line:
|
||||
`module.pprof=github.com/revel/modules/pprof`
|
||||
This will enable the pprof module.
|
||||
|
||||
2. Next, open your routes file and add:
|
||||
`module:pprof` **Note:** Do not change these routes. The pprof command-line tool by default assumes these routes.
|
||||
|
||||
Congrats! You can now profile your application. To use the web interface, visit `http://<host>:<port>/debug/pprof`. You can also use the `go tool pprof` command to profile your application and get a little deeper. Use the command by running `go tool pprof <binary> http://<host>:<port>`. For example, if you modified the booking sample, you would run: `go tool pprof $GOPATH/bin/booking http://localhost:9000` (assuming you used the default `revel run` arguments.
|
||||
|
||||
The command-line tool will take a 30-second CPU profile, and save the results to a temporary file in your `$HOME/pprof` directory. You can reference this file at a later time by using the same command as above, but by specifying the filename instead of the server address.
|
||||
|
||||
In order to fully utilize the command-line tool, you may need the graphviz utilities. If you're on OS X, you can install these easily using Homebrew: `brew install graphviz`.
|
||||
|
||||
To read more about profiling Go programs, here is some reading material: [The Go Blog](http://blog.golang.org/profiling-go-programs) : [net/pprof package documentation](http://golang.org/pkg/net/http/pprof/)
|
@ -0,0 +1,39 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
type Pprof struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// The PprofHandler type makes it easy to call the net/http/pprof handler methods
|
||||
// since they all have the same method signature
|
||||
type PprofHandler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func (r PprofHandler) Apply(req *revel.Request, resp *revel.Response) {
|
||||
r(resp.Out, req.Request)
|
||||
}
|
||||
|
||||
func (c Pprof) Profile() revel.Result {
|
||||
return PprofHandler(pprof.Profile)
|
||||
}
|
||||
|
||||
func (c Pprof) Symbol() revel.Result {
|
||||
return PprofHandler(pprof.Symbol)
|
||||
}
|
||||
|
||||
func (c Pprof) Cmdline() revel.Result {
|
||||
return PprofHandler(pprof.Cmdline)
|
||||
}
|
||||
|
||||
func (c Pprof) Trace() revel.Result {
|
||||
return PprofHandler(pprof.Trace)
|
||||
}
|
||||
|
||||
func (c Pprof) Index() revel.Result {
|
||||
return PprofHandler(pprof.Index)
|
||||
}
|
6
bin/src/github.com/revel/modules/pprof/conf/routes
Normal file
6
bin/src/github.com/revel/modules/pprof/conf/routes
Normal file
@ -0,0 +1,6 @@
|
||||
GET /pprof/profile Pprof.Profile
|
||||
GET /pprof/symbol Pprof.Symbol
|
||||
GET /debug/pprof/cmdline Pprof.Cmdline
|
||||
GET /debug/pprof/trace Pprof.Trace
|
||||
GET /debug/pprof/* Pprof.Index
|
||||
GET /debug/pprof/ Pprof.Index
|
@ -0,0 +1,121 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"os"
|
||||
fpath "path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Static struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// This method handles requests for files. The supplied prefix may be absolute
|
||||
// or relative. If the prefix is relative it is assumed to be relative to the
|
||||
// application directory. The filepath may either be just a file or an
|
||||
// additional filepath to search for the given file. This response may return
|
||||
// the following responses in the event of an error or invalid request;
|
||||
// 403(Forbidden): If the prefix filepath combination results in a directory.
|
||||
// 404(Not found): If the prefix and filepath combination results in a non-existent file.
|
||||
// 500(Internal Server Error): There are a few edge cases that would likely indicate some configuration error outside of revel.
|
||||
//
|
||||
// Note that when defining routes in routes/conf the parameters must not have
|
||||
// spaces around the comma.
|
||||
// Bad: Static.Serve("public/img", "favicon.png")
|
||||
// Good: Static.Serve("public/img","favicon.png")
|
||||
//
|
||||
// Examples:
|
||||
// Serving a directory
|
||||
// Route (conf/routes):
|
||||
// GET /public/{<.*>filepath} Static.Serve("public")
|
||||
// Request:
|
||||
// public/js/sessvars.js
|
||||
// Calls
|
||||
// Static.Serve("public","js/sessvars.js")
|
||||
//
|
||||
// Serving a file
|
||||
// Route (conf/routes):
|
||||
// GET /favicon.ico Static.Serve("public/img","favicon.png")
|
||||
// Request:
|
||||
// favicon.ico
|
||||
// Calls:
|
||||
// Static.Serve("public/img", "favicon.png")
|
||||
func (c Static) Serve(prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
return serve(c, prefix, filepath)
|
||||
}
|
||||
|
||||
// This method allows modules to serve binary files. The parameters are the same
|
||||
// as Static.Serve with the additional module name pre-pended to the list of
|
||||
// arguments.
|
||||
func (c Static) ServeModule(moduleName, prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
var basePath string
|
||||
for _, module := range revel.Modules {
|
||||
if module.Name == moduleName {
|
||||
basePath = module.Path
|
||||
}
|
||||
}
|
||||
|
||||
absPath := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
|
||||
return serve(c, absPath, filepath)
|
||||
}
|
||||
|
||||
|
||||
// This method allows static serving of application files in a verified manner.
|
||||
func serve(c Static, prefix, filepath string) revel.Result {
|
||||
var basePath string
|
||||
if !fpath.IsAbs(prefix) {
|
||||
basePath = revel.BasePath
|
||||
}
|
||||
|
||||
basePathPrefix := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
fname := fpath.Join(basePathPrefix, fpath.FromSlash(filepath))
|
||||
// Verify the request file path is within the application's scope of access
|
||||
if !strings.HasPrefix(fname, basePathPrefix) {
|
||||
revel.WARN.Printf("Attempted to read file outside of base path: %s", fname)
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
// Verify file path is accessible
|
||||
finfo, err := os.Stat(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err.(*os.PathError).Err == syscall.ENOTDIR {
|
||||
revel.WARN.Printf("File not found (%s): %s ", fname, err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
revel.ERROR.Printf("Error trying to get fileinfo for '%s': %s", fname, err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
|
||||
// Disallow directory listing
|
||||
if finfo.Mode().IsDir() {
|
||||
revel.WARN.Printf("Attempted directory listing of %s", fname)
|
||||
return c.Forbidden("Directory listing not allowed")
|
||||
}
|
||||
|
||||
// Open request file path
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
revel.WARN.Printf("File not found (%s): %s ", fname, err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
revel.ERROR.Printf("Error opening '%s': %s", fname, err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
return c.RenderFile(file, revel.Inline)
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/testing"
|
||||
)
|
||||
|
||||
// TestRunner is a controller which is used for running application tests in browser.
|
||||
type TestRunner struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// TestSuiteDesc is used for storing information about a single test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteDesc struct {
|
||||
Name string
|
||||
Tests []TestDesc
|
||||
|
||||
// Elem is reflect.Type which can be used for accessing methods
|
||||
// of the test suite.
|
||||
Elem reflect.Type
|
||||
}
|
||||
|
||||
// TestDesc is used for describing a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestDesc struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// TestSuiteResult stores the results the whole test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
Results []TestResult
|
||||
}
|
||||
|
||||
// TestResult represents the results of running a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
ErrorHTML template.HTML
|
||||
ErrorSummary string
|
||||
}
|
||||
|
||||
var (
|
||||
testSuites []TestSuiteDesc // A list of all available tests.
|
||||
|
||||
none = []reflect.Value{} // It is used as input for reflect call in a few places.
|
||||
|
||||
// registeredTests simplifies the search of test suites by their name.
|
||||
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
|
||||
registeredTests map[string]int
|
||||
)
|
||||
|
||||
/*
|
||||
Controller's action methods are below.
|
||||
*/
|
||||
|
||||
// Index is an action which renders the full list of available test suites and their tests.
|
||||
func (c TestRunner) Index() revel.Result {
|
||||
return c.Render(testSuites)
|
||||
}
|
||||
|
||||
// Run runs a single test, given by the argument.
|
||||
func (c TestRunner) Run(suite, test string) revel.Result {
|
||||
// Check whether requested test exists.
|
||||
suiteIndex, ok := registeredTests[suite+"."+test]
|
||||
if !ok {
|
||||
return c.NotFound("Test %s.%s does not exist", suite, test)
|
||||
}
|
||||
|
||||
result := TestResult{Name: test}
|
||||
|
||||
// Found the suite, create a new instance and run the named method.
|
||||
t := testSuites[suiteIndex].Elem
|
||||
v := reflect.New(t)
|
||||
func() {
|
||||
// When the function stops executing try to recover from panic.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// If panic error is empty, exit.
|
||||
panicErr := revel.NewErrorFromPanic(err)
|
||||
if panicErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, prepare and format the response of server if possible.
|
||||
testSuite := v.Elem().FieldByName("TestSuite").Interface().(testing.TestSuite)
|
||||
res := formatResponse(testSuite)
|
||||
|
||||
// Render the error and save to the result structure.
|
||||
var buffer bytes.Buffer
|
||||
tmpl, _ := revel.MainTemplateLoader.Template("TestRunner/FailureDetail.html")
|
||||
tmpl.Render(&buffer, map[string]interface{}{
|
||||
"error": panicErr,
|
||||
"response": res,
|
||||
"postfix": suite + "_" + test,
|
||||
})
|
||||
result.ErrorSummary = errorSummary(panicErr)
|
||||
result.ErrorHTML = template.HTML(buffer.String())
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize the test suite with a NewTestSuite()
|
||||
testSuiteInstance := v.Elem().FieldByName("TestSuite")
|
||||
testSuiteInstance.Set(reflect.ValueOf(testing.NewTestSuite()))
|
||||
|
||||
// Make sure After method will be executed at the end.
|
||||
if m := v.MethodByName("After"); m.IsValid() {
|
||||
defer m.Call(none)
|
||||
}
|
||||
|
||||
// Start from running Before method of test suite if exists.
|
||||
if m := v.MethodByName("Before"); m.IsValid() {
|
||||
m.Call(none)
|
||||
}
|
||||
|
||||
// Start the test method itself.
|
||||
v.MethodByName(test).Call(none)
|
||||
|
||||
// No panic means success.
|
||||
result.Passed = true
|
||||
}()
|
||||
|
||||
return c.RenderJson(result)
|
||||
}
|
||||
|
||||
// List returns a JSON list of test suites and tests.
|
||||
// It is used by revel test command line tool.
|
||||
func (c TestRunner) List() revel.Result {
|
||||
return c.RenderJson(testSuites)
|
||||
}
|
||||
|
||||
/*
|
||||
Below are helper functions.
|
||||
*/
|
||||
|
||||
// describeSuite expects testsuite interface as input parameter
|
||||
// and returns its description in a form of TestSuiteDesc structure.
|
||||
func describeSuite(testSuite interface{}) TestSuiteDesc {
|
||||
t := reflect.TypeOf(testSuite)
|
||||
|
||||
// Get a list of methods of the embedded test type.
|
||||
// It will be used to make sure the same tests are not included in multiple test suites.
|
||||
super := t.Elem().Field(0).Type
|
||||
superMethods := map[string]bool{}
|
||||
for i := 0; i < super.NumMethod(); i++ {
|
||||
// Save the current method's name.
|
||||
superMethods[super.Method(i).Name] = true
|
||||
}
|
||||
|
||||
// Get a list of methods on the test suite that take no parameters, return
|
||||
// no results, and were not part of the embedded type's method set.
|
||||
var tests []TestDesc
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
mt := m.Type
|
||||
|
||||
// Make sure the test method meets the criterias:
|
||||
// - method of testSuite without input parameters;
|
||||
// - nothing is returned;
|
||||
// - has "Test" prefix;
|
||||
// - doesn't belong to the embedded structure.
|
||||
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
|
||||
nothingReturned := (mt.NumOut() == 0)
|
||||
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
|
||||
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
|
||||
// Register the test suite's index so we can quickly find it by test's name later.
|
||||
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
|
||||
|
||||
// Add test to the list of tests.
|
||||
tests = append(tests, TestDesc{m.Name})
|
||||
}
|
||||
}
|
||||
|
||||
return TestSuiteDesc{
|
||||
Name: t.Elem().Name(),
|
||||
Tests: tests,
|
||||
Elem: t.Elem(),
|
||||
}
|
||||
}
|
||||
|
||||
// errorSummary gets an error and returns its summary in human readable format.
|
||||
func errorSummary(err *revel.Error) (message string) {
|
||||
message = fmt.Sprintf("%4sStatus: %s\n%4sIn %s", "", err.Description, "", err.Path)
|
||||
|
||||
// If line of error isn't known return the message as is.
|
||||
if err.Line == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, include info about the line number and the relevant
|
||||
// source code lines.
|
||||
message += fmt.Sprintf(" (around line %d): ", err.Line)
|
||||
for _, line := range err.ContextSource() {
|
||||
if line.IsError {
|
||||
message += line.Source
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// formatResponse gets *revel.TestSuite as input parameter and
|
||||
// transform response related info into a readable format.
|
||||
func formatResponse(t testing.TestSuite) map[string]string {
|
||||
if t.Response == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
// Beautify the response JSON to make it human readable.
|
||||
resp, err := json.MarshalIndent(t.Response, "", " ")
|
||||
if err != nil {
|
||||
revel.ERROR.Println(err)
|
||||
}
|
||||
|
||||
// Remove extra new line symbols so they do not take too much space on a result page.
|
||||
// Allow no more than 1 line break at a time.
|
||||
body := strings.Replace(string(t.ResponseBody), "\n\n", "\n", -1)
|
||||
body = strings.Replace(body, "\r\n\r\n", "\r\n", -1)
|
||||
|
||||
return map[string]string{
|
||||
"Headers": string(resp),
|
||||
"Body": strings.TrimSpace(body),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Every time app is restarted convert the list of available test suites
|
||||
// provided by the revel testing package into a format which will be used by
|
||||
// the testrunner module and revel test cmd.
|
||||
revel.OnAppStart(func() {
|
||||
// Extracting info about available test suites from revel/testing package.
|
||||
registeredTests = map[string]int{}
|
||||
for _, testSuite := range testing.TestSuites {
|
||||
testSuites = append(testSuites, describeSuite(testSuite))
|
||||
}
|
||||
})
|
||||
}
|
12
bin/src/github.com/revel/modules/testrunner/app/plugin.go
Normal file
12
bin/src/github.com/revel/modules/testrunner/app/plugin.go
Normal file
@ -0,0 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
fmt.Println("Go to /@tests to run the tests.")
|
||||
})
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<b>{{.error.Description}}</b>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a href="#error_{{.postfix}}" role="tab" data-toggle="tab">Error</a></li>
|
||||
<li><a href="#stack_{{.postfix}}" role="tab" data-toggle="tab">Stack</a></li>
|
||||
{{if .response}}
|
||||
<li><a href="#headers_{{.postfix}}" role="tab" data-toggle="tab">Headers</a></li>
|
||||
<li><a href="#body_{{.postfix}}" role="tab" data-toggle="tab">Response Body</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="result_{{.postfix}}">
|
||||
<div class="tab-pane active" id="error_{{.postfix}}">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
In {{.error.Path}}{{if .error.Line}} (around {{if .error.Line}}line {{.error.Line}}{{end}}{{if .error.Column}} column {{.error.Column}}{{end}}){{end}}:
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{range .error.ContextSource}}
|
||||
{{if .IsError}}
|
||||
<pre><code class="go">{{.Source}}</code></pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="stack_{{.postfix}}">
|
||||
<pre><code class="bash">{{.error.Stack}}</code></pre>
|
||||
</div>
|
||||
{{if .response}}
|
||||
<div class="tab-pane" id="headers_{{.postfix}}">
|
||||
<pre><code class="json">{{.response.Headers}}</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane" id="body_{{.postfix}}">
|
||||
<pre><code class="html">{{.response.Body}}</code></pre>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link href="{{url `Root`}}/@tests/public/css/bootstrap.min.css" type="text/css" rel="stylesheet"></link>
|
||||
<link href="{{url `Root`}}/@tests/public/css/github.css" type="text/css" rel="stylesheet"></link>
|
||||
<script src="{{url `Root`}}/@tests/public/js/jquery-1.9.1.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/bootstrap.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/highlight.pack.js" type="text/javascript"></script>
|
||||
<style>
|
||||
header { padding:20px 0; background-color:#ADD8E6 }
|
||||
.passed td { background-color: #90EE90 !important; }
|
||||
.failed td { background-color: #FFB6C1 !important; }
|
||||
.tests td.name, .tests td.result { padding-top: 13px; }
|
||||
pre { font-size:10px; white-space: pre; }
|
||||
.name { width: 25%; }
|
||||
.w100 { width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<table class="w100"><tr><td>
|
||||
<h1>Test Runner</h1>
|
||||
<p class="lead">Run all of your application's tests from here.</p>
|
||||
</td><td style="padding-left:150px;" class="text-right">
|
||||
<button class="btn btn-lg btn-success" all-tests="">Run All Tests</button>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
{{range .testSuites}}
|
||||
<p class="lead" style="margin-top:20px;">{{.Name}}</p>
|
||||
<table class="table table-striped tests" suite="{{.Name}}">
|
||||
{{range .Tests}}
|
||||
<tr>
|
||||
<td class="name">{{.Name}}</td>
|
||||
<td class="result">
|
||||
</td>
|
||||
<td class="text-right"><button test="{{.Name}}" class="btn btn-success">Run</button></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var buttons = [];
|
||||
var running;
|
||||
|
||||
$("button[test]").click(function() {
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
addToQueue(button);
|
||||
});
|
||||
|
||||
$("button[all-tests]").click(function() {
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
$("button[test]").click();
|
||||
});
|
||||
|
||||
function addToQueue(button) {
|
||||
buttons.push(button);
|
||||
if (!running) {
|
||||
running = true;
|
||||
nextTest();
|
||||
}
|
||||
}
|
||||
|
||||
function nextTest() {
|
||||
if (buttons.length == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
var next = buttons.shift();
|
||||
runTest(next);
|
||||
}
|
||||
}
|
||||
|
||||
function runTest(button) {
|
||||
var suite = button.parents("table").attr("suite");
|
||||
var test = button.attr("test");
|
||||
var row = button.parents("tr");
|
||||
var resultCell = row.children(".result");
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: "{{url `Root`}}/@tests/"+suite+"/"+test,
|
||||
success: function(result) {
|
||||
row.attr("class", result.Passed ? "passed" : "failed");
|
||||
if (result.Passed) {
|
||||
resultCell.html("");
|
||||
} else {
|
||||
resultCell.html(result.ErrorHTML);
|
||||
|
||||
$("#result_" + suite + "_" + test + " pre code").each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
button.removeClass("disabled").text("Run");
|
||||
if (buttons.length == 0) {
|
||||
$("button[all-tests]").removeClass("disabled").text("Run All Tests");
|
||||
}
|
||||
nextTest();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
header { padding:20px 0; }
|
||||
header.passed { background-color: #90EE90 !important; }
|
||||
header.failed { background-color: #FFB6C1 !important; }
|
||||
table { margin-top: 20px; padding: 8px; line-height: 20px; }
|
||||
td { vertical-align: top; padding-right:20px; }
|
||||
a { color: #0088cc; }
|
||||
.container { margin-left: auto; margin-right: auto; width: 940px; overflow: hidden; }
|
||||
.result h2 { font-size: 16px; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.2em; }
|
||||
.result.failed b { font-weight:bold; color: #C00; font-size: 14px; }
|
||||
.result.failed h2 { color: #C00; }
|
||||
.result .info { font-size: 12px; }
|
||||
.result .info pre { overflow: auto; background-color: #f0f0f0; width: 100%; max-height: 500px; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="{{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div class="container">
|
||||
<h1>{{.Name}}</h1>
|
||||
<p>{{if .Passed}}PASSED{{else}}FAILED{{end}}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
{{range .Results}}
|
||||
<div class="result {{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div><h2>{{.Name}}</h2></div>
|
||||
<div class="info">{{if .ErrorHTML}}{{.ErrorHTML}}{{else}}PASSED{{end}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
4
bin/src/github.com/revel/modules/testrunner/conf/routes
Normal file
4
bin/src/github.com/revel/modules/testrunner/conf/routes
Normal file
@ -0,0 +1,4 @@
|
||||
GET /@tests TestRunner.Index
|
||||
GET /@tests.list TestRunner.List
|
||||
GET /@tests/public/*filepath Static.ServeModule(testrunner,public)
|
||||
GET /@tests/:suite/:test TestRunner.Run
|
5
bin/src/github.com/revel/modules/testrunner/public/css/bootstrap.min.css
vendored
Normal file
5
bin/src/github.com/revel/modules/testrunner/public/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,127 @@
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-template_comment,
|
||||
.diff .hljs-header,
|
||||
.hljs-javadoc {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.css .rule .hljs-keyword,
|
||||
.hljs-winutils,
|
||||
.javascript .hljs-title,
|
||||
.nginx .hljs-title,
|
||||
.hljs-subst,
|
||||
.hljs-request,
|
||||
.hljs-status {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-hexcolor,
|
||||
.ruby .hljs-constant {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-tag .hljs-value,
|
||||
.hljs-phpdoc,
|
||||
.hljs-dartdoc,
|
||||
.tex .hljs-formula {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-id,
|
||||
.scss .hljs-preprocessor {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.javascript .hljs-title,
|
||||
.hljs-list .hljs-keyword,
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-type,
|
||||
.vhdl .hljs-literal,
|
||||
.tex .hljs-command {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title,
|
||||
.hljs-rules .hljs-property,
|
||||
.django .hljs-tag .hljs-keyword {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-variable,
|
||||
.lisp .hljs-body {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-regexp {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.ruby .hljs-symbol .hljs-string,
|
||||
.lisp .hljs-keyword,
|
||||
.clojure .hljs-keyword,
|
||||
.scheme .hljs-keyword,
|
||||
.tex .hljs-special,
|
||||
.hljs-prompt {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-preprocessor,
|
||||
.hljs-pragma,
|
||||
.hljs-pi,
|
||||
.hljs-doctype,
|
||||
.hljs-shebang,
|
||||
.hljs-cdata {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.diff .hljs-change {
|
||||
background: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-chunk {
|
||||
color: #aaa;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
6
bin/src/github.com/revel/modules/testrunner/public/js/bootstrap.min.js
vendored
Normal file
6
bin/src/github.com/revel/modules/testrunner/public/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5
bin/src/github.com/revel/modules/testrunner/public/js/jquery-1.9.1.min.js
vendored
Normal file
5
bin/src/github.com/revel/modules/testrunner/public/js/jquery-1.9.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user