~edwargix/git.sr.ht

fb5c5da69908b77db27394500e5e1967dfcc5b99 — Drew DeVault 5 years ago a85c2dc
API: move recover function into separate file
3 files changed, 130 insertions(+), 109 deletions(-)

M api/go.mod
A api/graph/recover.go
M api/server.go
M api/go.mod => api/go.mod +2 -2
@@ 14,7 14,7 @@ require (
	github.com/gorilla/websocket v1.4.2 // indirect
	github.com/hashicorp/golang-lru v0.5.4 // indirect
	github.com/lib/pq v1.3.0
	github.com/martinlindhe/base36 v1.0.0 // indirect
	github.com/martinlindhe/base36 v1.0.0
	github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 // indirect
	github.com/mitchellh/mapstructure v1.3.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect


@@ 25,6 25,6 @@ require (
	github.com/vektah/gqlparser/v2 v2.0.1
	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
	golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97 // indirect
	gopkg.in/mail.v2 v2.3.1 // indirect
	gopkg.in/mail.v2 v2.3.1
	gopkg.in/yaml.v2 v2.3.0 // indirect
)

A api/graph/recover.go => api/graph/recover.go +127 -0
@@ 0,0 1,127 @@
package graph

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/binary"
	"errors"
	"fmt"
	"log"
	"net/mail"
	"os"
	"runtime"
	"strconv"
	"time"

	"github.com/99designs/gqlgen/graphql"
	"github.com/martinlindhe/base36"
	"github.com/vaughan0/go-ini"
	gomail "gopkg.in/mail.v2"

	"git.sr.ht/~sircmpwn/git.sr.ht/api/auth"
)

// Provides a graphql.RecoverFunc which will print the stack trace, and if
// debug mode is not enabled, email it to the administrator.
func EmailRecover(config ini.File, debug bool) graphql.RecoverFunc {
	return func (ctx context.Context, _origErr interface{}) error {
		var (
			ok      bool
			origErr error
		)
		if origErr, ok = _origErr.(error); !ok {
			log.Printf("Unexpected error in recover: %v\n", origErr)
			return fmt.Errorf("internal system error")
		}

		if errors.Is(origErr, context.Canceled) {
			return origErr
		}

		if errors.Is(origErr, context.DeadlineExceeded) {
			return origErr
		}

		if origErr.Error() == "pq: canceling statement due to user request" {
			return origErr
		}

		stack := make([]byte, 32768) // 32 KiB
		i := runtime.Stack(stack, false)
		log.Println(string(stack[:i]))
		if debug {
			return fmt.Errorf("internal system error")
		}

		to, ok := config.Get("mail", "error-to")
		if !ok {
			return fmt.Errorf("internal system error")
		}
		from, _ := config.Get("mail", "error-from")
		portStr, ok := config.Get("mail", "smtp-port")
		if !ok {
			return fmt.Errorf("internal system error")
		}
		port, _ := strconv.Atoi(portStr)
		host, _ := config.Get("mail", "smtp-host")
		user, _ := config.Get("mail", "smtp-user")
		pass, _ := config.Get("mail", "smtp-password")

		m := gomail.NewMessage()
		sender, err := mail.ParseAddress(from)
		if err != nil {
			log.Fatalf("Failed to parse sender address")
		}
		m.SetAddressHeader("From", sender.Address, sender.Name)
		recipient, err := mail.ParseAddress(to)
		if err != nil {
			log.Fatalf("Failed to parse recipient address")
		}
		m.SetAddressHeader("To", recipient.Address, recipient.Name)
		m.SetHeader("Message-ID", GenerateMessageID())
		m.SetHeader("Subject", fmt.Sprintf(
			"[git.sr.ht] GraphQL query error: %v", origErr))

		quser := auth.ForContext(ctx)
		octx := graphql.GetOperationContext(ctx)

		m.SetBody("text/plain", fmt.Sprintf(`Error occured processing GraphQL request:

	%v

	When running the following query on behalf of %s <%s>:

	%s

	The following stack trace was produced:

	%s`, origErr, quser.Username, quser.Email, octx.RawQuery, string(stack[:i])))

		d := gomail.NewDialer(host, port, user, pass)
		if err := d.DialAndSend(m); err != nil {
			log.Printf("Error sending email: %v\n", err)
		}
		return fmt.Errorf("internal system error")
	}
}

