<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 toolkit for modular web development in Golang, designed to work directly with net/http. | |||||
Rex is a library for modular web development in [Go](http://golang.org/), designed to work directly with net/http. | |||||
<img alt="wrk" src="https://raw.githubusercontent.com/goanywhere/rex/assets/images/wrk.png"> | <img alt="wrk" src="https://raw.githubusercontent.com/goanywhere/rex/assets/images/wrk.png"> | ||||
/* ---------------------------------------------------------------------- | |||||
* ______ ___ __ | |||||
* / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||||
* / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||||
* / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||||
* \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||||
* /____/ | |||||
* | |||||
* (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/gzip" | |||||
"compress/zlib" | |||||
"fmt" | |||||
"io" | |||||
"net/http" | |||||
"regexp" | |||||
"strings" | |||||
"github.com/goanywhere/rex/modules/livereload" | |||||
"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, livereload.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 LiveReload(next http.Handler) http.Handler { | |||||
// ONLY run this under debug mode. | |||||
if !env.Bool("DEBUG", true) { | |||||
return next | |||||
} | |||||
livereload.Start() | |||||
fn := func(w http.ResponseWriter, r *http.Request) { | |||||
if r.URL.Path == livereload.URL.WebSocket { | |||||
livereload.ServeWebSocket(w, r) | |||||
} else if r.URL.Path == livereload.URL.JavaScript { | |||||
livereload.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" | |||||
"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 ( | |||||
"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() | |||||
} |