| @@ -1,7 +1,7 @@ | |||
| <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"> | |||
| @@ -1,109 +0,0 @@ | |||
| /* ---------------------------------------------------------------------- | |||
| * ______ ___ __ | |||
| * / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||
| * / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||
| * / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||
| * \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||
| * /____/ | |||
| * | |||
| * (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) | |||
| } | |||
| @@ -1,147 +0,0 @@ | |||
| /* ---------------------------------------------------------------------- | |||
| * ______ ___ __ | |||
| * / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||
| * / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||
| * / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||
| * \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||
| * /____/ | |||
| * | |||
| * (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() | |||
| }) | |||
| } | |||
| @@ -1,42 +0,0 @@ | |||
| /* ---------------------------------------------------------------------- | |||
| * ______ ___ __ | |||
| * / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||
| * / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||
| * / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||
| * \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||
| * /____/ | |||
| * | |||
| * (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 | |||
| } | |||
| ) | |||
| @@ -1,79 +0,0 @@ | |||
| /* ---------------------------------------------------------------------- | |||
| * ______ ___ __ | |||
| * / ____/___ / | ____ __ ___ __/ /_ ___ ________ | |||
| * / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \ | |||
| * / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/ | |||
| * \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/ | |||
| * /____/ | |||
| * | |||
| * (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() | |||
| } | |||