package diag import ( "bytes" "fmt" "io" "net/http" "strings" "time" ) // maxReportSize bounds a stored report, on both the sending and the // receiving side. const maxReportSize = 4 << 20 // 4 MB // Upload posts a report to a relay's diagnostics endpoint and returns the URL // it can be fetched from. // // The point is getting a report off a machine that is awkward to copy from — // a headless NAS, a Windows box mid-debugging — without pasting thousands of // lines by hand. func Upload(relayURL, reportID string, report *Report) (string, error) { data, err := report.JSON() if err != nil { return "", fmt.Errorf("encoding report: %w", err) } if len(data) > maxReportSize { return "", fmt.Errorf("report is %d bytes, over the %d byte limit", len(data), maxReportSize) } target, err := DiagURL(relayURL, reportID) if err != nil { return "", err } req, err := http.NewRequest(http.MethodPut, target, bytes.NewReader(data)) if err != nil { return "", fmt.Errorf("building request: %w", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("uploading to %s: %w", target, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return "", fmt.Errorf("relay refused the report: %s: %s", resp.Status, strings.TrimSpace(string(body))) } return target, nil } // DiagURL builds the diagnostics URL for a report ID on a relay. // // It accepts the same address forms the client's relay setting does, so the // user does not have to remember a second syntax. func DiagURL(relayURL, reportID string) (string, error) { if reportID == "" { return "", fmt.Errorf("a report ID is required") } if strings.ContainsAny(reportID, "/?#") { return "", fmt.Errorf("report ID must not contain /, ? or #") } base := strings.TrimSuffix(strings.TrimSpace(relayURL), "/") base = strings.TrimSuffix(base, "/ws") switch { case strings.HasPrefix(base, "ws://"): base = "http://" + strings.TrimPrefix(base, "ws://") case strings.HasPrefix(base, "wss://"): base = "https://" + strings.TrimPrefix(base, "wss://") case strings.HasPrefix(base, "http://"), strings.HasPrefix(base, "https://"): // already fine default: base = "http://" + base } return base + "/diag/" + reportID, nil } // RetentionNote describes how long an uploaded report survives on the relay. // Kept here so the client can say so without importing the relay package. const RetentionNote = "24 hours"