package main import ( "context" "crypto/tls" "fmt" "io" "net/http" "net/url" "os" "time" "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gemini" "tildegit.org/tjp/sliderule/spartan" ) const usage = `Resource fetcher for the small web. Usage: sw-fetch (-h | --help) sw-fetch [-v | --verbose] [-o PATH | --output PATH] [-k | --keyfile PATH] [ -c | --certfile PATH ] [ -s | --skip-verify ] [ -t | --timeout TIMEOUT ] [ -u | --upload ] URL Options: -h --help Show this screen. -v --verbose Display more diagnostic information on standard error. -o --output PATH Send the fetched resource to PATH instead of standard out. -k --keyfile PATH Path to the TLS key file to use. -c --certfile PATH Path to the TLS certificate file to use. -s --skip-verify Don't verify server TLS certificates. -t --timeout TIMEOUT Fail after the given timeout (like "15s"). -u --upload Use stdin as the request body on supported protocols and don't follow redirects. ` func main() { conf := configure() cl := sliderule.NewClient(conf.clientTLS) ctx := context.Background() if conf.timeout != 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, conf.timeout) defer cancel() } var response *sliderule.Response var err error if conf.upload { response, err = cl.Upload(ctx, conf.url.String(), os.Stdin) } else { response, err = cl.Fetch(ctx, conf.url.String()) } if err != nil { fail(err.Error() + "\n") } defer func() { _ = response.Close() _ = conf.output.Close() }() success := printResponse(response, conf) if !success { os.Exit(1) } } type config struct { verbose bool upload bool output io.WriteCloser url *url.URL clientTLS *tls.Config timeout time.Duration } func configure() config { if len(os.Args) == 1 { fail(usage) } conf := config{output: os.Stdout} key := "" cert := "" verify := true var err error for i := 1; i <= len(os.Args)-1; i += 1 { switch os.Args[i] { case "-h", "--help": os.Stdout.WriteString(usage) os.Exit(0) case "-v", "--verbose": conf.verbose = true case "-o", "--output": if i+1 == len(os.Args)-1 { fail(usage) } out := os.Args[i+1] if out != "-" { conf.output, err = os.OpenFile(out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { fmt.Println(err.Error()) failf("'%s' is not a valid path\n", out) } } i += 1 case "-k", "--keyfile": if i+1 == len(os.Args)-1 { fail(usage) } i += 1 key = os.Args[i] case "-c", "--certfile": if i+1 == len(os.Args)-1 { fail(usage) } i += 1 cert = os.Args[i] case "-s", "--skip-verify": verify = false case "-t", "--timeout": if i+1 == len(os.Args)-1 { fail(usage) } i += 1 conf.timeout, err = time.ParseDuration(os.Args[i]) if err != nil { fail(err.Error()) } case "-u", "--upload": conf.upload = true } } conf.clientTLS = &tls.Config{} if key != "" || cert != "" { if key == "" || cert == "" { fail("-k|--keyfile and -c|--certfile must both be present, or neither\n") } tlsConf, err := gemini.FileTLS(cert, key) if err != nil { failf("failed to load TLS key pair: %s", err.Error()) } conf.clientTLS = tlsConf } conf.clientTLS.InsecureSkipVerify = !verify u, err := url.Parse(os.Args[len(os.Args)-1]) if err != nil || u.Scheme == "" { fail(usage) } conf.url = u return conf } func fail(msg string) { os.Stderr.WriteString(msg) os.Exit(1) } func failf(msg string, args ...any) { fmt.Fprintf(os.Stderr, msg, args...) os.Exit(1) } func printResponse(response *sliderule.Response, conf config) bool { success := true switch conf.url.Scheme { case "http", "https": switch int(response.Status) / 100 { case 4, 5: fmt.Fprintf(os.Stderr, "http %d: %s\n", response.Status, http.StatusText(int(response.Status))) success = false } case "gemini": //, "titan" switch gemini.ResponseCategoryForStatus(response.Status) { case gemini.ResponseCategoryTemporaryFailure, gemini.ResponseCategoryPermanentFailure, gemini.ResponseCategoryCertificateRequired: fmt.Fprintf(os.Stderr, "gemini %d: %s\n", response.Status, response.Meta.(string)) success = false } case "spartan": switch response.Status { case spartan.StatusClientError, spartan.StatusServerError: fmt.Fprintf(os.Stderr, "spartan %d: %s\n", response.Status, response.Meta.(string)) success = false } } if _, err := io.Copy(conf.output, response.Body); err != nil { fail(err.Error()) } return success }