192 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			192 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Package webserver is a UniFi Poller plugin that exports running data to a web interface.
 | |
| package webserver
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/gorilla/mux"
 | |
| 	"github.com/unpoller/unpoller/pkg/poller"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// PluginName identifies this output plugin.
 | |
| 	PluginName = "WebServer"
 | |
| 	// DefaultPort is the default web http port.
 | |
| 	DefaultPort = 37288
 | |
| 	// DefaultEvents is the default number of events stored per plugin.
 | |
| 	DefaultEvents = 200
 | |
| )
 | |
| 
 | |
| // Config is the webserver library input config.
 | |
| type Config struct {
 | |
| 	Enable     bool     `json:"enable"        toml:"enable"        xml:"enable,attr"   yaml:"enable"`
 | |
| 	SSLCrtPath string   `json:"ssl_cert_path" toml:"ssl_cert_path" xml:"ssl_cert_path" yaml:"ssl_cert_path"`
 | |
| 	SSLKeyPath string   `json:"ssl_key_path"  toml:"ssl_key_path"  xml:"ssl_key_path"  yaml:"ssl_key_path"`
 | |
| 	Port       uint     `json:"port"          toml:"port"          xml:"port"          yaml:"port"`
 | |
| 	Accounts   accounts `json:"accounts"      toml:"accounts"      xml:"accounts"      yaml:"accounts"`
 | |
| 	HTMLPath   string   `json:"html_path"     toml:"html_path"     xml:"html_path"     yaml:"html_path"`
 | |
| 	MaxEvents  uint     `json:"max_events"    toml:"max_events"    xml:"max_events"    yaml:"max_events"`
 | |
| }
 | |
| 
 | |
| // accounts stores a map of usernames and password hashes.
 | |
| type accounts map[string]string
 | |
| 
 | |
| // Server is the main library struct/data.
 | |
| type Server struct {
 | |
| 	*Config `json:"webserver" toml:"webserver" xml:"webserver" yaml:"webserver"`
 | |
| 	server  *http.Server
 | |
| 	plugins *webPlugins
 | |
| 	Collect poller.Collect
 | |
| 	start   time.Time
 | |
| }
 | |
| 
 | |
| var _ poller.OutputPlugin = &Server{}
 | |
| 
 | |
| // init is how this modular code is initialized by the main app.
 | |
| // This module adds itself as an output module to the poller core.
 | |
