@ -0,0 +1,64 @@ | |||||
package alert | |||||
import ( | |||||
"fmt" | |||||
"github.com/sfreiberg/gotwilio" | |||||
"time" | |||||
. "github.com/tendermint/tendermint/config" | |||||
) | |||||
var last int64 = 0 | |||||
var count int = 0 | |||||
func Alert(message string) { | |||||
log.Error("<!> ALERT <!>\n" + message) | |||||
now := time.Now().Unix() | |||||
if now-last > int64(Config.Alert.MinInterval) { | |||||
message = fmt.Sprintf("%v:%v", Config.Network, message) | |||||
if count > 0 { | |||||
message = fmt.Sprintf("%v (+%v more since)", message, count) | |||||
count = 0 | |||||
} | |||||
if len(Config.Alert.TwilioSid) > 0 { | |||||
go sendTwilio(message) | |||||
} | |||||
if len(Config.Alert.EmailRecipients) > 0 { | |||||
go sendEmail(message) | |||||
} | |||||
} else { | |||||
count++ | |||||
} | |||||
} | |||||
func sendTwilio(message string) { | |||||
defer func() { | |||||
if err := recover(); err != nil { | |||||
log.Error("sendTwilio error: %v", err) | |||||
} | |||||
}() | |||||
if len(message) > 50 { | |||||
message = message[:50] | |||||
} | |||||
twilio := gotwilio.NewTwilioClient(Config.Alert.TwilioSid, Config.Alert.TwilioToken) | |||||
res, exp, err := twilio.SendSMS(Config.Alert.TwilioFrom, Config.Alert.TwilioTo, message, "", "") | |||||
if exp != nil || err != nil { | |||||
log.Error("sendTwilio error: %v %v %v", res, exp, err) | |||||
} | |||||
} | |||||
func sendEmail(message string) { | |||||
defer func() { | |||||
if err := recover(); err != nil { | |||||
log.Error("sendEmail error: %v", err) | |||||
} | |||||
}() | |||||
subject := message | |||||
if len(subject) > 80 { | |||||
subject = subject[:80] | |||||
} | |||||
err := SendEmail(subject, message, Config.Alert.EmailRecipients) | |||||
if err != nil { | |||||
log.Error("sendEmail error: %v\n%v", err, message) | |||||
} | |||||
} |
@ -0,0 +1,178 @@ | |||||
// Forked from github.com/SlyMarbo/gmail | |||||
package alert | |||||
import ( | |||||
"bytes" | |||||
"crypto/tls" | |||||
"encoding/base64" | |||||
"errors" | |||||
"fmt" | |||||
"io/ioutil" | |||||
"net/smtp" | |||||
"path/filepath" | |||||
"regexp" | |||||
"strings" | |||||
. "github.com/tendermint/tendermint/config" | |||||
) | |||||
// Convenience function | |||||
func SendEmail(subject, body string, tos []string) error { | |||||
email := Compose(subject, body) | |||||
email.From = Config.SMTP.User | |||||
email.ContentType = "text/html; charset=utf-8" | |||||
email.AddRecipients(tos...) | |||||
err := email.Send() | |||||
return err | |||||
} | |||||
// Email represents a single message, which may contain | |||||
// attachments. | |||||
type Email struct { | |||||
From string | |||||
To []string | |||||
Subject string | |||||
ContentType string | |||||
Body string | |||||
Attachments map[string][]byte | |||||
} | |||||
// Compose begins a new email, filling the subject and body, | |||||
// and allocating memory for the list of recipients and the | |||||
// attachments. | |||||
func Compose(Subject, Body string) *Email { | |||||
out := new(Email) | |||||
out.To = make([]string, 0, 1) | |||||
out.Subject = Subject | |||||
out.Body = Body | |||||
out.Attachments = make(map[string][]byte) | |||||
return out | |||||
} | |||||
// Attach takes a filename and adds this to the message. | |||||
// Note that since only the filename is stored (and not | |||||
// its path, for privacy reasons), multiple files in | |||||
// different directories but with the same filename and | |||||
// extension cannot be sent. | |||||
func (e *Email) Attach(Filename string) error { | |||||
b, err := ioutil.ReadFile(Filename) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, fname := filepath.Split(Filename) | |||||
e.Attachments[fname] = b | |||||
return nil | |||||
} | |||||
// AddRecipient adds a single recipient. | |||||
func (e *Email) AddRecipient(Recipient string) { | |||||
e.To = append(e.To, Recipient) | |||||
} | |||||
// AddRecipients adds one or more recipients. | |||||
func (e *Email) AddRecipients(Recipients ...string) { | |||||
e.To = append(e.To, Recipients...) | |||||
} | |||||
// Send sends the email, returning any error encountered. | |||||
func (e *Email) Send() error { | |||||
if e.From == "" { | |||||
return errors.New("Error: No sender specified. Please set the Email.From field.") | |||||
} | |||||
if e.To == nil || len(e.To) == 0 { | |||||
return errors.New("Error: No recipient specified. Please set the Email.To field.") | |||||
} | |||||
auth := smtp.PlainAuth( | |||||
"", | |||||
Config.SMTP.User, | |||||
Config.SMTP.Password, | |||||
Config.SMTP.Host, | |||||
) | |||||
conn, err := smtp.Dial(fmt.Sprintf("%v:%v", Config.SMTP.Host, Config.SMTP.Port)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = conn.StartTLS(&tls.Config{}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = conn.Auth(auth) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = conn.Mail(e.From) | |||||
if err != nil { | |||||
if strings.Contains(err.Error(), "530 5.5.1") { | |||||
return errors.New("Error: Authentication failure. Your username or password is incorrect.") | |||||
} | |||||
return err | |||||
} | |||||
for _, recipient := range e.To { | |||||
err = conn.Rcpt(recipient) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
wc, err := conn.Data() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer wc.Close() | |||||
_, err = wc.Write(e.Bytes()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (e *Email) Bytes() []byte { | |||||
buf := bytes.NewBuffer(nil) | |||||
var subject = e.Subject | |||||
subject = regexp.MustCompile("\n+").ReplaceAllString(subject, " ") | |||||
subject = regexp.MustCompile(" +").ReplaceAllString(subject, " ") | |||||
buf.WriteString("Subject: " + subject + "\n") | |||||
buf.WriteString("To: <" + strings.Join(e.To, ">,<") + ">\n") | |||||
buf.WriteString("MIME-Version: 1.0\n") | |||||
// Boundary is used by MIME to separate files. | |||||
boundary := "f46d043c813270fc6b04c2d223da" | |||||
if len(e.Attachments) > 0 { | |||||
buf.WriteString("Content-Type: multipart/mixed; boundary=" + boundary + "\n") | |||||
buf.WriteString("--" + boundary + "\n") | |||||
} | |||||
if e.ContentType == "" { | |||||
e.ContentType = "text/plain; charset=utf-8" | |||||
} | |||||
buf.WriteString(fmt.Sprintf("Content-Type: %s\n\n", e.ContentType)) | |||||
buf.WriteString(e.Body) | |||||
if len(e.Attachments) > 0 { | |||||
for k, v := range e.Attachments { | |||||
buf.WriteString("\n\n--" + boundary + "\n") | |||||
buf.WriteString("Content-Type: application/octet-stream\n") | |||||
buf.WriteString("Content-Transfer-Encoding: base64\n") | |||||
buf.WriteString("Content-Disposition: attachment; filename=\"" + k + "\"\n\n") | |||||
b := make([]byte, base64.StdEncoding.EncodedLen(len(v))) | |||||
base64.StdEncoding.Encode(b, v) | |||||
buf.Write(b) | |||||
buf.WriteString("\n--" + boundary) | |||||
} | |||||
buf.WriteString("--") | |||||
} | |||||
return buf.Bytes() | |||||
} |
@ -0,0 +1,15 @@ | |||||
package alert | |||||
import ( | |||||
"github.com/op/go-logging" | |||||
) | |||||
var log = logging.MustGetLogger("alert") | |||||
func init() { | |||||
logging.SetFormatter(logging.MustStringFormatter("[%{level:.1s}] %{message}")) | |||||
} | |||||
func SetAlertLogger(l *logging.Logger) { | |||||
log = l | |||||
} |
@ -0,0 +1,131 @@ | |||||
package rpc | |||||
import ( | |||||
"encoding/json" | |||||
"fmt" | |||||
"net/http" | |||||
"net/url" | |||||
"runtime/debug" | |||||
"strings" | |||||
"time" | |||||
"github.com/tendermint/tendermint/alert" | |||||
) | |||||
type APIStatus string | |||||
const ( | |||||
API_OK APIStatus = "OK" | |||||
API_ERROR APIStatus = "ERROR" | |||||
API_INVALID_PARAM APIStatus = "INVALID_PARAM" | |||||
API_UNAUTHORIZED APIStatus = "UNAUTHORIZED" | |||||
API_REDIRECT APIStatus = "REDIRECT" | |||||
) | |||||
type APIResponse struct { | |||||
Status APIStatus `json:"status"` | |||||
Data interface{} `json:"data"` | |||||
} | |||||
func (res APIResponse) Error() string { | |||||
return fmt.Sprintf("Status(%v) %v", res.Status, res.Data) | |||||
} | |||||
// Throws a panic which the RecoverAndLogHandler catches. | |||||
func ReturnJSON(status APIStatus, data interface{}) { | |||||
res := APIResponse{} | |||||
res.Status = status | |||||
res.Data = data | |||||
panic(res) | |||||
} | |||||
// Wraps an HTTP handler, adding error logging. | |||||
// | |||||
// If the inner function panics, the outer function recovers, logs, sends an | |||||
// HTTP 500 error response. | |||||
func RecoverAndLogHandler(handler http.Handler) http.Handler { | |||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
// Wrap the ResponseWriter to remember the status | |||||
rww := &ResponseWriterWrapper{-1, w} | |||||
begin := time.Now() | |||||
// Common headers | |||||
origin := r.Header.Get("Origin") | |||||
originUrl, err := url.Parse(origin) | |||||
if err == nil { | |||||
originHost := strings.Split(originUrl.Host, ":")[0] | |||||
if strings.HasSuffix(originHost, ".ftnox.com") { | |||||
rww.Header().Set("Access-Control-Allow-Origin", origin) | |||||
rww.Header().Set("Access-Control-Allow-Credentials", "true") | |||||
rww.Header().Set("Access-Control-Expose-Headers", "X-Server-Time") | |||||
} | |||||
} | |||||
rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) | |||||
defer func() { | |||||
// Send a 500 error if a panic happens during a handler. | |||||
// Without this, Chrome & Firefox were retrying aborted ajax requests, | |||||
// at least to my localhost. | |||||
if e := recover(); e != nil { | |||||
// If APIResponse, | |||||
if res, ok := e.(APIResponse); ok { | |||||
resJSON, err := json.Marshal(res) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
rww.Header().Set("Content-Type", "application/json") | |||||
switch res.Status { | |||||
case API_OK: | |||||
rww.WriteHeader(200) | |||||
case API_ERROR: | |||||
rww.WriteHeader(400) | |||||
case API_UNAUTHORIZED: | |||||
rww.WriteHeader(401) | |||||
case API_INVALID_PARAM: | |||||
rww.WriteHeader(420) | |||||
case API_REDIRECT: | |||||
rww.WriteHeader(430) | |||||
default: | |||||
rww.WriteHeader(440) | |||||
} | |||||
rww.Write(resJSON) | |||||
} else { | |||||
// For the rest, | |||||
rww.WriteHeader(http.StatusInternalServerError) | |||||
rww.Write([]byte("Internal Server Error")) | |||||
log.Error("%s: %s", e, debug.Stack()) | |||||
} | |||||
} | |||||
// Finally, log. | |||||
durationMS := time.Since(begin).Nanoseconds() / 1000000 | |||||
if rww.Status == -1 { | |||||
rww.Status = 200 | |||||
} | |||||
log.Debug("%s %s %v %v %s", r.RemoteAddr, r.Method, rww.Status, durationMS, r.URL) | |||||
}() | |||||
handler.ServeHTTP(rww, r) | |||||
}) | |||||
} | |||||
// Remember the status for logging | |||||
type ResponseWriterWrapper struct { | |||||
Status int | |||||
http.ResponseWriter | |||||
} | |||||
func (w *ResponseWriterWrapper) WriteHeader(status int) { | |||||
w.Status = status | |||||
w.ResponseWriter.WriteHeader(status) | |||||
} | |||||
// Stick it as a deferred statement in gouroutines to prevent the program from crashing. | |||||
func Recover(daemonName string) { | |||||
if e := recover(); e != nil { | |||||
stack := string(debug.Stack()) | |||||
errorString := fmt.Sprintf("[%s] %s\n%s", daemonName, e, stack) | |||||
alert.Alert(errorString) | |||||
} | |||||
} |
@ -0,0 +1,23 @@ | |||||
package rpc | |||||
import ( | |||||
"fmt" | |||||
"net/http" | |||||
. "github.com/tendermint/tendermint/config" | |||||
) | |||||
func StartHTTPServer() { | |||||
//http.HandleFunc("/path", handler) | |||||
//http.HandleFunc("/path", handler) | |||||
// Serve HTTP on localhost only. | |||||
// Let something like Nginx handle HTTPS connections. | |||||
address := fmt.Sprintf("127.0.0.1:%v", Config.RPC.HTTPPort) | |||||
log.Info("Starting RPC HTTP server on http://%s", address) | |||||
go func() { | |||||
log.Fatal(http.ListenAndServe(address, RecoverAndLogHandler(http.DefaultServeMux))) | |||||
}() | |||||
} |
@ -0,0 +1,15 @@ | |||||
package rpc | |||||
import ( | |||||
"github.com/op/go-logging" | |||||
) | |||||
var log = logging.MustGetLogger("rpc") | |||||
func init() { | |||||
logging.SetFormatter(logging.MustStringFormatter("[%{level:.1s}] %{message}")) | |||||
} | |||||
func SetRPCLogger(l *logging.Logger) { | |||||
log = l | |||||
} |
@ -1,3 +0,0 @@ | |||||
package rpc | |||||
// TODO |