From cf5a426f9073eff85d4256a195c6f7b761758525 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 22 Oct 2020 12:19:19 -0400 Subject: [PATCH 1/2] fix #1346 --- irc/accounts.go | 204 +----------------------------------------------- irc/config.go | 13 +-- irc/database.go | 25 +++++- irc/hostserv.go | 145 +--------------------------------- irc/import.go | 4 +- irc/server.go | 3 - 6 files changed, 35 insertions(+), 359 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index 61e83b79..f5a06d15 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -12,7 +12,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "time" "unicode" @@ -44,23 +43,14 @@ const ( keyAccountModes = "account.modes %s" // user modes for the always-on client as a string keyAccountRealname = "account.realname %s" // client realname stored as string - keyVHostQueueAcctToId = "vhostQueue %s" - vhostRequestIdx = "vhostQueue" - maxCertfpsPerAccount = 5 ) // everything about accounts is persistent; therefore, the database is the authoritative // source of truth for all account information. anything on the heap is just a cache type AccountManager struct { - // XXX these are up here so they can be aligned to a 64-bit boundary, please forgive me - // autoincrementing ID for vhost requests: - vhostRequestID uint64 - vhostRequestPendingCount uint64 - sync.RWMutex // tier 2 serialCacheUpdateMutex sync.Mutex // tier 3 - vHostUpdateMutex sync.Mutex // tier 3 server *Server // track clients logged in to accounts @@ -80,7 +70,6 @@ func (am *AccountManager) Initialize(server *Server) { config := server.Config() am.buildNickToAccountIndex(config) - am.initVHostRequestQueue(config) am.createAlwaysOnClients(config) am.resetRegisterThrottle(config) } @@ -225,44 +214,6 @@ func (am *AccountManager) buildNickToAccountIndex(config *Config) { } } -func (am *AccountManager) initVHostRequestQueue(config *Config) { - if !config.Accounts.VHosts.Enabled { - return - } - - am.vHostUpdateMutex.Lock() - defer am.vHostUpdateMutex.Unlock() - - // the db maps the account name to the autoincrementing integer ID of its request - // create an numerically ordered index on ID, so we can list the oldest requests - // finally, collect the integer id of the newest request and the total request count - var total uint64 - var lastIDStr string - err := am.server.store.Update(func(tx *buntdb.Tx) error { - err := tx.CreateIndex(vhostRequestIdx, fmt.Sprintf(keyVHostQueueAcctToId, "*"), buntdb.IndexInt) - if err != nil { - return err - } - return tx.Descend(vhostRequestIdx, func(key, value string) bool { - if lastIDStr == "" { - lastIDStr = value - } - total++ - return true - }) - }) - - if err != nil { - am.server.logger.Error("internal", "could not create vhost queue index", err.Error()) - } - - lastID, _ := strconv.ParseUint(lastIDStr, 10, 64) - am.server.logger.Debug("services", fmt.Sprintf("vhost queue length is %d, autoincrementing id is %d", total, lastID)) - - atomic.StoreUint64(&am.vhostRequestID, lastID) - atomic.StoreUint64(&am.vhostRequestPendingCount, total) -} - func (am *AccountManager) NickToAccount(nick string) string { cfnick, err := CasefoldName(nick) if err != nil { @@ -1394,7 +1345,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error { nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) - vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount) lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount) @@ -1461,8 +1411,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error { tx.Delete(modesKey) tx.Delete(realnameKey) - _, err := tx.Delete(vhostQueueKey) - am.decrementVHostQueueCount(casefoldedAccount, err) return nil }) @@ -1635,42 +1583,8 @@ func (am *AccountManager) ModifyAccountSettings(account string, munger settingsM // represents someone's status in hostserv type VHostInfo struct { - ApprovedVHost string - Enabled bool - RequestedVHost string - RejectedVHost string - RejectionReason string - LastRequestTime time.Time -} - -// pair type, -type PendingVHostRequest struct { - VHostInfo - Account string -} - -type vhostThrottleExceeded struct { - timeRemaining time.Duration -} - -func (vhe *vhostThrottleExceeded) Error() string { - return fmt.Sprintf("Wait at least %v and try again", vhe.timeRemaining) -} - -func (vh *VHostInfo) checkThrottle(cooldown time.Duration) (err error) { - if cooldown == 0 { - return nil - } - - now := time.Now().UTC() - elapsed := now.Sub(vh.LastRequestTime) - if elapsed > cooldown { - // success - vh.LastRequestTime = now - return nil - } else { - return &vhostThrottleExceeded{timeRemaining: cooldown - elapsed} - } + ApprovedVHost string + Enabled bool } // callback type implementing the actual business logic of vhost operations @@ -1687,52 +1601,6 @@ func (am *AccountManager) VHostSet(account string, vhost string) (result VHostIn return am.performVHostChange(account, munger) } -func (am *AccountManager) VHostRequest(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) { - munger := func(input VHostInfo) (output VHostInfo, err error) { - output = input - // you can update your existing request, but if you were approved or rejected, - // you can't spam a new request - if output.RequestedVHost == "" { - err = output.checkThrottle(cooldown) - } - if err != nil { - return - } - output.RequestedVHost = vhost - output.RejectedVHost = "" - output.RejectionReason = "" - output.LastRequestTime = time.Now().UTC() - return - } - - return am.performVHostChange(account, munger) -} - -func (am *AccountManager) VHostApprove(account string) (result VHostInfo, err error) { - munger := func(input VHostInfo) (output VHostInfo, err error) { - output = input - output.Enabled = true - output.ApprovedVHost = input.RequestedVHost - output.RequestedVHost = "" - output.RejectionReason = "" - return - } - - return am.performVHostChange(account, munger) -} - -func (am *AccountManager) VHostReject(account string, reason string) (result VHostInfo, err error) { - munger := func(input VHostInfo) (output VHostInfo, err error) { - output = input - output.RejectedVHost = output.RequestedVHost - output.RequestedVHost = "" - output.RejectionReason = reason - return - } - - return am.performVHostChange(account, munger) -} - func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { if input.ApprovedVHost == "" { @@ -1759,9 +1627,6 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger) return } - am.vHostUpdateMutex.Lock() - defer am.vHostUpdateMutex.Unlock() - clientAccount, err := am.LoadAccount(account) if err != nil { err = errAccountDoesNotExist @@ -1784,25 +1649,9 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger) vhstr := string(vhtext) key := fmt.Sprintf(keyAccountVHost, account) - queueKey := fmt.Sprintf(keyVHostQueueAcctToId, account) err = am.server.store.Update(func(tx *buntdb.Tx) error { - if _, _, err := tx.Set(key, vhstr, nil); err != nil { - return err - } - - // update request queue - if clientAccount.VHost.RequestedVHost == "" && result.RequestedVHost != "" { - id := atomic.AddUint64(&am.vhostRequestID, 1) - if _, _, err = tx.Set(queueKey, strconv.FormatUint(id, 10), nil); err != nil { - return err - } - atomic.AddUint64(&am.vhostRequestPendingCount, 1) - } else if clientAccount.VHost.RequestedVHost != "" && result.RequestedVHost == "" { - _, err = tx.Delete(queueKey) - am.decrementVHostQueueCount(account, err) - } - - return nil + _, _, err := tx.Set(key, vhstr, nil) + return err }) if err != nil { @@ -1814,51 +1663,6 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger) return result, nil } -// XXX annoying helper method for keeping the queue count in sync with the DB -// `err` is the buntdb error returned from deleting the queue key -func (am *AccountManager) decrementVHostQueueCount(account string, err error) { - if err == nil { - // successfully deleted a queue entry, do a 2's complement decrement: - atomic.AddUint64(&am.vhostRequestPendingCount, ^uint64(0)) - } else if err != buntdb.ErrNotFound { - am.server.logger.Error("internal", "buntdb dequeue error", account, err.Error()) - } -} - -func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostRequest, total int) { - am.vHostUpdateMutex.Lock() - defer am.vHostUpdateMutex.Unlock() - - total = int(atomic.LoadUint64(&am.vhostRequestPendingCount)) - - prefix := fmt.Sprintf(keyVHostQueueAcctToId, "") - accounts := make([]string, 0, limit) - err := am.server.store.View(func(tx *buntdb.Tx) error { - return tx.Ascend(vhostRequestIdx, func(key, value string) bool { - accounts = append(accounts, strings.TrimPrefix(key, prefix)) - return len(accounts) < limit - }) - }) - - if err != nil { - am.server.logger.Error("internal", "couldn't traverse vhost queue", err.Error()) - return - } - - for _, account := range accounts { - accountInfo, err := am.LoadAccount(account) - if err == nil { - requests = append(requests, PendingVHostRequest{ - Account: account, - VHostInfo: accountInfo.VHost, - }) - } else { - am.server.logger.Error("internal", "corrupt account", account, err.Error()) - } - } - return -} - func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) { // if hostserv is disabled in config, then don't grant vhosts // that were previously approved while it was enabled diff --git a/irc/config.go b/irc/config.go index 4c5ee631..502e664f 100644 --- a/irc/config.go +++ b/irc/config.go @@ -316,12 +316,7 @@ type VHostConfig struct { Enabled bool MaxLength int `yaml:"max-length"` ValidRegexpRaw string `yaml:"valid-regexp"` - ValidRegexp *regexp.Regexp - UserRequests struct { - Enabled bool - Channel string - Cooldown custime.Duration - } `yaml:"user-requests"` + validRegexp *regexp.Regexp } type NickEnforcementMethod int @@ -1109,13 +1104,13 @@ func LoadConfig(filename string) (config *Config, err error) { if rawRegexp != "" { regexp, err := regexp.Compile(rawRegexp) if err == nil { - config.Accounts.VHosts.ValidRegexp = regexp + config.Accounts.VHosts.validRegexp = regexp } else { log.Printf("invalid vhost regexp: %s\n", err.Error()) } } - if config.Accounts.VHosts.ValidRegexp == nil { - config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex + if config.Accounts.VHosts.validRegexp == nil { + config.Accounts.VHosts.validRegexp = defaultValidVhostRegex } config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL" diff --git a/irc/database.go b/irc/database.go index 8b1c231c..06968652 100644 --- a/irc/database.go +++ b/irc/database.go @@ -24,7 +24,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "16" + latestDbSchema = "17" keyCloakSecret = "crypto.cloak_secret" ) @@ -835,6 +835,24 @@ func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error { return nil } +// #1346: remove vhost request queue +func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error { + prefix := "vhostQueue " + var keys []string + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + keys = append(keys, key) + return true + }) + + for _, key := range keys { + tx.Delete(key) + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -912,6 +930,11 @@ func init() { TargetVersion: "16", Changer: schemaChangeV15ToV16, }, + { + InitialVersion: "16", + TargetVersion: "17", + Changer: schemaChangeV16ToV17, + }, } // build the index diff --git a/irc/hostserv.go b/irc/hostserv.go index 1dc48459..add7330d 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -7,11 +7,9 @@ import ( "errors" "fmt" "regexp" - "time" "github.com/goshuirc/irc-go/ircfmt" - "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" ) @@ -32,10 +30,6 @@ func hostservEnabled(config *Config) bool { return config.Accounts.VHosts.Enabled } -func hostservRequestsEnabled(config *Config) bool { - return config.Accounts.VHosts.Enabled && config.Accounts.VHosts.UserRequests.Enabled -} - var ( hostservCommands = map[string]*serviceCommand{ "on": { @@ -56,17 +50,6 @@ OFF disables your vhost, if you have one approved.`, authRequired: true, enabled: hostservEnabled, }, - "request": { - handler: hsRequestHandler, - help: `Syntax: $bREQUEST $b - -REQUEST requests that a new vhost by assigned to your account. The request must -then be approved by a server operator.`, - helpShort: `$bREQUEST$b requests a new vhost, pending operator approval.`, - authRequired: true, - enabled: hostservRequestsEnabled, - minParams: 1, - }, "status": { handler: hsStatusHandler, help: `Syntax: $bSTATUS [user]$b @@ -96,39 +79,6 @@ DEL deletes a user's vhost.`, enabled: hostservEnabled, minParams: 1, }, - "waiting": { - handler: hsWaitingHandler, - help: `Syntax: $bWAITING$b - -WAITING shows a list of pending vhost requests, which can then be approved -or rejected.`, - helpShort: `$bWAITING$b shows a list of pending vhost requests.`, - capabs: []string{"vhosts"}, - enabled: hostservEnabled, - }, - "approve": { - handler: hsApproveHandler, - help: `Syntax: $bAPPROVE $b - -APPROVE approves a user's vhost request.`, - helpShort: `$bAPPROVE$b approves a user's vhost request.`, - capabs: []string{"vhosts"}, - enabled: hostservEnabled, - minParams: 1, - }, - "reject": { - handler: hsRejectHandler, - help: `Syntax: $bREJECT []$b - -REJECT rejects a user's vhost request, optionally giving them a reason -for the rejection.`, - helpShort: `$bREJECT$b rejects a user's vhost request.`, - capabs: []string{"vhosts"}, - enabled: hostservEnabled, - minParams: 1, - maxParams: 2, - unsplitFinalParam: true, - }, "setcloaksecret": { handler: hsSetCloakSecretHandler, help: `Syntax: $bSETCLOAKSECRET$b [code] @@ -150,19 +100,6 @@ func hsNotice(rb *ResponseBuffer, text string) { rb.Add(nil, hsNickMask, "NOTICE", rb.target.Nick(), text) } -// hsNotifyChannel notifies the designated channel of new vhost activity -func hsNotifyChannel(server *Server, message string) { - chname := server.Config().Accounts.VHosts.UserRequests.Channel - channel := server.channels.Get(chname) - if channel == nil { - return - } - chname = channel.Name() - for _, client := range channel.Members() { - client.Send(nil, hsNickMask, "PRIVMSG", chname, message) - } -} - func hsOnOffHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { enable := false if command == "on" { @@ -181,29 +118,6 @@ func hsOnOffHandler(server *Server, client *Client, command string, params []str } } -func hsRequestHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - vhost := params[0] - if validateVhost(server, vhost, false) != nil { - hsNotice(rb, client.t("Invalid vhost")) - return - } - - accountName := client.Account() - _, err := server.accounts.VHostRequest(accountName, vhost, time.Duration(server.Config().Accounts.VHosts.UserRequests.Cooldown)) - if err != nil { - if throttled, ok := err.(*vhostThrottleExceeded); ok { - hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining)) - } else { - hsNotice(rb, client.t("An error occurred")) - } - } else { - hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator")) - chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost) - hsNotifyChannel(server, chanMsg) - server.snomasks.Send(sno.LocalVhosts, chanMsg) - } -} - func hsStatusHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { var accountName string if len(params) > 0 { @@ -237,13 +151,6 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st } else { hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName)) } - if account.VHost.RequestedVHost != "" { - hsNotice(rb, fmt.Sprintf(client.t("A request is pending for vhost: %s"), account.VHost.RequestedVHost)) - } - if account.VHost.RejectedVHost != "" { - hsNotice(rb, fmt.Sprintf(client.t("A request was previously made for vhost: %s"), account.VHost.RejectedVHost)) - hsNotice(rb, fmt.Sprintf(client.t("It was rejected for reason: %s"), account.VHost.RejectionReason)) - } } func validateVhost(server *Server, vhost string, oper bool) error { @@ -251,7 +158,7 @@ func validateVhost(server *Server, vhost string, oper bool) error { if len(vhost) > config.Accounts.VHosts.MaxLength { return errVHostTooLong } - if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) { + if !config.Accounts.VHosts.validRegexp.MatchString(vhost) { return errVHostBadCharacters } return nil @@ -280,56 +187,6 @@ func hsSetHandler(server *Server, client *Client, command string, params []strin } } -func hsWaitingHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - requests, total := server.accounts.VHostListRequests(10) - hsNotice(rb, fmt.Sprintf(client.t("There are %[1]d pending requests for vhosts (%[2]d displayed)"), total, len(requests))) - for i, request := range requests { - hsNotice(rb, fmt.Sprintf(client.t("%[1]d. User %[2]s requests vhost: %[3]s"), i+1, request.Account, request.RequestedVHost)) - } -} - -func hsApproveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - user := params[0] - - vhostInfo, err := server.accounts.VHostApprove(user) - if err != nil { - hsNotice(rb, client.t("An error occurred")) - } else { - hsNotice(rb, fmt.Sprintf(client.t("Successfully approved vhost request for %s"), user)) - chanMsg := fmt.Sprintf("Oper %[1]s approved vhost %[2]s for account %[3]s", client.Nick(), vhostInfo.ApprovedVHost, user) - hsNotifyChannel(server, chanMsg) - server.snomasks.Send(sno.LocalVhosts, chanMsg) - for _, client := range server.accounts.AccountToClients(user) { - client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was approved by an administrator")) - } - } -} - -func hsRejectHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - var reason string - user := params[0] - if len(params) > 1 { - reason = params[1] - } - - vhostInfo, err := server.accounts.VHostReject(user, reason) - if err != nil { - hsNotice(rb, client.t("An error occurred")) - } else { - hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user)) - chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason) - hsNotifyChannel(server, chanMsg) - server.snomasks.Send(sno.LocalVhosts, chanMsg) - for _, client := range server.accounts.AccountToClients(user) { - if reason == "" { - client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was rejected by an administrator")) - } else { - client.Send(nil, hsNickMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason)) - } - } - } -} - func hsSetCloakSecretHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { secret := params[0] expectedCode := utils.ConfirmationCode(secret, server.ctime) diff --git a/irc/import.go b/irc/import.go index c9b9cb28..1499e15b 100644 --- a/irc/import.go +++ b/irc/import.go @@ -69,8 +69,8 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden // produce a hardcoded version of the database schema // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal // (to ensure that no matter what code changes happen elsewhere, we're still producing a - // version 14 db) - tx.Set(keySchemaVersion, "14", nil) + // db of the hardcoded version) + tx.Set(keySchemaVersion, "17", nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) for username, userInfo := range dbImport.Users { diff --git a/irc/server.go b/irc/server.go index 9e679e01..f92f0de1 100644 --- a/irc/server.go +++ b/irc/server.go @@ -570,9 +570,6 @@ func (server *Server) applyConfig(config *Config) (err error) { if !oldConfig.Accounts.NickReservation.Enabled { server.accounts.buildNickToAccountIndex(config) } - if !oldConfig.Accounts.VHosts.Enabled { - server.accounts.initVHostRequestQueue(config) - } if !oldConfig.Channels.Registration.Enabled { server.channels.loadRegisteredChannels(config) } From eb5a16821f2312f996b7aec0b12bd8150bb60b82 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 23 Oct 2020 00:12:53 -0400 Subject: [PATCH 2/2] review fix: remove config blocks for vhost requests --- conventional.yaml | 17 ----------------- default.yaml | 17 ----------------- 2 files changed, 34 deletions(-) diff --git a/conventional.yaml b/conventional.yaml index 44804021..c31a72d7 100644 --- a/conventional.yaml +++ b/conventional.yaml @@ -475,23 +475,6 @@ accounts: # (make sure any changes you make here are RFC-compliant) valid-regexp: '^[0-9A-Za-z.\-_/]+$' - # options controlling users requesting vhosts: - user-requests: - # can users request vhosts at all? if this is false, operators with the - # 'vhosts' capability can still assign vhosts manually - enabled: false - - # if uncommented, all new vhost requests will be dumped into the given - # channel, so opers can review them as they are sent in. ensure that you - # have registered and restricted the channel appropriately before you - # uncomment this. - #channel: "#vhosts" - - # after a user's vhost has been approved or rejected, they need to wait - # this long (starting from the time of their original request) - # before they can request a new one. - cooldown: 168h - # modes that are set by default when a user connects # if unset, no user modes will be set by default # +i is invisible (a user's channels are hidden from whois replies) diff --git a/default.yaml b/default.yaml index 81c1a72a..7440f00a 100644 --- a/default.yaml +++ b/default.yaml @@ -503,23 +503,6 @@ accounts: # (make sure any changes you make here are RFC-compliant) valid-regexp: '^[0-9A-Za-z.\-_/]+$' - # options controlling users requesting vhosts: - user-requests: - # can users request vhosts at all? if this is false, operators with the - # 'vhosts' capability can still assign vhosts manually - enabled: false - - # if uncommented, all new vhost requests will be dumped into the given - # channel, so opers can review them as they are sent in. ensure that you - # have registered and restricted the channel appropriately before you - # uncomment this. - #channel: "#vhosts" - - # after a user's vhost has been approved or rejected, they need to wait - # this long (starting from the time of their original request) - # before they can request a new one. - cooldown: 168h - # modes that are set by default when a user connects # if unset, no user modes will be set by default # +i is invisible (a user's channels are hidden from whois replies)