@synopackageland/cli 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. package/dist/compose/privilege.js +0 -102
@@ -1,412 +0,0 @@
1
- package main
2
-
3
- // The gateway is the only door to a package's container. DSM runs it as the
4
- // package account's CGI behind the DSM web server, so a request reaches the
5
- // container only after DSM's own session check, the access rule of the
6
- // package, and (for state-changing methods) a SynoToken.
7
-
8
- import (
9
- "context"
10
- "errors"
11
- "fmt"
12
- "io"
13
- "net"
14
- "net/http"
15
- "net/http/httputil"
16
- "net/url"
17
- "regexp"
18
- "slices"
19
- "strconv"
20
- "strings"
21
- "time"
22
- )
23
-
24
- const (
25
- accessUsers = "users"
26
- accessAdmins = "admins"
27
- accessGroups = "groups"
28
-
29
- adminGroup = "administrators"
30
-
31
- // gatewayScript is the CGI file name; what follows it in the URL path is the
32
- // path inside the container.
33
- gatewayScript = "/gateway.cgi"
34
-
35
- // keepErrorBodyHeader is the request header with which an app asks that its own error replies reach it
36
- // whole: see keepErrorBody.
37
- keepErrorBodyHeader = "X-Synopkgland-Keep-Error-Body"
38
-
39
- defaultMaxBodyBytes = 64 << 20
40
- defaultResponseTimeout = 60 * time.Second
41
- )
42
-
43
- type config struct {
44
- // Upstream is the container's published loopback address, host:port.
45
- Upstream string `json:"upstream"`
46
- // Access is users (any signed-in DSM user), admins or groups.
47
- Access string `json:"access"`
48
- Groups []string `json:"groups,omitempty"`
49
-
50
- MaxBodyBytes int64 `json:"maxBodyBytes,omitempty"`
51
- ResponseTimeoutMillis int `json:"responseTimeoutMillis,omitempty"`
52
- }
53
-
54
- var groupName = regexp.MustCompile(`^(?:[A-Za-z0-9._-]{1,64}|[A-Za-z0-9._-]{1,64}\\[A-Za-z0-9._-](?:[A-Za-z0-9._ -]{0,126}[A-Za-z0-9._-])?|[A-Za-z0-9._-](?:[A-Za-z0-9._ -]{0,126}[A-Za-z0-9._-])?@[A-Za-z0-9._-]{1,64})$`)
55
-
56
- func (c config) validate() error {
57
- host, port, err := net.SplitHostPort(c.Upstream)
58
- if err != nil || (host != "127.0.0.1" && host != "::1") {
59
- return fmt.Errorf("upstream %q must be a loopback host:port", c.Upstream)
60
- }
61
- if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 {
62
- return fmt.Errorf("upstream port %q is invalid", port)
63
- }
64
- switch c.Access {
65
- case accessUsers, accessAdmins:
66
- case accessGroups:
67
- if len(c.Groups) == 0 {
68
- return errors.New("access groups needs at least one group")
69
- }
70
- for _, g := range c.Groups {
71
- if !groupName.MatchString(g) {
72
- return fmt.Errorf("group %q is invalid", g)
73
- }
74
- }
75
- default:
76
- return fmt.Errorf("access %q must be users, admins or groups", c.Access)
77
- }
78
- return nil
79
- }
80
-
81
- type gateway struct {
82
- cfg config
83
- auth authenticator
84
- proxy *httputil.ReverseProxy
85
- }
86
-
87
- // newGateway refuses an unusable configuration or a missing authenticator, so
88
- // a mistake fails at startup instead of opening the door.
89
- func newGateway(cfg config, auth authenticator) (*gateway, error) {
90
- if auth == nil {
91
- return nil, errors.New("newGateway needs an authenticator")
92
- }
93
- if err := cfg.validate(); err != nil {
94
- return nil, err
95
- }
96
- if cfg.MaxBodyBytes <= 0 {
97
- cfg.MaxBodyBytes = defaultMaxBodyBytes
98
- }
99
- timeout := defaultResponseTimeout
100
- if cfg.ResponseTimeoutMillis > 0 {
101
- timeout = time.Duration(cfg.ResponseTimeoutMillis) * time.Millisecond
102
- }
103
- g := &gateway{cfg: cfg, auth: auth}
104
- g.proxy = &httputil.ReverseProxy{
105
- Rewrite: g.rewrite,
106
- ModifyResponse: g.modifyResponse,
107
- ErrorHandler: g.proxyError,
108
- Transport: &http.Transport{
109
- Proxy: nil,
110
- DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
111
- ResponseHeaderTimeout: timeout,
112
- DisableKeepAlives: true,
113
- },
114
- }
115
- return g, nil
116
- }
117
-
118
- type ctxKey struct{}
119
-
120
- // gatewayRequest is what ServeHTTP worked out before handing over to the proxy.
121
- type gatewayRequest struct {
122
- prefix string // escaped URL path up to and including the CGI file name
123
- rest string // escaped path inside the container, starting with "/"
124
- id identity
125
- // keepErrorBody: the app asked for DSM-safe statuses on the container's own error replies.
126
- keepErrorBody bool
127
- }
128
-
129
- func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
130
- // DSM's CGI gateway cuts the reply short if the CGI exits with request
131
- // bytes unread, so drain whatever the handling below did not consume.
132
- defer io.Copy(io.Discard, io.LimitReader(r.Body, g.cfg.MaxBodyBytes))
133
-
134
- prefix, rest, ok := splitGatewayPath(r.URL.EscapedPath())
135
- if !ok {
136
- reply(w, http.StatusNotFound, "not_found", "Not a gateway path.")
137
- return
138
- }
139
- id, err := g.auth.Authenticate(r)
140
- switch {
141
- case errors.Is(err, errNoSession):
142
- reply(w, http.StatusUnauthorized, "session_required", "Sign in to DSM to open this app.")
143
- return
144
- case err != nil:
145
- reply(w, http.StatusInternalServerError, "auth_unavailable", "DSM could not verify the session.")
146
- return
147
- }
148
- if !g.permits(id) {
149
- reply(w, http.StatusForbidden, "forbidden", "Your DSM account is not allowed to open this app.")
150
- return
151
- }
152
- if !safeMethod(r.Method) {
153
- if !wellFormedToken(r.Header.Get("X-SYNO-TOKEN")) {
154
- reply(w, http.StatusForbidden, "token_required", "State-changing requests need the DSM SynoToken.")
155
- return
156
- }
157
- if !sameOrigin(r) {
158
- reply(w, http.StatusForbidden, "cross_site_blocked", "State-changing requests must come from this DSM's own pages.")
159
- return
160
- }
161
- }
162
- if isUpgrade(r) {
163
- switch {
164
- case !isWebSocket(r):
165
- reply(w, http.StatusNotImplemented, "upgrade_unsupported", "Only WebSocket upgrades pass through the DSM gateway.")
166
- return
167
- case !sameOrigin(r):
168
- // A browser cannot add the SynoToken header to a WebSocket handshake, so the same-origin evidence
169
- // (the rule state-changing requests use) is what keeps another site from opening a socket with the user's DSM cookie.
170
- reply(w, http.StatusForbidden, "cross_site_blocked", "The WebSocket must be opened from this DSM's own pages.")
171
- return
172
- }
173
- }
174
- if rest == "" {
175
- // Relative links in the app only resolve under a trailing slash.
176
- location := strings.TrimPrefix(gatewayScript, "/") + "/"
177
- if r.URL.RawQuery != "" {
178
- location += "?" + r.URL.RawQuery
179
- }
180
- w.Header().Set("Location", location)
181
- reply(w, http.StatusPermanentRedirect, "redirect", "Open the app with a trailing slash.")
182
- return
183
- }
184
- if !safeContainerPath(rest) {
185
- reply(w, http.StatusBadRequest, "bad_path", "The path is not allowed.")
186
- return
187
- }
188
- r.Body = http.MaxBytesReader(w, r.Body, g.cfg.MaxBodyBytes)
189
- g.proxy.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, gatewayRequest{prefix: prefix, rest: rest, id: id, keepErrorBody: r.Header.Get(keepErrorBodyHeader) == "1"})))
190
- }
191
-
192
- func (g *gateway) permits(id identity) bool {
193
- switch g.cfg.Access {
194
- case accessUsers:
195
- return true
196
- case accessAdmins:
197
- return slices.Contains(id.Groups, adminGroup)
198
- default:
199
- return slices.ContainsFunc(id.Groups, func(group string) bool { return slices.Contains(g.cfg.Groups, group) })
200
- }
201
- }
202
-
203
- func splitGatewayPath(escaped string) (prefix, rest string, ok bool) {
204
- i := strings.Index(escaped, gatewayScript)
205
- if i < 0 {
206
- return "", "", false
207
- }
208
- end := i + len(gatewayScript)
209
- rest = escaped[end:]
210
- if rest != "" && rest[0] != '/' {
211
- return "", "", false
212
- }
213
- return escaped[:end], rest, true
214
- }
215
-
216
- // safeContainerPath keeps the request inside the container's own URL space:
217
- // no dot segments, no authority-looking "//", no NUL, no backslash.
218
- func safeContainerPath(escaped string) bool {
219
- decoded, err := url.PathUnescape(escaped)
220
- if err != nil || strings.HasPrefix(decoded, "//") || strings.ContainsAny(decoded, "\x00\\") {
221
- return false
222
- }
223
- return !slices.Contains(strings.Split(decoded, "/"), "..") && !slices.Contains(strings.Split(decoded, "/"), ".")
224
- }
225
-
226
- func safeMethod(method string) bool {
227
- return method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions
228
- }
229
-
230
- func isUpgrade(r *http.Request) bool {
231
- if r.Header.Get("Upgrade") != "" {
232
- return true
233
- }
234
- for _, v := range r.Header.Values("Connection") {
235
- for _, token := range strings.Split(v, ",") {
236
- if strings.EqualFold(strings.TrimSpace(token), "upgrade") {
237
- return true
238
- }
239
- }
240
- }
241
- return false
242
- }
243
-
244
- func isWebSocket(r *http.Request) bool {
245
- return strings.EqualFold(strings.TrimSpace(r.Header.Get("Upgrade")), "websocket")
246
- }
247
-
248
- // Inbound headers that carry or imitate identity, session or proxy state. The
249
- // container trusts only the X-Syno-User and X-Syno-Admin the gateway sets
250
- // after DSM verified the session, so the client's own copies never get through.
251
- var droppedExact = map[string]bool{"Cookie": true, "Remote-User": true, "Remote-Groups": true, "Remote-Email": true, "Remote-Name": true, "Forwarded": true}
252
-
253
- func dropsHeader(name string) bool {
254
- canonical := http.CanonicalHeaderKey(name)
255
- if droppedExact[canonical] {
256
- return true
257
- }
258
- for _, prefix := range []string{"X-Syno-", "X-Forwarded-", "X-Remote-", "X-Auth", "X-Real-", "Remote-"} {
259
- if strings.HasPrefix(canonical, prefix) {
260
- return true
261
- }
262
- }
263
- return false
264
- }
265
-
266
- func (g *gateway) rewrite(pr *httputil.ProxyRequest) {
267
- gr := pr.In.Context().Value(ctxKey{}).(gatewayRequest)
268
- decoded, _ := url.PathUnescape(gr.rest)
269
- pr.Out.URL = &url.URL{Scheme: "http", Host: g.cfg.Upstream, Path: decoded, RawPath: gr.rest, RawQuery: withoutSynoToken(pr.In.URL.RawQuery)}
270
- pr.Out.Host = g.cfg.Upstream
271
- if isUpgrade(pr.In) {
272
- // The Origin was checked against this host; the container's own same-origin check needs to see it too.
273
- pr.Out.Host = pr.In.Host
274
- }
275
- for name := range pr.Out.Header {
276
- if dropsHeader(name) {
277
- pr.Out.Header.Del(name)
278
- }
279
- }
280
- pr.Out.Header.Set("X-Syno-User", gr.id.User)
281
- pr.Out.Header.Set("X-Syno-Admin", strconv.FormatBool(slices.Contains(gr.id.Groups, adminGroup)))
282
- }
283
-
284
- // withoutSynoToken keeps the CSRF token out of the container's logs.
285
- func withoutSynoToken(rawQuery string) string {
286
- if rawQuery == "" {
287
- return ""
288
- }
289
- var kept []string
290
- for _, pair := range strings.Split(rawQuery, "&") {
291
- key, _, _ := strings.Cut(pair, "=")
292
- if k, err := url.QueryUnescape(key); err == nil && k == "SynoToken" {
293
- continue
294
- }
295
- kept = append(kept, pair)
296
- }
297
- return strings.Join(kept, "&")
298
- }
299
-
300
- func (g *gateway) modifyResponse(resp *http.Response) error {
301
- gr := resp.Request.Context().Value(ctxKey{}).(gatewayRequest)
302
- // The container shares DSM's origin; it must not be able to set or clear
303
- // DSM's own cookies.
304
- resp.Header.Del("Set-Cookie")
305
- if resp.StatusCode != http.StatusSwitchingProtocols {
306
- resp.Header.Set("Cache-Control", privateCacheControl(resp.Header.Get("Cache-Control")))
307
- resp.Header.Add("Vary", "Cookie")
308
- }
309
- if location := resp.Header.Get("Location"); location != "" {
310
- resp.Header.Set("Location", g.rewriteLocation(location, gr.prefix))
311
- }
312
- if gr.keepErrorBody {
313
- keepErrorBody(resp)
314
- }
315
- return nil
316
- }
317
-
318
- // keepErrorBody swaps a container status that DSM would replace for its stand-in, so the body and headers
319
- // the container sent reach the app. Opt-in per request: the app then reads X-Synopkgland-Status for the
320
- // status the container really sent. Without it the container's own status goes out untouched.
321
- func keepErrorBody(resp *http.Response) {
322
- if safe := dsmSafeStatus(resp.StatusCode); safe != resp.StatusCode {
323
- resp.Header.Set("X-Synopkgland-Status", strconv.Itoa(resp.StatusCode))
324
- resp.StatusCode, resp.Status = safe, strconv.Itoa(safe)+" "+http.StatusText(safe)
325
- }
326
- }
327
-
328
- func privateCacheControl(value string) string {
329
- kept := []string{"private"}
330
- for _, directive := range strings.Split(value, ",") {
331
- directive = strings.TrimSpace(directive)
332
- name, _, _ := strings.Cut(strings.ToLower(directive), "=")
333
- if directive != "" && name != "public" && name != "private" && name != "s-maxage" {
334
- kept = append(kept, directive)
335
- }
336
- }
337
- return strings.Join(kept, ", ")
338
- }
339
-
340
- // rewriteLocation points root-relative and container-absolute redirects back
341
- // under the gateway path; relative and foreign locations pass through.
342
- func (g *gateway) rewriteLocation(location, prefix string) string {
343
- u, err := url.Parse(location)
344
- if err != nil {
345
- return location
346
- }
347
- switch {
348
- case u.Host == g.cfg.Upstream && (u.Scheme == "http" || u.Scheme == "https"):
349
- u.Scheme, u.Host = "", ""
350
- case u.Scheme != "" || u.Host != "":
351
- return location
352
- }
353
- if !strings.HasPrefix(u.EscapedPath(), "/") {
354
- return location
355
- }
356
- u.Path, u.RawPath = "", prefix+u.EscapedPath()
357
- u.Path, _ = url.PathUnescape(u.RawPath)
358
- return u.String()
359
- }
360
-
361
- func (g *gateway) proxyError(w http.ResponseWriter, r *http.Request, err error) {
362
- var tooLarge *http.MaxBytesError
363
- var netErr net.Error
364
- var opErr *net.OpError
365
- switch {
366
- case errors.As(err, &tooLarge):
367
- reply(w, http.StatusRequestEntityTooLarge, "body_too_large", "The request body is too large.")
368
- case errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &netErr) && netErr.Timeout()):
369
- reply(w, http.StatusGatewayTimeout, "upstream_timeout", "The app did not answer in time.")
370
- case errors.As(err, &opErr) && opErr.Op == "dial":
371
- w.Header().Set("Retry-After", "5")
372
- reply(w, http.StatusServiceUnavailable, "upstream_unavailable", "The app is not running. Start the package in Package Center and try again.")
373
- default:
374
- reply(w, http.StatusBadGateway, "bad_gateway", "The app returned an invalid response.")
375
- }
376
- }
377
-
378
- // DSM's nginx runs `scgi_intercept_errors on` with `error_page 403 404 500 502 503 504`, so a CGI reply with
379
- // one of these statuses loses its body and headers to DSM's generic error page (measured on DSM 7.4-90069;
380
- // every other status, 4xx and 5xx alike, passes with body and headers intact). Each replaced status has a
381
- // stand-in the app can read; the status the reply would have had travels in X-Synopkgland-Status.
382
- var dsmReplacedStatus = map[int]int{
383
- http.StatusForbidden: http.StatusUnauthorized,
384
- http.StatusNotFound: http.StatusGone,
385
- http.StatusInternalServerError: 520, // unregistered (Cloudflare "unknown error"); clients treat it as 500
386
- http.StatusBadGateway: 520,
387
- http.StatusServiceUnavailable: 521, // unregistered (Cloudflare "web server is down")
388
- http.StatusGatewayTimeout: 524, // unregistered (Cloudflare "a timeout occurred")
389
- }
390
-
391
- func dsmSafeStatus(status int) int {
392
- if safe, replaced := dsmReplacedStatus[status]; replaced {
393
- return safe
394
- }
395
- return status
396
- }
397
-
398
- // reply answers with one of the gateway's own errors. `intended` is the status the error means; the status
399
- // sent is the one DSM lets through, and X-Synopkgland-Error carries the machine-readable code.
400
- func reply(w http.ResponseWriter, intended int, code, message string) {
401
- status := dsmSafeStatus(intended)
402
- h := w.Header()
403
- h.Set("Content-Type", "text/plain; charset=utf-8")
404
- h.Set("Cache-Control", "no-store")
405
- h.Set("X-Content-Type-Options", "nosniff")
406
- h.Set("X-Synopkgland-Error", code)
407
- if status != intended {
408
- h.Set("X-Synopkgland-Status", strconv.Itoa(intended))
409
- }
410
- w.WriteHeader(status)
411
- fmt.Fprintf(w, "%s\n%s\n", code, message)
412
- }