func (a *NPServer) Start() error { // You can't use property, err := sth, because golang will qq // This thing binds a classic TCP listener to the server var err error a.listener, err = net.Listen("tcp", environment.Env.Config.NP.BindingAddress) if err != nil { logger.Fatalf("Error listening; %s", err) } defer a.listener.Close() logger.Infof("Serving NP server on %s", environment.Env.Config.NP.BindingAddress) var tempDelay time.Duration for { conn, e := a.listener.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } logger.Errorf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } // Seperated it from the listener code to make it clean go a.HandleConnection(conn) } return nil }
func HandleCI25( conn net.Conn, connection_data *structs.ConnData, packet_data *structs.PacketData, stringParts []string, ) error { // Parse the token detection_id, guid, err := parseToken25(stringParts) if err != nil { return err } logger.Infof("DETECTION FROM %s: %d", connection_data.Username, detection_id) // We already know that the user is unclean, don't waste time handling it. if connection_data.IsUnclean { return nil } // check the hwid for bans if isHWIDBanned(guid) { detection_id = 10100 } // If reason equals NOT_DETECTED, then it's a check if detection_id == NOT_DETECTED || detection_id == 41009 { // Client is checked, allow it to connect connection_data.LastCI = time.Now() // Append the GUID err = AppendHWID(guid, structs.NpidToId(connection_data.Npid), false) if err != nil { return err } } else { // Set the connection to unclean connection_data.IsUnclean = true // Append the GUID err = AppendHWID(guid, structs.NpidToId(connection_data.Npid), true) if err != nil { return err } if environment.Env.Config.NP.AnticheatInstant { // Ban that user err = utils.BanUser(connection_data.Username, detection_id, time.Hour*24*14) if err != nil { return err } // Kick him! if connection_data.ServerId != 0 { err = utils.KickUser(connection_data.ServerId, connection_data.Npid, detection_id) } } else { // Add a delayed ban err = utils.AddDelayedBan(connection_data.Npid, detection_id, guid) if err != nil { return err } } } return nil }
func (a *HTTPServer) Start() { // Create a new token m := martini.Classic() // GET / - version info m.Get("/", func() string { // config.VERSION is a const containing the version string return "aiw3/np-server " + config.VERSION }) // Remote kick function. Secure before using. /*m.Get("/rkick/:name", func(params martini.Params) string { var rows []*struct { Id int } err := environment.Env.Database.Query(` SELECT id FROM misago_user WHERE username = ?`, params["name"]).Rows(&rows) if err != nil { return err.Error() } if len(rows) != 1 { return "User not found" } npid := structs.IdToNpid(rows[0].Id) connection := storage.GetClientConnection(npid) if connection == nil { return "Not connected" } connection.IsUnclean = true if connection.ServerId != 0 { err = utils.KickUser(connection.ServerId, npid, 10001) if err != nil { return "Cannot kick user" } return "User marked as unclean and kicked" } else { // ehmehgehrd spam with packets for _, server := range storage.Servers { if server != nil && server.Valid { utils.KickUser(server.Npid, npid, 10001) } } } return "User not connected to a server; marked as unclean" })*/ // GET /authenticate - remauth replacement m.Post("/authenticate", func(r *http.Request) string { // Read all data from the body into a variable // It might be insecure and cause high memory usage if anyone exploits it body, err := ioutil.ReadAll(r.Body) if err != nil { logger.Error(err) return GenerateResponse(false, "Error while reading from POST body", 0, "Anonymous", "*****@*****.**", "0") } // As specified in the client, "username" (email in our usecase) and password are seperated by && parts := strings.Split(string(body), "&&") // There MUST be two parts or the program will panic! if len(parts) != 2 { return GenerateResponse(false, "Invalid request", 0, "Anonymous", "*****@*****.**", "0") } // Trim newlines from the email and password // In original implementation \0 was also removed, but it seems that golang won't compile a const with \0 email := strings.Trim(parts[0], trim_cutset) password := strings.Trim(parts[1], trim_cutset) // Not sure if it's performant, but that's just like the example in Jet documentation // This struct is used for query result mapping var rows []*struct { Id int Username string Password string Rank int Email string Reason string Expires time.Time } // The larger query seems to be faster than two seperate ones // Might be because Jet's mapper uses reflection which isn't that fast err = environment.Env.Database.Query(` SELECT u.id AS id, u.username AS username, u.password AS password, u.rank_id AS rank, u.email AS email, b.reason_user AS reason, b.expires AS expires FROM misago_user AS u LEFT OUTER JOIN misago_ban AS b ON (b.test = 0 AND b.ban = u.username OR b.ban = u.email) OR (b.test = 1 AND b.ban = u.username) OR b.ban = u.email WHERE u.email = ?`, email).Rows(&rows) // Probably MySQL went down if err != nil { logger.Error(err) return GenerateResponse(false, "Database error", 0, "Anonymous", "*****@*****.**", "0") } // There's no email like that! if len(rows) != 1 { return GenerateResponse(false, "Wrong email or password", 0, "Anonymous", "*****@*****.**", "0") } // Put it here, because we already know if the user is banned. // TODO: Escape #s, either here or in the GenerateResponse function if rows[0].Reason != "" && rows[0].Expires.After(time.Now()) { return GenerateResponse( false, "Your account is banned. Reason: "+strings.Trim(rows[0].Reason, "\n\r#"), 0, "Anonymous", "*****@*****.**", "0", ) } // I'm too lazy to add the original Django hash implementation. Some people will have to login onto forums, // so Django's auth module can rehash their passwords. if rows[0].Password[:7] != "bcrypt$" { return GenerateResponse( false, "Your account is not compatible with the client. Please log in onto forums.", 0, "Anonymous", "*****@*****.**", "0", ) } // I wonder if the bcrypt password checker is timing-attack proof err = bcrypt.CompareHashAndPassword([]byte(rows[0].Password[7:]), []byte(password)) if err != nil { return GenerateResponse(false, "Wrong email or password", 0, "Anonymous", "*****@*****.**", "0") } // Unidecode the username username := unidecode.Unidecode(rows[0].Username) token := uniuri.NewLen(20) existing, err := environment.Env.Redis.Keys("session:" + strconv.Itoa(rows[0].Id) + ":*").Result() if err != nil { logger.Warning(err) return GenerateResponse(false, "Error while accessing session service", 0, "Anonymous", "*****@*****.**", "0") } if len(existing) > 0 { removed, err := environment.Env.Redis.Del(existing...).Result() if err != nil { logger.Warning(err) return GenerateResponse(false, "Error while accessing session service", 0, "Anonymous", "*****@*****.**", "0") } logger.Debugf("Removed %d old sessions while authenticating %s", removed, username) } status, err := environment.Env.Redis.Set("session:"+strconv.Itoa(rows[0].Id)+":"+token, structs.StripPort(r.RemoteAddr)+";"+strconv.Itoa(rows[0].Rank)+";"+username).Result() if err != nil { logger.Warning(err) return GenerateResponse(false, "Error while accessing session service", 0, "Anonymous", "*****@*****.**", "0") } if status != "OK" { logger.Warning("Authentication of %s failed. Redis returned %s on set.", username, status) return GenerateResponse(false, "Error while accessing session service", 0, "Anonymous", "*****@*****.**", "0") } // Return a "Success response" return GenerateResponse( true, "Success", rows[0].Id, strings.Trim(username, "\n\r#"), rows[0].Email, strconv.Itoa(rows[0].Id)+":"+token, ) }) // Standard HTTP serving method logger.Infof("Serving auth server on %s", environment.Env.Config.HTTP.BindingAddress) http.ListenAndServe(environment.Env.Config.HTTP.BindingAddress, m) }
func main() { // Add the Stdout logger logger.AddOutput(logger.Stdout{ MinLevel: logger.ERROR, //logger.DEBUG, Colored: true, }) // Load settings from config.toml in working directory settings := config.Load("./config.toml") /*logger.AddOutput(&logger.File{ MinLevel: logger.WARNING, Path: "./server.log", })*/ // Load the aCI3 key err := aci.LoadKey(settings.NP.AnticheatKeyPath) if err != nil { logger.Fatalf("Cannot load aCI3 key; %s", err) } else { logger.Infof("Loaded aCI3 key") } // Start the NewRelic client if it's enabled in the config file if settings.NewRelic.Enabled { agent := gorelic.NewAgent() agent.Verbose = settings.NewRelic.Verbose agent.NewrelicName = settings.NewRelic.Name agent.NewrelicLicense = settings.NewRelic.License agent.Run() } // Generate a Jet database connector. // Here, err shows if the connection string syntax is valid. // The actual creds are checked during the first query. database, err := jet.Open( settings.Database.Driver, settings.Database.ConnectionString, ) defer database.Close() if err != nil { logger.Fatalf("Cannot connect to database; %s", err) } // But Redis connects here! As far as I know, autoreconnect is implemented cache := redis.NewTCPClient(&redis.Options{ Addr: settings.Redis.Address, Password: settings.Redis.Password, DB: int64(settings.Redis.Database), }) defer cache.Close() // Set up a new environment object environment.SetEnvironment(&environment.Environment{ Config: settings, Database: database, Redis: cache, }) // This thing is massive // The main network platform server that game connects to if settings.NP.Enabled { np_server := np.New() go np_server.Start() } // HTTP-based remote authentication form and a simple API // Is supposed to replace half of 3k if settings.HTTP.Enabled { http_server := http.New() go http_server.Start() } /*if settings.PlayerLog.Enabled { playerlog_server := playerlog.Init(settings.PlayerLog) go playerlog_server.Start() } if settings.Misc.Enabled { misc_server := misc.Init(settings.Misc) go misc_server.Start() }*/ // select{} stops execution of the program until all goroutines close // TODO: add panic recovery! if settings.NP.Enabled || settings.PlayerLog.Enabled || settings.Misc.Enabled || settings.HTTP.Enabled { select {} } }