for release

This commit is contained in:
lealife
2017-07-27 15:31:52 +08:00
parent 1ff90dacde
commit 6ede5c1559
38 changed files with 1848 additions and 1 deletions

1
.gitignore vendored
View File

@ -7,7 +7,6 @@ bin/release
bin/test.sh bin/test.sh
bin/tmp bin/tmp
bin/test bin/test
bin/src
public/upload public/upload
app/routes/routes.go app/routes/routes.go
app/tmp/main.go app/tmp/main.go

View File

@ -0,0 +1,7 @@
modules
=======
Set of officially supported modules for Revel applications
### Caution
this is a work in progress

View File

@ -0,0 +1,6 @@
modules/auth
===============
Basic user/auth module
This should be modeled after [flask-security](https://github.com/mattupstate/flask-security)

View 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
// }

View 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)
}
}

View 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

View File

@ -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
}

View File

@ -0,0 +1,3 @@
Mysql Auth driver
==================

View File

@ -0,0 +1,3 @@
Postgresql Auth driver
==================

View File

@ -0,0 +1,3 @@
Sqlite Auth driver
==================

View 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()
// }

View 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)())
}
}
}

View 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")
}
}

View 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
}

View 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")
}
}

View 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)
}

View File

@ -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)
}
}

View 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()
}

View 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()
}()
}

View 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.")
})
}

View File

@ -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>

View File

@ -0,0 +1 @@
GET /@jobs Jobs.Status

View 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/)

View File

@ -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)
}

View 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

View File

@ -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)
}

View File

@ -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))
}
})
}

View 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.")
})
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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

File diff suppressed because one or more lines are too long

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long