1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
// HTTP resource encoding/decoding for varlink services that support binary data via upgrade
package varlinklib
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
// ResourceRequest represents a request for a binary resource from a varlink service
type ResourceRequest struct {
// Service is the varlink service address (e.g., "unix:/run/user/1000/de.Profpatsch.Maildir")
Service string `json:"service"`
// Method is the full varlink method name (e.g., "de.profpatsch.Maildir.InvokeAction")
Method string `json:"method"`
// Parameters are the varlink method parameters (including token)
Parameters map[string]any `json:"parameters"`
// Path is the HTTP resource path to request after upgrade (e.g., "/parts/0")
Path string `json:"path"`
}
// EncodeResourceURL encodes a ResourceRequest into a URL with query parameters.
// The resulting URL has the format: /r?request={url-encoded-json}
//
// This encoding is stable because json.Marshal produces deterministic output
// (sorts map keys alphabetically), ensuring the same parameters always produce
// the same URL.
func EncodeResourceURL(req ResourceRequest) (string, error) {
// Marshal to JSON (deterministic key ordering)
jsonBytes, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("failed to marshal resource request: %w", err)
}
// Build URL with query parameter
u := url.URL{
Path: "/r",
RawQuery: url.Values{
"request": []string{string(jsonBytes)},
}.Encode(),
}
return u.String(), nil
}
// DecodeResourceURL decodes a resource URL back into a ResourceRequest.
// Expects URLs in the format: /r?request={url-encoded-json}
func DecodeResourceURL(urlStr string) (*ResourceRequest, error) {
// Parse URL
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
// Check path
if u.Path != "/r" {
return nil, fmt.Errorf("invalid resource URL: expected /r path, got %s", u.Path)
}
// Get query parameter
requestJSON := u.Query().Get("request")
if requestJSON == "" {
return nil, fmt.Errorf("invalid resource URL: missing 'request' parameter")
}
// Unmarshal JSON
var req ResourceRequest
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
// Validate required fields
if req.Service == "" {
return nil, fmt.Errorf("invalid resource request: missing service")
}
if req.Method == "" {
return nil, fmt.Errorf("invalid resource request: missing method")
}
if req.Path == "" {
return nil, fmt.Errorf("invalid resource request: missing path")
}
return &req, nil
}
// ResourceDescriptor describes a binary resource available after HTTP upgrade.
// Services return these in their responses with resource_type: "http_listing"
type ResourceDescriptor struct {
// Path is the HTTP path for this resource (e.g., "/parts/0")
Path string `json:"path"`
// ContentType is the MIME type (e.g., "application/pdf")
ContentType string `json:"content_type"`
// Filename is the suggested download filename (optional)
Filename string `json:"filename,omitempty"`
// Size is the resource size in bytes
Size int64 `json:"size"`
// Description is a human-readable description (optional)
Description string `json:"description,omitempty"`
}
// MakeAbsoluteURL converts a relative resource path to an absolute URL using
// the provided base URL. This is useful for proxies that want to rewrite URLs.
//
// Example:
//
// desc.Path = "/parts/0"
// baseURL = "http://localhost:8080"
// result = "http://localhost:8080/parts/0"
func (rd *ResourceDescriptor) MakeAbsoluteURL(baseURL string) string {
baseURL = strings.TrimSuffix(baseURL, "/")
return baseURL + rd.Path
}
|