tor-dam

tor distributed announce mechanism (not a dht)
git clone https://git.parazyd.org/tor-dam
Log | Files | Refs | README | LICENSE

main.go (7515B)


      1 package main
      2 
      3 /*
      4  * Copyright (c) 2017-2018 Dyne.org Foundation
      5  * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
      6  *
      7  * This file is part of tor-dam
      8  *
      9  * This program is free software: you can redistribute it and/or modify
     10  * it under the terms of the GNU Affero General Public License as published by
     11  * the Free Software Foundation, either version 3 of the License, or
     12  * (at your option) any later version.
     13  *
     14  * This program is distributed in the hope that it will be useful,
     15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     17  * GNU Affero General Public License for more details.
     18  *
     19  * You should have received a copy of the GNU Affero General Public License
     20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     21  */
     22 
     23 import (
     24 	"encoding/json"
     25 	"flag"
     26 	"log"
     27 	"net/http"
     28 	"os"
     29 	"strconv"
     30 	"sync"
     31 	"time"
     32 
     33 	lib "github.com/parazyd/tor-dam/pkg/damlib"
     34 )
     35 
     36 // ListenAddress controls where our HTTP API daemon is listening.
     37 const ListenAddress = "127.0.0.1:49371"
     38 
     39 type nodeStruct struct {
     40 	Address   string
     41 	Message   string
     42 	Signature string
     43 	Secret    string
     44 	Pubkey    string
     45 	Firstseen int64
     46 	Lastseen  int64
     47 	Valid     int64
     48 }
     49 
     50 var (
     51 	testnet = flag.Bool("t", false, "Mark all new nodes valid initially.")
     52 	ttl     = flag.Int64("ttl", 0, "Set expiry time in minutes (TTL) for nodes.")
     53 	redconf = flag.String("redconf", "/usr/local/share/tor-dam/redis.conf", "Path to redis' redis.conf.")
     54 )
     55 
     56 func postback(rw http.ResponseWriter, data map[string]string, retCode int) error {
     57 	jsonVal, err := json.Marshal(data)
     58 	if err != nil {
     59 		return err
     60 	}
     61 	rw.Header().Set("Content-Type", "application/json")
     62 	rw.WriteHeader(retCode)
     63 	rw.Write(jsonVal)
     64 	return nil
     65 }
     66 
     67 func handlePost(rw http.ResponseWriter, request *http.Request) {
     68 	var ret map[string]string
     69 	var n nodeStruct
     70 
     71 	if request.Method != "POST" || request.Header["Content-Type"][0] != "application/json" {
     72 		ret = map[string]string{"secret": "Invalid request format."}
     73 		if err := postback(rw, ret, 400); err != nil {
     74 			lib.CheckError(err)
     75 		}
     76 		return
     77 	}
     78 
     79 	decoder := json.NewDecoder(request.Body)
     80 	if err := decoder.Decode(&n); err != nil {
     81 		log.Println("Failed decoding request:", err)
     82 		return
     83 	}
     84 
     85 	// Bail out as soon as possible.
     86 	if len(n.Address) == 0 || len(n.Message) == 0 || len(n.Signature) == 0 {
     87 		ret = map[string]string{"secret": "Invalid request format."}
     88 		if err := postback(rw, ret, 400); err != nil {
     89 			lib.CheckError(err)
     90 		}
     91 		return
     92 	}
     93 	if !(lib.ValidateOnionAddress(n.Address)) {
     94 		log.Println("Invalid onion address. Got:", n.Address)
     95 		ret = map[string]string{"secret": "Invalid onion address."}
     96 		if err := postback(rw, ret, 400); err != nil {
     97 			lib.CheckError(err)
     98 		}
     99 		return
    100 	}
    101 
    102 	req := map[string]string{
    103 		"address":   n.Address,
    104 		"message":   n.Message,
    105 		"pubkey":    n.Pubkey,
    106 		"signature": n.Signature,
    107 		"secret":    n.Secret,
    108 	}
    109 
    110 	// First handshake
    111 	if len(n.Message) != 88 && len(n.Secret) != 88 {
    112 		valid, msg := lib.ValidateFirstHandshake(req)
    113 		ret = map[string]string{"secret": msg}
    114 		if valid {
    115 			log.Printf("%s: 1/2 handshake valid.\n", n.Address)
    116 			log.Println("Sending nonce.")
    117 			if err := postback(rw, ret, 200); err != nil {
    118 				lib.CheckError(err)
    119 			}
    120 			return
    121 		}
    122 		log.Printf("%s: 1/2 handshake invalid: %s\n", n.Address, msg)
    123 		// Delete it all from redis.
    124 		_, err := lib.RedisCli.Del(lib.Rctx, n.Address).Result()
    125 		lib.CheckError(err)
    126 		if err := postback(rw, ret, 400); err != nil {
    127 			lib.CheckError(err)
    128 		}
    129 		return
    130 	}
    131 
    132 	// Second handshake
    133 	if len(req["secret"]) == 88 && len(req["message"]) == 88 {
    134 		valid, msg := lib.ValidateSecondHandshake(req)
    135 		ret = map[string]string{"secret": msg}
    136 
    137 		if valid {
    138 			log.Printf("%s: 2/2 handshake valid.\n", n.Address)
    139 			hasConsensus, err := lib.RedisCli.HGet(lib.Rctx, n.Address, "valid").Result()
    140 			lib.CheckError(err)
    141 
    142 			us := request.Host // Assume our name is what was requested as the URL.
    143 			nodemap := make(map[string]map[string]string)
    144 
    145 			if hasConsensus == "1" {
    146 				// The node does have consensus, we'll teach it about the valid
    147 				// nodes we know.
    148 				log.Printf("%s has consensus. Propagating our nodes to it...\n", n.Address)
    149 				nodes, err := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
    150 				lib.CheckError(err)
    151 				for _, i := range nodes {
    152 					if i == n.Address {
    153 						continue
    154 					}
    155 					nodedata, err := lib.RedisCli.HGetAll(lib.Rctx, i).Result()
    156 					lib.CheckError(err)
    157 					if nodedata["valid"] == "1" {
    158 						nodemap[i] = nodedata
    159 						delete(nodemap[i], "secret")
    160 					}
    161 				}
    162 			} else {
    163 				log.Printf("%s does not have consensus. Propagating ourself to it...\n", n.Address)
    164 				// The node doesn't have consensus in the network. We will only
    165 				// teach it about ourself.
    166 				nodedata, err := lib.RedisCli.HGetAll(lib.Rctx, us).Result()
    167 				lib.CheckError(err)
    168 				nodemap[us] = nodedata
    169 				delete(nodemap[us], "secret")
    170 			}
    171 
    172 			nodestr, err := json.Marshal(nodemap)
    173 			lib.CheckError(err)
    174 			comp, err := lib.GzipEncode(nodestr)
    175 			lib.CheckError(err)
    176 			ret = map[string]string{"secret": comp}
    177 			if err := postback(rw, ret, 200); err != nil {
    178 				lib.CheckError(err)
    179 			}
    180 
    181 			lib.PublishToRedis("am", n.Address)
    182 
    183 			return
    184 		}
    185 
    186 		// If we have't returned so far, the handshake is invalid.
    187 		log.Printf("%s: 2/2 handshake invalid.\n", n.Address)
    188 		// Delete it all from redis.
    189 		lib.PublishToRedis("d", n.Address)
    190 		_, err := lib.RedisCli.Del(lib.Rctx, n.Address).Result()
    191 		lib.CheckError(err)
    192 		if err := postback(rw, ret, 400); err != nil {
    193 			lib.CheckError(err)
    194 		}
    195 		return
    196 	}
    197 }
    198 
    199 func pollNodeTTL(interval int64) {
    200 	for {
    201 		log.Println("Polling redis for expired nodes")
    202 		nodes, err := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
    203 		lib.CheckError(err)
    204 		now := time.Now().Unix()
    205 
    206 		for _, i := range nodes {
    207 			res, err := lib.RedisCli.HGet(lib.Rctx, i, "lastseen").Result()
    208 			lib.CheckError(err)
    209 			lastseen, err := strconv.Atoi(res)
    210 			lib.CheckError(err)
    211 
    212 			diff := (now - int64(lastseen)) / 60
    213 			if diff > interval {
    214 				log.Printf("Deleting %s from redis because of expiration\n", i)
    215 				lib.PublishToRedis("d", i)
    216 				lib.RedisCli.Del(lib.Rctx, i)
    217 			}
    218 		}
    219 		time.Sleep(time.Duration(interval) * time.Minute)
    220 	}
    221 }
    222 
    223 // handleElse is a noop for anything that isn't /announce. We don't care about
    224 // other requests (yet).
    225 func handleElse(rw http.ResponseWriter, request *http.Request) {}
    226 
    227 func main() {
    228 	flag.Parse()
    229 	var wg sync.WaitGroup
    230 	if *testnet {
    231 		log.Println("Enabling testnet")
    232 		lib.Testnet = true
    233 	}
    234 
    235 	// Chdir to our working directory.
    236 	if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) {
    237 		err := os.Mkdir(lib.Workdir, 0700)
    238 		lib.CheckError(err)
    239 	}
    240 	err := os.Chdir(lib.Workdir)
    241 	lib.CheckError(err)
    242 
    243 	if _, err := lib.RedisCli.Ping(lib.Rctx).Result(); err != nil {
    244 		// We assume redis is not running. Start it up.
    245 		cmd, err := lib.StartRedis(*redconf)
    246 		defer cmd.Process.Kill()
    247 		lib.CheckError(err)
    248 	}
    249 
    250 	if lib.Testnet {
    251 		log.Println("Will mark all nodes valid by default.")
    252 	}
    253 
    254 	mux := http.NewServeMux()
    255 	mux.HandleFunc("/announce", handlePost)
    256 	mux.HandleFunc("/", handleElse)
    257 	srv := &http.Server{
    258 		Addr:         ListenAddress,
    259 		Handler:      mux,
    260 		ReadTimeout:  30 * time.Second,
    261 		WriteTimeout: 30 * time.Second,
    262 	}
    263 	wg.Add(1)
    264 	go srv.ListenAndServe()
    265 	log.Println("Listening on", ListenAddress)
    266 
    267 	if *ttl > 0 {
    268 		log.Printf("Enabling TTL polling (%d minute expire time).\n", *ttl)
    269 		go pollNodeTTL(*ttl)
    270 	}
    271 
    272 	wg.Wait()
    273 }