<a href="#"><img alt="rex" src="https://raw.githubusercontent.com/go-rex/rex/assets/images/rex.png" width="160px" height="64px"></a> | <a href="#"><img alt="rex" src="https://raw.githubusercontent.com/go-rex/rex/assets/images/rex.png" width="160px" height="64px"></a> | ||||
=== | === | ||||
Rex is a powerful framework for modular web development in Golang. | |||||
Rex is a powerful toolkit for modular web development in Golang, designed to work directly with net/http. | |||||
<img alt="wrk" src="https://raw.githubusercontent.com/goanywhere/rex/assets/images/wrk.png"> | |||||
## Getting Started | ## Getting Started | ||||
## Features | ## Features | ||||
* Flexible Env-based configurations. | * Flexible Env-based configurations. | ||||
* Awesome routing system provided by [Gorilla/Mux](//github.com/gorilla/mux). | * Awesome routing system provided by [Gorilla/Mux](//github.com/gorilla/mux). | ||||
* Django-syntax like templating-language backed by [flosch/pongo](//github.com/flosch/pongo2). | |||||
* Group routing system with middleware modules supports | |||||
* Non-intrusive/Modular design, extremely easy to use. | * Non-intrusive/Modular design, extremely easy to use. | ||||
* Standard & modular system based on [http.Handler](http://godoc.org/net/http#Handler) interface. | * Standard & modular system based on [http.Handler](http://godoc.org/net/http#Handler) interface. | ||||
* Command line tools | * Command line tools | ||||
* Auto-compile for .go & .html | |||||
* Browser-based Live reload supports | |||||
* Auto-compile/reload for .go & .html sources | |||||
* Browser-based Live reload supports for HTML templates | |||||
* **Fully compatible with the [http.Handler](http://godoc.org/net/http#Handler)/[http.HandlerFunc](http://godoc.org/net/http#HandlerFunc) interface.** | * **Fully compatible with the [http.Handler](http://godoc.org/net/http#Handler)/[http.HandlerFunc](http://godoc.org/net/http#HandlerFunc) interface.** | ||||
After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first server, we named it `server.go` here. | |||||
After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first server. | |||||
``` go | ``` go | ||||
package main | package main | ||||
## Modules | ## Modules | ||||
Modules work between http requests and the router, they are no different than the standard http.Handler. Existing modules (aka. middleware) from other frameworks like logging, authorization, session, gzipping are very easy to integrate into Rex. As long as it complies the `rex.Module` interface (shorcut to standard `func(http.Handler) http.Handler`), you can simply add one like this: | |||||
Modules (aka. middleware) work between http requests and the router, they are no different than the standard http.Handler. Existing modules from other frameworks like logging, authorization, session, gzipping are very easy to integrate into Rex. As long as it complies the standard `func(http.Handler) http.Handler` signature, you can simply add one like this: | |||||
``` go | ``` go | ||||
app.Use(modules.XSRF) | app.Use(modules.XSRF) | ||||
``` go | ``` go | ||||
app.Use(func(next http.Handler) http.Handler { | app.Use(func(next http.Handler) http.Handler { | ||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { | |||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
log.Printf("Custom Middleware Module Started") | log.Printf("Custom Middleware Module Started") | ||||
next.ServeHTTP(writer, request) | next.ServeHTTP(writer, request) | ||||
log.Printf("Custom Middleware Module Ended") | log.Printf("Custom Middleware Module Ended") | ||||
- [X] Test Suite | - [X] Test Suite | ||||
- [X] New Project Template | - [X] New Project Template | ||||
- [X] CLI Apps Integrations | - [X] CLI Apps Integrations | ||||
- [X] Improved Template Rendering | |||||
- [X] Performance Boost | - [X] Performance Boost | ||||
- [X] Hot-Compile Runner | - [X] Hot-Compile Runner | ||||
- [X] Live Reload Integration | - [X] Live Reload Integration | ||||
- [X] Template Functions | |||||
- [X] Common Modules | - [X] Common Modules | ||||
- [ ] Full Test Converage | |||||
- [ ] Improved Template Rendering | |||||
- [ ] Project Wiki | - [ ] Project Wiki | ||||
- [ ] Continuous Integration | - [ ] Continuous Integration | ||||
- [ ] Stable API | - [ ] Stable API |
"github.com/codegangsta/cli" | "github.com/codegangsta/cli" | ||||
"github.com/goanywhere/rex/internal" | "github.com/goanywhere/rex/internal" | ||||
"github.com/goanywhere/rex/modules/livereload" | |||||
"github.com/goanywhere/rex/livereload" | |||||
"github.com/goanywhere/x/cmd" | "github.com/goanywhere/x/cmd" | ||||
"github.com/goanywhere/x/env" | "github.com/goanywhere/x/env" |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package livereload | |||||
import ( | |||||
"encoding/json" | |||||
"net/http" | |||||
"sync" | |||||
"github.com/gorilla/websocket" | |||||
) | |||||
/* ---------------------------------------------------------------------- | |||||
* WebSocket Server | |||||
* ----------------------------------------------------------------------*/ | |||||
var ( | |||||
once sync.Once | |||||
broadcast chan []byte | |||||
tunnels map[*tunnel]bool | |||||
in chan *tunnel | |||||
out chan *tunnel | |||||
mutex sync.RWMutex | |||||
upgrader = websocket.Upgrader{ | |||||
ReadBufferSize: 1024, | |||||
WriteBufferSize: 1024, | |||||
} | |||||
URL = struct { | |||||
WebSocket string | |||||
JavaScript string | |||||
}{ | |||||
WebSocket: "/livereload", | |||||
JavaScript: "/livereload.js", | |||||
} | |||||
) | |||||
// Alert sends a notice message to browser's livereload.js. | |||||
func Alert(message string) { | |||||
go func() { | |||||
var bytes, _ = json.Marshal(&alert{ | |||||
Command: "alert", | |||||
Message: message, | |||||
}) | |||||
broadcast <- bytes | |||||
}() | |||||
} | |||||
// Reload sends a reload message to browser's livereload.js. | |||||
func Reload() { | |||||
go func() { | |||||
var bytes, _ = json.Marshal(&reload{ | |||||
Command: "reload", | |||||
Path: URL.WebSocket, | |||||
LiveCSS: true, | |||||
}) | |||||
broadcast <- bytes | |||||
}() | |||||
} | |||||
// run watches/dispatches all tunnel & tunnel messages. | |||||
func run() { | |||||
for { | |||||
select { | |||||
case tunnel := <-in: | |||||
mutex.Lock() | |||||
defer mutex.Unlock() | |||||
tunnels[tunnel] = true | |||||
case tunnel := <-out: | |||||
mutex.Lock() | |||||
defer mutex.Unlock() | |||||
delete(tunnels, tunnel) | |||||
close(tunnel.message) | |||||
case m := <-broadcast: | |||||
for tunnel := range tunnels { | |||||
select { | |||||
case tunnel.message <- m: | |||||
default: | |||||
mutex.Lock() | |||||
defer mutex.Unlock() | |||||
delete(tunnels, tunnel) | |||||
close(tunnel.message) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// Serve serves as a livereload server for accepting I/O tunnel messages. | |||||
func ServeWebSocket(w http.ResponseWriter, r *http.Request) { | |||||
var socket, err = upgrader.Upgrade(w, r, nil) | |||||
if err != nil { | |||||
return | |||||
} | |||||
tunnel := new(tunnel) | |||||
tunnel.socket = socket | |||||
tunnel.message = make(chan []byte, 256) | |||||
in <- tunnel | |||||
defer func() { out <- tunnel }() | |||||
tunnel.connect() | |||||
} | |||||
// ServeJavaScript serves livereload.js for browser. | |||||
func ServeJavaScript(w http.ResponseWriter, r *http.Request) { | |||||
w.Header().Set("Content-Type", "application/javascript") | |||||
w.Write(javascript) | |||||
} | |||||
// Start activates livereload server for accepting tunnel messages. | |||||
func Start() { | |||||
once.Do(func() { | |||||
broadcast = make(chan []byte) | |||||
tunnels = make(map[*tunnel]bool) | |||||
in = make(chan *tunnel) | |||||
out = make(chan *tunnel) | |||||
go run() | |||||
}) | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package livereload | |||||
type ( | |||||
hello struct { | |||||
Command string `json:"command"` | |||||
Protocols []string `json:"protocols"` | |||||
ServerName string `json:"serverName"` | |||||
} | |||||
alert struct { | |||||
Command string `json:"command"` | |||||
Message string `json:"message"` | |||||
} | |||||
reload struct { | |||||
Command string `json:"command"` | |||||
Path string `json:"path"` // as full as possible/known, absolute path preferred, file name only is OK | |||||
LiveCSS bool `json:"liveCSS"` // false to disable live CSS refresh | |||||
} | |||||
) |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package livereload | |||||
import ( | |||||
"bytes" | |||||
"compress/gzip" | |||||
"compress/zlib" | |||||
"fmt" | |||||
"io" | |||||
"net/http" | |||||
"regexp" | |||||
"strings" | |||||
"github.com/goanywhere/x/env" | |||||
) | |||||
type writer struct { | |||||
http.ResponseWriter | |||||
host string | |||||
} | |||||
func (self *writer) addJavaScript(data []byte) []byte { | |||||
javascript := fmt.Sprintf(`<script src="//%s%s"></script> | |||||
</head>`, self.host, URL.JavaScript) | |||||
return regexp.MustCompile(`</head>`).ReplaceAll(data, []byte(javascript)) | |||||
} | |||||
func (self *writer) Write(data []byte) (size int, e error) { | |||||
if strings.Contains(self.Header().Get("Content-Type"), "html") { | |||||
var encoding = self.Header().Get("Content-Encoding") | |||||
if encoding == "" { | |||||
data = self.addJavaScript(data) | |||||
} else { | |||||
var reader io.ReadCloser | |||||
var buffer *bytes.Buffer = new(bytes.Buffer) | |||||
if encoding == "gzip" { | |||||
// decode to add javascript reference. | |||||
reader, _ = gzip.NewReader(bytes.NewReader(data)) | |||||
io.Copy(buffer, reader) | |||||
output := self.addJavaScript(buffer.Bytes()) | |||||
reader.Close() | |||||
buffer.Reset() | |||||
// encode back to HTML with added javascript reference. | |||||
writer := gzip.NewWriter(buffer) | |||||
writer.Write(output) | |||||
writer.Close() | |||||
data = buffer.Bytes() | |||||
} else if encoding == "deflate" { | |||||
// decode to add javascript reference. | |||||
reader, _ = zlib.NewReader(bytes.NewReader(data)) | |||||
io.Copy(buffer, reader) | |||||
output := self.addJavaScript(buffer.Bytes()) | |||||
reader.Close() | |||||
buffer.Reset() | |||||
// encode back to HTML with added javascript reference. | |||||
writer := zlib.NewWriter(buffer) | |||||
writer.Write(output) | |||||
writer.Close() | |||||
data = buffer.Bytes() | |||||
} | |||||
} | |||||
} | |||||
return self.ResponseWriter.Write(data) | |||||
} | |||||
func Module(next http.Handler) http.Handler { | |||||
// ONLY run this under debug mode. | |||||
if !env.Bool("DEBUG", true) { | |||||
return next | |||||
} | |||||
Start() | |||||
fn := func(w http.ResponseWriter, r *http.Request) { | |||||
if r.URL.Path == URL.WebSocket { | |||||
ServeWebSocket(w, r) | |||||
} else if r.URL.Path == URL.JavaScript { | |||||
ServeJavaScript(w, r) | |||||
} else { | |||||
writer := &writer{w, r.Host} | |||||
next.ServeHTTP(writer, r) | |||||
} | |||||
} | |||||
return http.HandlerFunc(fn) | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package livereload | |||||
import ( | |||||
"encoding/json" | |||||
"regexp" | |||||
"github.com/gorilla/websocket" | |||||
) | |||||
var regexHandshake = regexp.MustCompile(`"command"\s*:\s*"hello"`) | |||||
/* ---------------------------------------------------------------------- | |||||
* WebSocket Server Tunnel | |||||
* ----------------------------------------------------------------------*/ | |||||
type tunnel struct { | |||||
socket *websocket.Conn | |||||
message chan []byte | |||||
} | |||||
// connect reads/writes message for livereload.js. | |||||
func (self *tunnel) connect() { | |||||
// *********************** | |||||
// WebSocket Tunnel#Write | |||||
// *********************** | |||||
go func() { | |||||
for message := range self.message { | |||||
if err := self.socket.WriteMessage(websocket.TextMessage, message); err != nil { | |||||
break | |||||
} else { | |||||
if regexHandshake.Find(message) != nil { | |||||
// Keep the tunnel opened after handshake(hello command). | |||||
Reload() | |||||
} | |||||
} | |||||
} | |||||
self.socket.Close() | |||||
}() | |||||
// *********************** | |||||
// WebSocket Tunnel#Read | |||||
// *********************** | |||||
for { | |||||
_, message, err := self.socket.ReadMessage() | |||||
if err != nil { | |||||
break | |||||
} | |||||
switch true { | |||||
case regexHandshake.Find(message) != nil: | |||||
var bytes, _ = json.Marshal(&hello{ | |||||
Command: "hello", | |||||
Protocols: []string{"http://livereload.com/protocols/official-7"}, | |||||
ServerName: "Rex#Livereload", | |||||
}) | |||||
self.message <- bytes | |||||
} | |||||
} | |||||
self.socket.Close() | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package modules | |||||
import ( | |||||
"bytes" | |||||
"compress/flate" | |||||
"compress/gzip" | |||||
"io" | |||||
"net/http" | |||||
"regexp" | |||||
"strings" | |||||
) | |||||
var ( | |||||
regexAcceptEncoding = regexp.MustCompile(`(gzip|deflate|\*)(;q=(1(\.0)?|0(\.[0-9])?))?`) | |||||
regexContentType = regexp.MustCompile(`((message|text)\/.+)|((application\/).*(javascript|json|xml))`) | |||||
) | |||||
type compression interface { | |||||
io.WriteCloser | |||||
} | |||||
type compressor struct { | |||||
http.ResponseWriter | |||||
encodings []string | |||||
} | |||||
// AcceptEncodings fetches the requested encodings from client with priority. | |||||
func (self *compressor) acceptEncodings(request *http.Request) (encodings []string) { | |||||
// find all encodings supported by backend server. | |||||
matches := regexAcceptEncoding.FindAllString(request.Header.Get("Accept-Encoding"), -1) | |||||
for _, item := range matches { | |||||
units := strings.SplitN(item, ";", 2) | |||||
// top priority with q=1|q=1.0|Not Specified. | |||||
if len(units) == 1 { | |||||
encodings = append(encodings, units[0]) | |||||
} else { | |||||
if strings.HasPrefix(units[1], "q=1") { | |||||
// insert the specified top priority to the first. | |||||
encodings = append([]string{units[0]}, encodings...) | |||||
} else if strings.HasSuffix(units[1], "0") { | |||||
// not acceptable at client side. | |||||
continue | |||||
} else { | |||||
// lower priority encoding | |||||
encodings = append(encodings, units[0]) | |||||
} | |||||
} | |||||
} | |||||
return | |||||
} | |||||
func (self *compressor) filter(src []byte) ([]byte, string) { | |||||
var mimetype = self.Header().Get("Content-Type") | |||||
if mimetype == "" { | |||||
mimetype = http.DetectContentType(src) | |||||
self.Header().Set("Content-Type", mimetype) | |||||
} | |||||
if self.Header().Get("Content-Encoding") != "" { | |||||
return src, "" | |||||
} | |||||
if !regexContentType.MatchString(strings.TrimSpace(strings.SplitN(mimetype, ";", 2)[0])) { | |||||
return src, "" | |||||
} | |||||
// okay to start compressing. | |||||
var e error | |||||
var encoding string | |||||
var writer compression | |||||
var buffer *bytes.Buffer = new(bytes.Buffer) | |||||
// try compress the data, if any error occrued, fallback to ResponseWriter. | |||||
if self.encodings[0] == "deflate" { | |||||
encoding = "deflate" | |||||
writer, e = flate.NewWriter(buffer, flate.DefaultCompression) | |||||
} else { | |||||
encoding = "gzip" | |||||
writer, e = gzip.NewWriterLevel(buffer, gzip.DefaultCompression) | |||||
} | |||||
_, e = writer.Write(src) | |||||
writer.Close() | |||||
if e == nil { | |||||
return buffer.Bytes(), encoding | |||||
} | |||||
// fallback to standard http.ResponseWriter, nothing happened~ (~__~"") | |||||
return src, "" | |||||
} | |||||
func (self *compressor) Write(data []byte) (size int, err error) { | |||||
if bytes, encoding := self.filter(data); encoding != "" { | |||||
self.Header().Set("Content-Encoding", encoding) | |||||
self.Header().Add("Vary", "Accept-Encoding") | |||||
self.Header().Del("Content-Length") | |||||
return self.ResponseWriter.Write(bytes) | |||||
} | |||||
return self.ResponseWriter.Write(data) | |||||
} | |||||
func Compress(next http.Handler) http.Handler { | |||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
if r.Header.Get("Sec-WebSocket-Key") != "" || r.Method == "HEAD" { | |||||
next.ServeHTTP(w, r) | |||||
} else { | |||||
compressor := new(compressor) | |||||
compressor.ResponseWriter = w | |||||
encodings := compressor.acceptEncodings(r) | |||||
if len(encodings) == 0 { | |||||
next.ServeHTTP(w, r) | |||||
} else { | |||||
compressor.encodings = encodings | |||||
next.ServeHTTP(compressor, r) | |||||
} | |||||
} | |||||
}) | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package modules |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package modules | |||||
import "net/http" | |||||
func Header(values map[string]string) func(http.Handler) http.Handler { | |||||
return func(next http.Handler) http.Handler { | |||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
for key, value := range values { | |||||
w.Header().Set(key, value) | |||||
} | |||||
next.ServeHTTP(w, r) | |||||
}) | |||||
} | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package modules | |||||
import ( | |||||
"bytes" | |||||
"crypto/hmac" | |||||
"crypto/sha1" | |||||
"crypto/subtle" | |||||
"encoding/base64" | |||||
"encoding/hex" | |||||
"fmt" | |||||
"net/http" | |||||
"net/url" | |||||
"regexp" | |||||
"strconv" | |||||
"time" | |||||
"github.com/goanywhere/x/crypto" | |||||
) | |||||
const ( | |||||
xsrfCookieName = "xsrf" | |||||
xsrfHeaderName = "X-XSRF-Token" | |||||
xsrfFieldName = "xsrftoken" | |||||
xsrfMaxAge = 3600 * 24 * 365 | |||||
xsrfTimeout = time.Hour * 24 * 365 | |||||
) | |||||
var ( | |||||
errXSRFReferer = "Referer URL is missing from the request or the value was malformed." | |||||
errXSRFToken = "Invalid XSRF tokens" | |||||
xsrfPattern = regexp.MustCompile("[^0-9a-zA-Z-_]") | |||||
unsafeMethods = regexp.MustCompile("^(DELETE|POST|PUT)$") | |||||
) | |||||
type xsrf struct { | |||||
*http.Request | |||||
http.ResponseWriter | |||||
token string | |||||
} | |||||
// See http://en.wikipedia.org/wiki/Same-origin_policy | |||||
func (self *xsrf) checkOrigin() bool { | |||||
if self.Request.URL.Scheme == "https" { | |||||
// See [OWASP]; Checking the Referer Header. | |||||
referer, err := url.Parse(self.Request.Header.Get("Referer")) | |||||
if err != nil || referer.String() == "" || | |||||
referer.Scheme != self.Request.URL.Scheme || | |||||
referer.Host != self.Request.URL.Host { | |||||
return false | |||||
} | |||||
} | |||||
return true | |||||
} | |||||
func (self *xsrf) checkToken(token string) bool { | |||||
// Header always takes precedance of form field since some popular | |||||
// JavaScript frameworks allow global custom headers for all AJAX requests. | |||||
query := self.Request.Header.Get(xsrfFieldName) | |||||
if query == "" { | |||||
query = self.Request.FormValue(xsrfFieldName) | |||||
} | |||||
// 1) basic length comparison. | |||||
if query == "" || len(query) != len(token) { | |||||
return false | |||||
} | |||||
// *sanitize* incoming masked token. | |||||
query = xsrfPattern.ReplaceAllString(query, "") | |||||
// 2) byte-based comparison. | |||||
a, _ := base64.URLEncoding.DecodeString(token) | |||||
b, _ := base64.URLEncoding.DecodeString(query) | |||||
if subtle.ConstantTimeCompare(a, b) != 1 { | |||||
return false | |||||
} | |||||
// 3) issued time checking. | |||||
index := bytes.LastIndex(b, []byte{'|'}) | |||||
if index != 40 { | |||||
return false | |||||
} | |||||
nanos, err := strconv.ParseInt(string(b[index+1:]), 10, 64) | |||||
if err != nil { | |||||
return false | |||||
} | |||||
now := time.Now() | |||||
issueTime := time.Unix(0, nanos) | |||||
if now.Sub(issueTime) >= xsrfTimeout { | |||||
return false | |||||
} | |||||
// Ensure the token is not from the *future*, allow 1 minute grace period. | |||||
if issueTime.After(now.Add(1 * time.Minute)) { | |||||
return false | |||||
} | |||||
return true | |||||
} | |||||
func (self *xsrf) generate() { | |||||
// Ensure we have XSRF token in the cookie first. | |||||
var token string | |||||
if cookie, err := self.Request.Cookie(xsrfCookieName); err == nil { | |||||
if cookie.Value != "" { | |||||
token = cookie.Value | |||||
} | |||||
} | |||||
if token == "" { | |||||
// Generate a base64-encoded token. | |||||
nano := time.Now().UnixNano() | |||||
hash := hmac.New(sha1.New, []byte(crypto.Random(32))) | |||||
fmt.Fprintf(hash, "%s|%d", crypto.Random(12), nano) | |||||
raw := fmt.Sprintf("%s|%d", hex.EncodeToString(hash.Sum(nil)), nano) | |||||
token = base64.URLEncoding.EncodeToString([]byte(raw)) | |||||
// The max-age directive takes priority over Expires. | |||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html | |||||
cookie := new(http.Cookie) | |||||
cookie.Name = xsrfCookieName | |||||
cookie.Value = token | |||||
cookie.MaxAge = xsrfMaxAge | |||||
cookie.Path = "/" | |||||
cookie.HttpOnly = true | |||||
http.SetCookie(self.ResponseWriter, cookie) | |||||
} | |||||
self.ResponseWriter.Header()[xsrfHeaderName] = []string{token} | |||||
self.token = token | |||||
} | |||||
// --------------------------------------------------------------------------- | |||||
// XSRF Middleware Supports | |||||
// --------------------------------------------------------------------------- | |||||
func XSRF(next http.Handler) http.Handler { | |||||
fn := func(w http.ResponseWriter, r *http.Request) { | |||||
x := new(xsrf) | |||||
x.Request = r | |||||
x.ResponseWriter = w | |||||
x.generate() | |||||
if unsafeMethods.MatchString(r.Method) { | |||||
// Ensure the URL came for "Referer" under HTTPS. | |||||
if !x.checkOrigin() { | |||||
http.Error(w, errXSRFReferer, http.StatusForbidden) | |||||
} | |||||
// length => bytes => issue time checkpoints. | |||||
if !x.checkToken(x.token) { | |||||
http.Error(w, errXSRFToken, http.StatusForbidden) | |||||
} | |||||
} | |||||
// ensure browser will invalidate the cached XSRF token. | |||||
w.Header().Add("Vary", "Cookie") | |||||
next.ServeHTTP(w, r) | |||||
} | |||||
return http.HandlerFunc(fn) | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package rex | |||||
import "net/http" | |||||
type response interface { | |||||
SetStatus(code int) | |||||
SetContentType(contentType string) | |||||
} | |||||
func Render(w http.ResponseWriter) { | |||||
} |
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (C) Copyright 2015 GoAnywhere (http://goanywhere.io). | |||||
* ---------------------------------------------------------------------- | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ----------------------------------------------------------------------*/ | |||||
package rex | |||||
import ( | |||||
"net/http" | |||||
"github.com/gorilla/mux" | |||||
) | |||||
// Vars returns the route variables for the current request, if any. | |||||
func Vars(r *http.Request) map[string]string { | |||||
return mux.Vars(r) | |||||
} |