Repository: oragono/oragono (go)

Issue:
DEBUG CRASHSERVER command
This would require operator permissions (like the rest of `DEBUG`). You'd enter `debug crashserver 4758281` (a confirmation code, like with `NS UNREGISTER`) and it would cause an unhandled panic on the server. This could be used to test failover in k8s and other settings.

Pull request description:
fix #791

Patch:
diff --git a/irc/chanserv.go b/irc/chanserv.go
index 6fb748e5..9817ccc2 100644
--- a/irc/chanserv.go
+++ b/irc/chanserv.go
@@ -4,17 +4,15 @@
 package irc
 
 import (
-	"bytes"
 	"fmt"
-	"hash/crc32"
 	"sort"
-	"strconv"
 	"strings"
 	"time"
 
 	"github.com/goshuirc/irc-go/ircfmt"
 	"github.com/oragono/oragono/irc/modes"
 	"github.com/oragono/oragono/irc/sno"
+	"github.com/oragono/oragono/irc/utils"
 )
 
 const chanservHelp = `ChanServ lets you register and manage channels.`
@@ -352,7 +350,7 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
 	}
 
 	info := channel.ExportRegistration(0)
-	expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
+	expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
 	if expectedCode != verificationCode {
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
 		csNotice(rb, fmt.Sprintf(client.t("To confirm channel unregistration, type: /CS UNREGISTER %[1]s %[2]s"), channelKey, expectedCode))
@@ -363,14 +361,6 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
 }
 
-// deterministically generates a confirmation code for unregistering a channel / account
-func unregisterConfirmationCode(name string, registeredAt time.Time) (code string) {
-	var codeInput bytes.Buffer
-	codeInput.WriteString(name)
-	codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16))
-	return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes())))
-}
-
 func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
 	channel := server.channels.Get(params[0])
 	if channel == nil {
@@ -426,7 +416,7 @@ func csTransferHandler(server *Server, client *Client, command string, params []
 		return
 	}
 	if targetAccount.NameCasefolded != account {
-		expectedCode := unregisterConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
+		expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
 		codeValidated := 2 < len(params) && params[2] == expectedCode
 		if !codeValidated {
 			csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
diff --git a/irc/handlers.go b/irc/handlers.go
index 8f3827b9..0a0f2329 100644
--- a/irc/handlers.go
+++ b/irc/handlers.go
@@ -804,6 +804,26 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
 	case "STOPCPUPROFILE":
 		pprof.StopCPUProfile()
 		rb.Notice(fmt.Sprintf("CPU profiling stopped"))
+
+	case "CRASHSERVER":
+		if !client.HasRoleCapabs("oper:rehash") {
+			rb.Notice(client.t("You must have rehash permissions in order to execute DEBUG CRASHSERVER"))
+			return false
+		}
+		code := utils.ConfirmationCode(server.name, server.ctime)
+		if len(msg.Params) == 1 || msg.Params[1] != code {
+			rb.Notice(fmt.Sprintf(client.t("To crash the server, issue the following command: /DEBUG CRASHSERVER %s"), code))
+			return false
+		}
+		server.logger.Error("server", fmt.Sprintf("DEBUG CRASHSERVER executed by operator %s", client.Oper().Name))
+		go func() {
+			// intentional nil dereference on a new goroutine, bypassing recover-from-errors
+			var i, j *int
+			*i = *j
+		}()
+
+	default:
+		rb.Notice(client.t("Unrecognized DEBUG subcommand"))
 	}
 	return false
 }
diff --git a/irc/help.go b/irc/help.go
index 4f0a19da..58de3277 100644
--- a/irc/help.go
+++ b/irc/help.go
@@ -153,13 +153,14 @@ https://gist.github.com/DanielOaks/c104ad6e8759c01eb5c826d627caf80d`,
 		oper: true,
 		text: `DEBUG <option>
 
-Prints debug information about the IRCd. <option> can be one of:
+Provides various debugging commands for the IRCd. <option> can be one of:
 
 * GCSTATS: Garbage control statistics.
 * NUMGOROUTINE: Number of goroutines in use.
 * STARTCPUPROFILE: Starts the CPU profiler.
 * STOPCPUPROFILE: Stops the CPU profiler.
-* PROFILEHEAP: Writes out the CPU profiler info.`,
+* PROFILEHEAP: Writes a memory profile.
+* CRASHSERVER: Crashes the server (for use in failover testing)`,
 	},
 	"dline": {
 		oper: true,
diff --git a/irc/nickserv.go b/irc/nickserv.go
index 16177c75..39db7590 100644
--- a/irc/nickserv.go
+++ b/irc/nickserv.go
@@ -732,7 +732,7 @@ func nsUnregisterHandler(server *Server, client *Client, command string, params
 		return
 	}
 
-	expectedCode := unregisterConfirmationCode(account.Name, account.RegisteredAt)
+	expectedCode := utils.ConfirmationCode(account.Name, account.RegisteredAt)
 	if expectedCode != verificationCode {
 		nsNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this account will remove its stored privileges.$b")))
 		nsNotice(rb, fmt.Sprintf(client.t("To confirm account unregistration, type: /NS UNREGISTER %[1]s %[2]s"), cfname, expectedCode))
diff --git a/irc/utils/confirmation.go b/irc/utils/confirmation.go
new file mode 100644
index 00000000..5d984a16
--- /dev/null
+++ b/irc/utils/confirmation.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
+// released under the MIT license
+
+package utils
+
+import (
+	"crypto/sha256"
+	"encoding/binary"
+	"time"
+)
+
+// Deterministically generates a confirmation code for some destructive activity;
+// `name` is typically the name of the identity being destroyed (a channel being
+// unregistered, or the server being crashed) and `createdAt` means a different
+// value is required each time.
+func ConfirmationCode(name string, createdAt time.Time) (code string) {
+	buf := make([]byte, len(name)+8)
+	binary.BigEndian.PutUint64(buf, uint64(createdAt.UnixNano()))
+	copy(buf[8:], name[:])
+	out := sha256.Sum256(buf)
+	return B32Encoder.EncodeToString(out[:3])
+}


Test patch:
diff --git a/irc/utils/confirmation_test.go b/irc/utils/confirmation_test.go
new file mode 100644
index 00000000..01954dac
--- /dev/null
+++ b/irc/utils/confirmation_test.go
@@ -0,0 +1,36 @@
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
+// released under the MIT license
+
+package utils
+
+import (
+	"testing"
+	"time"
+)
+
+func easyParse(timestamp string) time.Time {
+	result, err := time.Parse("2006-01-02 15:04:05Z", timestamp)
+	if err != nil {
+		panic(err)
+	}
+	return result
+}
+
+func TestConfirmation(t *testing.T) {
+	set := make(map[string]struct{})
+
+	set[ConfirmationCode("#darwin", easyParse("2006-01-01 00:00:00Z"))] = struct{}{}
+	set[ConfirmationCode("#darwin", easyParse("2006-01-02 00:00:00Z"))] = struct{}{}
+	set[ConfirmationCode("#xelpers", easyParse("2006-01-01 00:00:00Z"))] = struct{}{}
+	set[ConfirmationCode("#xelpers", easyParse("2006-01-02 00:00:00Z"))] = struct{}{}
+
+	if len(set) != 4 {
+		t.Error("confirmation codes are not unique")
+	}
+
+	for code := range set {
+		if len(code) <= 2 || len(code) >= 8 {
+			t.Errorf("bad code: %s", code)
+		}
+	}
+}