| func init() { // nolint: gochecknoinits
 | |
| 	s := &Server{plugins: plugins, start: time.Now(), Config: &Config{
 | |
| 		Port:      DefaultPort,
 | |
| 		HTMLPath:  filepath.Join(poller.DefaultObjPath(), "web"),
 | |
| 		MaxEvents: DefaultEvents,
 | |
| 	}}
 | |
| 	plugins.Config = s.Config
 | |
| 
 | |
| 	poller.NewOutput(&poller.Output{
 | |
| 		Name:         PluginName,
 | |
| 		Config:       s,
 | |
| 		OutputPlugin: s,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (s *Server) Enabled() bool {
 | |
| 	if s == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	
 | |
| 	if s.Config == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	return s.Enable
 | |
| }
 | |
| 
 | |
| // Run starts the server and gets things going.
 | |
| func (s *Server) Run(c poller.Collect) error {
 | |
| 	s.Collect = c
 | |
| 	if s.Config == nil || s.Port == 0 || s.HTMLPath == "" || !s.Enabled() {
 | |
| 		s.LogDebugf("Internal web server disabled!")
 | |
| 		
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	s.Logf("Internal web server enabled")
 | |
| 
 | |
| 	if _, err := os.Stat(s.HTMLPath); err != nil {
 | |
| 		return fmt.Errorf("problem with HTML path: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	UpdateOutput(&Output{Name: PluginName, Config: s.Config})
 | |
| 
 | |
| 	return s.Start()
 | |
| }
 | |
| 
 | |
| func (s *Server) DebugOutput() (bool, error) {
 | |
| 	if s == nil {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 	
 | |
| 	if !s.Enabled() {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 	
 | |
| 	if s.HTMLPath == "" {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 
 | |
| 	// check the html path
 | |
| 	if _, err := os.Stat(s.HTMLPath); err != nil {
 | |
| 		return false, fmt.Errorf("problem with HTML path: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// check the port
 | |
| 	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port))
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	_ = ln.Close()
 | |
| 
 | |
| 	return true, nil
 | |
| }
 | |
| 
 | |
| // Start gets the web server going.
 | |
| func (s *Server) Start() (err error) {
 | |
| 	s.server = &http.Server{
 | |
| 		Addr:         "0.0.0.0:" + strconv.Itoa(int(s.Port)),
 | |
| 		WriteTimeout: time.Minute,
 | |
| 		ReadTimeout:  time.Minute,
 | |
| 		IdleTimeout:  time.Minute,
 | |
| 		Handler:      s.newRouter(), // *mux.Router
 | |
| 	}
 | |
| 
 | |
| 	if s.SSLCrtPath == "" || s.SSLKeyPath == "" {
 | |
| 		s.Logf("Web server starting without SSL. Listening on HTTP port %d", s.Port)
 | |
| 		err = s.server.ListenAndServe()
 | |
| 	} else {
 | |
| 		s.Logf("Web server starting with SSL. Listening on HTTPS port %d", s.Port)
 | |
| 		err = s.server.ListenAndServeTLS(s.SSLCrtPath, s.SSLKeyPath)
 | |
| 	}
 | |
| 
 | |
| 	if !errors.Is(err, http.ErrServerClosed) {
 | |
| 		return fmt.Errorf("web server: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *Server) newRouter() *mux.Router {
 | |
| 	router := mux.NewRouter()
 | |
| 	// special routes
 | |
| 	router.Handle("/debug/vars", http.DefaultServeMux).Methods("GET")        // unauthenticated expvar
 | |
| 	router.HandleFunc("/health", s.handleLog(s.handleHealth)).Methods("GET") // unauthenticated health
 | |
| 	// main web app/files/js/css
 | |
| 	router.HandleFunc("/", s.basicAuth(s.handleIndex)).Methods("GET", "POST")
 | |
| 	router.PathPrefix("/{sub:css|js|img|image|images}/").Handler((s.basicAuth(s.handleStatic))).Methods("GET")
 | |
| 	// api paths for json dumps
 | |
| 	router.HandleFunc("/api/v1/config", s.basicAuth(s.handleConfig)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/config/{sub}", s.basicAuth(s.handleConfig)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/config/{sub}/{value}", s.basicAuth(s.handleConfig)).Methods("GET", "POST")
 | |
| 	router.HandleFunc("/api/v1/input/{input}", s.basicAuth(s.handleInput)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/input/{input}/{sub}", s.basicAuth(s.handleInput)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/input/{input}/{sub}/{value}", s.basicAuth(s.handleInput)).Methods("GET", "POST")
 | |
| 	router.HandleFunc("/api/v1/output/{output}", s.basicAuth(s.handleOutput)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/output/{output}/{sub}", s.basicAuth(s.handleOutput)).Methods("GET")
 | |
| 	router.HandleFunc("/api/v1/output/{output}/{sub}/{value}", s.basicAuth(s.handleOutput)).Methods("GET", "POST")
 | |
| 	router.PathPrefix("/").Handler(s.basicAuth(s.handleMissing)).Methods("GET", "POST", "PUT") // 404 everything.
 | |
| 
 | |
| 	return router
 | |
| }
 | |
| 
 | |
| // PasswordIsCorrect returns true if the provided password matches a user's account.
 | |
| func (a accounts) PasswordIsCorrect(user, pass string, ok bool) bool {
 | |
| 	if len(a) == 0 {
 | |
| 		return true // No accounts defined in config; allow anyone.
 | |
| 	} else if !ok {
 | |
| 		return false // r.BasicAuth() failed, not a valid user.
 | |
| 	} else if user, ok = a[user]; !ok { // The user var is now the password hash.
 | |
| 		return false // The username provided doesn't exist.
 | |
| 	}
 | |
| 
 | |
| 	// If this is returns nil, the provided password matches, so return true.
 | |
| 	return bcrypt.CompareHashAndPassword([]byte(user), []byte(pass)) == nil
 | |
| }
 |