# Sandstorm - Personal Cloud Sandbox # Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors # All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @0xa8cb0f2f1a756b32; using Cxx = import "/capnp/c++.capnp"; $Cxx.namespace("sandstorm"); using Grain = import "grain.capnp"; using Util = import "util.capnp"; struct HttpStatusDescriptor { id @0 :UInt16; title @1 :Text; } annotation httpStatus @0xaf480a0c6cab8887 (enumerant) :HttpStatusDescriptor; const httpStatusAnnotationId :UInt64 = 0xaf480a0c6cab8887; interface WebSession @0xa50711a14d35a8ce extends(Grain.UiSession) { # A UI session based on the web platform. The user's browser communicates to the server through # HTTP requests. # # Many of the details of HTTP are implemented by the platform and thus not exposed here. For # example, the platform may automatically set last-modified based on the last time the # application's storage was written and may automatically implement etags based on hashing the # content. struct Params { # Startup params for web sessions. See `UiView.newSession()`. basePath @0 :Text; # HTTP URL of the application's root directory as seen by this user, e.g. # "https://ioa5fiu34sm4w.example.com/i7efqesOldepw". Never includes the trailing '/'. Useful # for constructing intra-app link URLs, although in general you should try to use relative URLs # whenever possible. Note that the URL can change from session to session and from user to # user, hence it is only valid for the current session. userAgent @1 :Text; acceptableLanguages @2 :List(Text); # Content of User-Agent and Accept-Language headers. The platform will start a new session if # any of these change. # TODO(soon): Support utility factor (e.g. ";q=0.7"). } get @0 (path :Text, context :Context, ignoreBody :Bool) -> Response; # GET or HEAD request. # # If `ignoreBody` is true, then the caller intends to ignore any content body returned. The # caller may choose to return an empty body. (This is used e.g. for HEAD requests.) post @1 (path :Text, content :PostContent, context :Context) -> Response; put @3 (path :Text, content :PutContent, context :Context) -> Response; delete @4 (path :Text, context :Context) -> Response; patch @17 (path :Text, content :PostContent, context :Context) -> Response; postStreaming @5 (path :Text, mimeType :Text, context :Context, encoding :Text) -> (stream :RequestStream); putStreaming @6 (path :Text, mimeType :Text, context :Context, encoding :Text) -> (stream :RequestStream); # Streaming post/put requests, useful when the input is large. If these throw `unimplemented` # exceptions, the caller should fall back to regular post() / put() on the assumption that the # app doesn't implement streaming. # # The optional `encoding` field represents the Content-Encoding header. openWebSocket @2 (path :Text, context :Context, protocol :List(Text), clientStream :WebSocketStream) -> (protocol :List(Text), serverStream :WebSocketStream); # Open a new WebSocket. `protocol` corresponds to the `Sec-WebSocket-Protocol` header. # `clientStream` is the capability which will receive server -> client messages, while # serverStream represents client -> server. propfind @7 (path :Text, xmlContent :Text, depth :PropfindDepth, context :Context) -> Response; proppatch @8 (path :Text, xmlContent :Text, context :Context) -> Response; mkcol @9 (path :Text, content :PostContent, context :Context) -> Response; copy @10 (path :Text, destination :Text, noOverwrite :Bool, shallow :Bool, context :Context) -> Response; move @11 (path :Text, destination :Text, noOverwrite :Bool, context :Context) -> Response; lock @12 (path :Text, xmlContent :Text, shallow :Bool, context :Context) -> Response; unlock @13 (path :Text, lockToken :Text, context :Context) -> Response; acl @14 (path :Text, xmlContent :Text, context :Context) -> Response; report @15 (path :Text, content :PostContent, context :Context) -> Response; # WebDAV methods # # "destination" is a *path*, but *not* a URI -- the origin is stripped, and there is no leading # '/', just like with the `path` parameter. # "shallow = true" means "Depth: 0" # "noOverwrite = true" means "Overwrite: F"; note that this behaves a precondition -- if the # destination already exists then a preconditionFailed response is returned. # # (These boolean flags were intentionally chosen so that the spec-defined default values are # false.) options @16 (path :Text, context :Context) -> Options; # OPTIONS request. struct Context { # Additional per-request context. cookies @0 :List(Util.KeyValue); responseStream @1 :Util.ByteStream; # Stream to which the app can optionally write the response body. This is only actually # used in the case of a `content` response where the `body` union is set to `stream`. In that # case, after returning from the HTTP method, the app begins writing bytes to `responseStream`. # # Since it's not guaranteed that `responseStream` will be used, and because it would be # confusing to start receiving `write()` calls on it before receiving the HTTP response, # callers should typically initialize this field with a promise. When the response indicates # streaming, the caller can then resolve that promise and start receiving the content. # # Callers are required to provide this capability; apps need not handle it being null. accept @2 :List(AcceptedType); # This corresponds to the Accept header acceptEncoding @9 :List(AcceptedEncoding); # This corresponds to the Accept-Encoding header eTagPrecondition :union { none @4 :Void; # No precondition. exists @5 :Void; # If-Match: * doesntExist @8 :Void; # If-None-Match: * matchesOneOf @6 :List(ETag); # If-Match matchesNoneOf @7 :List(ETag); # If-None-Match } additionalHeaders @3 :List(Header); # Additional headers present in the request. Only whitelisted headers are # permitted. struct Header { name @0 :Text; # lower-cased name value @1 :Text; } const headerWhitelist :List(Text) = [ # Non-standard request headers which are whitelisted for backwards-compatibility # purposes. This whitelist exists to help avoid the need to modify code originally written # without Sandstorm in mind -- especially to avoid modifying client apps. Feel free # to send us pull requests adding additional headers. # Values in this list that end with '*' whitelist a prefix. "x-sandstorm-app-*", # For new headers introduced by Sandstorm apps. "oc-total-length", # Owncloud client "oc-chunk-size", # Owncloud client "x-oc-mtime", # Owncloud client "oc-fileid", # Owncloud client "oc-chunked", # Owncloud client "x-hgarg-*", # Mercurial client "x-phabricator-*", # Phabricator "x-requested-with", # JQuery header used by Rails and other frameworks ]; } struct PostContent { # TODO(apibump): Rename this to just `Content` or maybe `RequestContent`. mimeType @0 :Text; content @1 :Data; encoding @2 :Text; # Content-Encoding header (optional). } struct PutContent { # TODO(apibump): Remove this and replace it with `PostContent` (renamed to `Content`). mimeType @0 :Text; content @1 :Data; encoding @2 :Text; # Content-Encoding header (optional). } struct ETag { value @0 :Text; # does not include quotes weak @1 :Bool; # denotes that the resource may not be byte-for-byte identical, but is # semantically equivalent } struct Cookie { # Strings here must not contain ';' nor ','. Also, `name` cannot contain '='. name @0 :Text; value @1 :Text; expires :union { none @2 :Void; absolute @3 :Int64; # Unix timestamp. relative @4 :UInt64; # Seconds relative to time of receipt. } httpOnly @5 :Bool; path @6 :Text; # We don't include "secure" because the platform automatically forces all cookies to be secure. } struct AcceptedType { # In the accept header, there is a list of these elements. # The qValue is optional and defaults to 1. # # For example, the Accept header with value 'text/javascript; q=0.01' would have a mimeType of # "text/javascript" and a qValue of .01. mimeType @0 :Text; qValue @1 :Float32 = 1; } struct AcceptedEncoding { # The Accept-Encoding header contains a list of valid content codings. # Each content coding could be "*", indicating an arbitrary encoding. # Each content coding comes with a qValue, defaulting to 1. # For example, gzip;q=0.5 indicates the "gzip" coding with qValue "0.5" contentCoding @0 :Text; qValue @1 :Float32 = 1; } struct Response { setCookies @0 :List(Cookie); cachePolicy @16 :CachePolicy; enum SuccessCode { # 2xx-level status codes that we allow an app to return. # # We do not permit arbitrary status codes because some have semantic meaning that could # cause browsers to do things we don't expect. An unrecognized status code coming from a # sandboxed HTTP server will translate to 500, except for unrecognized 4xx codes which will # translate to 400. # # It's unclear how useful it is to even allow 201 or 202, but since a browser will certainly # treat them as equivalent to 200, we allow them. ok @0 $httpStatus(id = 200, title = "OK"); created @1 $httpStatus(id = 201, title = "Created"); accepted @2 $httpStatus(id = 202, title = "Accepted"); noContent @3 $httpStatus(id = 204, title = "No Content"); partialContent @4 $httpStatus(id = 206, title = "Partial Content"); multiStatus @5 $httpStatus(id = 207, title = "Multi-Status"); # This seems to fit better here than in the 3xx range notModified @6 $httpStatus(id = 304, title = "Not Modified"); # Not applicable: # 203 Non-Authoritative Information: Only applicable to proxies? # 205 Reset Content: Like 204, but even stranger. # Others: Not standard. } enum ClientErrorCode { # 4xx-level status codes that we allow an app to return. # # It's unclear whether status codes other than 400, 403, and 404 have any real utility; # arguably, all client errors should just use code 400 with an accompanying human-readable # error description. But, since browsers presumably treat them all equivalently to 400, it # seems harmless enough to allow them through. # # An unrecognized 4xx error code coming from a sandboxed HTTP server will translate to 400. badRequest @0 $httpStatus(id = 400, title = "Bad Request"); forbidden @1 $httpStatus(id = 403, title = "Forbidden"); notFound @2 $httpStatus(id = 404, title = "Not Found"); methodNotAllowed @3 $httpStatus(id = 405, title = "Method Not Allowed"); notAcceptable @4 $httpStatus(id = 406, title = "Not Acceptable"); conflict @5 $httpStatus(id = 409, title = "Conflict"); gone @6 $httpStatus(id = 410, title = "Gone"); preconditionFailed @11 $httpStatus(id = 412, title = "Precondition Failed"); requestEntityTooLarge @7 $httpStatus(id = 413, title = "Request Entity Too Large"); requestUriTooLong @8 $httpStatus(id = 414, title = "Request-URI Too Long"); unsupportedMediaType @9 $httpStatus(id = 415, title = "Unsupported Media Type"); imATeapot @10 $httpStatus(id = 418, title = "I'm a teapot"); unprocessableEntity @12 $httpStatus(id = 422, title = "Unprocessable Entity"); # Not applicable: # 401 Unauthorized: We don't do HTTP authentication. # 402 Payment Required: LOL # 407 Proxy Authentication Required: Not a proxy. # 408 Request Timeout: Not possible; the entire request is provided with the call. # 411 Length Required: Request is framed using Cap'n Proto. # 412 Precondition Failed: If we implement preconditions, they should be handled # separately from errors. # 416 Requested Range Not Satisfiable: Ranges not implemented (might be later). # 417 Expectation Failed: Like 412. # Others: Not standard. } union { content :group { # Return content (status code 200, or perhaps 201 or 202). statusCode @10 :SuccessCode; encoding @2 :Text; # Content-Encoding header (optional). language @3 :Text; # Content-Language header (optional). mimeType @4 :Text; # Content-Type header. eTag @17 :ETag; # Optional entity tag for this content. This can be used to express preconditions on future # requests, useful for implementing, for example, cache validation (on GETs) and optimistic # concurrency (on PUTs). See `eTagPrecondition` in `WebSession.Context`. body :union { bytes @5 :Data; stream @6 :Util.Handle; # Indicates that the content will be streamed to the `responseStream` offered in the # call's `Context`. The caller may cancel the stream by dropping the Handle. # # Note that to prevent a grain from being shut down in the middle of a large download, # it is necessary to call ping() on this handle every 60 seconds. } disposition :union { normal @13 :Void; download @14 :Text; # Prompt user to save as given file name. } } noContent :group { # Return successful, but with no content (status codes 204 and 205) shouldResetForm @15 :Bool; # If this is the response to a form submission, should the form be reset to empty? # Distinguishes between HTTP response 204 (False) and 205 (True) eTag @19 :ETag; # Optional entity tag header. Server can send this in a response to a modifying request # to indicate for example the new version of the modified resource. } preconditionFailed :group { # One of the preconditions specified in the request context was not met. # # If the request was a GET or HEAD and the precodition was If-None-Match, then this response # corresponds to HTTP 304 "Not Modified". In all other ctases, this response corresponds to # HTTP 412 "Precondition Failed". (We unify these two HTTP status codes because they really # mean the same thing and should be implemented by the same code.) matchingETag @18 :ETag; # If the precondition failed because the etag matched a tag specified in `matchesNoneOf`, # this is the tag that it matched. For other types of preconditions, this is null. # # (This is in particular used for GET requests where the result is "304 not modified".) } redirect :group { # Redirect to the given URL. # # Note that 3xx-level HTTP responses have specific semantic meanings, therefore we actually # represent that meaning here rather than having a 3xx status code enum. `redirect` # covers only 301, 302 (treated as 303), 303, 307, and 308. Other 3xx status codes # need to be handled in a completely different way, since they are not redirects. isPermanent @1 :Bool; # Is this a permanent (cacheable) redirect? switchToGet @12 :Bool; # Should the user-agent change the method to GET when accessing the new location? # Otherwise, it should repeat the same method as was used for this request. location @11 :Text; # New URL to which to redirect. # # TODO(security): Supervisor should prohibit locations outside the app's host. } clientError :group { # HTTP 4xx-level error. The platform will generate a suitable error page. statusCode @7 :ClientErrorCode; descriptionHtml @8 :Text; # Optional extended description of the error, as an HTML document. # # If the response is not text/html, use nonHtmlContent. # # TODO(apibump): Get rid of this and use only nonHtmlContent. nonHtmlBody @21 :ErrorBody; # Response body, of a type that isn't text/html. If present, descriptionHtml should be # ignored. However, older programs only know about descriptionHtml. } serverError :group { # HTTP 5xx-level error. The platform will generate a suitable error page. # # We don't support status codes here because basically none of them are applicable anyway # except 500. descriptionHtml @9 :Text; # Optional extended description of the error, as an HTML document. # # TODO(apibump): Get rid of this and use only nonHtmlContent. nonHtmlBody @22 :ErrorBody; # Response body, of a type that isn't text/html. If present, descriptionHtml should be # ignored. However, older programs only know about descriptionHtml. } # TODO(someday): Return blob directly from storage, so data doesn't have to stream through # the app? } additionalHeaders @20 :List(Header); # Additional headers present in the reponse. Only whitelisted headers are # permitted. struct Header { name @0 :Text; # lower-cased name value @1 :Text; } struct ErrorBody { data @0 :Data; encoding @1 :Text; # Content-Encoding header (optional). language @2 :Text; # Content-Language header (optional). mimeType @3 :Text; # Content-Type header. } const headerWhitelist :List(Text) = [ # Non-standard response headers which are whitelisted for backwards-compatibility # purposes. This whitelist exists to help avoid the need to modify code originally written # without Sandstorm in mind -- especially to avoid modifying client apps. # Feel free to send us pull requests adding additional headers. # Values in this list that end with '*' whitelist a prefix. "x-sandstorm-app-*", # For new headers introduced by Sandstorm apps. "x-oc-mtime", # Owncloud protocol ]; } interface RequestStream extends(Util.ByteStream) { # A streaming request. The request body is streamed in via the methods of ByteStream. getResponse @0 () -> Response; # Get the final HTTP response. The caller should call this immediately, before it has actually # written the request data. # # The method is allowed to return early, e.g. in order to start streaming the response while # the request is still uploading. Thus, full-duplex streaming is supported. This is useful in # some obscure cases. For example, an HTTP server that just encrypts the request could do so # by streaming back the response as the request comes in so that it does not need to buffer the # whole thing. # # If the response is completely transmitted before the request finishes uploading, the caller # may cancel the upload stream by simply dropping the RequestStream object (without calling # done()). Note that in the case of a streaming response, "completely transmitted" means that # the response stream's done() method has been called, or the response stream itself has been # dropped. } interface WebSocketStream { sendBytes @0 (message :Data); # Send some bytes. WARNING: At present, we just send the raw bytes of the WebSocket protocol. # In the future, this will be replaced with a `sendMessage()` method that sends one WebSocket # datagram at a time. # # TODO(apibump): Send whole WebSocket messages. } struct CachePolicy { enum Scope { # Defines the scope in which caching is allowed. For security reasons, the resource MUST NOT # be stored in a cache with a broader scope, even if it is never actually served from that # cache. none @0; # This resource must not be stored in any cache. perSession @1; # Caching is allowed on a per-session basis. perUser @2; # Caching is allowed on a per-user basis (across multiple sessions). perAppVersion @3; # Caching is allowed on a per-app-version basis (across all users). This is a # Sandstorm-specific notion. universal @4; # Caching is allowed universally, across all users and versions of the app. } withCheck @0 :Scope; # Within a cache serving this scope or a narrower scope, the resource may be stored in cache, # but if a non-negligible amount of time has gone by since the resource was last validated then # the client must check with the server that the resource hasn't changed (revalidate). # # "A non-negligible amount of time" means something on the order of the network latency between # the client and the server. For example, there is obviously no point in re-validating a cached # resource if it was last validated less than one network round trip ago. For optimization # reasons, we allow this to be expanded a bit -- something like a 15s timeout is OK. Ultimately # it is up to the infrastructure to decide, though; if an app is not OK with this, it should # specify `withCheck` = `none`. permanent @1 :Scope; # Within a cache serving this scope or a narrower scope, the resource may be assumed never to # change, and may be served directly from cache without checking with the server. # # Note that we do not allow specification of a cache duration other than "forever" because in # practice if the resource is mutable at all, you almost certainly don't know when it will next # change, and so setting a non-zero cache duration will lead to stale data bugs. variesOnCookie @2 :Bool; variesOnAccept @3 :Bool; # Indicates what inputs in `Context` would have caused a different response to be served. # If these are false and caching is enabled, it is assumed the resource is identical regardless # of these inputs. } struct Options { davClass1 @0 :Bool = false; davClass2 @1 :Bool = false; davClass3 @2 :Bool = false; davExtensions @3 :List(Text); } enum PropfindDepth { infinity @0 $Cxx.name("infinity_"); # INFINITY is a macro in C zero @1; one @2; } # Request headers that we will probably add later: # * Caching: # * Cache-Control # * If-* # * Range requests: # * Range # # Request headers that could be added later, but don't seem terribly important: # * Accept # * Accept-Charset # * Accept-Encoding # * Content-MD5 (MD5 is dead; perhaps we could introduce a modern alternative) # * Date # * From # * Max-Forwards # * Warning # * Pragma # # Request headers which will NOT be added ever: # * Sandstorm handles authorization: # * Authorization # * Sandstorm defines cross-origin request permissions: # * Access-Control-Request-Headers # * Access-Control-Request-Method # * Origin # * Redundant or irrelevant to Cap'n Proto RPC: # * Connection # * Content-Length # * Expect # * Host # * Keep-Alive # * TE # * Trailer # * Transfer-Encoding # * Upgrade # * Apps should not have this information: # * Referer # * Via # * Proxy-* # * Sec-* # * Sandstorm already prevents illicit tracking technically; no need for policy: # * DNT # Response headers that we will probably add later: # * Caching: # * Age # * Cange-Control # * ETag # * Expires # * Last-Modified # * Vary (but Sandstorm will always add "Authorization") # * Range requests: # * Accept-Ranges # * Content-Range # # Response headers that could be added later, but don't seem terribly important: # * Allow # * Content-Location # * Content-MD5 - MD5 is dead; perhaps we could introduce a modern alternative. # * Content-Disposition (filename part only) # * Link # * Pragma # * Refresh # * Retry-After # * Server # * Via # * Warning # # Response headers which will NEVER be implemented: # * Sandstorm defines cross-origin request permissions: # * Access-Control-* # * Sandstorm uses these for sandboxing: # * Content-Security-Policy # * X-Frame-Options # * Redundant or irrelevant to Cap'n Proto RPC: # * Connection # * Content-Length - Redundant. # * Trailer # * Transfer-Encoding # * Upgrade # * These belong to the domain owner, not the app: # * Public-Key-Pins # * Strict-Transport-Security # * Sandstorm controls authentication: # * WWW-Authenticate # * Irrelevant to servers: # * Proxy-Authenticate }