package sendoverhttp import ( "errors" "io/fs" "log" "net" "net/http" "net/url" "os" "path/filepath" ) // Config contains the configuration parameters for the service. type Config struct { // FilePath is the file to serve. FilePath string // ListenAddress is the network address to listen for connections. ListenAddress string // Network type to listen on. Network string } // Source a single listening address to listen on. func getListenAddress(config Config) (string, error) { switch config.Network { case "tcp", "tcp4", "tcp6": default: return config.ListenAddress, nil } addr, port, err := net.SplitHostPort(config.ListenAddress) if err != nil { addr = config.ListenAddress port = "0" } if addr != "" { ip := net.ParseIP(addr) if ip == nil { return config.ListenAddress, nil } if !ip.IsUnspecified() { return config.ListenAddress, nil } } ips, err := net.InterfaceAddrs() if err != nil { return "", err } // Valid fallback if no other interfaces available var loopback *net.IP = nil for _, addr := range ips { ipNet, ok := addr.(*net.IPNet) if !ok { continue } if config.Network == "tcp6" && ipNet.IP.To4() != nil { continue } if ipNet.IP.IsLoopback() { loopback = &ipNet.IP continue } return net.JoinHostPort(ipNet.IP.String(), port), nil } if loopback != nil { return net.JoinHostPort((*loopback).String(), port), nil } return "", errors.New("no valid network address could be found") } type Server struct { config Config closeCh chan struct{} mr *MultiRunner } type SingleFileDir string func (s SingleFileDir) Open(name string) (http.File, error) { if name == "index.html" { return nil, fs.ErrNotExist } fp := string(s) stat, err := os.Stat(fp) if err != nil { return nil, err } if stat.IsDir() { return http.Dir(fp).Open(name) } switch name { case "/": return nil, os.ErrNotExist case "/" + filepath.Base(fp): return os.Open(fp) default: return nil, os.ErrNotExist } } func NewServer(config Config) *Server { return &Server{ config: config, closeCh: make(chan struct{}), mr: NewMultiRunner(), } } func (s *Server) Start() error { listenAddr, err := getListenAddress(s.config) if err != nil { log.Fatal(err) } l, err := net.Listen(s.config.Network, listenAddr) if err != nil { log.Fatal(err) } u := url.URL{ Scheme: "http", Host: l.Addr().String(), } if stat, _ := os.Stat(s.config.FilePath); !stat.IsDir() { u.Path = filepath.Base(s.config.FilePath) } qrCodeShower := NewTerminalQRShower(u.String()) go s.mr.Run(qrCodeShower) var d http.FileSystem = http.Dir(s.config.FilePath) if stat, _ := os.Stat(s.config.FilePath); !stat.IsDir() { // Show user the name of the file d = SingleFileDir(s.config.FilePath) } httpSrv := &http.Server{ Handler: http.FileServer(d), } httpRunner := NewRunner() httpRunner.OnStart = func() error { return httpSrv.Serve(l) } httpRunner.OnStop = func() { _ = httpSrv.Close() } go s.mr.Run(httpRunner) <-s.closeCh close(s.closeCh) return nil } func (s *Server) Stop() { s.mr.Stop() return }