Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package goshs for openSUSE:Factory checked in at 2026-03-14 22:22:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/goshs (Old) and /work/SRC/openSUSE:Factory/.goshs.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "goshs" Sat Mar 14 22:22:57 2026 rev:3 rq:1338866 version:1.1.4 Changes: -------- --- /work/SRC/openSUSE:Factory/goshs/goshs.changes 2026-03-11 20:59:26.996140154 +0100 +++ /work/SRC/openSUSE:Factory/.goshs.new.8177/goshs.changes 2026-03-14 22:24:23.298084496 +0100 @@ -1,0 +2,11 @@ +Sat Mar 14 09:25:12 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 1.1.4 + * add a new feature where you can tunnel goshs to localhost.run + to make it available online even if your network can not be + reached directly from the internet. + For more information look at https://localhost.run/. + Bug Fix + * Fixed an upload problem tracked in Issue #134. + +------------------------------------------------------------------- Old: ---- goshs-1.1.3.tar.gz New: ---- goshs-1.1.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ goshs.spec ++++++ --- /var/tmp/diff_new_pack.uMd2sl/_old 2026-03-14 22:24:23.930110679 +0100 +++ /var/tmp/diff_new_pack.uMd2sl/_new 2026-03-14 22:24:23.930110679 +0100 @@ -1,7 +1,7 @@ # # spec file for package goshs # -# Copyright (c) 2021-2025, Martin Hauke <[email protected]> +# Copyright (c) 2021-2026, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -16,14 +16,14 @@ # Name: goshs -Version: 1.1.3 +Version: 1.1.4 Release: 0 Summary: A simple HTTP server License: MIT Group: Productivity/Networking/Web/Servers URL: https://goshs.de/ #Git-Clone: https://github.com/patrickhener/goshs.git -Source: https://github.com/patrickhener/goshs/archive/refs/tags/%{version}.tar.gz#/%{name}-%{version}.tar.gz +Source: https://github.com/patrickhener/goshs/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz Source1: vendor.tar.gz BuildRequires: go >= 1.24.1 BuildRequires: golang-packaging ++++++ goshs-1.1.3.tar.gz -> goshs-1.1.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/README.md new/goshs-1.1.4/README.md --- old/goshs-1.1.3/README.md 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/README.md 2026-03-13 15:11:55.000000000 +0100 @@ -1,4 +1,4 @@ - + [](https://github.com/patrickhener/goshs/blob/master/LICENSE)  [](https://github.com/patrickhener/goshs/issues) @@ -74,6 +74,8 @@ * Share files without authentication * Use Download Limit * Use Time Limit +* Tunnel connection via localhost.run + * Expose your local goshs to the internet via localhost.run # Installation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/config/config.go new/goshs-1.1.4/config/config.go --- old/goshs-1.1.3/config/config.go 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/config/config.go 2026-03-13 15:11:55.000000000 +0100 @@ -52,6 +52,7 @@ SFTPHostKeyFile string `json:"sftp_host_keyfile"` Whitelist string `json:"whitelist"` TrustedProxies string `json:"trusted_proxies"` + Tunnel bool `json:"tunnel"` } func Load(configpath string) (Config, error) { @@ -112,6 +113,7 @@ SFTPHostKeyFile: "", Whitelist: "", TrustedProxies: "", + Tunnel: false, } b, err := json.MarshalIndent(defaultConfig, "", " ") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/example/goshs.json.example new/goshs-1.1.4/example/goshs.json.example --- old/goshs-1.1.3/example/goshs.json.example 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/example/goshs.json.example 2026-03-13 15:11:55.000000000 +0100 @@ -41,5 +41,6 @@ "sftp_keyfile": "", "sftp_host_keyfile": "", "whitelist": "", - "trusted_proxies": "" + "trusted_proxies": "", + "tunnel": false } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/goshsversion/version.go new/goshs-1.1.4/goshsversion/version.go --- old/goshs-1.1.3/goshsversion/version.go 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/goshsversion/version.go 2026-03-13 15:11:55.000000000 +0100 @@ -1,3 +1,3 @@ package goshsversion -var GoshsVersion = "1.1.3" +var GoshsVersion = "v1.1.4" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/httpserver/server.go new/goshs-1.1.4/httpserver/server.go --- old/goshs-1.1.3/httpserver/server.go 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/httpserver/server.go 2026-03-13 15:11:55.000000000 +0100 @@ -9,12 +9,14 @@ "net/http" "os" "runtime" + "strings" "time" "github.com/howeyc/gopass" "github.com/patrickhener/goshs/ca" "github.com/patrickhener/goshs/clipboard" "github.com/patrickhener/goshs/logger" + "github.com/patrickhener/goshs/tunnel" "github.com/patrickhener/goshs/ws" "golang.org/x/net/webdav" "software.sslmate.com/src/go-pkcs12" @@ -46,17 +48,29 @@ mux.Use(fs.ServerHeaderMiddleware) // Define routes - mux.HandleFunc("POST /upload", func(w http.ResponseWriter, r *http.Request) { - fs.upload(w, r) - runtime.GC() - }) mux.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) { - fs.logOnly(w, r) + if strings.HasSuffix(r.URL.Path, "/upload") { + fs.upload(w, r) + runtime.GC() + } else { + fs.logOnly(w, r) + } }) mux.HandleFunc("PUT /", func(w http.ResponseWriter, r *http.Request) { fs.put(w, r) }) - mux.HandleFunc("/", fs.handler) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + // Handle CORS preflight + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, PUT, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.WriteHeader(http.StatusOK) + fs.logOnly(w, r) + } else { + fs.handler(w, r) + } + }) addr = fmt.Sprintf("%+v:%+v", fs.IP, fs.Port) case "webdav": @@ -252,6 +266,17 @@ // Print all embedded files as info to the console fs.PrintEmbeddedFiles() + // Start tunnel if enabled + if fs.Tunnel { + t, err := tunnel.Start(fs.IP, fs.Port) + if err != nil { + logger.Errorf("error starting tunnel: %+v", err) + } else { + defer t.Close() + logger.Infof("Public tunnel URL: %s", t.PublicURL) + } + } + // Create SharedLinks map fs.SharedLinks = make(map[string]SharedLink) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/httpserver/structs.go new/goshs-1.1.4/httpserver/structs.go --- old/goshs-1.1.3/httpserver/structs.go 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/httpserver/structs.go 2026-03-13 15:11:55.000000000 +0100 @@ -84,6 +84,7 @@ Clipboard *clipboard.Clipboard Whitelist *Whitelist SharedLinks map[string]SharedLink + Tunnel bool } type httperror struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/main.go new/goshs-1.1.4/main.go --- old/goshs-1.1.3/main.go 2025-12-16 10:29:53.000000000 +0100 +++ new/goshs-1.1.4/main.go 2026-03-13 15:11:55.000000000 +0100 @@ -66,6 +66,7 @@ trustedProxies = "" MDNS = false invisible = false + tunnel = false ) // Man page @@ -91,6 +92,7 @@ -c, --cli Enable cli (only with auth and tls) (default: false) -e, --embedded Show embedded files in UI (default: false) -o, --output Write output to logfile (default: false) + -t, --tunnel Enable tunnel (default: false) TLS options: -s, --ssl Use TLS @@ -240,6 +242,8 @@ flag.BoolVar(&MDNS, "mdns", MDNS, "Enable zeroconf mDNS registration") flag.BoolVar(&invisible, "I", invisible, "Enable invisible mode") flag.BoolVar(&invisible, "invisible", invisible, "Enable invisible mode") + flag.BoolVar(&tunnel, "t", tunnel, "Enable tunnel") + flag.BoolVar(&tunnel, "tunnel", tunnel, "Enable tunnel") updateGoshs := flag.Bool("update", false, "update") hash := flag.Bool("H", false, "hash") @@ -412,6 +416,7 @@ whitelist = cfg.Whitelist trustedProxies = cfg.TrustedProxies invisible = cfg.Invisible + tunnel = cfg.Tunnel // Abspath for webroot // Trim trailing / for linux/mac and \ for windows @@ -565,6 +570,7 @@ Webhook: *webhook, Verbose: verbose, Whitelist: wl, + Tunnel: tunnel, Version: goshsversion.GoshsVersion, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/goshs-1.1.3/tunnel/tunnel.go new/goshs-1.1.4/tunnel/tunnel.go --- old/goshs-1.1.3/tunnel/tunnel.go 1970-01-01 01:00:00.000000000 +0100 +++ new/goshs-1.1.4/tunnel/tunnel.go 2026-03-13 15:11:55.000000000 +0100 @@ -0,0 +1,189 @@ +package tunnel + +import ( + "bufio" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/patrickhener/goshs/logger" + "golang.org/x/crypto/ssh" +) + +type Tunnel struct { + PublicURL string + client *ssh.Client + session *ssh.Session + stop chan struct{} +} + +type closeWriter interface { + CloseWrite() error +} + +func Start(localIP string, localPort int) (*Tunnel, error) { + config := &ssh.ClientConfig{ + User: "nokey", + Auth: []ssh.AuthMethod{ssh.Password("")}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, + BannerCallback: func(banner string) error { return nil }, + } + + client, err := ssh.Dial("tcp", "localhost.run:22", config) + if err != nil { + return nil, fmt.Errorf("connecting to localhost.run: %w", err) + } + + // Register handler for forwarded-tcpip BEFORE sending the global + // request — channels can arrive immediately after the request is + // accepted, before we get to Accept() calls + chanCh := client.HandleChannelOpen("forwarded-tcpip") + if chanCh == nil { + client.Close() + return nil, fmt.Errorf("could not register forwarded-tcpip handler") + } + + // Send the tcpip-forward global request directly instead of using + // client.Listen() — this avoids the address matching issue entirely + ok, _, err := client.SendRequest("tcpip-forward", true, ssh.Marshal(struct { + BindAddr string + BindPort uint32 + }{"localhost", 80})) + if err != nil || !ok { + client.Close() + return nil, fmt.Errorf("tcpip-forward request failed: %w", err) + } + + // Open session for URL capture + session, err := client.NewSession() + if err != nil { + client.Close() + return nil, fmt.Errorf("opening session: %w", err) + } + + pr, pw := io.Pipe() + session.Stdout = pw + + if err := session.RequestPty("xterm", 80, 40, ssh.TerminalModes{ + ssh.ECHO: 0, + }); err != nil { + session.Close() + client.Close() + return nil, fmt.Errorf("requesting pty: %w", err) + } + + if err := session.Shell(); err != nil { + session.Close() + client.Close() + return nil, fmt.Errorf("requesting shell: %w", err) + } + + urlCh := make(chan string, 1) + errCh := make(chan error, 1) + + go func() { + found := false + scanner := bufio.NewScanner(pr) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if !found && strings.Contains(line, "tunneled with tls termination") { + parts := strings.Split(line, ", ") + if len(parts) == 2 && strings.HasPrefix(parts[1], "https://") { + urlCh <- strings.TrimSpace(parts[1]) + found = true + } + } + } + if !found { + errCh <- fmt.Errorf("session ended before URL was found") + } + }() + + select { + case url := <-urlCh: + t := &Tunnel{ + PublicURL: url, + client: client, + session: session, + stop: make(chan struct{}), + } + go t.accept(chanCh, localIP, localPort) + return t, nil + case err := <-errCh: + session.Close() + client.Close() + return nil, fmt.Errorf("capturing URL: %w", err) + case <-time.After(15 * time.Second): + session.Close() + client.Close() + return nil, fmt.Errorf("timed out waiting for tunnel URL") + } +} + +// accept handles incoming forwarded-tcpip new channel requests directly, +// bypassing the address matching in client.Listen() +func (t *Tunnel) accept(chanCh <-chan ssh.NewChannel, localIP string, localPort int) { + for { + select { + case <-t.stop: + return + case newChan, ok := <-chanCh: + if !ok { + return + } + logger.Debugf("tunnel: incoming forwarded-tcpip channel") + ch, reqs, err := newChan.Accept() + if err != nil { + logger.Debugf("tunnel: accepting channel failed: %v", err) + continue + } + // Discard channel-level requests (window adjustments etc.) + go ssh.DiscardRequests(reqs) + go proxy(ch, localIP, localPort) + } + } +} + +func (t *Tunnel) Close() { + close(t.stop) + t.session.Close() + t.client.Close() +} +func proxy(remote ssh.Channel, localIP string, localPort int) { + defer remote.Close() + logger.Debugf("tunnel: proxy connecting to %s:%d", localIP, localPort) + local, err := net.DialTimeout("tcp", + fmt.Sprintf("%s:%d", localIP, localPort), 5*time.Second) + if err != nil { + logger.Debugf("tunnel: proxy dial failed: %v", err) + return + } + defer local.Close() + logger.Debugf("tunnel: proxy connection established, copying") + + done := make(chan struct{}, 2) + + go func() { + n, err := io.Copy(local, remote) + logger.Debugf("tunnel: remote→local done: %d bytes, err: %v", n, err) + if cw, ok := local.(closeWriter); ok { + cw.CloseWrite() + } + done <- struct{}{} + }() + + go func() { + n, err := io.Copy(remote, local) + logger.Debugf("tunnel: local→remote done: %d bytes, err: %v", n, err) + if cw, ok := remote.(closeWriter); ok { + cw.CloseWrite() + } + done <- struct{}{} + }() + + <-done + <-done +} ++++++ vendor.tar.gz ++++++
