Hammerhead signs in to the site's Bamboo SSH server through the SSH port an edge exposes, with a key of its own, and relays the shell to the browser over a WebSocket (xterm.js, served by Hammerhead). The SSH dialog shows Hammerhead's public key to add to authorized_keys. Host keys are saved on first use and a changed one is refused; the terminal opens only from the dashboard's own origin, is audited, and closes after 30 minutes idle. Errors reach the page as messages over the socket. An end-to-end test runs a real shell through the edge, tunnel and Bamboo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/smtp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
st := s.st.Settings
|
|
st.SMTP.Pass = "" // write-only
|
|
st.Sandbar.Token, st.Sandbar.Monitors = "", nil // write-only
|
|
st.Terminal = TerminalSettings{} // the private key never leaves the server
|
|
writeJSON(w, 200, map[string]any{"settings": st, "smtpPassSet": s.st.Settings.SMTP.Pass != "", "sandbarTokenSet": s.st.Settings.Sandbar.Token != "", "sandbarSync": s.sandbarStatus})
|
|
}
|
|
|
|
func (s *Server) putSettings(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
var in Settings
|
|
if !decode(w, r, &in) {
|
|
return
|
|
}
|
|
in.DashboardURL = strings.TrimRight(strings.TrimSpace(in.DashboardURL), "/")
|
|
if in.DashboardURL != "" && !strings.HasPrefix(in.DashboardURL, "http://") && !strings.HasPrefix(in.DashboardURL, "https://") {
|
|
fail(w, 400, errors.New("dashboard URL must start with http:// or https://"))
|
|
return
|
|
}
|
|
if m := in.SMTP.Mode; m != "" && m != "starttls" && m != "tls" && m != "none" {
|
|
fail(w, 400, errors.New("smtp mode must be starttls, tls or none"))
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
if in.SMTP.Pass == "" {
|
|
in.SMTP.Pass = s.st.Settings.SMTP.Pass // blank keeps the saved password
|
|
}
|
|
if err := s.cleanSandbar(&in.Sandbar); err != nil {
|
|
s.mu.Unlock()
|
|
fail(w, 400, err)
|
|
return
|
|
}
|
|
if s.opts.DashboardURL != "" {
|
|
in.DashboardURL = s.st.Settings.DashboardURL // pinned by flag/env
|
|
}
|
|
in.SelfEdgeID = s.st.Settings.SelfEdgeID // not editable here; losing it would make the next start create a second self edge
|
|
in.Terminal = s.st.Settings.Terminal // the terminal key and trusted host keys are not editable here either
|
|
s.st.Settings = in
|
|
s.bump() // edges receive the dashboard URL and ACME settings
|
|
s.mu.Unlock()
|
|
s.audit("", p.User.Email, "settings.update", "")
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) testMail(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
if err := s.Mailer(p.User.Email, "Hammerhead test email", "If you can read this, outgoing email works."); err != nil {
|
|
fail(w, 400, err)
|
|
return
|
|
}
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) mailConfigured() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.st.Settings.SMTP.Host != ""
|
|
}
|
|
|
|
// smtpSend is the default Mailer.
|
|
func (s *Server) smtpSend(to, subject, body string) error {
|
|
s.mu.Lock()
|
|
c := s.st.Settings.SMTP
|
|
s.mu.Unlock()
|
|
if c.Host == "" {
|
|
return errors.New("Email is not set up yet. Set it up under Server, Settings.")
|
|
}
|
|
if c.Port == 0 {
|
|
c.Port = 587
|
|
if c.Mode == "tls" {
|
|
c.Port = 465
|
|
}
|
|
}
|
|
if c.From == "" {
|
|
c.From = c.User
|
|
}
|
|
addr := net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
|
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s\r\n",
|
|
c.From, to, strings.NewReplacer("\r", "", "\n", " ").Replace(subject), body)
|
|
|
|
d := net.Dialer{Timeout: 10 * time.Second}
|
|
conn, err := d.Dial("tcp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.Mode == "tls" {
|
|
conn = tls.Client(conn, &tls.Config{ServerName: c.Host})
|
|
}
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
cl, err := smtp.NewClient(conn, c.Host)
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
defer cl.Close()
|
|
if c.Mode == "" || c.Mode == "starttls" {
|
|
if ok, _ := cl.Extension("STARTTLS"); ok {
|
|
if err := cl.StartTLS(&tls.Config{ServerName: c.Host}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if c.User != "" {
|
|
if err := cl.Auth(smtp.PlainAuth("", c.User, c.Pass, c.Host)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := cl.Mail(c.From); err != nil {
|
|
return err
|
|
}
|
|
if err := cl.Rcpt(to); err != nil {
|
|
return err
|
|
}
|
|
wc, err := cl.Data()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := wc.Write([]byte(msg)); err != nil {
|
|
return err
|
|
}
|
|
if err := wc.Close(); err != nil {
|
|
return err
|
|
}
|
|
return cl.Quit()
|
|
}
|