// Generates an RFC 2822-compliant Message-Id based on the informational draft
// "Recommendations for generating Message IDs", for lack of a better
// authoritative source.
func GenerateMessageID() string {
	var (
		now   bytes.Buffer
		nonce []byte = make([]byte, 8)
	)
	binary.Write(&now, binary.BigEndian, time.Now().UnixNano())
	rand.Read(nonce)
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "localhost"
	}
	return fmt.Sprintf("<%s.%s@%s>",
		base36.EncodeBytes(now.Bytes()),
		base36.EncodeBytes(nonce),
		hostname)
}

M api/server.go => api/server.go +1 -107
@@ 1,31 1,20 @@
package main

import (
	"bytes"
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/binary"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/mail"
	"os"
	"runtime"
	"strconv"
	"time"

	"git.sr.ht/~sircmpwn/getopt"
	"github.com/99designs/gqlgen/graphql"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/99designs/gqlgen/handler"
	"github.com/go-chi/chi"
	"github.com/go-chi/chi/middleware"
	"github.com/martinlindhe/base36"
	"github.com/vaughan0/go-ini"
	_ "github.com/lib/pq"
	gomail "gopkg.in/mail.v2"

	"git.sr.ht/~sircmpwn/git.sr.ht/api/auth"
	"git.sr.ht/~sircmpwn/git.sr.ht/api/crypto"


@@ 112,82 101,7 @@ func main() {
	srv := handler.GraphQL(
		api.NewExecutableSchema(gqlConfig),
		handler.ComplexityLimit(complexity),
		handler.RecoverFunc(func(ctx context.Context, _origErr interface{}) error {
			var origErr error
			if origErr, ok = _origErr.(error); !ok {
				log.Printf("Unexpected error in recover: %v\n", origErr)
				return fmt.Errorf("internal system error")
			}

			if errors.Is(origErr, context.Canceled) {
				return origErr
			}

			if errors.Is(origErr, context.DeadlineExceeded) {
				return origErr
			}

			if origErr.Error() == "pq: canceling statement due to user request" {
				return origErr
			}

			stack := make([]byte, 32768) // 32 KiB
			i := runtime.Stack(stack, false)
			log.Println(string(stack[:i]))
			if debug {
				return fmt.Errorf("internal system error")
			}

			to, ok := config.Get("mail", "error-to")
			if !ok {
				return fmt.Errorf("internal system error")
			}
			from, _ := config.Get("mail", "error-from")
			portStr, ok := config.Get("mail", "smtp-port")
			if !ok {
				return fmt.Errorf("internal system error")
			}
			port, _ := strconv.Atoi(portStr)
			host, _ := config.Get("mail", "smtp-host")
			user, _ := config.Get("mail", "smtp-user")
			pass, _ := config.Get("mail", "smtp-password")

			m := gomail.NewMessage()
			sender, err := mail.ParseAddress(from)
			if err != nil {
				log.Fatalf("Failed to parse sender address")
			}
			m.SetAddressHeader("From", sender.Address, sender.Name)
			recipient, err := mail.ParseAddress(to)
			if err != nil {
				log.Fatalf("Failed to parse recipient address")
			}
			m.SetAddressHeader("To", recipient.Address, recipient.Name)
			m.SetHeader("Message-ID", GenerateMessageID())
			m.SetHeader("Subject", fmt.Sprintf(
				"[git.sr.ht] GraphQL query error: %v", origErr))

			quser := auth.ForContext(ctx)
			octx := graphql.GetOperationContext(ctx)

			m.SetBody("text/plain", fmt.Sprintf(`Error occured processing GraphQL request:

%v

When running the following query on behalf of %s <%s>:

%s

The following stack trace was produced:

%s`, origErr, quser.Username, quser.Email, octx.RawQuery, string(stack[:i])))

			d := gomail.NewDialer(host, port, user, pass)
			if err := d.DialAndSend(m); err != nil {
				log.Printf("Error sending email: %v\n", err)
			}
			return fmt.Errorf("internal system error")
		}))
		handler.RecoverFunc(graph.EmailRecover(config, debug)))

	router.Handle("/query", srv)



@@ 198,23 112,3 @@ The following stack trace was produced:
	log.Printf("running on %s", addr)
	log.Fatal(http.ListenAndServe(addr, router))
}

// Generates an RFC 2822-compliant Message-Id based on the informational draft
// "Recommendations for generating Message IDs", for lack of a better
// authoritative source.
func GenerateMessageID() string {
	var (
		now   bytes.Buffer
		nonce []byte = make([]byte, 8)
	)
	binary.Write(&now, binary.BigEndian, time.Now().UnixNano())
	rand.Read(nonce)
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "localhost"
	}
	return fmt.Sprintf("<%s.%s@%s>",
		base36.EncodeBytes(now.Bytes()),
		base36.EncodeBytes(nonce),
		hostname)
}