package main //go:generate go-bindata -o assets/assets_gen.go -pkg assets public/... import ( "container/list" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net" "net/http" "os" "os/user" "path/filepath" "strings" "sync" "time" "github.com/gabriel-vasile/mimetype" "varia.zone/mist-connections/assets" ) func init() { // silence underlying http logger which complains about the awful hacks we // use to override the request/response handler. this keeps the command-line // free from endless spurious logger warnings log.SetOutput(ioutil.Discard) } const help = `mist connections A missed connection is a type of personal advertisement which arises after two people meet but are too shy or otherwise unable to exchange contact details. Options: -n misty identifier (default: your system username) -p path to serve (default: current working directory) -h output help ` const indexPagePortFlag = 1312 const homePagePortFlag = 5555 var helpFlag bool var mistyID string var sharePath string func main() { systemUser, err := user.Current() if err != nil { log.Fatal(err) } handleCliFlags(systemUser.Name) sharePathAbs, err := filepath.Abs(sharePath) if err != nil { log.Fatal(err) } if helpFlag { fmt.Printf(help) os.Exit(0) } conf := &config{ MistyID: mistyID, IndexPagePort: indexPagePortFlag, HomePagePort: homePagePortFlag, SharePath: sharePathAbs, } serve(conf) } func handleCliFlags(defaultUsername string) { flag.StringVar(&mistyID, "n", defaultUsername, "misty identifier") flag.StringVar(&sharePath, "p", ".", "path to serve") flag.BoolVar(&helpFlag, "h", false, "output help") flag.Parse() } type config struct { MistyID string `json:"mistyID"` IndexPagePort int `json:"webPort"` HomePagePort int `json:"filePort"` SharePath string `json:"sharePath"` } type announcer struct { conf *config } type nodeInfo struct { MistyID string `json:"mistyID"` Addr string `json:"addr"` IndexPagePort int `json:"webPort"` LastMulticast int64 `json:"lastMulticast"` } var ( nodeMutex sync.Mutex ) func newAnnouncePacket(n *nodeInfo) (string, error) { jsonMessage, err := json.Marshal(n) if err != nil { return "", err } message := fmt.Sprintf("%s%s%s", header, nodeAnnounceCommand, jsonMessage) return message, nil } func announceNode(nodeInfo *nodeInfo) { address, err := net.ResolveUDPAddr("udp", multicastAddress) if err != nil { return } conn, err := net.DialUDP("udp", nil, address) if err != nil { return } for { message, err := newAnnouncePacket(nodeInfo) if err != nil { log.Fatal(err) } conn.Write([]byte(message)) time.Sleep(announceIntervalSec * time.Second) } } const ( multicastAddress = "239.6.6.6:1337" multicastBufferSize = 4096 nodeAnnounceCommand = "\x01" header = "\x60\x0D\xF0\x0D" minPackageSize = 6 expireTimeoutSec = 15 announceIntervalSec = 10 ) func announcedNodeHandler(ninfo *nodeInfo, nodeList *list.List) { nodeMutex.Lock() updateNodeList(ninfo, nodeList) nodeMutex.Unlock() } func updateNodeList(ninfo *nodeInfo, nodeList *list.List) { nodeExists := false for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*nodeInfo) if tmp.MistyID == ninfo.MistyID { tmp.LastMulticast = time.Now().Unix() nodeExists = true break } } for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*nodeInfo) if isNodeExpired(tmp, expireTimeoutSec) { nodeList.Remove(el) } } if !nodeExists { ninfo.LastMulticast = time.Now().Unix() nodeList.PushBack(ninfo) } } func isNodeExpired(nodeInfo *nodeInfo, timeout int) bool { diff := time.Now().Unix() - nodeInfo.LastMulticast return diff > int64(timeout) } func parseAnnouncePacket(size int, addr *net.UDPAddr, packet []byte) (*nodeInfo, error) { if size <= minPackageSize { return nil, fmt.Errorf("Invalid packet size") } if strings.Compare(string(packet[0:len(header)]), header) != 0 { return nil, fmt.Errorf("Invalid packet header") } if string(packet[len(header):len(header)+1]) != nodeAnnounceCommand[0:] { return nil, fmt.Errorf("Command different than nodeAnnounceCommand") } payload := string(packet[len(header)+1:]) payload = strings.Trim(payload, "\x00") nodeInfo := &nodeInfo{} err := json.Unmarshal([]byte(payload), nodeInfo) nodeInfo.Addr = addr.IP.String() nodeInfo.MistyID = fmt.Sprintf("%s", nodeInfo.MistyID) if err != nil { return nil, err } return nodeInfo, nil } func listenForNodes(nodeList *list.List) { address, err := net.ResolveUDPAddr("udp", multicastAddress) if err != nil { return } conn, err := net.ListenMulticastUDP("udp", nil, address) if err != nil { return } conn.SetReadBuffer(multicastBufferSize) for { packet := make([]byte, multicastBufferSize) size, udpAddr, err := conn.ReadFromUDP(packet) if err != nil { log.Fatal(err) } nodeInfo, err := parseAnnouncePacket(size, udpAddr, packet) if err != nil { log.Fatal(err) } go announcedNodeHandler(nodeInfo, nodeList) } } func (a *announcer) Start(nodeList *list.List) { nodeInfo := &nodeInfo{ MistyID: a.conf.MistyID, Addr: "", IndexPagePort: a.conf.IndexPagePort, LastMulticast: 0, } go announceNode(nodeInfo) go listenForNodes(nodeList) } func startAnnouncer(conf *config, nodeList *list.List) { announcer := &announcer{conf: conf} announcer.Start(nodeList) } func serve(conf *config) { nodeList := list.New() go startAnnouncer(conf, nodeList) go fileServe(conf) go dashboardServe(conf, nodeList) fmt.Printf("getting misty @ http://localhost:%v :: %s\n", conf.HomePagePort, conf.SharePath) select {} // hack to hang here forever until ctrl-c } var topHTML = ` ` var bottomHTML = `
Back Home ` func fileServe(conf *config) { fs := http.FileServer(http.Dir(conf.SharePath)) var handler http.HandlerFunc handler = func(w http.ResponseWriter, r *http.Request) { r.Header.Del("If-Modified-Since") // squash cache fpath := filepath.Join(conf.SharePath, r.RequestURI) mtype, _ := mimetype.DetectFile(fpath) renderHTML := mtype.String() == "application/octet-stream" if renderHTML { w.Write([]byte(topHTML)) } fs.ServeHTTP(w, r) if renderHTML { w.Write([]byte(bottomHTML)) } } address := fmt.Sprintf("0.0.0.0:%v", conf.IndexPagePort) http.ListenAndServe(address, handler) } func nodesHandler(nodeList *list.List) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { nodes := make([]*nodeInfo, 0) for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*nodeInfo) nodes = append(nodes, tmp) } data, err := json.Marshal(nodes) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") w.Write(data) } } func dashboardServe(conf *config, nodeList *list.List) { mux := http.NewServeMux() mux.Handle("/", http.FileServer(assets.AssetFS())) mux.HandleFunc("/api/nodes", nodesHandler(nodeList)) address := fmt.Sprintf("%s:%v", "0.0.0.0", conf.HomePagePort) err := http.ListenAndServe(address, mux) if err != nil { log.Fatal(err) } }