func getServerInfo(host string, timeout int) ([]byte, error) { conn, err := net.DialTimeout("udp", host, time.Duration(timeout)*time.Second) if err != nil { logger.LogSteamError(ErrHostConnection(err.Error())) return nil, ErrHostConnection(err.Error()) } defer conn.Close() conn.SetDeadline(time.Now().Add(time.Duration(timeout-1) * time.Second)) _, err = conn.Write(infoChallengeReq) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } var buf [maxPacketSize]byte numread, err := conn.Read(buf[:maxPacketSize]) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } serverInfo := make([]byte, numread) copy(serverInfo, buf[:numread]) if !bytes.HasPrefix(serverInfo, expectedInfoRespHeader) { logger.LogSteamError(ErrPacketHeader) return nil, ErrPacketHeader } return serverInfo, nil }
func parseRuleInfo(ruleinfo []byte) (map[string]string, error) { if !bytes.HasPrefix(ruleinfo, expectedRuleChunkHeader) { logger.LogSteamError(ErrPacketHeader) return nil, ErrPacketHeader } ruleinfo = bytes.TrimLeft(ruleinfo, headerStr) numrules := int(binary.LittleEndian.Uint16(ruleinfo[1:3])) if numrules == 0 { return nil, ErrNoRules } b := bytes.Split(ruleinfo[3:], []byte{0x00}) m := make(map[string]string) var key string for i, y := range b { if i%2 != 1 { key = strings.TrimRight(string(y), "\x00") } else { m[key] = strings.TrimRight(string(b[i]), "\x00") } } return m, nil }
func getRulesInfo(host string, timeout int) ([]byte, error) { conn, err := net.DialTimeout("udp", host, time.Duration(timeout)*time.Second) if err != nil { logger.LogSteamError(ErrHostConnection(err.Error())) return nil, ErrHostConnection(err.Error()) } conn.SetDeadline(time.Now().Add(time.Duration(timeout-1) * time.Second)) defer conn.Close() _, err = conn.Write(rulesChallengeReq) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } challengeNumResp := make([]byte, maxPacketSize) _, err = conn.Read(challengeNumResp) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } if !bytes.HasPrefix(challengeNumResp, expectedRulesRespHeader) { logger.LogSteamError(ErrChallengeResponse) return nil, ErrChallengeResponse } challengeNum := bytes.TrimLeft(challengeNumResp, headerStr) challengeNum = challengeNum[1:5] request := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x56} request = append(request, challengeNum...) _, err = conn.Write(request) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } var buf [maxPacketSize]byte numread, err := conn.Read(buf[:maxPacketSize]) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } var rulesInfo []byte if bytes.HasPrefix(buf[:maxPacketSize], multiPacketRespHeader) { // handle multi-packet response first := buf[:maxPacketSize] first = first[:numread] rulesInfo, err = handleMultiPacketResponse(conn, first) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } } else { rulesInfo = make([]byte, numread) copy(rulesInfo, buf[:numread]) } return rulesInfo, nil }
func queryMasterServer(conn net.Conn, startaddress string, filter filters.Filter) ([]byte, error) { // Note: the connection is closed by the caller, do not close here, otherwise // Steam will continue to send the first batch of IPs and won't progress to the next batch startaddress = fmt.Sprintf("%s\x00", startaddress) addr := []byte(startaddress) request := []byte{0x31} request = append(request, filter.Region...) request = append(request, addr...) for i, f := range filter.Filters { for _, b := range f { request = append(request, b) } if i == len(filter.Filters)-1 { request = append(request, 0x00) } } _, err := conn.Write(request) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } var buf [maxPacketSize]byte numread, err := conn.Read(buf[:maxPacketSize]) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } masterResponse := make([]byte, numread) copy(masterResponse, buf[:numread]) if !bytes.HasPrefix(masterResponse, expectedMasterRespHeader) { logger.LogSteamError(ErrPacketHeader) return nil, ErrPacketHeader } return masterResponse, nil }
func parsePlayerInfo(unparsed []byte) ([]models.SteamPlayerInfo, error) { if !bytes.HasPrefix(unparsed, expectedPlayerChunkHeader) { logger.LogSteamError(ErrPacketHeader) return nil, ErrPacketHeader } unparsed = bytes.TrimLeft(unparsed, headerStr) numplayers := int(unparsed[1]) if numplayers == 0 { return nil, ErrNoPlayers } players := []models.SteamPlayerInfo{} // index 0 = '44' | 1 = 'numplayers' byte | 2 = player 1 separator byte '00' // | 3 = start of player 1 name; additional player start indexes are player separator + 1 startidx := 3 var b []byte for i := 0; i < numplayers; i++ { if i == 0 { b = unparsed[startidx:] } else { b = b[startidx+1:] } nul := bytes.IndexByte(b, 0x00) name := b[:nul] // string (variable length) score := b[nul+1 : nul+5] // long (4 bytes) duration := b[nul+5 : nul+9] // float (4 bytes) startidx = nul + 9 seconds, timeformatted := getDuration(duration) players = append(players, models.SteamPlayerInfo{ Name: string(name), Score: int32(binary.LittleEndian.Uint32(score)), TimeConnectedSecs: seconds, TimeConnectedTot: timeformatted, }) } return players, nil }
func getPlayerInfo(host string, timeout int) ([]byte, error) { conn, err := net.DialTimeout("udp", host, time.Duration(timeout)*time.Second) if err != nil { logger.LogSteamError(ErrHostConnection(err.Error())) return nil, ErrHostConnection(err.Error()) } defer conn.Close() conn.SetDeadline(time.Now().Add(time.Duration(timeout-1) * time.Second)) _, err = conn.Write(playerChallengeReq) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } challengeNumResp := make([]byte, maxPacketSize) _, err = conn.Read(challengeNumResp) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } if !bytes.HasPrefix(challengeNumResp, expectedPlayerRespHeader) { logger.LogSteamError(ErrChallengeResponse) return nil, ErrChallengeResponse } challengeNum := bytes.TrimLeft(challengeNumResp, headerStr) challengeNum = challengeNum[1:5] request := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x55} request = append(request, challengeNum...) _, err = conn.Write(request) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } var buf [maxPacketSize]byte numread, err := conn.Read(buf[:maxPacketSize]) if err != nil { logger.LogSteamError(ErrDataTransmit(err.Error())) return nil, ErrDataTransmit(err.Error()) } pi := make([]byte, numread) copy(pi, buf[:numread]) return pi, nil }
func getServers(filter filters.Filter) ([]string, error) { maxHosts := config.Config.SteamConfig.MaximumHostsToReceive var serverlist []string var c net.Conn var err error retrieved := 0 addr := "0.0.0.0:0" c, err = net.DialTimeout("udp", masterServerHost, time.Duration(QueryTimeout)*time.Second) if err != nil { logger.LogSteamError(ErrHostConnection(err.Error())) return nil, ErrHostConnection(err.Error()) } defer c.Close() c.SetDeadline(time.Now().Add(time.Duration(QueryTimeout) * time.Second)) for { s, err := queryMasterServer(c, addr, filter) if err != nil { // usually timeout - Valve throttles >30 UDP packets (>6930 servers) per min logger.WriteDebug("Master query error, likely due to Valve throttle/timeout :%s", err) break } // get hosts:ports beginning after header (0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0x0A) ips, total, err := extractHosts(s[6:]) if err != nil { return nil, logger.LogAppErrorf("Error when extracting addresses: %s", err) } retrieved = retrieved + total if retrieved >= maxHosts { logger.LogSteamInfo("Max host limit of %d reached!", maxHosts) logger.WriteDebug("Max host limit of %d reached!", maxHosts) break } logger.LogSteamInfo("%d hosts retrieved so far from master.", retrieved) logger.WriteDebug("%d hosts retrieved so far from master.", retrieved) for _, ip := range ips { serverlist = append(serverlist, ip) } if (serverlist[len(serverlist)-1]) != "0.0.0.0:0" { logger.LogSteamInfo("More hosts need to be retrieved. Last IP was: %s", serverlist[len(serverlist)-1]) logger.WriteDebug("More hosts need to be retrieved. Last IP was: %s", serverlist[len(serverlist)-1]) addr = serverlist[len(serverlist)-1] } else { logger.LogSteamInfo("IP retrieval complete!") logger.WriteDebug("IP retrieval complete!") break } } // remove 0.0.0.0:0 if len(serverlist) != 0 { if serverlist[len(serverlist)-1] == "0.0.0.0:0" { serverlist = serverlist[:len(serverlist)-1] } } return serverlist, nil }
// Handle multi-packet responses for Source engine games. func handleMultiPacketResponse(c net.Conn, firstReceived []byte) ([]byte, error) { // header: 4 bytes, 0xFFFFFFFE (already verified in caller) // ID: 4 bytes, signed // total # of packets: 1 byte, unsigned // current packet #, starts at zero: 1 byte, unsigned // size: 2 bytes, only for Orange Box Engine and Newer, signed // size & CRC32 sum for bzip2 compressed packets; but no longer used since late 2005 // first 4 bytes [0:4] determine if split; we've already determined that it is id := int32(binary.LittleEndian.Uint32(firstReceived[4:8])) total := uint32(firstReceived[8]) curNum := uint32(firstReceived[9]) // note: size won't exist for 4 ancient appids (215,17550,17700,240 w/protocol 7) //size := int16(binary.LittleEndian.Uint16(firstReceived[10:12])) packets := make(map[uint32][]byte, total) packets[0] = firstReceived[12:] var buf [maxPacketSize]byte prevNum := curNum for { if curNum+1 == total { break } numread, err := c.Read(buf[:maxPacketSize]) if err != nil { logger.LogSteamError(ErrMultiPacketTransmit(err.Error())) return nil, ErrMultiPacketTransmit(err.Error()) } packet := buf[:maxPacketSize] packet = packet[:numread] curNum = uint32(packet[9]) if prevNum == curNum { return nil, ErrMultiPacketDuplicate } prevNum = curNum if int32(binary.LittleEndian.Uint32(packet[4:8])) != id { logger.LogSteamError(ErrMultiPacketIDMismatch) return nil, ErrMultiPacketIDMismatch } if uint32(packet[9]) > total { logger.LogSteamError(ErrMultiPacketNumExceeded) return nil, ErrMultiPacketNumExceeded } // skip the header p := make([]byte, numread-12) copy(p, packet[12:]) packets[curNum] = p } // sort packet keys var pnums u32slice for key := range packets { pnums = append(pnums, key) } pnums.Sort() var rules []byte for _, pn := range pnums { rules = append(rules, packets[pn]...) } return rules, nil }
func parseServerInfo(serverinfo []byte) (models.SteamServerInfo, error) { if !bytes.HasPrefix(serverinfo, expectedInfoRespHeader) { logger.LogSteamError(ErrPacketHeader) return models.SteamServerInfo{}, ErrPacketHeader } serverinfo = bytes.TrimLeft(serverinfo, headerStr) // no info (should usually not happen) if len(serverinfo) <= 1 { logger.LogSteamError(ErrNoInfo) return models.SteamServerInfo{}, ErrNoInfo } serverinfo = serverinfo[1:] // 0x49 protocol := int(serverinfo[0]) serverinfo = serverinfo[1:] name := util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(name)+1:] mapname := util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(mapname)+1:] folder := util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(folder)+1:] game := util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(game)+1:] id := int16(binary.LittleEndian.Uint16(serverinfo[:2])) serverinfo = serverinfo[2:] if id >= 2400 && id <= 2412 { return models.SteamServerInfo{}, logger.LogSteamErrorf("The Ship servers are not supported") } players := int16(serverinfo[0]) serverinfo = serverinfo[1:] maxplayers := int16(serverinfo[0]) serverinfo = serverinfo[1:] bots := int16(serverinfo[0]) serverinfo = serverinfo[1:] servertype := string(serverinfo[0]) serverinfo = serverinfo[1:] environment := string(serverinfo[0]) serverinfo = serverinfo[1:] visibility := int16(serverinfo[0]) serverinfo = serverinfo[1:] vac := int16(serverinfo[0]) serverinfo = serverinfo[1:] version := util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(version)+1:] // extra data flags var port int16 var steamid uint64 var sourcetvport int16 var sourcetvname string var keywords string var gameid uint64 edf := serverinfo[0] serverinfo = serverinfo[1:] if edf != 0x00 { if edf&0x80 > 0 { port = int16(binary.LittleEndian.Uint16(serverinfo[:2])) serverinfo = serverinfo[2:] } if edf&0x10 > 0 { steamid = binary.LittleEndian.Uint64(serverinfo[:8]) serverinfo = serverinfo[8:] } if edf&0x40 > 0 { sourcetvport = int16(binary.LittleEndian.Uint16(serverinfo[:2])) serverinfo = serverinfo[2:] sourcetvname = util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(sourcetvname)+1:] } if edf&0x20 > 0 { keywords = util.ReadTillNul(serverinfo) serverinfo = serverinfo[len(keywords)+1:] } if edf&0x01 > 0 { gameid = binary.LittleEndian.Uint64(serverinfo[:8]) serverinfo = serverinfo[len(serverinfo):] } } // format a few ambiguous values if environment == "l" { environment = "Linux" } if environment == "w" { environment = "Windows" } if environment == "m" || environment == "o" { environment = "Mac" } if servertype == "d" { servertype = "dedicated" } if servertype == "l" { servertype = "listen" } if servertype == "p" { servertype = "sourcetv" } return models.SteamServerInfo{ Protocol: protocol, Name: name, Map: mapname, Folder: folder, Game: game, ID: id, Players: players, MaxPlayers: maxplayers, Bots: bots, ServerType: servertype, Environment: environment, Visibility: visibility, VAC: vac, Version: version, ExtraData: models.SteamExtraData{ Port: port, SteamID: steamid, SourceTVPort: sourcetvport, SourceTVName: sourcetvname, Keywords: keywords, GameID: gameid, }, }, nil }