// Provide case-insensitive matching for URL paths and query strings func pathQStrToLowerMatcherFunc(router *mux.Router, routepath string, querystrings []querystring, requiredQsCount int) func(req *http.Request, rt *mux.RouteMatch) bool { return func(req *http.Request, rt *mux.RouteMatch) bool { pathok, qstrok := false, false // case-insensitive paths if strings.HasPrefix(strings.ToLower(req.URL.Path), strings.ToLower(routepath)) { logger.WriteDebug("PATH: %s matches route path: %s", req.URL.Path, routepath) pathok = true } //case-insensitive query strings // not all API routes will make use of query strings if len(querystrings) == 0 { qstrok = true } else { qry := req.URL.Query() truecount := 0 for key := range qry { logger.WriteDebug("URL query string key is: %s", key) for _, qs := range querystrings { if strings.EqualFold(key, qs.name) && qs.required { logger.WriteDebug("KEY: %s matches query string: %s", key, qs.name) truecount++ break } } } if truecount == requiredQsCount { qstrok = true } } return pathok && qstrok } }
// GetIDsAPIQuery Retrieves the server ID numbers, hosts, and game name for a given // set of hosts (represented by query string values) from the server database // file in response to a query from the API. Sends the results over a DbServerID // channel for consumption. func (sdb *SDB) GetIDsAPIQuery(result chan *models.DbServerID, hosts []string) { m := &models.DbServerID{} for _, h := range hosts { logger.WriteDebug("DB: GetIDsAPIQuery, host: %s", h) rows, err := sdb.db.Query( "SELECT server_id, host, game FROM servers WHERE host LIKE ?", fmt.Sprintf("%%%s%%", h)) if err != nil { logger.LogAppErrorf( "GetIDsAPIQuery: Error querying database to retrieve ID for host %s: %s", h, err) return } defer rows.Close() var id int64 host, game := "", "" for rows.Next() { sid := models.DbServer{} if err := rows.Scan(&id, &host, &game); err != nil { logger.LogAppErrorf( "GetIDsAPIQuery: Error querying database to retrieve ID for host %s: %s", h, err) return } sid.ID = id sid.Host = host sid.Game = game m.Servers = append(m.Servers, sid) } } m.ServerCount = len(m.Servers) result <- m }
// DirectQuery allows a user to query any host even if it is not in the internal // server ID database. It is primarily intended for testing as it has two main // issues: 1) obvious security implications, 2) determining which game a user- // supplied host represents rests on potentially unreliable assumptions, which if // not true would cause games with incomplete support for all three A2S queries // (e.g. Reflex) to always fail. A production environment should use Query() instead. func DirectQuery(hosts []string) (*models.APIServerList, error) { hg := make(map[string]filters.Game, len(hosts)) // Try to account for the fact that we can't determine the game ahead of time // for user-specified direct host queries -- a number of assumptions: // (1) A2S_INFO for game/host, (2) extra data A2S_INFO flag & field w/ appid, //(3) game has been defined in game.go with the correct AppID and A2S ignore flags info := batchInfoQuery(hosts) needsRules := make([]string, len(hosts)) needsPlayers := make([]string, len(hosts)) for _, h := range hosts { logger.WriteDebug("direct query for %s. will try to figure out needed queries", h) if (info[h] != models.SteamServerInfo{}) { logger.WriteDebug("A2S_INFO not empty. got gameid: %d", info[h].ExtraData.GameID) fg := filters.GetGameByAppID(info[h].ExtraData.GameID) hg[h] = fg if !fg.IgnoreRules { logger.WriteDebug("based on game %s for %s, will need to get A2S_RULES", fg.Name, h) needsRules = append(needsRules, h) } if !fg.IgnorePlayers { logger.WriteDebug("based on game %s for %s, will need to get A2S_PLAYERS", fg.Name, h) needsPlayers = append(needsPlayers, h) } } else { logger.WriteDebug("A2S_INFO is nil. game will be unspecified; results may vary") hg[h] = filters.GameUnspecified } } data := a2sData{ HostsGames: hg, Info: info, Rules: batchRuleQuery(needsRules), Players: batchPlayerQuery(needsPlayers), } sl, err := buildServerList(data, true) if err != nil { return models.GetDefaultServerList(), logger.LogAppError(err) } return sl, nil }
func queryServerAddrs(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") if !config.Config.WebConfig.AllowDirectUserQueries { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `{"error": {"code": 400,"message": "Direct server queries are disabled. Use the %s parameter."}}`, qsQueryServerIDs) return } addresses := getQStringValues(r.URL.Query(), qsQueryServerAddrs) logger.WriteDebug("addresses length: %d", len(addresses)) logger.WriteDebug("addresses are: %s", addresses) if addresses == nil { w.WriteHeader(http.StatusOK) logger.WriteDebug("queryServerAddr: Got empty address query. Ignoring.") writeJSONResponse(w, models.GetDefaultServerList()) return } var parsedaddresses []string for _, addr := range addresses { host, err := net.ResolveTCPAddr("tcp4", addr) if err != nil { continue } parsedaddresses = append(parsedaddresses, fmt.Sprintf("%s:%d", host.IP, host.Port)) } if len(parsedaddresses) == 0 { w.WriteHeader(http.StatusOK) logger.WriteDebug("queryServerAddr: No valid addresses for query. Ignoring.") writeJSONResponse(w, models.GetDefaultServerList()) return } if len(parsedaddresses) > config.Config.WebConfig.MaximumHostsPerAPIQuery { logger.WriteDebug("Maximum number of allowed API query hosts exceeded, truncating") parsedaddresses = parsedaddresses[:config.Config.WebConfig.MaximumHostsPerAPIQuery] } queryServerAddrRetriever(w, parsedaddresses) }
func queryServerIDs(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") ids := getQStringValues(r.URL.Query(), qsQueryServerIDs) logger.WriteDebug("queryServerID: ids length: %d", len(ids)) logger.WriteDebug("queryServerID: ids are: %s", ids) if ids == nil { w.WriteHeader(http.StatusOK) logger.WriteDebug("queryServerID: Got empty query. Ignoring.") writeJSONResponse(w, models.GetDefaultServerList()) return } if len(ids) > config.Config.WebConfig.MaximumHostsPerAPIQuery { logger.WriteDebug("Maximum number of allowed API query hosts exceeded, truncating") ids = ids[:config.Config.WebConfig.MaximumHostsPerAPIQuery] } queryServerIDRetriever(w, ids) }
// StartMasterRetrieval starts a timed retrieval of servers specified by a given // filter from the Steam Master server after an initial delay of initialDelay // seconds. It retrieves the list every timeBetweenQueries seconds thereafter. // A bool can be sent to the stop channel to cancel all timed retrievals. func StartMasterRetrieval(stop chan bool, filter filters.Filter, initialDelay int, timeBetweenQueries int) { retrticker := time.NewTicker(time.Duration(timeBetweenQueries) * time.Second) logger.WriteDebug( "Waiting %d seconds before grabbing %s servers. Will retrieve servers every %d secs afterwards.", initialDelay, filter.Game.Name, timeBetweenQueries) logger.LogAppInfo( "Waiting %d seconds before grabbing %s servers from master. Will retrieve every %d secs afterwards.", initialDelay, filter.Game.Name, timeBetweenQueries) firstretrieval := time.NewTimer(time.Duration(initialDelay) * time.Second) <-firstretrieval.C logger.WriteDebug("Starting first retrieval of %s servers from master.", filter.Game.Name) sl, err := retrieve(filter) if err != nil { logger.LogAppErrorf("Error when performing timed master retrieval: %s", err) } models.MasterList = sl for { select { case <-retrticker.C: go func(filters.Filter) { logger.WriteDebug("%s: Starting %s master server query", time.Now().Format( "Mon Jan 2 15:04:05 2006 EST"), filter.Game.Name) logger.LogAppInfo("%s: Starting %s master server query", time.Now().Format( "Mon Jan 2 15:04:05 2006 EST"), filter.Game.Name) sl, err := retrieve(filter) if err != nil { logger.LogAppErrorf("Error when performing timed master retrieval: %s", err) } models.MasterList = sl }(filter) case <-stop: retrticker.Stop() return } } }
func getServerIDs(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") hosts := getQStringValues(r.URL.Query(), qsGetServerIDs) for _, v := range hosts { logger.WriteDebug("host slice values: %s", v) // basically require at least 2 octets if len(v) < 4 { w.WriteHeader(http.StatusBadRequest) writeJSONResponse(w, models.GetDefaultServerID()) return } } getServerIDRetriever(w, hosts) }
func getServers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") var asl *models.APIServerList if config.Config.DebugConfig.ServerDumpFileAsMasterList { asl = useDumpFileAsMasterList(constants.DumpFileFullPath( config.Config.DebugConfig.ServerDumpFilename)) } else { asl = models.MasterList } // Empty (i.e. during first retrieval/startup) if asl == nil { writeJSONResponse(w, models.GetDefaultServerList()) return } srvfilters := getSrvFilterFromQString(r.URL.Query(), getServersQueryStrings) logger.WriteDebug("server list will be filtered with: %v", srvfilters) list := filterServers(srvfilters, asl) writeJSONResponse(w, list) }
func getServersWeb(filter filters.Filter) ([]string, error) { var fsl []string for _, f := range filter.Filters { fsl = append(fsl, string(f)) } filterStr := strings.Join(fsl, "") response, err := http.Get(steamWebAPIURL(config.Config.SteamConfig.SteamWebAPIKey, filterStr, config.Config.SteamConfig.MaximumHostsToReceive)) if err != nil { return nil, err } defer response.Body.Close() var webAPIResponseModel webGameServerList var servers []string apiResult := json.NewDecoder(response.Body) if err := apiResult.Decode(&webAPIResponseModel); err != nil { logger.WriteDebug("Error decoding Steam Web API response: %s", err) return nil, err } for _, server := range webAPIResponseModel.Response.Servers { servers = append(servers, server.Addr) } return servers, 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 }
func buildServerList(data a2sData, addtoServerDB bool) (*models.APIServerList, error) { // Cannot ignore all three requests for _, g := range data.HostsGames { if g.IgnoreInfo && g.IgnorePlayers && g.IgnoreRules { return nil, logger.LogAppErrorf("Cannot ignore all three A2S_ requests!") } } successcount := 0 var success bool srvDBhosts := make(map[string]string, len(data.HostsGames)) sl := &models.APIServerList{ Servers: make([]models.APIServer, 0), FailedServers: make([]string, 0), } for host, game := range data.HostsGames { info, iok := data.Info[host] players, pok := data.Players[host] if players == nil { // return empty array instead of nil pointers (null) in json players = make([]models.SteamPlayerInfo, 0) } rules, rok := data.Rules[host] success = iok && rok && pok if game.IgnoreInfo { success = pok && rok } if game.IgnorePlayers { success = iok && rok } if game.IgnoreRules { rules = make(map[string]string, 0) success = iok && pok } if game.IgnoreInfo && game.IgnorePlayers { success = rok } if game.IgnoreInfo && game.IgnoreRules { success = pok } if game.IgnorePlayers && game.IgnoreRules { success = iok } if success { srv := models.APIServer{ Game: game.Name, Players: players, FilteredPlayers: removeBuggedPlayers(players), Rules: rules, Info: info, } // Gametype support: gametype can be found in rules, info, or not // at all depending on the game (currently just for QuakeLive & Reflex) srv.Info.GameTypeShort, srv.Info.GameTypeFull = getGameType(game, srv) ip, port, serr := net.SplitHostPort(host) if serr == nil { srv.IP = ip srv.Host = host p, perr := strconv.Atoi(port) if perr == nil { srv.Port = p } if !strings.EqualFold(game.Name, filters.GameUnspecified.String()) { srvDBhosts[host] = game.Name } loc := make(chan models.DbCountry, 1) go db.CountryDB.GetCountryInfo(loc, ip) srv.CountryInfo = <-loc } sl.Servers = append(sl.Servers, srv) successcount++ } else { sl.FailedServers = append(sl.FailedServers, host) } } sl.RetrievedAt = time.Now().Format("Mon Jan 2 15:04:05 2006 EST") sl.RetrievedTimeStamp = time.Now().Unix() sl.ServerCount = len(sl.Servers) sl.FailedCount = len(sl.FailedServers) if len(srvDBhosts) != 0 { go db.ServerDB.AddServersToDB(srvDBhosts) sl.Servers = setServerIDsForList(sl.Servers) } logger.LogAppInfo( "Successfully queried (%d/%d) servers. %d timed out or otherwise failed.", successcount, len(data.HostsGames), sl.FailedCount) logger.WriteDebug("Server Queries: Successful: (%d/%d) servers\tFailed: %d servers", successcount, len(data.HostsGames), sl.FailedCount) return sl, nil }