├── .travis.yml ├── Makefile ├── README.md ├── cmd ├── wakeup │ └── main.go └── wakeupbr │ └── main.go ├── go.mod ├── go.sum ├── http ├── http.go └── http_test.go ├── static ├── .jshintrc ├── app.js ├── index.html └── screenshot.png └── wol ├── bridge.go ├── bridge_test.go ├── wol.go └── wol_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - stable 5 | 6 | env: 7 | - GO111MODULE=on 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | XGOARCH := arm 2 | XGOOS := linux 3 | XBINS := $(XGOOS)_$(XGOARCH)/wakeup $(XGOOS)_$(XGOARCH)/wakeupbr 4 | 5 | .PHONY: $(XBINS) 6 | 7 | all: lint test install 8 | 9 | test: 10 | go test ./... 11 | 12 | vet: 13 | go vet ./... 14 | 15 | check-fmt: 16 | bash -c "diff --line-format='%L' <(echo -n) <(gofmt -d -s .)" 17 | 18 | lint: check-fmt vet 19 | 20 | install: 21 | go install ./... 22 | 23 | xinstall: 24 | env GOOS=$(XGOOS) GOARCH=$(XGOARCH) go install ./... 25 | 26 | publish: $(XBINS) 27 | 28 | $(XBINS): 29 | ifndef DEST_PATH 30 | $(error DEST_PATH must be set when publishing) 31 | endif 32 | rsync -a $(GOPATH)/bin/$@ $(DEST_PATH)/$@ 33 | @sha256sum $(GOPATH)/bin/$@ 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wakeup 2 | 3 | [](https://travis-ci.org/mpolden/wakeup) 4 | 5 | `wakeup` provides a small HTTP API and JavaScript front-end for 6 | sending [Wake-on-LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) messages to a 7 | target device. 8 | 9 | ## `wakeup` usage 10 | 11 | ``` 12 | $ wakeup -h 13 | Usage: 14 | wakeup [OPTIONS] 15 | 16 | Application Options: 17 | -c, --cache=FILE Path to cache file 18 | -b, --bind=IP IP address to bind to when sending WOL packets 19 | -l, --listen=ADDR Listen address (default: :8080) 20 | -s, --static=DIR Path to directory containing static assets 21 | 22 | Help Options: 23 | -h, --help Show this help message 24 | ``` 25 | 26 | ## `wakeupbr` usage 27 | 28 | ``` 29 | $ wakeupbr -h 30 | Usage: 31 | wakeupbr [OPTIONS] 32 | 33 | Application Options: 34 | -l, --listen=IP Listen address to use when listening for WOL packets (default: 0.0.0.0:9) 35 | -o, --forward=IP Address of interface where received WOL packets should be forwarded 36 | 37 | Help Options: 38 | -h, --help Show this help message 39 | ``` 40 | 41 | ## API 42 | 43 | Wake a device: 44 | 45 | `$ curl -XPOST -d '{"macAddress":"AB:CD:EF:12:34:56"}' http://localhost:8080/api/v1/wake` 46 | 47 | A name for the device can also be provided, to make it easy to identify later: 48 | 49 | `$ curl -XPOST -d '{"name":"foo","macAddress":"AB:CD:EF:12:34:56"}' http://localhost:8080/api/v1/wake` 50 | 51 | List devices that have previously been woken: 52 | 53 | ``` 54 | $ curl -s http://localhost:8080/api/v1/wake | jq . 55 | { 56 | "devices": [ 57 | { 58 | "name": "foo", 59 | "macAddress": "AB:CD:EF:12:34:56" 60 | } 61 | ] 62 | } 63 | ``` 64 | 65 | Delete a device: 66 | 67 | `$ curl -XDELETE -d '{"macAddress":"AB:CD:EF:12:34:56"}' http://localhost:8080/api/v1/wake` 68 | 69 | ## Front-end 70 | 71 | A basic JavaScript front-end is included. It can be served by `wakeup` by 72 | passing the path to `static` as the `-s` option. 73 | 74 |  75 | 76 | ## Bridge 77 | 78 | The `wakeupbr` program acts as bridge for Wake-on-LAN packets. The program 79 | listens for Wake-on-LAN packets on the incoming interface and forwards any 80 | received packets to the outgoing interface. 81 | 82 | Example: 83 | 84 | A device has two interfaces, one wired (`eth0`) with the address `172.16.0.10` 85 | and one wireless (`wlan0`) with the address `10.0.0.10`. The device we want to 86 | wake is on the wired network. We want to pick up Wake-on-LAN packets that are 87 | received on the wireless network and send them out on the wired network. This 88 | can be accomplished with the following command: 89 | 90 | ``` 91 | $ wakeupbr -l 10.0.0.10 -o 172.16.0.10 92 | ``` 93 | 94 | Any Wake-on-LAN packet that is broadcast on the wireless network will then be 95 | forwarded. When a packet is received and forwarded, a message will be logged: 96 | 97 | ``` 98 | 2017/07/28 19:34:54 Forwarded magic packet for AA:BB:CC:12:34:56 to 172.16.0.10 99 | ``` 100 | 101 | The command above listens on UDP port 9 for Wake-on-LAN packets. As port 9 is a 102 | privileged port, `wakeupbr` must be run as root. This is less than ideal, but 103 | Wake-on-LAN packets are always broadcast to port 9. To avoid binding to a 104 | privileged port we can use a `iptables` rule: 105 | 106 | ``` 107 | $ iptables -t nat -A PREROUTING -i wlan0 -p udp --dport 9 -j REDIRECT --to-port 9000 108 | ``` 109 | 110 | You should also ensure that traffic to UDP port 9000 is accepted by the `INPUT` 111 | chain: 112 | 113 | ``` 114 | $ iptables -A INPUT -p udp --dport 9000 -j ACCEPT 115 | ``` 116 | 117 | This will redirect all packets on UDP port 9 to port 9000. `wakeupbr` can then 118 | listen on port 9000 and run as a regular user: 119 | 120 | ``` 121 | $ wakeupbr -l 10.0.0.10:9000 -o 172.16.0.10 122 | ``` 123 | -------------------------------------------------------------------------------- /cmd/wakeup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | flags "github.com/jessevdk/go-flags" 10 | "github.com/mpolden/wakeup/http" 11 | ) 12 | 13 | func main() { 14 | var opts struct { 15 | CacheFile string `short:"c" long:"cache" description:"Path to cache file" required:"true" value-name:"FILE"` 16 | SourceIP string `short:"b" long:"bind" description:"IP address to bind to when sending WOL packets" value-name:"IP"` 17 | Listen string `short:"l" long:"listen" description:"Listen address" value-name:"ADDR" default:":8080"` 18 | StaticDir string `short:"s" long:"static" description:"Path to directory containing static assets" value-name:"DIR"` 19 | } 20 | _, err := flags.ParseArgs(&opts, os.Args) 21 | if err != nil { 22 | os.Exit(1) 23 | } 24 | 25 | sourceIP := net.ParseIP(opts.SourceIP) 26 | if opts.SourceIP != "" && sourceIP == nil { 27 | log.Fatalf("invalid ip: %s", opts.SourceIP) 28 | } 29 | 30 | server := http.New(opts.CacheFile) 31 | server.StaticDir = opts.StaticDir 32 | server.SourceIP = sourceIP 33 | if strings.HasPrefix(opts.Listen, ":") { 34 | log.Printf("Serving at http://0.0.0.0%s", opts.Listen) 35 | } else { 36 | log.Printf("Serving at http://%s", opts.Listen) 37 | } 38 | if err := server.ListenAndServe(opts.Listen); err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/wakeupbr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | flags "github.com/jessevdk/go-flags" 10 | "github.com/mpolden/wakeup/wol" 11 | ) 12 | 13 | func main() { 14 | var opts struct { 15 | ListenAddr string `short:"l" long:"listen" description:"Listen address to use when listening for WOL packets" value-name:"IP" default:"0.0.0.0:9"` 16 | ForwardAddr string `short:"o" long:"forward" description:"Address of interface where received WOL packets should be forwarded" required:"true" value-name:"IP"` 17 | } 18 | _, err := flags.ParseArgs(&opts, os.Args) 19 | if err != nil { 20 | os.Exit(1) 21 | } 22 | 23 | forwardAddr := net.ParseIP(opts.ForwardAddr) 24 | if forwardAddr == nil { 25 | log.Fatalf("invalid ip: %s", opts.ForwardAddr) 26 | } 27 | 28 | b, err := wol.Listen(opts.ListenAddr) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | for { 33 | sent, err := b.Forward(forwardAddr) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | if sent != nil { 38 | log.Printf("Forwarded magic packet for %s to %s", strings.ToUpper(sent.HardwareAddr().String()), forwardAddr) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mpolden/wakeup 2 | 3 | go 1.13 4 | 5 | require github.com/jessevdk/go-flags v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 2 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 3 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "sort" 13 | "strings" 14 | "sync" 15 | 16 | "github.com/mpolden/wakeup/wol" 17 | ) 18 | 19 | type wakeFunc func(net.IP, net.HardwareAddr) error 20 | 21 | type Server struct { 22 | SourceIP net.IP 23 | StaticDir string 24 | cacheFile string 25 | mu sync.RWMutex 26 | wakeFunc 27 | } 28 | 29 | type Error struct { 30 | err error 31 | Status int `json:"status"` 32 | Message string `json:"message"` 33 | } 34 | 35 | type Devices struct { 36 | Devices []Device `json:"devices"` 37 | } 38 | 39 | type Device struct { 40 | Name string `json:"name,omitempty"` 41 | MACAddress string `json:"macAddress"` 42 | } 43 | 44 | func (d *Devices) add(device Device) { 45 | for _, v := range d.Devices { 46 | if device.MACAddress == v.MACAddress { 47 | return 48 | } 49 | } 50 | d.Devices = append(d.Devices, device) 51 | } 52 | 53 | func (d *Devices) remove(device Device) { 54 | var keep []Device 55 | for _, v := range d.Devices { 56 | if device.MACAddress == v.MACAddress { 57 | continue 58 | } 59 | keep = append(keep, v) 60 | } 61 | d.Devices = keep 62 | } 63 | 64 | func New(cacheFile string) *Server { return &Server{cacheFile: cacheFile, wakeFunc: wol.Wake} } 65 | 66 | func (s *Server) readDevices() (*Devices, error) { 67 | f, err := os.OpenFile(s.cacheFile, os.O_CREATE|os.O_RDONLY, 0644) 68 | if err != nil { 69 | return nil, err 70 | } 71 | defer f.Close() 72 | data, err := ioutil.ReadAll(f) 73 | if err != nil { 74 | return nil, err 75 | } 76 | var i Devices 77 | if len(data) == 0 { 78 | i.Devices = make([]Device, 0) 79 | return &i, nil 80 | } 81 | if err := json.Unmarshal(data, &i); err != nil { 82 | return nil, err 83 | } 84 | if i.Devices == nil { 85 | i.Devices = make([]Device, 0) 86 | } 87 | sort.Slice(i.Devices, func(j, k int) bool { return i.Devices[j].MACAddress < i.Devices[k].MACAddress }) 88 | return &i, nil 89 | } 90 | 91 | func (s *Server) writeDevice(device Device, add bool) error { 92 | i, err := s.readDevices() 93 | if err != nil { 94 | return err 95 | } 96 | f, err := os.OpenFile(s.cacheFile, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) 97 | if err != nil { 98 | return err 99 | } 100 | defer f.Close() 101 | if add { 102 | i.add(device) 103 | } else { 104 | i.remove(device) 105 | } 106 | enc := json.NewEncoder(f) 107 | if err := enc.Encode(i); err != nil && err != io.EOF { 108 | return err 109 | } 110 | return nil 111 | } 112 | 113 | func (s *Server) defaultHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { 114 | defer r.Body.Close() 115 | if r.Method == http.MethodGet { 116 | s.mu.RLock() 117 | defer s.mu.RUnlock() 118 | i, err := s.readDevices() 119 | if err != nil { 120 | return nil, &Error{err: err, Status: http.StatusInternalServerError, Message: "Could not unmarshal JSON"} 121 | } 122 | return i, nil 123 | } 124 | add := r.Method == http.MethodPost 125 | remove := r.Method == http.MethodDelete 126 | if add || remove { 127 | dec := json.NewDecoder(r.Body) 128 | var device Device 129 | if err := dec.Decode(&device); err != nil { 130 | return nil, &Error{Status: http.StatusBadRequest, Message: "Malformed JSON"} 131 | } 132 | if add { 133 | macAddress, err := net.ParseMAC(device.MACAddress) 134 | if err != nil { 135 | return nil, &Error{Status: http.StatusBadRequest, Message: fmt.Sprintf("Invalid MAC address: %s", device.MACAddress)} 136 | } 137 | if err := s.wakeFunc(s.SourceIP, macAddress); err != nil { 138 | return nil, &Error{Status: http.StatusBadRequest, Message: fmt.Sprintf("Failed to wake device with address %s", device.MACAddress)} 139 | } 140 | } 141 | s.mu.Lock() 142 | defer s.mu.Unlock() 143 | if err := s.writeDevice(device, add); err != nil { 144 | return nil, &Error{err: err, Status: http.StatusInternalServerError, Message: "Could not unmarshal JSON"} 145 | } 146 | w.WriteHeader(http.StatusNoContent) 147 | return nil, nil 148 | } 149 | return nil, &Error{ 150 | Status: http.StatusMethodNotAllowed, 151 | Message: fmt.Sprintf("Invalid method %s, must be %s or %s", r.Method, http.MethodGet, http.MethodPost), 152 | } 153 | } 154 | 155 | func notFoundHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { 156 | return nil, &Error{ 157 | Status: http.StatusNotFound, 158 | Message: "Resource not found", 159 | } 160 | } 161 | 162 | type appHandler func(http.ResponseWriter, *http.Request) (interface{}, *Error) 163 | 164 | func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 165 | data, e := fn(w, r) 166 | if e != nil { // e is *Error, not os.Error. 167 | if e.err != nil { 168 | log.Print(e.err) 169 | } 170 | out, err := json.Marshal(e) 171 | if err != nil { 172 | panic(err) 173 | } 174 | w.WriteHeader(e.Status) 175 | w.Write(out) 176 | } else if data != nil { 177 | out, err := json.Marshal(data) 178 | if err != nil { 179 | panic(err) 180 | } 181 | w.Write(out) 182 | } 183 | } 184 | 185 | func requestFilter(next http.Handler) http.Handler { 186 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 187 | if strings.HasPrefix(r.URL.Path, "/api/") { 188 | w.Header().Set("Content-Type", "application/json") 189 | } 190 | next.ServeHTTP(w, r) 191 | }) 192 | } 193 | 194 | func (s *Server) Handler() http.Handler { 195 | mux := http.NewServeMux() 196 | mux.Handle("/api/v1/wake", appHandler(s.defaultHandler)) 197 | // Return 404 in JSON for all unknown requests under /api/ 198 | mux.Handle("/api/", appHandler(notFoundHandler)) 199 | if s.StaticDir != "" { 200 | fs := http.FileServer(http.Dir(s.StaticDir)) 201 | mux.Handle("/", fs) 202 | } 203 | return requestFilter(mux) 204 | } 205 | 206 | func (s *Server) ListenAndServe(addr string) error { 207 | return http.ListenAndServe(addr, s.Handler()) 208 | } 209 | -------------------------------------------------------------------------------- /http/http_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func httpGet(url string) (string, int, error) { 15 | res, err := http.Get(url) 16 | if err != nil { 17 | return "", 0, err 18 | } 19 | defer res.Body.Close() 20 | data, err := ioutil.ReadAll(res.Body) 21 | if err != nil { 22 | return "", 0, err 23 | } 24 | return string(data), res.StatusCode, nil 25 | } 26 | 27 | func httpRequest(method, url, body string) (string, int, error) { 28 | r, err := http.NewRequest(method, url, strings.NewReader(body)) 29 | if err != nil { 30 | return "", 0, err 31 | } 32 | res, err := http.DefaultClient.Do(r) 33 | if err != nil { 34 | return "", 0, err 35 | } 36 | defer res.Body.Close() 37 | data, err := ioutil.ReadAll(res.Body) 38 | if err != nil { 39 | return "", 0, err 40 | } 41 | return string(data), res.StatusCode, nil 42 | } 43 | 44 | func httpPost(url, body string) (string, int, error) { 45 | return httpRequest(http.MethodPost, url, body) 46 | } 47 | 48 | func httpDelete(url, body string) (string, int, error) { 49 | return httpRequest(http.MethodDelete, url, body) 50 | } 51 | 52 | func testServer() (*httptest.Server, string) { 53 | file, err := ioutil.TempFile("", "wakeonlan") 54 | if err != nil { 55 | panic(err) 56 | } 57 | api := Server{ 58 | wakeFunc: func(net.IP, net.HardwareAddr) error { return nil }, 59 | cacheFile: file.Name(), 60 | } 61 | log.SetOutput(ioutil.Discard) 62 | return httptest.NewServer(api.Handler()), file.Name() 63 | } 64 | 65 | func TestRequests(t *testing.T) { 66 | server, cacheFile := testServer() 67 | defer os.Remove(cacheFile) 68 | defer server.Close() 69 | 70 | var tests = []struct { 71 | method string 72 | body string 73 | url string 74 | response string 75 | status int 76 | }{ 77 | // Unknown resources 78 | {"GET", "", "/not-found", "404 page not found\n", 404}, 79 | {"GET", "", "/api/not-found", `{"status":404,"message":"Resource not found"}`, 404}, 80 | // Invalid JSON 81 | {"POST", "", "/api/v1/wake", `{"status":400,"message":"Malformed JSON"}`, 400}, 82 | // Invalid MAC address 83 | {"POST", `{"macAddress":"foo"}`, "/api/v1/wake", `{"status":400,"message":"Invalid MAC address: foo"}`, 400}, 84 | // List devices 85 | {"GET", "", "/api/v1/wake", `{"devices":[]}`, 200}, 86 | // Wake device 87 | {"POST", `{"macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 88 | {"GET", "", "/api/v1/wake", `{"devices":[{"macAddress":"AB:CD:EF:12:34:56"}]}`, 200}, 89 | // Waking same device does not result in duplicates 90 | {"POST", `{"macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 91 | {"GET", "", "/api/v1/wake", `{"devices":[{"macAddress":"AB:CD:EF:12:34:56"}]}`, 200}, 92 | // Delete 93 | {"DELETE", `{"macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 94 | {"GET", "", "/api/v1/wake", `{"devices":[]}`, 200}, 95 | // Add multiple devices 96 | {"POST", `{"macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 97 | {"POST", `{"macAddress":"12:34:56:AB:CD:EF"}`, "/api/v1/wake", "", 204}, 98 | {"GET", "", "/api/v1/wake", `{"devices":[{"macAddress":"12:34:56:AB:CD:EF"},{"macAddress":"AB:CD:EF:12:34:56"}]}`, 200}, 99 | {"DELETE", `{"macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 100 | {"DELETE", `{"macAddress":"12:34:56:AB:CD:EF"}`, "/api/v1/wake", "", 204}, 101 | // Add device with name 102 | {"POST", `{"name":"foo","macAddress":"AB:CD:EF:12:34:56"}`, "/api/v1/wake", "", 204}, 103 | {"GET", "", "/api/v1/wake", `{"devices":[{"name":"foo","macAddress":"AB:CD:EF:12:34:56"}]}`, 200}, 104 | } 105 | 106 | for _, tt := range tests { 107 | var ( 108 | data string 109 | status int 110 | err error 111 | ) 112 | switch tt.method { 113 | case http.MethodGet: 114 | data, status, err = httpGet(server.URL + tt.url) 115 | case http.MethodPost: 116 | data, status, err = httpPost(server.URL+tt.url, tt.body) 117 | case http.MethodDelete: 118 | data, status, err = httpDelete(server.URL+tt.url, tt.body) 119 | default: 120 | t.Fatal("invalid method: " + tt.method) 121 | } 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | if got := status; status != tt.status { 126 | t.Errorf("want status %d for %q, got %d", tt.status, tt.url, got) 127 | } 128 | if got := string(data); got != tt.response { 129 | t.Errorf("want response %q for %s, got %q", tt.response, tt.url, got) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /static/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "esversion": 6, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonew": true, 14 | "plusplus": true, 15 | "quotmark": "single", 16 | "undef": true, 17 | "unused": true, 18 | "trailing": true, 19 | "maxparams": 5, 20 | "maxdepth": 5, 21 | "maxstatements": 20, 22 | "maxcomplexity": 20, 23 | "maxlen": 120, 24 | "browser": true, 25 | "globals": { 26 | "m": true, 27 | "Mousetrap": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | var wol = wol || {}; 2 | 3 | wol.state = { 4 | devices: [], 5 | toWake: { 6 | name: '', 7 | macAddress: '', 8 | setName: function(v) { 9 | wol.state.toWake.name = v; 10 | }, 11 | setMacAddress: function(v) { 12 | wol.state.toWake.macAddress = v; 13 | }, 14 | }, 15 | success: { 16 | timeout: null, 17 | device: {} 18 | }, 19 | error: {}, 20 | wake: function (device) { 21 | if (typeof device !== 'undefined') { 22 | wol.wakeDevice(device); 23 | } else { 24 | // Copy the toWake object here to avoid input values binding 25 | wol.wakeDevice({name: wol.state.toWake.name, 26 | macAddress: wol.state.toWake.macAddress}); 27 | } 28 | }, 29 | remove: function (device) { 30 | wol.removeDevice(device); 31 | }, 32 | add: function (device) { 33 | var exists = wol.state.devices.some(function (d) { 34 | return d.macAddress === device.macAddress; 35 | }); 36 | if (!exists) { 37 | wol.state.devices.push(device); 38 | wol.state.devices.sort(function (a, b) { 39 | if (a.macAddress < b.macAddress) { 40 | return -1; 41 | } 42 | if (a.macAddress > b.macAddress) { 43 | return 1; 44 | } 45 | return 0; 46 | }); 47 | } 48 | wol.state.toWake.setName(''); 49 | wol.state.toWake.setMacAddress(''); 50 | wol.state.error = {}; 51 | }, 52 | setSuccess: function (device) { 53 | wol.state.success.device = device; 54 | // Clear any pending timeout so that timeout is extended for each new 55 | // wake-up 56 | clearTimeout(wol.state.success.timeout); 57 | wol.state.success.timeout = setTimeout(function () { 58 | wol.state.success.device = {}; 59 | m.redraw(); 60 | }, 4000); 61 | } 62 | }; 63 | 64 | wol.getDevices = function() { 65 | m.request({method: 'GET', url: '/api/v1/wake'}) 66 | .then(function (data) { 67 | wol.state.devices = data.devices; 68 | return data; 69 | }, function (data) { 70 | wol.state.error = data; 71 | }); 72 | }; 73 | 74 | wol.wakeDevice = function(device) { 75 | m.request({method: 'POST', url: '/api/v1/wake', data: device}) 76 | .then(function (data) { 77 | wol.state.add(device); 78 | wol.state.setSuccess(device); 79 | return data; 80 | }, function (data) { 81 | wol.state.error = data; 82 | }); 83 | }; 84 | 85 | wol.removeDevice = function (device) { 86 | m.request({method: 'DELETE', url: '/api/v1/wake', data: device}) 87 | .then(function (data) { 88 | wol.state.devices = wol.state.devices.filter(function (d) { 89 | return d.macAddress !== device.macAddress; 90 | }); 91 | return data; 92 | }, function (data) { 93 | wol.state.error = data; 94 | }); 95 | }; 96 | 97 | wol.alertView = function () { 98 | var e = wol.state.error; 99 | var isError = Object.keys(e).length !== 0; 100 | var text = isError ? e.message + ' (' + e.status + ')' : ''; 101 | var cls = 'alert-danger' + (isError ? '' : ' hidden'); 102 | return m('div.alert', {class: cls}, [ 103 | m('span', {class: 'glyphicon glyphicon-exclamation-sign'}), 104 | m('strong', ' Error: '), text 105 | ]); 106 | }; 107 | 108 | wol.successView = function () { 109 | var device = wol.state.success.device; 110 | var isSuccess = Object.keys(device).length !== 0; 111 | var name = device.name ? ' (' + device.name + ')' : ''; 112 | var cls = 'alert-success' + (isSuccess ? '' : ' hidden'); 113 | return m('div.alert', {class: cls}, [ 114 | m('span', {class: 'glyphicon glyphicon-ok'}), 115 | ' Successfully woke ', m('strong', device.macAddress), name 116 | ]); 117 | }; 118 | 119 | wol.devicesView = function () { 120 | var form = m('form', { 121 | id: 'wake-form', 122 | onsubmit: function (e) { 123 | e.preventDefault(); 124 | wol.state.wake(); 125 | } 126 | }); 127 | var firstRow = m('tr', [ 128 | m('td', m('input[type=text]', {'form': form.attrs.id, 129 | onchange: m.withAttr('value', wol.state.toWake.setName), 130 | value: wol.state.toWake.name, 131 | class: 'form-control', 132 | placeholder: 'Name'})), 133 | m('td', m('input[type=text]', {'form': form.attrs.id, 134 | onchange: m.withAttr('value', wol.state.toWake.setMacAddress), 135 | value: wol.state.toWake.macAddress, 136 | class: 'form-control', 137 | placeholder: 'MAC address'})), 138 | m('td', m('button[type=submit]', 139 | {'form': form.attrs.id, class: 'btn btn-success btn-remove'}, 140 | m('span', {class: 'glyphicon glyphicon-off'}))), 141 | m('td') 142 | ]); 143 | var rows = wol.state.devices.map(function (device) { 144 | return m('tr', [ 145 | m('td', device.name || ''), 146 | m('td', m('code', device.macAddress)), 147 | m('td', 148 | m('button[type=button]', {class: 'btn btn-success btn-remove', 149 | onclick: function () { wol.state.wake(device); } }, 150 | m('span', {class: 'glyphicon glyphicon-off'}) 151 | ) 152 | ), 153 | m('td', 154 | m('button[type=button]', {class: 'btn btn-danger btn-remove', 155 | onclick: function () { wol.state.remove(device); } }, 156 | m('span', {class: 'glyphicon glyphicon-remove'}) 157 | ) 158 | ) 159 | ]); 160 | }); 161 | return [form, 162 | m('table.table', {class: ''}, 163 | m('thead', m('tr', [ 164 | m('th', {class: 'col-md-2'}, 'Device name'), 165 | m('th', {class: 'col-md-2'}, 'MAC address'), 166 | m('th', {class: 'col-md-1'}), 167 | m('th', {class: 'col-md-1'}) 168 | ])), 169 | m('tbody', [firstRow].concat(rows)) 170 | )]; 171 | }; 172 | 173 | wol.oncreate = wol.getDevices; 174 | 175 | wol.view = function() { 176 | return m('div.container', [ 177 | m('div.row', 178 | m('div.col-md-6', m('h1', m('span', {class: 'glyphicon glyphicon-flash'}), ' wake-on-lan')) 179 | ), 180 | m('div.row', m('div.col-md-6', wol.alertView())), 181 | m('div.row', m('div.col-md-6', wol.successView())), 182 | m('div.row', m('div.col-md-6', wol.devicesView())) 183 | ]); 184 | }; 185 | 186 | m.mount(document.getElementById('app'), wol); 187 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |