package main import ( "errors" "fmt" "io" "net" "net/http" "os" "path/filepath" "strings" "time" ) // errHostUnreachable marks a transport-level failure (tunnel down, host server // not running) as distinct from an HTTP-level rejection, so the client can fall // back to the local opener only when forwarding genuinely failed. var errHostUnreachable = errors.New("host unreachable") var httpClient = &http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, }, } func runClient(args []string, openerName string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "open: missing operand") return 2 } token, err := envToken() if err != nil { fmt.Fprintf(os.Stderr, "open: %v\n", err) return 1 } c := &client{ base: "http://" + envAddr(), token: token, } exit := 0 for _, arg := range args { err := c.openOne(arg) if errors.Is(err, errHostUnreachable) { if ferr := localOpen(openerName, arg); ferr != nil { fmt.Fprintf(os.Stderr, "open: %s: host unreachable, local fallback failed: %v\n", arg, ferr) exit = 1 } continue } if err != nil { fmt.Fprintf(os.Stderr, "open: %s: %v\n", arg, err) exit = 1 } } return exit } type client struct { base string token string } func (c *client) openOne(arg string) error { // Existing local file takes priority: copy it so self-contained files render // on the host. Directories and missing paths fall through to string/URL. if info, err := os.Stat(arg); err == nil && info.Mode().IsRegular() { return c.sendFile(arg) } return c.sendString(arg) } func (c *client) sendFile(path string) error { f, err := os.Open(path) if err != nil { return err } defer func() { _ = f.Close() }() req, err := http.NewRequest(http.MethodPost, c.base+"/file", f) if err != nil { return err } req.Header.Set(fileNameHeader, filepath.Base(path)) req.Header.Set("Content-Type", "application/octet-stream") return c.do(req) } func (c *client) sendString(s string) error { req, err := http.NewRequest(http.MethodPost, c.base+"/open", strings.NewReader(s)) if err != nil { return err } req.Header.Set("Content-Type", "text/plain") return c.do(req) } func (c *client) do(req *http.Request) error { if c.token != "" { req.Header.Set(tokenHeader, c.token) } resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("%w: %s: %v", errHostUnreachable, c.base, err) } defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("host returned %s: %s", resp.Status, strings.TrimSpace(string(body))) } return nil }