Skip to content
This repository has been archived by the owner on Jan 8, 2021. It is now read-only.

Commit

Permalink
Reducing possibility of channel connection refused ssh errors during …
Browse files Browse the repository at this point in the history
…file serving
  • Loading branch information
rupor-github committed Sep 17, 2019
1 parent deadf6a commit 307ae91
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 42 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ endif()

# Project version number
set(PRJ_VERSION_MAJOR 1)
set(PRJ_VERSION_MINOR 2.0)
set(PRJ_VERSION_MINOR 2.1)

if (EXISTS "${PROJECT_SOURCE_DIR}/.git" AND IS_DIRECTORY "${PROJECT_SOURCE_DIR}/.git")
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/githash.sh ${GIT_EXECUTABLE}
Expand Down
133 changes: 92 additions & 41 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package client

import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -71,66 +75,109 @@ func getLocalHostIPv4() string {
return ""
}

func serveFile(fname string, translateLoopbackIP, debug bool) (string, <-chan struct{}, error) {
func getAddresses(port int, translateLoopbackIP, debug bool) (string, string) {

const anywhere = `:0`
portStr := ":" + strconv.Itoa(port)

addrListen, addrSend := `localhost:0`, `http://localhost:%d/%s`
addrListen, addrSend := `localhost`+portStr, `http://localhost:%d/%s`
if translateLoopbackIP {
// open to the outside world - direct connection expected
addrListen, addrSend = anywhere, `http://127.0.0.1:%d/%s`
addrListen, addrSend = portStr, `http://127.0.0.1:%d/%s`
} else {
if addr := getSSHSessionAddr(); len(addr) != 0 {
// if we run in SSH session - expect dynamic port forwarding
addrListen, addrSend = anywhere, `http://`+addr+`:%d/%s`
addrListen, addrSend = portStr, `http://`+addr+`:%d/%s`
} else if addr = getLocalHostIPv4(); len(addr) != 0 {
// See if could derive extrnal IPv4 for our host
addrListen, addrSend = anywhere, `http://`+addr+`:%d/%s`
addrListen, addrSend = portStr, `http://`+addr+`:%d/%s`
}
}
if debug {
log.Printf("Serving addresses - listen: '%s' send: '%s'", addrListen, addrSend)
}

l, err := net.Listen("tcp", addrListen)
if err != nil {
return "", nil, err
log.Printf("serveFile listen address: '%s' send URL: '%s'", addrListen, addrSend)
}
return addrListen, addrSend
}

finished := make(chan struct{})
go func() {
func getfileHandler(fname string, srv *http.Server, finished chan *http.Server, debug bool) func(w http.ResponseWriter, r *http.Request) {
// NOTE: There is still a chance that serving actual file will be completed before any additional requests from the browser
// generating ssh "channel X: open failed: connect failed: Connection refused" messages, especially when everything is slow
// due to network congestion or excessive debug logging.
return func(w http.ResponseWriter, r *http.Request) {

if debug {
log.Printf("Serving '%s' on %s", fname, l.Addr())
log.Printf("Processing request '%s'", r.URL)
}

_ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// Kill caching
for _, v := range etagHeaders {
r.Header.Del(v)
}
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
if filepath.Base(fname) != path.Base(r.URL.String()) {
if debug {
log.Print("Not serving...")
}
http.Error(w, "not serving...", http.StatusNotFound)
return
}

f, err := os.Open(fname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
// Kill caching
for _, v := range etagHeaders {
r.Header.Del(v)
}
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}

if debug {
log.Printf("Transferring file '%s'", fname)
}

http.ServeContent(w, r, fname, time.Unix(0, 0), f)
if wf, ok := w.(http.Flusher); ok {
wf.Flush()
f, err := os.Open(fname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() {
if debug {
log.Printf("Servng '%s' is completed", fname)
}
f.Close()
}()

finished <- struct{}{}
}))
http.ServeContent(w, r, fname, time.Unix(0, 0), f)
finished <- srv
}
}

// NOTE: we actuall need real server here - browsers like to ask for /favicon.ico etc. especially when ports are selected randomly and
// request url is changing. If not answered properly it will generate channel errors when ssh dynamic port forwarding is used.
func serveFile(fname string, port int, timeout time.Duration, translateLoopbackIP, debug bool) (string, <-chan *http.Server, error) {

addrListen, addrSend := getAddresses(port, translateLoopbackIP, debug)

l, err := net.Listen("tcp", addrListen)
if err != nil {
return "", nil, err
}

finished := make(chan *http.Server)

srv := &http.Server{
Addr: addrListen,
ReadTimeout: timeout,
WriteTimeout: timeout,
}
m := http.NewServeMux()
m.HandleFunc("/", getfileHandler(fname, srv, finished, debug))
srv.Handler = m

go func() {
if debug {
log.Printf("Starting http server for '%s'", fname)
}
_ = srv.Serve(l)
}()

return fmt.Sprintf(addrSend, l.Addr().(*net.TCPAddr).Port, fname), finished, nil
if port == 0 {
port = l.Addr().(*net.TCPAddr).Port
}
return fmt.Sprintf(addrSend, port, fname), finished, nil
}

// Open implements client "open" command.
Expand All @@ -141,10 +188,10 @@ func Open(c *lemon.CLI) error {
log.Printf("Client URI.Open '%s' to %s:%d", uri, c.Host, c.Port)
}

var finished <-chan struct{}
var finished <-chan *http.Server
if c.TransLocalfile && fileExists(uri) {
var err error
uri, finished, err = serveFile(uri, c.TransLoopback, c.Debug)
uri, finished, err = serveFile(uri, c.TransFilePort, c.TransFileTimeout, c.TransLoopback, c.Debug)
if err != nil {
return err
}
Expand All @@ -165,18 +212,22 @@ func Open(c *lemon.CLI) error {
}

if finished != nil {

// First we wait for file to be served
timer := time.NewTimer(c.TransFileTimeout)
defer timer.Stop()

select {
case <-finished:
case srv := <-finished:
if c.Debug {
log.Printf("Client URI.Open to %s:%d done", c.Host, c.Port)
}
// And then we try to end gracefully to avoid ssh channel complaints.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(c.TransFileTimeout))
_ = srv.Shutdown(ctx)
cancel()
case <-timer.C:
log.Printf("Client URI.Open to %s:%d temeouted waiting for file request", c.Host, c.Port)
log.Printf("Client URI.Open to %s:%d timeout waiting for file request", c.Host, c.Port)
}

}
return nil
}
Expand Down
2 changes: 2 additions & 0 deletions lemon/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type CLI struct {
TransLoopback bool
TransLocalfile bool
TransFileTimeout time.Duration
TransFilePort int
LineEnding string
Help bool
Debug bool
Expand All @@ -65,6 +66,7 @@ func New() *CLI {
c.Flags.StringVar(&c.LineEnding, "line-ending", "", "Convert Line Endings (LF/CRLF)")
c.Flags.BoolVar(&c.TransLoopback, "trans-loopback", true, "Replace loopback address [open command only]")
c.Flags.BoolVar(&c.TransLocalfile, "trans-localfile", true, "Transfer local file [open command only]")
c.Flags.IntVar(&c.TransFilePort, "trans-localfile-port", 2490, "Port to listen on transfer local file [open command only]")
c.Flags.DurationVar(&c.TransFileTimeout, "trans-localfile-timeout", time.Second, "How long to wait for local file transfer request [open command only]")
c.Flags.BoolVar(&c.Debug, "debug", false, "Print verbose debugging information")

Expand Down

0 comments on commit 307ae91

Please sign in to comment.