344 lines
7.2 KiB
Go
344 lines
7.2 KiB
Go
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 = `
|
|
<html>
|
|
<head>
|
|
<style>
|
|
a { color: magenta; }
|
|
</style>
|
|
</head>
|
|
<body>`
|
|
|
|
var bottomHTML = `
|
|
<hr></hr>
|
|
<a href="../">Back</a>
|
|
<a href="http://localhost:5555">Home</a>
|
|
</body>
|
|
</html>`
|
|
|
|
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)
|
|
}
|
|
}
|