// Parse a UDP byte buffer, return response from udpTracker func parseUDP(buf []byte, addr *net.UDPAddr) ([]byte, error) { // Attempt to grab generic UDP connection fields packet := new(udp.Packet) err := packet.UnmarshalBinary(buf) if err != nil { // Because no transaction ID is present on failure, we must return nil return nil, errUDPHandshake } // Create a udpTracker to handle this client udpTracker := tracker.UDPTracker{TransID: packet.TransID} // Check for maintenance mode if common.Static.Maintenance { // Return tracker error with maintenance message return udpTracker.Error("Maintenance: " + common.Static.StatusMessage), nil } // Action switch // Action 0: Connect if packet.Action == 0 { // Validate UDP udpTracker handshake if packet.ConnID != udpInitID { return udpTracker.Error("Invalid UDP udpTracker handshake"), errUDPHandshake } // Generate a connection ID, which will be expected for this client next call expID := uint64(common.RandRange(1, 1000000000)) // Store this client's address and ID in map udpAddrToID[addr.String()] = expID // Generate connect response connect := udp.ConnectResponse{ Action: 0, TransID: packet.TransID, ConnID: expID, } // Grab bytes from connect response connectBuf, err := connect.MarshalBinary() if err != nil { log.Println(err.Error()) return udpTracker.Error("Could not generate UDP connect response"), errUDPWrite } return connectBuf, nil } // For all udpTracker actions other than connect, we must validate the connection ID for this // address, ensuring it matches the previously set value // Ensure connection ID map contains this IP address expID, ok := udpAddrToID[addr.String()] if !ok { return udpTracker.Error("Client must properly handshake before announce"), errUDPHandshake } // Validate expected connection ID using map if packet.ConnID != expID { return udpTracker.Error("Invalid UDP connection ID"), errUDPHandshake } // Clear this IP from the connection map after 2 minutes // note: this is done to conserve memory and prevent session fixation go func(addr *net.UDPAddr) { <-time.After(2 * time.Minute) delete(udpAddrToID, addr.String()) }(addr) // Action 1: Announce if packet.Action == 1 { // Retrieve UDP announce request from byte buffer announce := new(udp.AnnounceRequest) err := announce.UnmarshalBinary(buf) log.Println(announce) if err != nil { return udpTracker.Error("Malformed UDP announce"), errUDPInteger } // Convert UDP announce to query map query := announce.ToValues() // Check if a proper IP was set, and if not, use the UDP connection address if query.Get("ip") == "0" { query.Set("ip", strings.Split(addr.String(), ":")[0]) } // Trigger an anonymous announce return tracker.Announce(udpTracker, data.UserRecord{}, query), nil } // Action 2: Scrape if packet.Action == 2 { // Generate UDP scrape packet from byte buffer scrape := new(udp.ScrapeRequest) err := scrape.UnmarshalBinary(buf) if err != nil { return udpTracker.Error("Malformed UDP scrape"), errUDPHandshake } // Convert UDP scrape to query map query := scrape.ToValues() // Store IP in query map query.Set("ip", strings.Split(addr.String(), ":")[0]) // Trigger a scrape return tracker.Scrape(udpTracker, query), nil } // No action matched return udpTracker.Error("Invalid action"), errUDPAction }
// Parse incoming HTTP connections before making tracker calls func parseHTTP(w http.ResponseWriter, r *http.Request) { // Create a tracker to handle this client httpTracker := tracker.HTTPTracker{} // Count incoming connections atomic.AddInt64(&common.Static.HTTP.Current, 1) atomic.AddInt64(&common.Static.HTTP.Total, 1) // Add header to identify goat w.Header().Add("Server", fmt.Sprintf("%s/%s", App, Version)) // Store current URL path url := r.URL.Path // Split URL into segments urlArr := strings.Split(url, "/") // If configured, Detect if client is making an API call url = urlArr[1] if url == "api" { // Output JSON w.Header().Add("Content-Type", "application/json") // API enabled if common.Static.Config.API { // API authentication auth := new(api.BasicAuthenticator).Auth(r) if !auth { http.Error(w, api.ErrorResponse("Authentication failed"), 401) return } // Handle API calls, output JSON api.Router(w, r) return } http.Error(w, api.ErrorResponse("API is currently disabled"), 503) return } // Check for maintenance mode if common.Static.Maintenance { // Return tracker error with maintenance message if _, err := w.Write(httpTracker.Error("Maintenance: " + common.Static.StatusMessage)); err != nil { log.Println(err.Error()) } return } // Detect if passkey present in URL var passkey string if len(urlArr) == 3 { passkey = urlArr[1] url = urlArr[2] } // Make sure URL is valid torrent function if url != "announce" && url != "scrape" { if _, err := w.Write(httpTracker.Error("Malformed announce")); err != nil { log.Println(err.Error()) } return } // Verify that torrent client is advertising its User-Agent, so we can use a whitelist if r.Header.Get("User-Agent") == "" { if _, err := w.Write(httpTracker.Error("Your client is not identifying itself")); err != nil { log.Println(err.Error()) } return } client := r.Header.Get("User-Agent") // If configured, verify that torrent client is on whitelist if common.Static.Config.Whitelist { whitelist := new(data.WhitelistRecord).Load(client, "client") if whitelist == (data.WhitelistRecord{}) || !whitelist.Approved { if _, err := w.Write(httpTracker.Error("Your client is not whitelisted")); err != nil { log.Println(err.Error()) } // Block things like browsers and web crawlers, because they will just clutter up the table if strings.Contains(client, "Mozilla") || strings.Contains(client, "Opera") { return } // Insert unknown clients into list for later approval if whitelist == (data.WhitelistRecord{}) { whitelist.Client = client whitelist.Approved = false log.Printf("whitelist: detected new client '%s', awaiting manual approval", client) go whitelist.Save() } return } } // Parse querystring into a Values map query := r.URL.Query() // Check if IP was previously set if query.Get("ip") == "" { // If no IP set, detect and store it in query map query.Set("ip", strings.Split(r.RemoteAddr, ":")[0]) } // Put client in query map query.Set("client", client) // Check if server is configured for passkey announce if common.Static.Config.Passkey && passkey == "" { if _, err := w.Write(httpTracker.Error("No passkey found in announce URL")); err != nil { log.Println(err.Error()) } return } // Validate passkey if needed user := new(data.UserRecord).Load(passkey, "passkey") if common.Static.Config.Passkey && user == (data.UserRecord{}) { if _, err := w.Write(httpTracker.Error("Invalid passkey")); err != nil { log.Println(err.Error()) } return } // Put passkey in query map query.Set("passkey", user.Passkey) // Mark client as HTTP query.Set("udp", "0") // Get user's total number of active torrents seeding := user.Seeding() leeching := user.Leeching() if seeding == -1 || leeching == -1 { if _, err := w.Write(httpTracker.Error("Failed to calculate active torrents")); err != nil { log.Println(err.Error()) } return } // Verify that client has not exceeded this user's torrent limit activeSum := seeding + leeching if user.TorrentLimit < activeSum { msg := fmt.Sprintf("Exceeded active torrent limit: %d > %d", activeSum, user.TorrentLimit) if _, err := w.Write(httpTracker.Error(msg)); err != nil { log.Println(err.Error()) } return } // Tracker announce if url == "announce" { // Validate required parameter input required := []string{"info_hash", "ip", "port", "uploaded", "downloaded", "left"} // Validate required integer input reqInt := []string{"port", "uploaded", "downloaded", "left"} // Check for required parameters for _, r := range required { if query.Get(r) == "" { if _, err := w.Write(httpTracker.Error("Missing required parameter: " + r)); err != nil { log.Println(err.Error()) } return } } // Check for all valid integers for _, r := range reqInt { if query.Get(r) != "" { _, err := strconv.Atoi(query.Get(r)) if err != nil { if _, err := w.Write(httpTracker.Error("Invalid integer parameter: " + r)); err != nil { log.Println(err.Error()) } return } } } // Only allow compact announce if query.Get("compact") == "" || query.Get("compact") != "1" { if _, err := w.Write(httpTracker.Error("Your client does not support compact announce")); err != nil { log.Println(err.Error()) } return } // NOTE: currently, we do not bother using gzip to compress the tracker announce response // This is done for two reasons: // 1) Clients may or may not support gzip in the first place // 2) gzip may actually make announce response larger, as per testing in What.CD's ocelot // Perform tracker announce if _, err := w.Write(tracker.Announce(httpTracker, user, query)); err != nil { log.Println(err.Error()) } return } // Tracker scrape if url == "scrape" { // Check for required parameter info_hash if query.Get("info_hash") == "" { if _, err := w.Write(httpTracker.Error("Missing required parameter: info_hash")); err != nil { log.Println(err.Error()) } return } if _, err := w.Write(tracker.Scrape(httpTracker, query)); err != nil { log.Println(err.Error()) } return } return }