diff --git a/docs/faq.md b/docs/faq.md index aa9891bd3..78816c5bf 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -32,7 +32,7 @@ This error can occur when you are running tusd's disk storage on a file system w ### How can I prevent users from downloading the uploaded files? -tusd allows any user to retrieve a previously uploaded file by issuing a HTTP GET request to the corresponding upload URL. This is possible as long as the uploaded files on the datastore have not been deleted or moved to another location. While it is a handy feature for debugging and testing your setup, we know that there are situations where you don't want to allow downloads or where you want more control about who downloads what. In these scenarios we recommend to place a proxy in front of tusd which takes on the task of access control or even preventing HTTP GET requests entirely. tusd has no feature built in for controling or disabling downloads on its own because the main focus is on accepting uploads, not serving files. +tusd allows any user to retrieve a previously uploaded file by issuing a HTTP GET request to the corresponding upload URL. This is possible as long as the uploaded files on the datastore have not been deleted or moved to another location. While it is a handy feature for debugging and testing your setup, we know that there are situations where you don't want to allow downloads or where you want more control about who downloads what. In these scenarios you can use `-disable-download` option or a `pre-access` hook to control access to uploaded files based on http request headers. You can also place a proxy in front of tusd which takes on the task of access control or even preventing HTTP GET requests entirely. ### How can I keep the original filename for the uploads? diff --git a/docs/hooks.md b/docs/hooks.md index 02be61ba9..a26b7dc49 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -21,6 +21,7 @@ The table below provides an overview of all available hooks. | pre-finish | Yes | after all upload data has been received but before a response is sent. | sending custom data when an upload is finished | Yes | | post-finish | No | after all upload data has been received and after a response is sent. | post-processing of upload, logging of upload end | Yes | | post-terminate | No | after an upload has been terminated. | clean up of allocated resources | Yes | +| pre-access | Yes | before an existing upload is access (Head/Get/Patch/Delete/Upload-Concat). | validation of user authentication | No | Users should be aware of following things: - If a hook is _blocking_, tusd will wait with further processing until the hook is completed. This is useful for validation and authentication, where further processing should be stopped if the hook determines to do so. However, long execution time may impact the user experience because the upload processing is blocked while the hook executes. @@ -104,6 +105,17 @@ Below you can find an annotated, JSON-ish encoded example of a hook request: ] // and more ... } + }, + + // Information about the files that are begin accessed, to check authorization for instance + "Access": { + // read (Head/Get/Upload-Concat) or write (Patch/Delete) + "Mode": "read" + // All files info that will be access by http request + // Use an array because of Upload-Concat that may target several files + "Uploads": [ + // same as Upload + ] } } } @@ -169,6 +181,11 @@ Below you can find an annotated, JSON-ish encoded example of a hook response: // it is ignored. Use the HTTPResponse field to send details about the stop // to the client. "StopUpload": true + + // In case of pre-access or pre-create (when Upload-Concat), reject access to uploads. + // When true, http request will end with 403 status code by default, changeable with + // HTTPResponse override + "RejectAccess": false, } ``` @@ -303,7 +320,7 @@ For example, assume that every upload must belong to a specific user project. Th ### Authenticating Users -User authentication can be achieved by two ways: Either, user tokens can be included in the upload meta data, as described in the above example. Alternatively, traditional header fields, such as `Authorization` or `Cookie` can be used to carry user-identifying information. These header values are also present for the hook requests and are accessible for the `pre-create` hook, where the authorization tokens or cookies can be validated to authenticate the user. +User authentication can be achieved by two ways: Either, user tokens can be included in the upload meta data, as described in the above example. Alternatively, traditional header fields, such as `Authorization` or `Cookie` can be used to carry user-identifying information. These header values are also present for the hook requests and are accessible for the `pre-access` hook, where the authorization tokens or cookies can be validated to authenticate the user. If the authentication is successful, the hook can return an empty hook response to indicate tusd that the upload should continue as normal. If the authentication fails, the hook can instruct tusd to reject the upload and return a custom error response to the client. For example, this is a possible hook response: @@ -321,7 +338,9 @@ If the authentication is successful, the hook can return an empty hook response } ``` -Note that this handles authentication during the initial POST request when creating an upload. When tusd responds, it sends a random upload URL to the client, which is used to transmit the remaining data via PATCH and resume the upload via HEAD requests. Currently, there is no mechanism to ensure that the upload is resumed by the same user that created it. We plan on addressing this in the future. However, since the upload URL is randomly generated and only short-lived, it is hard to guess for uninvolved parties. +Note that listen `pre-create` hook only handles authentication during the initial POST request when creating an upload. When tusd responds, it sends a random upload URL to the client, which is used to transmit the remaining data via PATCH and resume the upload via HEAD requests. Since the upload URL is randomly generated and only short-lived, it is hard to guess for uninvolved parties. + +To have full protection, consider adding `pre-access` hook too. ### Interrupting Uploads diff --git a/examples/hooks/file/README.md b/examples/hooks/file/README.md new file mode 100644 index 000000000..b9b3ac90d --- /dev/null +++ b/examples/hooks/file/README.md @@ -0,0 +1,5 @@ +# Run file hook exemple + + tusd -hooks-dir=./ -hooks-enabled-events=pre-create,pre-finish,pre-access,post-create,post-receive,post-terminate,post-finish + +Adapt enabled-events hooks list for your needs. diff --git a/examples/hooks/file/pre-access b/examples/hooks/file/pre-access new file mode 100755 index 000000000..6da9f9151 --- /dev/null +++ b/examples/hooks/file/pre-access @@ -0,0 +1,10 @@ +#!/bin/sh + +# This example demonstrates how to read the hook event details +# from stdin, and output debug messages. + +# We use >&2 to write debugging output to stderr. tusd +# will forward these to its stderr. Any output from the +# hook on stdout will be captured by tusd and interpreted +# as a response. +cat /dev/stdin | jq . >&2 diff --git a/examples/hooks/grpc/Makefile b/examples/hooks/grpc/Makefile index 20053d369..c63adcd2a 100644 --- a/examples/hooks/grpc/Makefile +++ b/examples/hooks/grpc/Makefile @@ -1,2 +1,2 @@ -hook_pb2.py: ../../../cmd/tusd/cli/hooks/proto/v2/hook.proto - python3 -m grpc_tools.protoc --proto_path=../../../cmd/tusd/cli/hooks/proto/v2/ hook.proto --python_out=. --grpc_python_out=. +hook_pb2.py: ../../../pkg/hooks/grpc/proto/hook.proto + python3 -m grpc_tools.protoc --proto_path=../../../pkg/hooks/grpc/proto/ hook.proto --python_out=. --grpc_python_out=. diff --git a/examples/hooks/grpc/README.md b/examples/hooks/grpc/README.md new file mode 100644 index 000000000..52bf51e6d --- /dev/null +++ b/examples/hooks/grpc/README.md @@ -0,0 +1,7 @@ +# Run grpc hook exemple + + python3 server.py + + tusd -hooks-grpc=localhost:8000 -hooks-enabled-events=pre-create,pre-finish,pre-access,post-create,post-receive,post-terminate,post-finish + +Adapt enabled-events hooks list for your needs. diff --git a/examples/hooks/grpc/hook_pb2.py b/examples/hooks/grpc/hook_pb2.py index 634c7e95e..e76cae896 100644 --- a/examples/hooks/grpc/hook_pb2.py +++ b/examples/hooks/grpc/hook_pb2.py @@ -1,11 +1,13 @@ -# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: hook.proto -"""Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -13,51 +15,816 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nhook.proto\x12\x02v2\"5\n\x0bHookRequest\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x05\x65vent\x18\x02 \x01(\x0b\x32\t.v2.Event\"K\n\x05\x45vent\x12\x1c\n\x06upload\x18\x01 \x01(\x0b\x32\x0c.v2.FileInfo\x12$\n\x0bhttpRequest\x18\x02 \x01(\x0b\x32\x0f.v2.HTTPRequest\"\xc3\x02\n\x08\x46ileInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x16\n\x0esizeIsDeferred\x18\x03 \x01(\x08\x12\x0e\n\x06offset\x18\x04 \x01(\x03\x12,\n\x08metaData\x18\x05 \x03(\x0b\x32\x1a.v2.FileInfo.MetaDataEntry\x12\x11\n\tisPartial\x18\x06 \x01(\x08\x12\x0f\n\x07isFinal\x18\x07 \x01(\x08\x12\x16\n\x0epartialUploads\x18\x08 \x03(\t\x12*\n\x07storage\x18\t \x03(\x0b\x32\x19.v2.FileInfo.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe6\x01\n\x0f\x46ileInfoChanges\x12\n\n\x02id\x18\x01 \x01(\t\x12\x33\n\x08metaData\x18\x02 \x03(\x0b\x32!.v2.FileInfoChanges.MetaDataEntry\x12\x31\n\x07storage\x18\x03 \x03(\x0b\x32 .v2.FileInfoChanges.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9a\x01\n\x0bHTTPRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x12\n\nremoteAddr\x18\x03 \x01(\t\x12+\n\x06header\x18\x04 \x03(\x0b\x32\x1b.v2.HTTPRequest.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x01\n\x0cHookResponse\x12&\n\x0chttpResponse\x18\x01 \x01(\x0b\x32\x10.v2.HTTPResponse\x12\x14\n\x0crejectUpload\x18\x02 \x01(\x08\x12+\n\x0e\x63hangeFileInfo\x18\x04 \x01(\x0b\x32\x13.v2.FileInfoChanges\x12\x12\n\nstopUpload\x18\x03 \x01(\x08\"\x90\x01\n\x0cHTTPResponse\x12\x12\n\nstatusCode\x18\x01 \x01(\x03\x12.\n\x07headers\x18\x02 \x03(\x0b\x32\x1d.v2.HTTPResponse.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32@\n\x0bHookHandler\x12\x31\n\nInvokeHook\x12\x0f.v2.HookRequest\x1a\x10.v2.HookResponse\"\x00\x62\x06proto3') - -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hook_pb2', globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _FILEINFO_METADATAENTRY._options = None - _FILEINFO_METADATAENTRY._serialized_options = b'8\001' - _FILEINFO_STORAGEENTRY._options = None - _FILEINFO_STORAGEENTRY._serialized_options = b'8\001' - _FILEINFOCHANGES_METADATAENTRY._options = None - _FILEINFOCHANGES_METADATAENTRY._serialized_options = b'8\001' - _FILEINFOCHANGES_STORAGEENTRY._options = None - _FILEINFOCHANGES_STORAGEENTRY._serialized_options = b'8\001' - _HTTPREQUEST_HEADERENTRY._options = None - _HTTPREQUEST_HEADERENTRY._serialized_options = b'8\001' - _HTTPRESPONSE_HEADERSENTRY._options = None - _HTTPRESPONSE_HEADERSENTRY._serialized_options = b'8\001' - _HOOKREQUEST._serialized_start=18 - _HOOKREQUEST._serialized_end=71 - _EVENT._serialized_start=73 - _EVENT._serialized_end=148 - _FILEINFO._serialized_start=151 - _FILEINFO._serialized_end=474 - _FILEINFO_METADATAENTRY._serialized_start=379 - _FILEINFO_METADATAENTRY._serialized_end=426 - _FILEINFO_STORAGEENTRY._serialized_start=428 - _FILEINFO_STORAGEENTRY._serialized_end=474 - _FILEINFOCHANGES._serialized_start=477 - _FILEINFOCHANGES._serialized_end=707 - _FILEINFOCHANGES_METADATAENTRY._serialized_start=379 - _FILEINFOCHANGES_METADATAENTRY._serialized_end=426 - _FILEINFOCHANGES_STORAGEENTRY._serialized_start=428 - _FILEINFOCHANGES_STORAGEENTRY._serialized_end=474 - _HTTPREQUEST._serialized_start=710 - _HTTPREQUEST._serialized_end=864 - _HTTPREQUEST_HEADERENTRY._serialized_start=819 - _HTTPREQUEST_HEADERENTRY._serialized_end=864 - _HOOKRESPONSE._serialized_start=867 - _HOOKRESPONSE._serialized_end=1008 - _HTTPRESPONSE._serialized_start=1011 - _HTTPRESPONSE._serialized_end=1155 - _HTTPRESPONSE_HEADERSENTRY._serialized_start=1109 - _HTTPRESPONSE_HEADERSENTRY._serialized_end=1155 - _HOOKHANDLER._serialized_start=1157 - _HOOKHANDLER._serialized_end=1221 +DESCRIPTOR = _descriptor.FileDescriptor( + name='hook.proto', + package='proto', + syntax='proto3', + serialized_pb=_b('\n\nhook.proto\x12\x05proto\"8\n\x0bHookRequest\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x1b\n\x05\x65vent\x18\x02 \x01(\x0b\x32\x0c.proto.Event\"t\n\x05\x45vent\x12\x1f\n\x06upload\x18\x01 \x01(\x0b\x32\x0f.proto.FileInfo\x12\'\n\x0bhttpRequest\x18\x02 \x01(\x0b\x32\x12.proto.HTTPRequest\x12!\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32\x11.proto.AccessInfo\"\xc9\x02\n\x08\x46ileInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x16\n\x0esizeIsDeferred\x18\x03 \x01(\x08\x12\x0e\n\x06offset\x18\x04 \x01(\x03\x12/\n\x08metaData\x18\x05 \x03(\x0b\x32\x1d.proto.FileInfo.MetaDataEntry\x12\x11\n\tisPartial\x18\x06 \x01(\x08\x12\x0f\n\x07isFinal\x18\x07 \x01(\x08\x12\x16\n\x0epartialUploads\x18\x08 \x03(\t\x12-\n\x07storage\x18\t \x03(\x0b\x32\x1c.proto.FileInfo.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"<\n\nAccessInfo\x12\x0c\n\x04mode\x18\x01 \x01(\t\x12 \n\x07uploads\x18\x02 \x03(\x0b\x32\x0f.proto.FileInfo\"\xec\x01\n\x0f\x46ileInfoChanges\x12\n\n\x02id\x18\x01 \x01(\t\x12\x36\n\x08metaData\x18\x02 \x03(\x0b\x32$.proto.FileInfoChanges.MetaDataEntry\x12\x34\n\x07storage\x18\x03 \x03(\x0b\x32#.proto.FileInfoChanges.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9d\x01\n\x0bHTTPRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x12\n\nremoteAddr\x18\x03 \x01(\t\x12.\n\x06header\x18\x04 \x03(\x0b\x32\x1e.proto.HTTPRequest.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa9\x01\n\x0cHookResponse\x12)\n\x0chttpResponse\x18\x01 \x01(\x0b\x32\x13.proto.HTTPResponse\x12\x14\n\x0crejectUpload\x18\x02 \x01(\x08\x12.\n\x0e\x63hangeFileInfo\x18\x04 \x01(\x0b\x32\x16.proto.FileInfoChanges\x12\x12\n\nstopUpload\x18\x03 \x01(\x08\x12\x14\n\x0crejectAccess\x18\x05 \x01(\x08\"\x90\x01\n\x0cHTTPResponse\x12\x12\n\nstatusCode\x18\x01 \x01(\x03\x12/\n\x06header\x18\x02 \x03(\x0b\x32\x1f.proto.HTTPResponse.HeaderEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\t\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32\x46\n\x0bHookHandler\x12\x37\n\nInvokeHook\x12\x12.proto.HookRequest\x1a\x13.proto.HookResponse\"\x00\x42\x12Z\x10hooks/grpc/protob\x06proto3') +) + + + + +_HOOKREQUEST = _descriptor.Descriptor( + name='HookRequest', + full_name='proto.HookRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='proto.HookRequest.type', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='event', full_name='proto.HookRequest.event', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=21, + serialized_end=77, +) + + +_EVENT = _descriptor.Descriptor( + name='Event', + full_name='proto.Event', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='upload', full_name='proto.Event.upload', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='httpRequest', full_name='proto.Event.httpRequest', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='access', full_name='proto.Event.access', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=79, + serialized_end=195, +) + + +_FILEINFO_METADATAENTRY = _descriptor.Descriptor( + name='MetaDataEntry', + full_name='proto.FileInfo.MetaDataEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.FileInfo.MetaDataEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.FileInfo.MetaDataEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=432, + serialized_end=479, +) + +_FILEINFO_STORAGEENTRY = _descriptor.Descriptor( + name='StorageEntry', + full_name='proto.FileInfo.StorageEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.FileInfo.StorageEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.FileInfo.StorageEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=481, + serialized_end=527, +) + +_FILEINFO = _descriptor.Descriptor( + name='FileInfo', + full_name='proto.FileInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='proto.FileInfo.id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='size', full_name='proto.FileInfo.size', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sizeIsDeferred', full_name='proto.FileInfo.sizeIsDeferred', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='offset', full_name='proto.FileInfo.offset', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='metaData', full_name='proto.FileInfo.metaData', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='isPartial', full_name='proto.FileInfo.isPartial', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='isFinal', full_name='proto.FileInfo.isFinal', index=6, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='partialUploads', full_name='proto.FileInfo.partialUploads', index=7, + number=8, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='storage', full_name='proto.FileInfo.storage', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_FILEINFO_METADATAENTRY, _FILEINFO_STORAGEENTRY, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=198, + serialized_end=527, +) + + +_ACCESSINFO = _descriptor.Descriptor( + name='AccessInfo', + full_name='proto.AccessInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='mode', full_name='proto.AccessInfo.mode', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uploads', full_name='proto.AccessInfo.uploads', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=529, + serialized_end=589, +) + + +_FILEINFOCHANGES_METADATAENTRY = _descriptor.Descriptor( + name='MetaDataEntry', + full_name='proto.FileInfoChanges.MetaDataEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.FileInfoChanges.MetaDataEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.FileInfoChanges.MetaDataEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=432, + serialized_end=479, +) + +_FILEINFOCHANGES_STORAGEENTRY = _descriptor.Descriptor( + name='StorageEntry', + full_name='proto.FileInfoChanges.StorageEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.FileInfoChanges.StorageEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.FileInfoChanges.StorageEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=481, + serialized_end=527, +) + +_FILEINFOCHANGES = _descriptor.Descriptor( + name='FileInfoChanges', + full_name='proto.FileInfoChanges', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='proto.FileInfoChanges.id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='metaData', full_name='proto.FileInfoChanges.metaData', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='storage', full_name='proto.FileInfoChanges.storage', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_FILEINFOCHANGES_METADATAENTRY, _FILEINFOCHANGES_STORAGEENTRY, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=592, + serialized_end=828, +) + + +_HTTPREQUEST_HEADERENTRY = _descriptor.Descriptor( + name='HeaderEntry', + full_name='proto.HTTPRequest.HeaderEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.HTTPRequest.HeaderEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.HTTPRequest.HeaderEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=943, + serialized_end=988, +) + +_HTTPREQUEST = _descriptor.Descriptor( + name='HTTPRequest', + full_name='proto.HTTPRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='method', full_name='proto.HTTPRequest.method', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uri', full_name='proto.HTTPRequest.uri', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='remoteAddr', full_name='proto.HTTPRequest.remoteAddr', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='header', full_name='proto.HTTPRequest.header', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_HTTPREQUEST_HEADERENTRY, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=831, + serialized_end=988, +) + + +_HOOKRESPONSE = _descriptor.Descriptor( + name='HookResponse', + full_name='proto.HookResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='httpResponse', full_name='proto.HookResponse.httpResponse', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='rejectUpload', full_name='proto.HookResponse.rejectUpload', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='changeFileInfo', full_name='proto.HookResponse.changeFileInfo', index=2, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='stopUpload', full_name='proto.HookResponse.stopUpload', index=3, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='rejectAccess', full_name='proto.HookResponse.rejectAccess', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=991, + serialized_end=1160, +) + + +_HTTPRESPONSE_HEADERENTRY = _descriptor.Descriptor( + name='HeaderEntry', + full_name='proto.HTTPResponse.HeaderEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='proto.HTTPResponse.HeaderEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='proto.HTTPResponse.HeaderEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=943, + serialized_end=988, +) + +_HTTPRESPONSE = _descriptor.Descriptor( + name='HTTPResponse', + full_name='proto.HTTPResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='statusCode', full_name='proto.HTTPResponse.statusCode', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='header', full_name='proto.HTTPResponse.header', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='body', full_name='proto.HTTPResponse.body', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_HTTPRESPONSE_HEADERENTRY, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1163, + serialized_end=1307, +) + +_HOOKREQUEST.fields_by_name['event'].message_type = _EVENT +_EVENT.fields_by_name['upload'].message_type = _FILEINFO +_EVENT.fields_by_name['httpRequest'].message_type = _HTTPREQUEST +_EVENT.fields_by_name['access'].message_type = _ACCESSINFO +_FILEINFO_METADATAENTRY.containing_type = _FILEINFO +_FILEINFO_STORAGEENTRY.containing_type = _FILEINFO +_FILEINFO.fields_by_name['metaData'].message_type = _FILEINFO_METADATAENTRY +_FILEINFO.fields_by_name['storage'].message_type = _FILEINFO_STORAGEENTRY +_ACCESSINFO.fields_by_name['uploads'].message_type = _FILEINFO +_FILEINFOCHANGES_METADATAENTRY.containing_type = _FILEINFOCHANGES +_FILEINFOCHANGES_STORAGEENTRY.containing_type = _FILEINFOCHANGES +_FILEINFOCHANGES.fields_by_name['metaData'].message_type = _FILEINFOCHANGES_METADATAENTRY +_FILEINFOCHANGES.fields_by_name['storage'].message_type = _FILEINFOCHANGES_STORAGEENTRY +_HTTPREQUEST_HEADERENTRY.containing_type = _HTTPREQUEST +_HTTPREQUEST.fields_by_name['header'].message_type = _HTTPREQUEST_HEADERENTRY +_HOOKRESPONSE.fields_by_name['httpResponse'].message_type = _HTTPRESPONSE +_HOOKRESPONSE.fields_by_name['changeFileInfo'].message_type = _FILEINFOCHANGES +_HTTPRESPONSE_HEADERENTRY.containing_type = _HTTPRESPONSE +_HTTPRESPONSE.fields_by_name['header'].message_type = _HTTPRESPONSE_HEADERENTRY +DESCRIPTOR.message_types_by_name['HookRequest'] = _HOOKREQUEST +DESCRIPTOR.message_types_by_name['Event'] = _EVENT +DESCRIPTOR.message_types_by_name['FileInfo'] = _FILEINFO +DESCRIPTOR.message_types_by_name['AccessInfo'] = _ACCESSINFO +DESCRIPTOR.message_types_by_name['FileInfoChanges'] = _FILEINFOCHANGES +DESCRIPTOR.message_types_by_name['HTTPRequest'] = _HTTPREQUEST +DESCRIPTOR.message_types_by_name['HookResponse'] = _HOOKRESPONSE +DESCRIPTOR.message_types_by_name['HTTPResponse'] = _HTTPRESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +HookRequest = _reflection.GeneratedProtocolMessageType('HookRequest', (_message.Message,), dict( + DESCRIPTOR = _HOOKREQUEST, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HookRequest) + )) +_sym_db.RegisterMessage(HookRequest) + +Event = _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), dict( + DESCRIPTOR = _EVENT, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.Event) + )) +_sym_db.RegisterMessage(Event) + +FileInfo = _reflection.GeneratedProtocolMessageType('FileInfo', (_message.Message,), dict( + + MetaDataEntry = _reflection.GeneratedProtocolMessageType('MetaDataEntry', (_message.Message,), dict( + DESCRIPTOR = _FILEINFO_METADATAENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfo.MetaDataEntry) + )) + , + + StorageEntry = _reflection.GeneratedProtocolMessageType('StorageEntry', (_message.Message,), dict( + DESCRIPTOR = _FILEINFO_STORAGEENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfo.StorageEntry) + )) + , + DESCRIPTOR = _FILEINFO, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfo) + )) +_sym_db.RegisterMessage(FileInfo) +_sym_db.RegisterMessage(FileInfo.MetaDataEntry) +_sym_db.RegisterMessage(FileInfo.StorageEntry) + +AccessInfo = _reflection.GeneratedProtocolMessageType('AccessInfo', (_message.Message,), dict( + DESCRIPTOR = _ACCESSINFO, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.AccessInfo) + )) +_sym_db.RegisterMessage(AccessInfo) + +FileInfoChanges = _reflection.GeneratedProtocolMessageType('FileInfoChanges', (_message.Message,), dict( + + MetaDataEntry = _reflection.GeneratedProtocolMessageType('MetaDataEntry', (_message.Message,), dict( + DESCRIPTOR = _FILEINFOCHANGES_METADATAENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfoChanges.MetaDataEntry) + )) + , + + StorageEntry = _reflection.GeneratedProtocolMessageType('StorageEntry', (_message.Message,), dict( + DESCRIPTOR = _FILEINFOCHANGES_STORAGEENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfoChanges.StorageEntry) + )) + , + DESCRIPTOR = _FILEINFOCHANGES, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.FileInfoChanges) + )) +_sym_db.RegisterMessage(FileInfoChanges) +_sym_db.RegisterMessage(FileInfoChanges.MetaDataEntry) +_sym_db.RegisterMessage(FileInfoChanges.StorageEntry) + +HTTPRequest = _reflection.GeneratedProtocolMessageType('HTTPRequest', (_message.Message,), dict( + + HeaderEntry = _reflection.GeneratedProtocolMessageType('HeaderEntry', (_message.Message,), dict( + DESCRIPTOR = _HTTPREQUEST_HEADERENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HTTPRequest.HeaderEntry) + )) + , + DESCRIPTOR = _HTTPREQUEST, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HTTPRequest) + )) +_sym_db.RegisterMessage(HTTPRequest) +_sym_db.RegisterMessage(HTTPRequest.HeaderEntry) + +HookResponse = _reflection.GeneratedProtocolMessageType('HookResponse', (_message.Message,), dict( + DESCRIPTOR = _HOOKRESPONSE, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HookResponse) + )) +_sym_db.RegisterMessage(HookResponse) + +HTTPResponse = _reflection.GeneratedProtocolMessageType('HTTPResponse', (_message.Message,), dict( + + HeaderEntry = _reflection.GeneratedProtocolMessageType('HeaderEntry', (_message.Message,), dict( + DESCRIPTOR = _HTTPRESPONSE_HEADERENTRY, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HTTPResponse.HeaderEntry) + )) + , + DESCRIPTOR = _HTTPRESPONSE, + __module__ = 'hook_pb2' + # @@protoc_insertion_point(class_scope:proto.HTTPResponse) + )) +_sym_db.RegisterMessage(HTTPResponse) +_sym_db.RegisterMessage(HTTPResponse.HeaderEntry) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('Z\020hooks/grpc/proto')) +_FILEINFO_METADATAENTRY.has_options = True +_FILEINFO_METADATAENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) +_FILEINFO_STORAGEENTRY.has_options = True +_FILEINFO_STORAGEENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) +_FILEINFOCHANGES_METADATAENTRY.has_options = True +_FILEINFOCHANGES_METADATAENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) +_FILEINFOCHANGES_STORAGEENTRY.has_options = True +_FILEINFOCHANGES_STORAGEENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) +_HTTPREQUEST_HEADERENTRY.has_options = True +_HTTPREQUEST_HEADERENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) +_HTTPRESPONSE_HEADERENTRY.has_options = True +_HTTPRESPONSE_HEADERENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) + +_HOOKHANDLER = _descriptor.ServiceDescriptor( + name='HookHandler', + full_name='proto.HookHandler', + file=DESCRIPTOR, + index=0, + options=None, + serialized_start=1309, + serialized_end=1379, + methods=[ + _descriptor.MethodDescriptor( + name='InvokeHook', + full_name='proto.HookHandler.InvokeHook', + index=0, + containing_service=None, + input_type=_HOOKREQUEST, + output_type=_HOOKRESPONSE, + options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_HOOKHANDLER) + +DESCRIPTOR.services_by_name['HookHandler'] = _HOOKHANDLER + # @@protoc_insertion_point(module_scope) diff --git a/examples/hooks/grpc/hook_pb2_grpc.py b/examples/hooks/grpc/hook_pb2_grpc.py index 241c0d019..ffdbd5309 100644 --- a/examples/hooks/grpc/hook_pb2_grpc.py +++ b/examples/hooks/grpc/hook_pb2_grpc.py @@ -1,74 +1,50 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" import grpc import hook_pb2 as hook__pb2 class HookHandlerStub(object): - """The hook service definition. - """ + """The hook service definition. + """ - def __init__(self, channel): - """Constructor. + def __init__(self, channel): + """Constructor. - Args: - channel: A grpc.Channel. - """ - self.InvokeHook = channel.unary_unary( - '/v2.HookHandler/InvokeHook', - request_serializer=hook__pb2.HookRequest.SerializeToString, - response_deserializer=hook__pb2.HookResponse.FromString, - ) + Args: + channel: A grpc.Channel. + """ + self.InvokeHook = channel.unary_unary( + '/proto.HookHandler/InvokeHook', + request_serializer=hook__pb2.HookRequest.SerializeToString, + response_deserializer=hook__pb2.HookResponse.FromString, + ) class HookHandlerServicer(object): - """The hook service definition. + """The hook service definition. + """ + + def InvokeHook(self, request, context): + """InvokeHook is invoked for every hook that is executed. HookRequest contains the + corresponding information about the hook type, the involved upload, and + causing HTTP request. + The return value HookResponse allows to stop or reject an upload, as well as modifying + the HTTP response. See the documentation for HookResponse for more details. """ - - def InvokeHook(self, request, context): - """InvokeHook is invoked for every hook that is executed. HookRequest contains the - corresponding information about the hook type, the involved upload, and - causing HTTP request. - The return value HookResponse allows to stop or reject an upload, as well as modifying - the HTTP response. See the documentation for HookResponse for more details. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') def add_HookHandlerServicer_to_server(servicer, server): - rpc_method_handlers = { - 'InvokeHook': grpc.unary_unary_rpc_method_handler( - servicer.InvokeHook, - request_deserializer=hook__pb2.HookRequest.FromString, - response_serializer=hook__pb2.HookResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'v2.HookHandler', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class HookHandler(object): - """The hook service definition. - """ - - @staticmethod - def InvokeHook(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/v2.HookHandler/InvokeHook', - hook__pb2.HookRequest.SerializeToString, - hook__pb2.HookResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + rpc_method_handlers = { + 'InvokeHook': grpc.unary_unary_rpc_method_handler( + servicer.InvokeHook, + request_deserializer=hook__pb2.HookRequest.FromString, + response_serializer=hook__pb2.HookResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'proto.HookHandler', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) diff --git a/examples/hooks/grpc/server.py b/examples/hooks/grpc/server.py index 15c8b3a4c..745d2cf63 100644 --- a/examples/hooks/grpc/server.py +++ b/examples/hooks/grpc/server.py @@ -37,6 +37,14 @@ def InvokeHook(self, hook_request, context): hook_response.changeFileInfo.metaData['filename'] = metaData['filename'] hook_response.changeFileInfo.metaData['creation_time'] = time.ctime() + + # Example: Use the pre-access hook to print each upload access + if hook_request.type == 'pre-access': + mode = hook_request.event.access.mode + id = hook_request.event.access.uploads[0].id + size = hook_request.event.access.uploads[0].size + print(f'Access {id} (mode={mode}, size={size} bytes)') + # Example: Use the post-finish hook to print information about a completed upload, # including its storage location. if hook_request.type == 'post-finish': diff --git a/examples/hooks/http/README.md b/examples/hooks/http/README.md new file mode 100644 index 000000000..ecf9ade8f --- /dev/null +++ b/examples/hooks/http/README.md @@ -0,0 +1,7 @@ +# Run http hook exemple + + python3 server.py + + tusd -hooks-http=http://localhost:8000 -hooks-enabled-events=pre-create,pre-finish,pre-access,post-create,post-receive,post-terminate,post-finish + +Adapt enabled-events hooks list for your needs. diff --git a/examples/hooks/http/server.py b/examples/hooks/http/server.py index 4f692126a..e0921b01a 100644 --- a/examples/hooks/http/server.py +++ b/examples/hooks/http/server.py @@ -50,6 +50,12 @@ def do_POST(self): 'creation_time': time.ctime(), } + # Example: Use the pre-access hook to print each upload access + if hook_request['Type'] == 'pre-access': + mode = hook_request['Event']['Access']['Mode'] + id = hook_request['Event']['Access']['Uploads'][0]['ID'] + size = hook_request['Event']['Access']['Uploads'][0]['Size'] + print(f'Access {id} (mode={mode}, size={size} bytes)') # Example: Use the post-finish hook to print information about a completed upload, # including its storage location. diff --git a/examples/hooks/plugin/README.md b/examples/hooks/plugin/README.md new file mode 100644 index 000000000..df91ef0f6 --- /dev/null +++ b/examples/hooks/plugin/README.md @@ -0,0 +1,7 @@ +# Run plugin hook exemple + + make + + tusd -hooks-http=./hook_handler -hooks-enabled-events=pre-create,pre-finish,pre-access,post-create,post-receive,post-terminate,post-finish + +Adapt enabled-events hooks list for your needs. diff --git a/examples/hooks/plugin/hook_handler.go b/examples/hooks/plugin/hook_handler.go index 0d359e4ee..00c0839a8 100644 --- a/examples/hooks/plugin/hook_handler.go +++ b/examples/hooks/plugin/hook_handler.go @@ -41,6 +41,15 @@ func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookRespons } } + // Example: Use the pre-access hook to print each upload access + if req.Type == hooks.HookPreAccess { + mode := req.Event.Access.Mode + id := req.Event.Access.Uploads[0].ID + size := req.Event.Access.Uploads[0].Size + + log.Printf("Access %s (mode=%s, size=%d bytes) \n", id, mode, size) + } + // Example: Use the post-finish hook to print information about a completed upload, // including its storage location. if req.Type == hooks.HookPreFinish { diff --git a/pkg/handler/config.go b/pkg/handler/config.go index 81660ae34..ba40731b0 100644 --- a/pkg/handler/config.go +++ b/pkg/handler/config.go @@ -69,6 +69,12 @@ type Config struct { // that should be overwriten before the upload is create. See its type definition for // more details on its behavior. If you do not want to make any changes, return an empty struct. PreUploadCreateCallback func(hook HookEvent) (HTTPResponse, FileInfoChanges, error) + // PreUploadAccessCallback will be invoked before accessing an upload, if the + // property is supplied, on Get/Head/Patch/Delete/Upload-Concat requests. + // If the callback returns no error, the requests will continue. + // If the error is non-nil, the requests will be rejected. This can be used to implement + // authorization. + PreUploadAccessCallback func(hook HookEvent) error // PreFinishResponseCallback will be invoked after an upload is completed but before // a response is returned to the client. This can be used to implement post-processing validation. // If the callback returns no error, optional values from HTTPResponse will be contained in the HTTP response. diff --git a/pkg/handler/get_test.go b/pkg/handler/get_test.go index b3a26b548..ff1bd944f 100644 --- a/pkg/handler/get_test.go +++ b/pkg/handler/get_test.go @@ -164,4 +164,78 @@ func TestGet(t *testing.T) { ResBody: "", }).Run(handler, t) }) + + SubTest(t, "RejectAccess", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + Size: 20, + Offset: 20, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + PreUploadAccessCallback: func(event HookEvent) error { + return ErrAccessRejectedByServer + }, + }) + + (&httpTest{ + Method: "GET", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: ErrAccessRejectedByServer.HTTPResponse.StatusCode, + ResHeader: ErrAccessRejectedByServer.HTTPResponse.Header, + ResBody: ErrAccessRejectedByServer.HTTPResponse.Body, + }).Run(handler, t) + }) + + SubTest(t, "RejectAccessCustomResponse", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + Size: 20, + Offset: 20, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + PreUploadAccessCallback: func(event HookEvent) error { + err := ErrAccessRejectedByServer + err.HTTPResponse = HTTPResponse{ + StatusCode: 409, + Body: "Custom response", + Header: HTTPHeader{ + "X-Foo": "bar", + }, + } + return err + }, + }) + + (&httpTest{ + Method: "GET", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: 409, + ResHeader: map[string]string{ + "X-Foo": "bar", + }, + ResBody: "Custom response", + }).Run(handler, t) + }) } diff --git a/pkg/handler/head_test.go b/pkg/handler/head_test.go index e6c0d24a3..bc45e83cd 100644 --- a/pkg/handler/head_test.go +++ b/pkg/handler/head_test.go @@ -144,6 +144,37 @@ func TestHead(t *testing.T) { }).Run(handler, t) }) + SubTest(t, "RejectAccess", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + SizeIsDeferred: true, + Size: 0, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + PreUploadAccessCallback: func(event HookEvent) error { + return ErrAccessRejectedByServer + }, + }) + + (&httpTest{ + Method: "HEAD", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: ErrAccessRejectedByServer.HTTPResponse.StatusCode, + ResHeader: ErrAccessRejectedByServer.HTTPResponse.Header, + }).Run(handler, t) + }) + SubTest(t, "ExperimentalProtocol", func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { SubTest(t, "IncompleteUpload", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { ctrl := gomock.NewController(t) diff --git a/pkg/handler/hooks.go b/pkg/handler/hooks.go index c1c6f1133..1e4956c2c 100644 --- a/pkg/handler/hooks.go +++ b/pkg/handler/hooks.go @@ -26,23 +26,56 @@ type HookEvent struct { // HTTPRequest contains details about the HTTP request that reached // tusd. HTTPRequest HTTPRequest + // Only use by pre-access and pre-create (when Upload-Concat) hook, + // for uploads protection for instance + Access AccessInfo } -func newHookEvent(c *httpContext, info FileInfo) HookEvent { +type AccessMode = string + +const ( + AccessModeRead AccessMode = "read" + AccessModeWrite AccessMode = "write" +) + +type AccessInfo struct { + // read (Head/Get/Upload-Concat) or write (Patch/Delete) + Mode AccessMode + + // All files info that will be access by http request + // Use an array because of Upload-Concat that may target several files + Uploads []FileInfo +} + +func newHookEvent(c *httpContext, info *FileInfo) HookEvent { // The Host header field is not present in the header map, see https://pkg.go.dev/net/http#Request: // > For incoming requests, the Host header is promoted to the // > Request.Host field and removed from the Header map. // That's why we add it back manually. c.req.Header.Set("Host", c.req.Host) + var upload FileInfo + if info != nil { + upload = *info + } return HookEvent{ Context: c, - Upload: info, + Upload: upload, HTTPRequest: HTTPRequest{ Method: c.req.Method, URI: c.req.RequestURI, RemoteAddr: c.req.RemoteAddr, Header: c.req.Header, }, + Access: AccessInfo{}, + } +} + +func newHookAccessEvent(c *httpContext, mode AccessMode, uploads []FileInfo) HookEvent { + event := newHookEvent(c, nil) + event.Access = AccessInfo{ + Mode: mode, + Uploads: uploads, } + return event } diff --git a/pkg/handler/patch_test.go b/pkg/handler/patch_test.go index 36e8b2c97..b779f7c7b 100644 --- a/pkg/handler/patch_test.go +++ b/pkg/handler/patch_test.go @@ -706,6 +706,42 @@ func TestPatch(t *testing.T) { a.False(more) }) + SubTest(t, "RejectAccess", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 0, + Size: 5, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + PreUploadAccessCallback: func(event HookEvent) error { + return ErrAccessRejectedByServer + }, + }) + + (&httpTest{ + Method: "PATCH", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Offset": "0", + "Content-Type": "application/offset+octet-stream", + }, + ReqBody: strings.NewReader("hello"), + Code: ErrAccessRejectedByServer.HTTPResponse.StatusCode, + ResHeader: ErrAccessRejectedByServer.HTTPResponse.Header, + ResBody: ErrAccessRejectedByServer.HTTPResponse.Body, + }).Run(handler, t) + }) + SubTest(t, "BodyReadError", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { // This test ensure that error that occurr from reading the request body are not forwarded to the // storage backend but are still causing an diff --git a/pkg/handler/post_test.go b/pkg/handler/post_test.go index 96b97c11d..120af07f8 100644 --- a/pkg/handler/post_test.go +++ b/pkg/handler/post_test.go @@ -543,6 +543,104 @@ func TestPost(t *testing.T) { Code: http.StatusForbidden, }).Run(handler, t) }) + + SubTest(t, "UploadConcat", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + uploadA := NewMockFullUpload(ctrl) + uploadB := NewMockFullUpload(ctrl) + uploadC := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "a").Return(uploadA, nil), + uploadA.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "a", + Offset: 5, + Size: 5, + }, nil), + + store.EXPECT().GetUpload(gomock.Any(), "b").Return(uploadB, nil), + uploadB.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "b", + Offset: 10, + Size: 10, + }, nil), + + store.EXPECT().NewUpload(gomock.Any(), FileInfo{ + Size: 15, + MetaData: map[string]string{}, + IsFinal: true, + PartialUploads: []string{"a", "b"}, + }).Return(uploadC, nil), + uploadC.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "foo", + Size: 15, + MetaData: map[string]string{}, + }, nil), + + store.EXPECT().AsConcatableUpload(uploadC).Return(uploadC), + uploadC.EXPECT().ConcatUploads(gomock.Any(), []Upload{uploadA, uploadB}).Return(nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + BasePath: "/files/", + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Concat": "final;http://tus.io/files/a http://tus.io/files/b", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + }, + }).Run(handler, t) + }) + + SubTest(t, "RejectAccessConcat", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + uploadA := NewMockFullUpload(ctrl) + uploadB := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "a").Return(uploadA, nil), + uploadA.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "a", + Offset: 5, + Size: 5, + }, nil), + + store.EXPECT().GetUpload(gomock.Any(), "b").Return(uploadB, nil), + uploadB.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "b", + Offset: 10, + Size: 10, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + BasePath: "/files/", + PreUploadAccessCallback: func(event HookEvent) error { + return ErrAccessRejectedByServer + }, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Concat": "final;http://tus.io/files/a http://tus.io/files/b", + }, + Code: ErrAccessRejectedByServer.HTTPResponse.StatusCode, + ResHeader: ErrAccessRejectedByServer.HTTPResponse.Header, + ResBody: ErrAccessRejectedByServer.HTTPResponse.Body, + }).Run(handler, t) + }) }) SubTest(t, "ExperimentalProtocol", func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { diff --git a/pkg/handler/terminate_test.go b/pkg/handler/terminate_test.go index 95131309e..11b71a304 100644 --- a/pkg/handler/terminate_test.go +++ b/pkg/handler/terminate_test.go @@ -100,4 +100,36 @@ func TestTerminate(t *testing.T) { Code: http.StatusNotImplemented, }).Run(http.HandlerFunc(handler.DelFile), t) }) + + SubTest(t, "RejectAccess", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + Size: 20, + Offset: 20, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + PreUploadAccessCallback: func(event HookEvent) error { + return ErrAccessRejectedByServer + }, + }) + + (&httpTest{ + Method: "DELETE", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: ErrAccessRejectedByServer.HTTPResponse.StatusCode, + ResHeader: ErrAccessRejectedByServer.HTTPResponse.Header, + ResBody: ErrAccessRejectedByServer.HTTPResponse.Body, + }).Run(handler, t) + }) } diff --git a/pkg/handler/unrouted_handler.go b/pkg/handler/unrouted_handler.go index 14b471d99..13ffa7f47 100644 --- a/pkg/handler/unrouted_handler.go +++ b/pkg/handler/unrouted_handler.go @@ -50,6 +50,7 @@ var ( ErrUploadStoppedByServer = NewError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest) ErrUploadRejectedByServer = NewError("ERR_UPLOAD_REJECTED", "upload creation has been rejected by server", http.StatusBadRequest) ErrUploadInterrupted = NewError("ERR_UPLOAD_INTERRUPTED", "upload has been interrupted by another request for this upload resource", http.StatusBadRequest) + ErrAccessRejectedByServer = NewError("ERR_ACCESS_REJECTED", "upload access has been rejected by server", http.StatusForbidden) ErrServerShutdown = NewError("ERR_SERVER_SHUTDOWN", "request has been interrupted because the server is shutting down", http.StatusServiceUnavailable) ErrOriginNotAllowed = NewError("ERR_ORIGIN_NOT_ALLOWED", "request origin is not allowed", http.StatusForbidden) @@ -292,6 +293,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) var size int64 var sizeIsDeferred bool var partialUploads []Upload + var partialFileInfos []FileInfo if isFinal { // A final upload must not contain a chunk within the creation request if containsChunk { @@ -299,11 +301,19 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) return } - partialUploads, size, err = handler.sizeOfUploads(c, partialUploadIDs) + partialUploads, partialFileInfos, size, err = handler.sizeOfUploads(c, partialUploadIDs) if err != nil { handler.sendError(c, err) return } + + if handler.config.PreUploadAccessCallback != nil { + err := handler.config.PreUploadAccessCallback(newHookAccessEvent(c, AccessModeRead, partialFileInfos)) + if err != nil { + handler.sendError(c, err) + return + } + } } else { uploadLengthHeader := r.Header.Get("Upload-Length") uploadDeferLengthHeader := r.Header.Get("Upload-Defer-Length") @@ -338,7 +348,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) } if handler.config.PreUploadCreateCallback != nil { - resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, info)) + resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, &info)) if err != nil { handler.sendError(c, err) return @@ -388,7 +398,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) c.log.Info("UploadCreated", "id", id, "size", size, "url", url) if handler.config.NotifyCreatedUploads { - handler.CreatedUploads <- newHookEvent(c, info) + handler.CreatedUploads <- newHookEvent(c, &info) } if isFinal { @@ -400,7 +410,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) info.Offset = size if handler.config.NotifyCompleteUploads { - handler.CompleteUploads <- newHookEvent(c, info) + handler.CompleteUploads <- newHookEvent(c, &info) } } @@ -491,7 +501,7 @@ func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Reques // 1. Create upload resource if handler.config.PreUploadCreateCallback != nil { - resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, info)) + resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, &info)) if err != nil { handler.sendError(c, err) return @@ -543,7 +553,7 @@ func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Reques c.log.Info("UploadCreated", "size", info.Size, "url", url) if handler.config.NotifyCreatedUploads { - handler.CreatedUploads <- newHookEvent(c, info) + handler.CreatedUploads <- newHookEvent(c, &info) } // 2. Lock upload @@ -627,6 +637,14 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) return } + if handler.config.PreUploadAccessCallback != nil { + err := handler.config.PreUploadAccessCallback(newHookAccessEvent(c, AccessModeRead, []FileInfo{info})) + if err != nil { + handler.sendError(c, err) + return + } + } + resp := HTTPResponse{ Header: HTTPHeader{ "Cache-Control": "no-store", @@ -729,6 +747,14 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request return } + if handler.config.PreUploadAccessCallback != nil { + err := handler.config.PreUploadAccessCallback(newHookAccessEvent(c, AccessModeWrite, []FileInfo{info})) + if err != nil { + handler.sendError(c, err) + return + } + } + // Modifying a final upload is not allowed if info.IsFinal { handler.sendError(c, ErrModifyFinal) @@ -939,7 +965,7 @@ func (handler *UnroutedHandler) finishUploadIfComplete(c *httpContext, resp HTTP // ... allow the hook callback to run before sending the response if handler.config.PreFinishResponseCallback != nil { - resp2, err := handler.config.PreFinishResponseCallback(newHookEvent(c, info)) + resp2, err := handler.config.PreFinishResponseCallback(newHookEvent(c, &info)) if err != nil { return resp, err } @@ -951,7 +977,7 @@ func (handler *UnroutedHandler) finishUploadIfComplete(c *httpContext, resp HTTP // ... send the info out to the channel if handler.config.NotifyCompleteUploads { - handler.CompleteUploads <- newHookEvent(c, info) + handler.CompleteUploads <- newHookEvent(c, &info) } } @@ -992,6 +1018,14 @@ func (handler *UnroutedHandler) GetFile(w http.ResponseWriter, r *http.Request) return } + if handler.config.PreUploadAccessCallback != nil { + err := handler.config.PreUploadAccessCallback(newHookAccessEvent(c, AccessModeRead, []FileInfo{info})) + if err != nil { + handler.sendError(c, err) + return + } + } + contentType, contentDisposition := filterContentType(info) resp := HTTPResponse{ StatusCode: http.StatusOK, @@ -1117,7 +1151,7 @@ func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request) } var info FileInfo - if handler.config.NotifyTerminatedUploads { + if handler.config.NotifyTerminatedUploads || handler.config.PreUploadAccessCallback != nil { info, err = upload.GetInfo(c) if err != nil { handler.sendError(c, err) @@ -1125,6 +1159,14 @@ func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request) } } + if handler.config.PreUploadAccessCallback != nil { + err := handler.config.PreUploadAccessCallback(newHookAccessEvent(c, AccessModeWrite, []FileInfo{info})) + if err != nil { + handler.sendError(c, err) + return + } + } + err = handler.terminateUpload(c, upload, info) if err != nil { handler.sendError(c, err) @@ -1150,7 +1192,7 @@ func (handler *UnroutedHandler) terminateUpload(c *httpContext, upload Upload, i } if handler.config.NotifyTerminatedUploads { - handler.TerminatedUploads <- newHookEvent(c, info) + handler.TerminatedUploads <- newHookEvent(c, &info) } c.log.Info("UploadTerminated") @@ -1206,7 +1248,7 @@ func (handler *UnroutedHandler) absFileURL(r *http.Request, id string) string { // indicating how much data has been transfered to the server. // It will stop sending these instances once the provided context is done. func (handler *UnroutedHandler) sendProgressMessages(c *httpContext, info FileInfo) { - hook := newHookEvent(c, info) + hook := newHookEvent(c, &info) previousOffset := int64(0) originalOffset := hook.Upload.Offset @@ -1273,27 +1315,29 @@ func getHostAndProtocol(r *http.Request, allowForwarded bool) (host, proto strin // The get sum of all sizes for a list of upload ids while checking whether // all of these uploads are finished yet. This is used to calculate the size // of a final resource. -func (handler *UnroutedHandler) sizeOfUploads(ctx context.Context, ids []string) (partialUploads []Upload, size int64, err error) { +func (handler *UnroutedHandler) sizeOfUploads(ctx context.Context, ids []string) (partialUploads []Upload, partialFileInfos []FileInfo, size int64, err error) { partialUploads = make([]Upload, len(ids)) + partialFileInfos = make([]FileInfo, len(ids)) for i, id := range ids { upload, err := handler.composer.Core.GetUpload(ctx, id) if err != nil { - return nil, 0, err + return nil, nil, 0, err } info, err := upload.GetInfo(ctx) if err != nil { - return nil, 0, err + return nil, nil, 0, err } if info.SizeIsDeferred || info.Offset != info.Size { err = ErrUploadNotFinished - return nil, 0, err + return nil, nil, 0, err } size += info.Size partialUploads[i] = upload + partialFileInfos[i] = info } return diff --git a/pkg/hooks/grpc/grpc.go b/pkg/hooks/grpc/grpc.go index 92b0e1c45..d78a50d9d 100644 --- a/pkg/hooks/grpc/grpc.go +++ b/pkg/hooks/grpc/grpc.go @@ -9,6 +9,7 @@ import ( "time" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + "github.com/tus/tusd/v2/pkg/handler" "github.com/tus/tusd/v2/pkg/hooks" pb "github.com/tus/tusd/v2/pkg/hooks/grpc/proto" "google.golang.org/grpc" @@ -74,6 +75,10 @@ func marshal(hookReq hooks.HookRequest) *pb.HookRequest { RemoteAddr: event.HTTPRequest.RemoteAddr, Header: getHeader(event.HTTPRequest.Header), }, + Access: &pb.AccessInfo{ + Mode: event.Access.Mode, + Uploads: getAccessFiles(event.Access.Uploads), + }, }, } } @@ -88,9 +93,28 @@ func getHeader(httpHeader http.Header) (hookHeader map[string]string) { return hookHeader } +func getAccessFiles(files []handler.FileInfo) (hookFiles []*pb.FileInfo) { + hookFiles = make([]*pb.FileInfo, len(files)) + for i, file := range files { + hookFiles[i] = &pb.FileInfo{ + Id: file.ID, + Size: file.Size, + SizeIsDeferred: file.SizeIsDeferred, + Offset: file.Offset, + MetaData: file.MetaData, + IsPartial: file.IsPartial, + IsFinal: file.IsFinal, + PartialUploads: file.PartialUploads, + Storage: file.Storage, + } + } + return hookFiles +} + func unmarshal(res *pb.HookResponse) (hookRes hooks.HookResponse) { hookRes.RejectUpload = res.RejectUpload hookRes.StopUpload = res.StopUpload + hookRes.RejectAccess = res.RejectAccess httpRes := res.HttpResponse if httpRes != nil { diff --git a/pkg/hooks/grpc/proto/hook.pb.go b/pkg/hooks/grpc/proto/hook.pb.go index d130d6ac9..4c168a2c9 100644 --- a/pkg/hooks/grpc/proto/hook.pb.go +++ b/pkg/hooks/grpc/proto/hook.pb.go @@ -7,8 +7,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 -// protoc v3.21.12 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: pkg/hooks/grpc/proto/hook.proto package proto @@ -98,6 +98,8 @@ type Event struct { // HTTPRequest contains details about the HTTP request that reached // tusd. HttpRequest *HTTPRequest `protobuf:"bytes,2,opt,name=httpRequest,proto3" json:"httpRequest,omitempty"` + // Only use by pre-access and pre-create (when Upload-Concat) hook + Access *AccessInfo `protobuf:"bytes,3,opt,name=access,proto3" json:"access,omitempty"` } func (x *Event) Reset() { @@ -146,6 +148,13 @@ func (x *Event) GetHttpRequest() *HTTPRequest { return nil } +func (x *Event) GetAccess() *AccessInfo { + if x != nil { + return x.Access + } + return nil +} + // FileInfo contains information about a single upload resource. type FileInfo struct { state protoimpl.MessageState @@ -272,6 +281,65 @@ func (x *FileInfo) GetStorage() map[string]string { return nil } +// For pre-access and pre-create (when Upload-Concat) hook to protect access for instance +type AccessInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // read (Head/Get/Upload-Concat) or write (Patch/Delete) + Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"` + // All files that will be access by http request + // Use an array because of Upload-Concat that may target seeral files + Uploads []*FileInfo `protobuf:"bytes,2,rep,name=uploads,proto3" json:"uploads,omitempty"` +} + +func (x *AccessInfo) Reset() { + *x = AccessInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessInfo) ProtoMessage() {} + +func (x *AccessInfo) ProtoReflect() protoreflect.Message { + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessInfo.ProtoReflect.Descriptor instead. +func (*AccessInfo) Descriptor() ([]byte, []int) { + return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{3} +} + +func (x *AccessInfo) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *AccessInfo) GetUploads() []*FileInfo { + if x != nil { + return x.Uploads + } + return nil +} + // FileInfoChanges collects changes the should be made to a FileInfo object. This // can be done using the PreUploadCreateCallback to modify certain properties before // an upload is created. Properties which should not be modified (e.g. Size or Offset) @@ -305,7 +373,7 @@ type FileInfoChanges struct { func (x *FileInfoChanges) Reset() { *x = FileInfoChanges{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[3] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -318,7 +386,7 @@ func (x *FileInfoChanges) String() string { func (*FileInfoChanges) ProtoMessage() {} func (x *FileInfoChanges) ProtoReflect() protoreflect.Message { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[3] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -331,7 +399,7 @@ func (x *FileInfoChanges) ProtoReflect() protoreflect.Message { // Deprecated: Use FileInfoChanges.ProtoReflect.Descriptor instead. func (*FileInfoChanges) Descriptor() ([]byte, []int) { - return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{3} + return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{4} } func (x *FileInfoChanges) GetId() string { @@ -374,7 +442,7 @@ type HTTPRequest struct { func (x *HTTPRequest) Reset() { *x = HTTPRequest{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[4] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -387,7 +455,7 @@ func (x *HTTPRequest) String() string { func (*HTTPRequest) ProtoMessage() {} func (x *HTTPRequest) ProtoReflect() protoreflect.Message { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[4] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -400,7 +468,7 @@ func (x *HTTPRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPRequest.ProtoReflect.Descriptor instead. func (*HTTPRequest) Descriptor() ([]byte, []int) { - return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{4} + return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{5} } func (x *HTTPRequest) GetMethod() string { @@ -463,12 +531,16 @@ type HookResponse struct { // it is ignored. Use the HTTPResponse field to send details about the stop // to the client. StopUpload bool `protobuf:"varint,3,opt,name=stopUpload,proto3" json:"stopUpload,omitempty"` + // In case of pre-access or pre-create (when Upload-Concat), reject access to upload + // When true, http request will end with 403 status code by default, changeable with + // HTTPResponse override + RejectAccess bool `protobuf:"varint,5,opt,name=rejectAccess,proto3" json:"rejectAccess,omitempty"` } func (x *HookResponse) Reset() { *x = HookResponse{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[5] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -481,7 +553,7 @@ func (x *HookResponse) String() string { func (*HookResponse) ProtoMessage() {} func (x *HookResponse) ProtoReflect() protoreflect.Message { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[5] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -494,7 +566,7 @@ func (x *HookResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HookResponse.ProtoReflect.Descriptor instead. func (*HookResponse) Descriptor() ([]byte, []int) { - return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{5} + return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{6} } func (x *HookResponse) GetHttpResponse() *HTTPResponse { @@ -525,6 +597,13 @@ func (x *HookResponse) GetStopUpload() bool { return false } +func (x *HookResponse) GetRejectAccess() bool { + if x != nil { + return x.RejectAccess + } + return false +} + // HTTPResponse contains basic details of an outgoing HTTP response. type HTTPResponse struct { state protoimpl.MessageState @@ -542,7 +621,7 @@ type HTTPResponse struct { func (x *HTTPResponse) Reset() { *x = HTTPResponse{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[6] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -555,7 +634,7 @@ func (x *HTTPResponse) String() string { func (*HTTPResponse) ProtoMessage() {} func (x *HTTPResponse) ProtoReflect() protoreflect.Message { - mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[6] + mi := &file_pkg_hooks_grpc_proto_hook_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -568,7 +647,7 @@ func (x *HTTPResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead. func (*HTTPResponse) Descriptor() ([]byte, []int) { - return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{6} + return file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP(), []int{7} } func (x *HTTPResponse) GetStatusCode() int64 { @@ -602,102 +681,112 @@ var file_pkg_hooks_grpc_proto_hook_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, - 0x66, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x06, 0x75, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, - 0x64, 0x12, 0x34, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, - 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xba, 0x03, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x69, 0x7a, 0x65, - 0x49, 0x73, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0e, 0x73, 0x69, 0x7a, 0x65, 0x49, 0x73, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, - 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, - 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x73, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x70, - 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x08, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, 0x09, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, - 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, - 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x91, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x06, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x75, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x12, 0x34, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x68, 0x74, 0x74, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x22, 0xba, 0x03, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x69, 0x7a, 0x65, 0x49, 0x73, 0x44, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x73, 0x69, + 0x7a, 0x65, 0x49, 0x73, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, + 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x18, 0x0a, + 0x07, 0x69, 0x73, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x69, 0x73, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, + 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, + 0x36, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x44, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9b, 0x02, 0x0a, 0x0f, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, - 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x40, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x12, 0x3d, 0x0a, 0x07, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, - 0x61, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xca, 0x01, 0x0a, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, - 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1e, 0x0a, 0x0a, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, 0x06, - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x1a, 0x39, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x4b, 0x0a, 0x0a, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, + 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, + 0x64, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x22, 0x9b, 0x02, + 0x0a, 0x0f, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x40, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x3d, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xcb, 0x01, 0x0a, 0x0c, 0x48, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, - 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x68, 0x74, 0x74, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6a, - 0x65, 0x63, 0x74, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3e, 0x0a, - 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x52, 0x0e, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x74, 0x6f, 0x70, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x73, 0x74, 0x6f, 0x70, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xb6, 0x01, - 0x0a, 0x0c, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, - 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x37, - 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x39, 0x0a, 0x0b, 0x48, - 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0x46, 0x0a, 0x0b, 0x48, 0x6f, 0x6f, 0x6b, 0x48, 0x61, - 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0a, 0x49, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x48, - 0x6f, 0x6f, 0x6b, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x48, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x12, - 0x5a, 0x10, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x3a, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xca, 0x01, 0x0a, 0x0b, + 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, + 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x54, + 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x1a, 0x39, 0x0a, + 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xef, 0x01, 0x0a, 0x0c, 0x48, 0x6f, 0x6f, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0c, 0x68, 0x74, 0x74, + 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3e, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x46, 0x69, + 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x6f, 0x70, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x74, 0x6f, 0x70, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, + 0x6a, 0x65, 0x63, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0xb6, 0x01, 0x0a, 0x0c, 0x48, + 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x39, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x32, 0x46, 0x0a, 0x0b, 0x48, 0x6f, 0x6f, 0x6b, 0x48, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0a, 0x49, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x48, 0x6f, 0x6f, 0x6b, + 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x6f, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x12, 0x5a, 0x10, 0x68, + 0x6f, 0x6f, 0x6b, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -712,41 +801,44 @@ func file_pkg_hooks_grpc_proto_hook_proto_rawDescGZIP() []byte { return file_pkg_hooks_grpc_proto_hook_proto_rawDescData } -var file_pkg_hooks_grpc_proto_hook_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_pkg_hooks_grpc_proto_hook_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_pkg_hooks_grpc_proto_hook_proto_goTypes = []interface{}{ (*HookRequest)(nil), // 0: proto.HookRequest (*Event)(nil), // 1: proto.Event (*FileInfo)(nil), // 2: proto.FileInfo - (*FileInfoChanges)(nil), // 3: proto.FileInfoChanges - (*HTTPRequest)(nil), // 4: proto.HTTPRequest - (*HookResponse)(nil), // 5: proto.HookResponse - (*HTTPResponse)(nil), // 6: proto.HTTPResponse - nil, // 7: proto.FileInfo.MetaDataEntry - nil, // 8: proto.FileInfo.StorageEntry - nil, // 9: proto.FileInfoChanges.MetaDataEntry - nil, // 10: proto.FileInfoChanges.StorageEntry - nil, // 11: proto.HTTPRequest.HeaderEntry - nil, // 12: proto.HTTPResponse.HeaderEntry + (*AccessInfo)(nil), // 3: proto.AccessInfo + (*FileInfoChanges)(nil), // 4: proto.FileInfoChanges + (*HTTPRequest)(nil), // 5: proto.HTTPRequest + (*HookResponse)(nil), // 6: proto.HookResponse + (*HTTPResponse)(nil), // 7: proto.HTTPResponse + nil, // 8: proto.FileInfo.MetaDataEntry + nil, // 9: proto.FileInfo.StorageEntry + nil, // 10: proto.FileInfoChanges.MetaDataEntry + nil, // 11: proto.FileInfoChanges.StorageEntry + nil, // 12: proto.HTTPRequest.HeaderEntry + nil, // 13: proto.HTTPResponse.HeaderEntry } var file_pkg_hooks_grpc_proto_hook_proto_depIdxs = []int32{ 1, // 0: proto.HookRequest.event:type_name -> proto.Event 2, // 1: proto.Event.upload:type_name -> proto.FileInfo - 4, // 2: proto.Event.httpRequest:type_name -> proto.HTTPRequest - 7, // 3: proto.FileInfo.metaData:type_name -> proto.FileInfo.MetaDataEntry - 8, // 4: proto.FileInfo.storage:type_name -> proto.FileInfo.StorageEntry - 9, // 5: proto.FileInfoChanges.metaData:type_name -> proto.FileInfoChanges.MetaDataEntry - 10, // 6: proto.FileInfoChanges.storage:type_name -> proto.FileInfoChanges.StorageEntry - 11, // 7: proto.HTTPRequest.header:type_name -> proto.HTTPRequest.HeaderEntry - 6, // 8: proto.HookResponse.httpResponse:type_name -> proto.HTTPResponse - 3, // 9: proto.HookResponse.changeFileInfo:type_name -> proto.FileInfoChanges - 12, // 10: proto.HTTPResponse.header:type_name -> proto.HTTPResponse.HeaderEntry - 0, // 11: proto.HookHandler.InvokeHook:input_type -> proto.HookRequest - 5, // 12: proto.HookHandler.InvokeHook:output_type -> proto.HookResponse - 12, // [12:13] is the sub-list for method output_type - 11, // [11:12] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 5, // 2: proto.Event.httpRequest:type_name -> proto.HTTPRequest + 3, // 3: proto.Event.access:type_name -> proto.AccessInfo + 8, // 4: proto.FileInfo.metaData:type_name -> proto.FileInfo.MetaDataEntry + 9, // 5: proto.FileInfo.storage:type_name -> proto.FileInfo.StorageEntry + 2, // 6: proto.AccessInfo.uploads:type_name -> proto.FileInfo + 10, // 7: proto.FileInfoChanges.metaData:type_name -> proto.FileInfoChanges.MetaDataEntry + 11, // 8: proto.FileInfoChanges.storage:type_name -> proto.FileInfoChanges.StorageEntry + 12, // 9: proto.HTTPRequest.header:type_name -> proto.HTTPRequest.HeaderEntry + 7, // 10: proto.HookResponse.httpResponse:type_name -> proto.HTTPResponse + 4, // 11: proto.HookResponse.changeFileInfo:type_name -> proto.FileInfoChanges + 13, // 12: proto.HTTPResponse.header:type_name -> proto.HTTPResponse.HeaderEntry + 0, // 13: proto.HookHandler.InvokeHook:input_type -> proto.HookRequest + 6, // 14: proto.HookHandler.InvokeHook:output_type -> proto.HookResponse + 14, // [14:15] is the sub-list for method output_type + 13, // [13:14] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_pkg_hooks_grpc_proto_hook_proto_init() } @@ -792,7 +884,7 @@ func file_pkg_hooks_grpc_proto_hook_proto_init() { } } file_pkg_hooks_grpc_proto_hook_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FileInfoChanges); i { + switch v := v.(*AccessInfo); i { case 0: return &v.state case 1: @@ -804,7 +896,7 @@ func file_pkg_hooks_grpc_proto_hook_proto_init() { } } file_pkg_hooks_grpc_proto_hook_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HTTPRequest); i { + switch v := v.(*FileInfoChanges); i { case 0: return &v.state case 1: @@ -816,7 +908,7 @@ func file_pkg_hooks_grpc_proto_hook_proto_init() { } } file_pkg_hooks_grpc_proto_hook_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HookResponse); i { + switch v := v.(*HTTPRequest); i { case 0: return &v.state case 1: @@ -828,6 +920,18 @@ func file_pkg_hooks_grpc_proto_hook_proto_init() { } } file_pkg_hooks_grpc_proto_hook_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HookResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_hooks_grpc_proto_hook_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HTTPResponse); i { case 0: return &v.state @@ -846,7 +950,7 @@ func file_pkg_hooks_grpc_proto_hook_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_pkg_hooks_grpc_proto_hook_proto_rawDesc, NumEnums: 0, - NumMessages: 13, + NumMessages: 14, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/hooks/grpc/proto/hook.proto b/pkg/hooks/grpc/proto/hook.proto index b6f4adc47..6f309f058 100644 --- a/pkg/hooks/grpc/proto/hook.proto +++ b/pkg/hooks/grpc/proto/hook.proto @@ -29,6 +29,9 @@ message Event { // HTTPRequest contains details about the HTTP request that reached // tusd. HTTPRequest httpRequest = 2; + + // Only use by pre-access and pre-create (when Upload-Concat) hook + AccessInfo access = 3; } // FileInfo contains information about a single upload resource. @@ -58,6 +61,16 @@ message FileInfo { map storage = 9; } +// For pre-access and pre-create (when Upload-Concat) hook to protect access for instance +message AccessInfo { + // read (Head/Get/Upload-Concat) or write (Patch/Delete) + string mode = 1; + + // All files that will be access by http request + // Use an array because of Upload-Concat that may target seeral files + repeated FileInfo uploads = 2; +} + // FileInfoChanges collects changes the should be made to a FileInfo object. This // can be done using the PreUploadCreateCallback to modify certain properties before // an upload is created. Properties which should not be modified (e.g. Size or Offset) @@ -130,6 +143,11 @@ message HookResponse { // it is ignored. Use the HTTPResponse field to send details about the stop // to the client. bool stopUpload = 3; + + // In case of pre-access or pre-create (when Upload-Concat), reject access to upload + // When true, http request will end with 403 status code by default, changeable with + // HTTPResponse override + bool rejectAccess = 5; } // HTTPResponse contains basic details of an outgoing HTTP response. diff --git a/pkg/hooks/grpc/proto/hook_grpc.pb.go b/pkg/hooks/grpc/proto/hook_grpc.pb.go index d0c9dca16..3cf82ddcb 100644 --- a/pkg/hooks/grpc/proto/hook_grpc.pb.go +++ b/pkg/hooks/grpc/proto/hook_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v3.21.12 +// - protoc v4.25.2 // source: pkg/hooks/grpc/proto/hook.proto package proto diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go index 1251f9198..a2fe72c13 100644 --- a/pkg/hooks/hooks.go +++ b/pkg/hooks/hooks.go @@ -80,6 +80,11 @@ type HookResponse struct { // it is ignored. Use the HTTPResponse field to send details about the stop // to the client. StopUpload bool + + // In case of pre-access or pre-create (when Upload-Concat), reject access to upload + // When true, http request will end with 403 status code by default, changeable with + // HTTPResponse override + RejectAccess bool } type HookType string @@ -91,10 +96,11 @@ const ( HookPostCreate HookType = "post-create" HookPreCreate HookType = "pre-create" HookPreFinish HookType = "pre-finish" + HookPreAccess HookType = "pre-access" ) // AvailableHooks is a slice of all hooks that are implemented by tusd. -var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish} +var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish, HookPreAccess} func preCreateCallback(event handler.HookEvent, hookHandler HookHandler) (handler.HTTPResponse, handler.FileInfoChanges, error) { ok, hookRes, err := invokeHookSync(HookPreCreate, event, hookHandler) @@ -118,6 +124,25 @@ func preCreateCallback(event handler.HookEvent, hookHandler HookHandler) (handle return httpRes, changes, nil } +func preAccessCallback(event handler.HookEvent, hookHandler HookHandler) error { + ok, hookRes, err := invokeHookSync(HookPreAccess, event, hookHandler) + if !ok || err != nil { + return err + } + + httpRes := hookRes.HTTPResponse + + // If the hook response includes the instruction to reject access, reuse the error code + // and message from ErrAccessRejectedByServer, but also include custom HTTP response values. + if hookRes.RejectAccess { + err := handler.ErrAccessRejectedByServer + err.HTTPResponse = err.HTTPResponse.MergeWith(httpRes) + + return err + } + return nil +} + func preFinishCallback(event handler.HookEvent, hookHandler HookHandler) (handler.HTTPResponse, error) { ok, hookRes, err := invokeHookSync(HookPreFinish, event, hookHandler) if !ok || err != nil { @@ -166,12 +191,14 @@ func SetupHookMetrics() { MetricsHookErrorsTotal.WithLabelValues(string(HookPostCreate)).Add(0) MetricsHookErrorsTotal.WithLabelValues(string(HookPreCreate)).Add(0) MetricsHookErrorsTotal.WithLabelValues(string(HookPreFinish)).Add(0) + MetricsHookErrorsTotal.WithLabelValues(string(HookPreAccess)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPostFinish)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPostTerminate)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPostReceive)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPostCreate)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPreCreate)).Add(0) MetricsHookInvocationsTotal.WithLabelValues(string(HookPreFinish)).Add(0) + MetricsHookInvocationsTotal.WithLabelValues(string(HookPreAccess)).Add(0) } func invokeHookAsync(typ HookType, event handler.HookEvent, hookHandler HookHandler) { @@ -218,8 +245,9 @@ func invokeHookSync(typ HookType, event handler.HookEvent, hookHandler HookHandl // // If you want to create an UnroutedHandler instead of the routed handler, you can first create a routed handler and then // extract an unrouted one: -// routedHandler := hooks.NewHandlerWithHooks(...) -// unroutedHandler := routedHandler.UnroutedHandler +// +// routedHandler := hooks.NewHandlerWithHooks(...) +// unroutedHandler := routedHandler.UnroutedHandler // // Note: NewHandlerWithHooks sets up a goroutine to consume the notfication channels (CompleteUploads, TerminatedUploads, // CreatedUploads, UploadProgress) on the created handler. These channels must not be consumed by the caller or otherwise @@ -246,6 +274,11 @@ func NewHandlerWithHooks(config *handler.Config, hookHandler HookHandler, enable return preFinishCallback(event, hookHandler) } } + if slices.Contains(enabledHooks, HookPreAccess) { + config.PreUploadAccessCallback = func(event handler.HookEvent) error { + return preAccessCallback(event, hookHandler) + } + } // Create handler handler, err := handler.NewHandler(*config) diff --git a/pkg/hooks/hooks_test.go b/pkg/hooks/hooks_test.go index 693a8b6ad..b89afa8a2 100644 --- a/pkg/hooks/hooks_test.go +++ b/pkg/hooks/hooks_test.go @@ -42,6 +42,27 @@ func TestNewHandlerWithHooks(t *testing.T) { }, } + preAccessEvent := handler.HookEvent{ + HTTPRequest: handler.HTTPRequest{ + Method: "POST", + URI: "/files/", + Header: http.Header{ + "X-Hello": []string{"there"}, + }, + }, + Access: handler.AccessInfo{ + Mode: handler.AccessModeRead, + Uploads: []handler.FileInfo{ + { + ID: "id", + MetaData: handler.MetaData{ + "hello": "world", + }, + }, + }, + }, + } + response := handler.HTTPResponse{ StatusCode: 200, Body: "foobar", @@ -89,6 +110,25 @@ func TestNewHandlerWithHooks(t *testing.T) { Type: HookPreFinish, Event: event, }).Return(HookResponse{}, error), + hookHandler.EXPECT().InvokeHook(HookRequest{ + Type: HookPreAccess, + Event: preAccessEvent, + }).Return(HookResponse{ + HTTPResponse: response, + }, nil), + hookHandler.EXPECT().InvokeHook(HookRequest{ + Type: HookPreAccess, + Event: preAccessEvent, + }).Return(HookResponse{ + RejectAccess: true, + }, nil), + hookHandler.EXPECT().InvokeHook(HookRequest{ + Type: HookPreAccess, + Event: preAccessEvent, + }).Return(HookResponse{ + HTTPResponse: response, + RejectAccess: true, + }, nil), ) // The hooks are executed asynchronously, so we don't know their execution order. @@ -112,7 +152,7 @@ func TestNewHandlerWithHooks(t *testing.T) { Event: event, }) - uploadHandler, err := NewHandlerWithHooks(&config, hookHandler, []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish}) + uploadHandler, err := NewHandlerWithHooks(&config, hookHandler, []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish, HookPreAccess}) a.NoError(err) // Successful pre-create hook @@ -148,6 +188,35 @@ func TestNewHandlerWithHooks(t *testing.T) { a.Equal(error, err) a.Equal(handler.HTTPResponse{}, resp_got) + // Successful pre-access hook + err = config.PreUploadAccessCallback(preAccessEvent) + a.NoError(err) + + // Pre-access hook with rejection + err = config.PreUploadAccessCallback(preAccessEvent) + a.Equal(handler.Error{ + ErrorCode: handler.ErrAccessRejectedByServer.ErrorCode, + Message: handler.ErrAccessRejectedByServer.Message, + HTTPResponse: handler.ErrAccessRejectedByServer.HTTPResponse, + }, err) + a.Equal(handler.HTTPResponse{}, resp_got) + + // Pre-access hook with rejection and http override + err = config.PreUploadAccessCallback(preAccessEvent) + a.Equal(handler.Error{ + ErrorCode: handler.ErrAccessRejectedByServer.ErrorCode, + Message: handler.ErrAccessRejectedByServer.Message, + HTTPResponse: handler.HTTPResponse{ + StatusCode: 200, + Body: "foobar", + Header: handler.HTTPHeader{ + "X-Hello": "here", + "Content-Type": "text/plain; charset=utf-8", + }, + }, + }, err) + a.Equal(handler.HTTPResponse{}, resp_got) + // Successful post-* hooks uploadHandler.CreatedUploads <- event uploadHandler.UploadProgress <- event