fix: secure download URLs when downloading tar archive of exported files

This commit is contained in:
garethgeorge
2024-05-05 12:42:30 -07:00
committed by Gareth
parent 912772df14
commit 59fb5b66ac
12 changed files with 279 additions and 79 deletions
+41 -35
View File
@@ -741,7 +741,7 @@ var file_v1_service_proto_rawDesc = []byte{
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74,
0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0xa7, 0x08, 0x0a, 0x08, 0x42, 0x61,
0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0xe2, 0x08, 0x0a, 0x08, 0x42, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31,
@@ -800,18 +800,22 @@ var file_v1_service_proto_rawDesc = []byte{
0x32, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11,
0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74,
0x6f, 0x72, 0x79, 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69,
0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75,
0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70,
0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11,
0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73,
0x74, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62,
0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76,
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f,
0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e,
0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41,
0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17,
0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d,
0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74,
0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65,
0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x42, 0x2c,
0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72,
0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65,
0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -866,28 +870,30 @@ var file_v1_service_proto_depIdxs = []int32{
12, // 14: v1.Backrest.Stats:input_type -> types.StringValue
13, // 15: v1.Backrest.Cancel:input_type -> types.Int64Value
7, // 16: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest
0, // 17: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
12, // 18: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
10, // 19: v1.Backrest.GetConfig:output_type -> v1.Config
10, // 20: v1.Backrest.SetConfig:output_type -> v1.Config
10, // 21: v1.Backrest.AddRepo:output_type -> v1.Config
14, // 22: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
15, // 23: v1.Backrest.GetOperations:output_type -> v1.OperationList
16, // 24: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
6, // 25: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
9, // 26: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
9, // 27: v1.Backrest.Backup:output_type -> google.protobuf.Empty
9, // 28: v1.Backrest.Prune:output_type -> google.protobuf.Empty
9, // 29: v1.Backrest.Forget:output_type -> google.protobuf.Empty
9, // 30: v1.Backrest.Restore:output_type -> google.protobuf.Empty
9, // 31: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
9, // 32: v1.Backrest.Stats:output_type -> google.protobuf.Empty
9, // 33: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
17, // 34: v1.Backrest.GetLogs:output_type -> types.BytesValue
9, // 35: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
18, // 36: v1.Backrest.PathAutocomplete:output_type -> types.StringList
19, // [19:37] is the sub-list for method output_type
1, // [1:19] is the sub-list for method input_type
13, // 17: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value
0, // 18: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
12, // 19: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
10, // 20: v1.Backrest.GetConfig:output_type -> v1.Config
10, // 21: v1.Backrest.SetConfig:output_type -> v1.Config
10, // 22: v1.Backrest.AddRepo:output_type -> v1.Config
14, // 23: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
15, // 24: v1.Backrest.GetOperations:output_type -> v1.OperationList
16, // 25: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
6, // 26: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
9, // 27: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
9, // 28: v1.Backrest.Backup:output_type -> google.protobuf.Empty
9, // 29: v1.Backrest.Prune:output_type -> google.protobuf.Empty
9, // 30: v1.Backrest.Forget:output_type -> google.protobuf.Empty
9, // 31: v1.Backrest.Restore:output_type -> google.protobuf.Empty
9, // 32: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
9, // 33: v1.Backrest.Stats:output_type -> google.protobuf.Empty
9, // 34: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
17, // 35: v1.Backrest.GetLogs:output_type -> types.BytesValue
12, // 36: v1.Backrest.GetDownloadURL:output_type -> types.StringValue
9, // 37: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
18, // 38: v1.Backrest.PathAutocomplete:output_type -> types.StringList
20, // [20:39] is the sub-list for method output_type
1, // [1:20] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
+41 -2
View File
@@ -37,6 +37,7 @@ const (
Backrest_Stats_FullMethodName = "/v1.Backrest/Stats"
Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel"
Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs"
Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL"
Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory"
Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete"
)
@@ -68,8 +69,10 @@ type BackrestClient interface {
Stats(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error)
// GetBigOperationData returns the keyed large data for the given operation.
// GetLogs returns the keyed large data for the given operation.
GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (*types.BytesValue, error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error)
// Clears the history of operations
ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -251,6 +254,15 @@ func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts .
return out, nil
}
func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) {
out := new(types.StringValue)
err := c.cc.Invoke(ctx, Backrest_GetDownloadURL_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backrestClient) ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Backrest_ClearHistory_FullMethodName, in, out, opts...)
@@ -296,8 +308,10 @@ type BackrestServer interface {
Stats(context.Context, *types.StringValue) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error)
// GetBigOperationData returns the keyed large data for the given operation.
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error)
// Clears the history of operations
ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -357,6 +371,9 @@ func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (*
func (UnimplementedBackrestServer) GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
}
func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented")
}
func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented")
}
@@ -667,6 +684,24 @@ func _Backrest_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(in
return interceptor(ctx, in, info, handler)
}
func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(types.Int64Value)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BackrestServer).GetDownloadURL(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Backrest_GetDownloadURL_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BackrestServer).GetDownloadURL(ctx, req.(*types.Int64Value))
}
return interceptor(ctx, in, info, handler)
}
func _Backrest_ClearHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearHistoryRequest)
if err := dec(in); err != nil {
@@ -770,6 +805,10 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetLogs",
Handler: _Backrest_GetLogs_Handler,
},
{
MethodName: "GetDownloadURL",
Handler: _Backrest_GetDownloadURL_Handler,
},
{
MethodName: "ClearHistory",
Handler: _Backrest_ClearHistory_Handler,
+33 -2
View File
@@ -69,6 +69,8 @@ const (
BackrestCancelProcedure = "/v1.Backrest/Cancel"
// BackrestGetLogsProcedure is the fully-qualified name of the Backrest's GetLogs RPC.
BackrestGetLogsProcedure = "/v1.Backrest/GetLogs"
// BackrestGetDownloadURLProcedure is the fully-qualified name of the Backrest's GetDownloadURL RPC.
BackrestGetDownloadURLProcedure = "/v1.Backrest/GetDownloadURL"
// BackrestClearHistoryProcedure is the fully-qualified name of the Backrest's ClearHistory RPC.
BackrestClearHistoryProcedure = "/v1.Backrest/ClearHistory"
// BackrestPathAutocompleteProcedure is the fully-qualified name of the Backrest's PathAutocomplete
@@ -95,6 +97,7 @@ var (
backrestStatsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Stats")
backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel")
backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs")
backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL")
backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory")
backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete")
)
@@ -124,8 +127,10 @@ type BackrestClient interface {
Stats(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
// GetBigOperationData returns the keyed large data for the given operation.
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -238,6 +243,12 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithClientOptions(opts...),
),
getDownloadURL: connect.NewClient[types.Int64Value, types.StringValue](
httpClient,
baseURL+BackrestGetDownloadURLProcedure,
connect.WithSchema(backrestGetDownloadURLMethodDescriptor),
connect.WithClientOptions(opts...),
),
clearHistory: connect.NewClient[v1.ClearHistoryRequest, emptypb.Empty](
httpClient,
baseURL+BackrestClearHistoryProcedure,
@@ -271,6 +282,7 @@ type backrestClient struct {
stats *connect.Client[types.StringValue, emptypb.Empty]
cancel *connect.Client[types.Int64Value, emptypb.Empty]
getLogs *connect.Client[v1.LogDataRequest, types.BytesValue]
getDownloadURL *connect.Client[types.Int64Value, types.StringValue]
clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty]
pathAutocomplete *connect.Client[types.StringValue, types.StringList]
}
@@ -355,6 +367,11 @@ func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.Lo
return c.getLogs.CallUnary(ctx, req)
}
// GetDownloadURL calls v1.Backrest.GetDownloadURL.
func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
return c.getDownloadURL.CallUnary(ctx, req)
}
// ClearHistory calls v1.Backrest.ClearHistory.
func (c *backrestClient) ClearHistory(ctx context.Context, req *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) {
return c.clearHistory.CallUnary(ctx, req)
@@ -390,8 +407,10 @@ type BackrestHandler interface {
Stats(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
// GetBigOperationData returns the keyed large data for the given operation.
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -500,6 +519,12 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
backrestGetDownloadURLHandler := connect.NewUnaryHandler(
BackrestGetDownloadURLProcedure,
svc.GetDownloadURL,
connect.WithSchema(backrestGetDownloadURLMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
backrestClearHistoryHandler := connect.NewUnaryHandler(
BackrestClearHistoryProcedure,
svc.ClearHistory,
@@ -546,6 +571,8 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
backrestCancelHandler.ServeHTTP(w, r)
case BackrestGetLogsProcedure:
backrestGetLogsHandler.ServeHTTP(w, r)
case BackrestGetDownloadURLProcedure:
backrestGetDownloadURLHandler.ServeHTTP(w, r)
case BackrestClearHistoryProcedure:
backrestClearHistoryHandler.ServeHTTP(w, r)
case BackrestPathAutocompleteProcedure:
@@ -623,6 +650,10 @@ func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented"))
}
func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetDownloadURL is not implemented"))
}
func (UnimplementedBackrestHandler) ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.ClearHistory is not implemented"))
}
+22
View File
@@ -2,6 +2,8 @@ package api
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
@@ -466,6 +468,26 @@ func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.L
return connect.NewResponse(&types.BytesValue{Value: data}), nil
}
func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
op, err := s.oplog.Get(req.Msg.Value)
if err != nil {
return nil, fmt.Errorf("failed to get operation %v: %w", req.Msg.Value, err)
}
_, ok := op.Op.(*v1.Operation_OperationRestore)
if !ok {
return nil, fmt.Errorf("operation %v is not a restore operation", req.Msg.Value)
}
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(op.Id))
signature, err := generateSignature(b)
if err != nil {
return nil, fmt.Errorf("failed to generate signature: %w", err)
}
return connect.NewResponse(&types.StringValue{
Value: fmt.Sprintf("./download/%x-%s/", op.Id, hex.EncodeToString(signature)),
}), nil
}
func (s *BackrestHandler) PathAutocomplete(ctx context.Context, path *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) {
ents, err := os.ReadDir(path.Msg.Value)
if errors.Is(err, os.ErrNotExist) {
+87 -21
View File
@@ -1,7 +1,11 @@
package api
import (
"archive/zip"
"archive/tar"
"compress/gzip"
"crypto/hmac"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net/http"
@@ -9,6 +13,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
"github.com/garethgeorge/backrest/internal/oplog"
@@ -18,18 +23,18 @@ import (
func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path[1:]
sep := strings.Index(p, "/")
if sep == -1 {
opID, signature, filePath, err := parseDownloadPath(p)
if err != nil {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
restoreID := p[:sep]
filePath := p[sep+1:]
opID, err := strconv.ParseInt(restoreID, 16, 64)
if err != nil {
http.Error(w, "invalid restore ID: "+err.Error(), http.StatusBadRequest)
if ok, err := checkDownloadURLSignature(opID, signature); err != nil || !ok {
http.Error(w, fmt.Sprintf("invalid signature: %v", err), http.StatusForbidden)
return
}
op, err := oplog.Get(int64(opID))
if err != nil {
http.Error(w, "restore not found", http.StatusNotFound)
@@ -47,12 +52,14 @@ func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
}
fullPath := filepath.Join(targetPath, filePath)
w.Header().Set("Content-Disposition", "attachment; filename=archive.zip")
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=archive-%v.tar.gz", time.Now().Format("2006-01-02-15-04-05")))
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Transfer-Encoding", "binary")
z := zip.NewWriter(w)
zap.L().Info("creating zip archive", zap.String("path", fullPath))
gzw := gzip.NewWriter(w)
defer gzw.Close()
t := tar.NewWriter(gzw)
zap.L().Info("creating tar archive", zap.String("path", fullPath))
if err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -60,25 +67,84 @@ func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
if info.IsDir() {
return nil
}
stat, err := os.Stat(path)
if err != nil {
zap.L().Warn("error stating file", zap.String("path", path), zap.Error(err))
return nil
}
file, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
zap.L().Warn("error opening file", zap.String("path", path), zap.Error(err))
return nil
}
defer file.Close()
f, err := z.Create(path[len(fullPath)+1:])
if err != nil {
return fmt.Errorf("add file to zip archive: %w", err)
if err := t.WriteHeader(&tar.Header{
Name: path[len(fullPath)+1:],
Size: stat.Size(),
Mode: int64(stat.Mode()),
ModTime: stat.ModTime(),
}); err != nil {
zap.L().Warn("error writing tar header", zap.String("path", path), zap.Error(err))
return nil
}
if n, err := io.CopyN(t, file, stat.Size()); err != nil {
zap.L().Warn("error copying file to tar archive", zap.String("path", path), zap.Error(err))
} else if n != stat.Size() {
zap.L().Warn("error copying file to tar archive: short write", zap.String("path", path))
}
io.Copy(f, file)
return nil
}); err != nil {
zap.S().Errorf("error creating zip archive: %v", err)
http.Error(w, "error creating zip archive", http.StatusInternalServerError)
zap.S().Errorf("error creating tar archive: %v", err)
http.Error(w, "error creating tar archive", http.StatusInternalServerError)
}
if err := z.Close(); err != nil {
zap.S().Errorf("error closing zip archive: %v", err)
http.Error(w, "error closing zip archive", http.StatusInternalServerError)
t.Flush()
if err := t.Close(); err != nil {
zap.S().Errorf("error closing tar archive: %v", err)
http.Error(w, "error closing tar archive", http.StatusInternalServerError)
}
})
}
func parseDownloadPath(p string) (int64, string, string, error) {
sep := strings.Index(p, "/")
if sep == -1 {
return 0, "", "", fmt.Errorf("invalid path")
}
restoreID := p[:sep]
filePath := p[sep+1:]
dash := strings.Index(restoreID, "-")
if dash == -1 {
return 0, "", "", fmt.Errorf("invalid restore ID")
}
opID, err := strconv.ParseInt(restoreID[:dash], 16, 64)
if err != nil {
return 0, "", "", fmt.Errorf("invalid restore ID: %w", err)
}
signature := restoreID[dash+1:]
return opID, signature, filePath, nil
}
func checkDownloadURLSignature(id int64, signature string) (bool, error) {
wantSignatureBytes, err := signOperationIDForDownload(id)
if err != nil {
return false, err
}
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return false, err
}
return hmac.Equal(wantSignatureBytes, signatureBytes), nil
}
func signOperationIDForDownload(id int64) ([]byte, error) {
opIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(opIDBytes, uint64(id))
signature, err := generateSignature(opIDBytes)
if err != nil {
return nil, err
}
return signature, nil
}
+26
View File
@@ -0,0 +1,26 @@
package api
import (
"crypto"
"crypto/hmac"
"crypto/rand"
)
var (
secret = make([]byte, 32)
)
func init() {
n, err := rand.Read(secret)
if n != 32 || err != nil {
panic("failed to generate secret key")
}
}
func generateSignature(data []byte) ([]byte, error) {
h := hmac.New(crypto.SHA256.New, secret)
if n, err := h.Write(data); n != len(data) || err != nil {
panic("failed to write data to hmac")
}
return h.Sum(nil), nil
}
-2
View File
@@ -154,8 +154,6 @@ Task: "{{ .Task }}" at {{ .FormatTime .CurTime }}
{{ if .Error -}}
Error: {{ .Error }}
{{ end }}
{{ if .Items -}}
`
var templateForSnapshotStart = `
+5 -13
View File
@@ -87,8 +87,7 @@ func NewOpLog(databasePath string) (*OpLog, error) {
// Scan checks the log for incomplete operations. Should only be called at startup.
func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
removeIds := make([]int64, 0)
zap.L().Debug("scanning oplog for incomplete operations")
err := o.db.Update(func(tx *bolt.Tx) error {
sysBucket := tx.Bucket(SystemBucket)
opLogBucket := tx.Bucket(OpLogBucket)
@@ -96,7 +95,7 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
if lastValidated := sysBucket.Get([]byte("last_validated")); lastValidated != nil {
c.Seek(lastValidated)
}
for k, v := c.First(); k != nil; k, v = c.Next() {
for k, v := c.Next(); k != nil; k, v = c.Next() {
op := &v1.Operation{}
if err := proto.Unmarshal(v, op); err != nil {
zap.L().Error("error unmarshalling operation, there may be corruption in the oplog", zap.Error(err))
@@ -104,20 +103,18 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
}
if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED || op.Status == v1.OperationStatus_STATUS_USER_CANCELLED || op.Status == v1.OperationStatus_STATUS_UNKNOWN {
// remove pending or user cancelled operations.
removeIds = append(removeIds, op.Id)
o.deleteOperationHelper(tx, op.Id)
continue
} else if op.Status == v1.OperationStatus_STATUS_INPROGRESS {
onIncomplete(op)
removeIds = append(removeIds, op.Id)
}
if err := o.addOperationHelper(tx, op); err != nil {
zap.L().Error("error re-adding operation, there may be corruption in the oplog", zap.Error(err))
continue
}
}
if lastValidated, _ := c.Last(); lastValidated != nil {
zap.L().Debug("checkpointing last_validated key")
if err := sysBucket.Put([]byte("last_validated"), lastValidated); err != nil {
return fmt.Errorf("checkpointing last_validated key: %w", err)
}
@@ -127,12 +124,7 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
if err != nil {
return fmt.Errorf("scanning log: %v", err)
}
if len(removeIds) > 0 {
if err := o.Delete(removeIds...); err != nil {
return fmt.Errorf("removing incomplete operations: %w", err)
}
}
zap.L().Debug("scan complete")
return nil
}
+1 -1
View File
@@ -24,7 +24,7 @@ func ValidateOperation(op *v1.Operation) error {
return errors.New("operation.plan_id is required")
}
if op.InstanceId == "" {
zap.L().Warn("operation.instance_id should typically be set")
zap.L().Warn("operation.instance_id should typically be set", zap.Any("operation", op))
}
if op.SnapshotId != "" {
if err := restic.ValidateSnapshotId(op.SnapshotId); err != nil {
+4 -1
View File
@@ -50,9 +50,12 @@ service Backrest {
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) {}
// GetBigOperationData returns the keyed large data for the given operation.
// GetLogs returns the keyed large data for the given operation.
rpc GetLogs(LogDataRequest) returns (types.BytesValue) {}
// GetDownloadURL returns a signed download URL given a forget operation ID.
rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {}
// Clears the history of operations
rpc ClearHistory(ClearHistoryRequest) returns (google.protobuf.Empty) {}
+12 -1
View File
@@ -168,7 +168,7 @@ export const Backrest = {
kind: MethodKind.Unary,
},
/**
* GetBigOperationData returns the keyed large data for the given operation.
* GetLogs returns the keyed large data for the given operation.
*
* @generated from rpc v1.Backrest.GetLogs
*/
@@ -178,6 +178,17 @@ export const Backrest = {
O: BytesValue,
kind: MethodKind.Unary,
},
/**
* GetDownloadURL returns a signed download URL given a forget operation ID.
*
* @generated from rpc v1.Backrest.GetDownloadURL
*/
getDownloadURL: {
name: "GetDownloadURL",
I: Int64Value,
O: StringValue,
kind: MethodKind.Unary,
},
/**
* Clears the history of operations
*
+7 -1
View File
@@ -196,7 +196,13 @@ export const OperationRow = ({
) : null}
{operation.status == OperationStatus.STATUS_SUCCESS ? (<>
<br />
<Button type="link" href={"/download/" + operation.id.toString(16) + '/'} target="_blank">Download File(s)</Button>
<Button type="link" onClick={() => {
backrestService.getDownloadURL({ value: operation.id }).then((resp) => {
window.open(resp.value, "_blank");
}).catch((e) => {
alertApi?.error("Failed to fetch download URL: " + e.message);
});
}}>Download File(s)</Button>
</>) : null}
</>
);