mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-23 22:29:59 -05:00
feat: Add new collaboration service (WOPI)
This commit is contained in:
1
Makefile
1
Makefile
@@ -29,6 +29,7 @@ OCIS_MODULES = \
|
||||
services/auth-machine \
|
||||
services/auth-service \
|
||||
services/clientlog \
|
||||
services/collaboration \
|
||||
services/eventhistory \
|
||||
services/frontend \
|
||||
services/gateway \
|
||||
|
||||
2
go.mod
2
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/MicahParks/keyfunc v1.9.0
|
||||
github.com/Nerzal/gocloak/v13 v13.9.0
|
||||
github.com/bbalet/stopwords v1.0.0
|
||||
github.com/beevik/etree v1.3.0
|
||||
github.com/blevesearch/bleve/v2 v2.4.0
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
@@ -133,7 +134,6 @@ require (
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.45.1 // indirect
|
||||
github.com/beevik/etree v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1031,6 +1031,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
|
||||
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
|
||||
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/config"
|
||||
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
|
||||
clientlog "github.com/owncloud/ocis/v2/services/clientlog/pkg/config"
|
||||
collaboration "github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/config"
|
||||
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/config"
|
||||
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/config"
|
||||
@@ -88,6 +89,7 @@ type Config struct {
|
||||
AuthMachine *authmachine.Config `yaml:"auth_machine"`
|
||||
AuthService *authservice.Config `yaml:"auth_service"`
|
||||
Clientlog *clientlog.Config `yaml:"clientlog"`
|
||||
Collaboration *collaboration.Config `yaml:"collaboration"`
|
||||
EventHistory *eventhistory.Config `yaml:"eventhistory"`
|
||||
Frontend *frontend.Config `yaml:"frontend"`
|
||||
Gateway *gateway.Config `yaml:"gateway"`
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/config/defaults"
|
||||
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/config/defaults"
|
||||
clientlog "github.com/owncloud/ocis/v2/services/clientlog/pkg/config/defaults"
|
||||
collaboration "github.com/owncloud/ocis/v2/services/collaboration/pkg/config/defaults"
|
||||
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/config/defaults"
|
||||
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/config/defaults"
|
||||
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/config/defaults"
|
||||
@@ -60,6 +61,7 @@ func DefaultConfig() *Config {
|
||||
AuthMachine: authmachine.DefaultConfig(),
|
||||
AuthService: authservice.DefaultConfig(),
|
||||
Clientlog: clientlog.DefaultConfig(),
|
||||
Collaboration: collaboration.DefaultConfig(),
|
||||
EventHistory: eventhistory.DefaultConfig(),
|
||||
Frontend: frontend.DefaultConfig(),
|
||||
Gateway: gateway.DefaultConfig(),
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/command"
|
||||
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/command"
|
||||
clientlog "github.com/owncloud/ocis/v2/services/clientlog/pkg/command"
|
||||
collaboration "github.com/owncloud/ocis/v2/services/collaboration/pkg/command"
|
||||
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/command"
|
||||
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/command"
|
||||
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/command"
|
||||
@@ -96,6 +97,11 @@ var svccmds = []register.Command{
|
||||
cfg.Clientlog.Commons = cfg.Commons
|
||||
})
|
||||
},
|
||||
func(cfg *config.Config) *cli.Command {
|
||||
return ServiceCommand(cfg, cfg.Collaboration.Service.Name, collaboration.GetCommands(cfg.Collaboration), func(c *config.Config) {
|
||||
cfg.Collaboration.Commons = cfg.Commons
|
||||
})
|
||||
},
|
||||
func(cfg *config.Config) *cli.Command {
|
||||
return ServiceCommand(cfg, cfg.EventHistory.Service.Name, eventhistory.GetCommands(cfg.EventHistory), func(c *config.Config) {
|
||||
cfg.EventHistory.Commons = cfg.Commons
|
||||
|
||||
37
services/collaboration/Makefile
Normal file
37
services/collaboration/Makefile
Normal file
@@ -0,0 +1,37 @@
|
||||
SHELL := bash
|
||||
NAME := collaboration
|
||||
|
||||
include ../../.make/recursion.mk
|
||||
|
||||
############ tooling ############
|
||||
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
include ../../.bingo/Variables.mk
|
||||
endif
|
||||
|
||||
############ go tooling ############
|
||||
include ../../.make/go.mk
|
||||
|
||||
############ release ############
|
||||
include ../../.make/release.mk
|
||||
|
||||
############ docs generate ############
|
||||
include ../../.make/docs.mk
|
||||
|
||||
.PHONY: docs-generate
|
||||
docs-generate: config-docs-generate
|
||||
|
||||
############ generate ############
|
||||
include ../../.make/generate.mk
|
||||
|
||||
.PHONY: ci-go-generate
|
||||
ci-go-generate: # CI runs ci-node-generate automatically before this target
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
############ licenses ############
|
||||
.PHONY: ci-node-check-licenses
|
||||
ci-node-check-licenses:
|
||||
|
||||
.PHONY: ci-node-save-licenses
|
||||
ci-node-save-licenses:
|
||||
29
services/collaboration/pkg/command/root.go
Normal file
29
services/collaboration/pkg/command/root.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
Server(cfg),
|
||||
//Health(cfg),
|
||||
//Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the antivirus command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "collaboration",
|
||||
Usage: "Serve WOPI for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
109
services/collaboration/pkg/command/server.go
Normal file
109
services/collaboration/pkg/command/server.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/cs3wopiserver"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/logging"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/grpc"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/http"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gr := run.Group{}
|
||||
ctx, cancel := func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
defer cancel()
|
||||
|
||||
app, err := cs3wopiserver.Start(cfg) // grpc server needs decoupling
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grpcServer, teardown, err := grpc.Server(
|
||||
grpc.App(app),
|
||||
grpc.Config(cfg),
|
||||
grpc.Logger(logger),
|
||||
)
|
||||
defer teardown()
|
||||
if err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Str("transport", "grpc").
|
||||
Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(func() error {
|
||||
l, err := net.Listen("tcp", cfg.GRPC.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grpcServer.Serve(l)
|
||||
return nil
|
||||
},
|
||||
func(_ error) {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("server", "grpc").
|
||||
Msg("shutting down server")
|
||||
cancel()
|
||||
})
|
||||
|
||||
/*
|
||||
server, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(server.ListenAndServe, func(_ error) {
|
||||
_ = server.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
*/
|
||||
server, err := http.Server(
|
||||
http.App(app),
|
||||
http.Logger(logger),
|
||||
http.Config(cfg),
|
||||
http.Context(ctx),
|
||||
http.TracerProvider(traceProvider),
|
||||
)
|
||||
gr.Add(server.Run, func(_ error) {
|
||||
cancel()
|
||||
})
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
9
services/collaboration/pkg/config/app.go
Normal file
9
services/collaboration/pkg/config/app.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// App defines the available app configuration.
|
||||
type App struct {
|
||||
Name string `yaml:"name" env:"COLLABORATION_APP_NAME" desc:"The name of the app"`
|
||||
Description string `yaml:"description" env:"COLLABORATION_APP_DESCRIPTION" desc:"App description"`
|
||||
Icon string `yaml:"icon" env:"COLLABORATION_APP_ICON" desc:"Icon for the app"`
|
||||
LockName string `yaml:"lockname" env:"COLLABORATION_APP_LOCKNAME" desc:"Name for the app lock"`
|
||||
}
|
||||
47
services/collaboration/pkg/config/config.go
Normal file
47
services/collaboration/pkg/config/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
App App `yaml:"app"`
|
||||
|
||||
Secret string `yaml:"secret" env:"COLLABORATION_SECRET" desc:"Used as JWT token and to encrypt access token."`
|
||||
|
||||
GRPC GRPC `yaml:"grpc"`
|
||||
HTTP HTTP `yaml:"http"`
|
||||
WopiApp WopiApp `yaml:"wopiapp"`
|
||||
CS3Api CS3Api `yaml:"cs3api"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Tracing defines the available tracing configuration. Not used at the moment
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;COLLABORATION_TRACING_ENABLED" desc:"Activates tracing."`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;COLLABORATION_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now."`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;COLLABORATION_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;COLLABORATION_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
|
||||
}
|
||||
|
||||
// Convert Tracing to the tracing package's Config struct.
|
||||
func (t Tracing) Convert() tracing.Config {
|
||||
return tracing.Config{
|
||||
Enabled: t.Enabled,
|
||||
Type: t.Type,
|
||||
Endpoint: t.Endpoint,
|
||||
Collector: t.Collector,
|
||||
}
|
||||
}
|
||||
15
services/collaboration/pkg/config/cs3api.go
Normal file
15
services/collaboration/pkg/config/cs3api.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
// WopiApp defines the available configuration in order to connect to a WOPI app.
|
||||
type CS3Api struct {
|
||||
Gateway Gateway `yaml:"gateway"`
|
||||
DataGateway DataGateway `yaml:"datagateway"`
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
Name string `yaml: "name" env:"COLLABORATION_CS3API_GATEWAY_NAME" desc:"service name of the CS3API gateway"`
|
||||
}
|
||||
|
||||
type DataGateway struct {
|
||||
Insecure bool `yaml:"insecure" env:"COLLABORATION_CS3API_DATAGATEWAY_INSECURE" desc:"connect to the CS3API data gateway insecurely"`
|
||||
}
|
||||
9
services/collaboration/pkg/config/debug.go
Normal file
9
services/collaboration/pkg/config/debug.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration. Not used at the moment
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"COLLABORATION_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
|
||||
Token string `yaml:"token" env:"COLLABORATION_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."`
|
||||
Pprof bool `yaml:"pprof" env:"COLLABORATION_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."`
|
||||
Zpages bool `yaml:"zpages" env:"COLLABORATION_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
|
||||
}
|
||||
83
services/collaboration/pkg/config/defaults/defaultconfig.go
Normal file
83
services/collaboration/pkg/config/defaults/defaultconfig.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
)
|
||||
|
||||
// FullDefaultConfig returns a fully initialized default configuration
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DefaultConfig returns a basic default configuration
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Service: config.Service{
|
||||
Name: "collaboration",
|
||||
},
|
||||
App: config.App{
|
||||
Name: "WOPI app",
|
||||
Description: "Open office documents with a WOPI app",
|
||||
Icon: "image-edit",
|
||||
LockName: "com.github.owncloud.collaboration",
|
||||
},
|
||||
Secret: uniuri.NewLen(32),
|
||||
GRPC: config.GRPC{
|
||||
Addr: "127.0.0.1:56778",
|
||||
Namespace: "com.owncloud.collaboration",
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:6789",
|
||||
Namespace: "com.owncloud.collaboration",
|
||||
//Scheme: "http",
|
||||
},
|
||||
WopiApp: config.WopiApp{
|
||||
Addr: "https://127.0.0.1:8080",
|
||||
Insecure: true, // TODO: this should have a secure default
|
||||
},
|
||||
CS3Api: config.CS3Api{
|
||||
Gateway: config.Gateway{
|
||||
Name: "com.owncloud.api.gateway",
|
||||
},
|
||||
DataGateway: config.DataGateway{
|
||||
Insecure: true, // TODO: this should have a secure default
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureDefaults adds default values to the configuration if they are not set yet
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for "envdecode".
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for "envdecode".
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitized the configuration
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
}
|
||||
7
services/collaboration/pkg/config/grpc.go
Normal file
7
services/collaboration/pkg/config/grpc.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// Service defines the available grpc configuration.
|
||||
type GRPC struct {
|
||||
Addr string `yaml:"addr" env:"COLLABORATION_GRPC_ADDR" desc:"The bind address of the GRPC service"`
|
||||
Namespace string `yaml:"-"`
|
||||
}
|
||||
14
services/collaboration/pkg/config/http.go
Normal file
14
services/collaboration/pkg/config/http.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"COLLABORATION_HTTP_ADDR" desc:"The address of the HTTP service."`
|
||||
BindAddr string `yaml:"bindaddr" env:"COLLABORATION_HTTP_BINDADDR" desc:"The bind address of the HTTP service."`
|
||||
Namespace string `yaml:"-"`
|
||||
Scheme string `yaml:"scheme" env:"COLLABORATION_HTTP_SCHEME" desc:"Either http or https"`
|
||||
TLS shared.HTTPServiceTLS `yaml:"tls"`
|
||||
}
|
||||
9
services/collaboration/pkg/config/log.go
Normal file
9
services/collaboration/pkg/config/log.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;COLLABORATION_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'."`
|
||||
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;COLLABORATION_LOG_PRETTY" desc:"Activates pretty log output."`
|
||||
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;COLLABORATION_LOG_COLOR" desc:"Activates colorized log output."`
|
||||
File string `yaml:"file" env:"OCIS_LOG_FILE;COLLABORATION_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
|
||||
}
|
||||
38
services/collaboration/pkg/config/parser/parse.go
Normal file
38
services/collaboration/pkg/config/parser/parse.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config/defaults"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func Validate(cfg *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
6
services/collaboration/pkg/config/service.go
Normal file
6
services/collaboration/pkg/config/service.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
7
services/collaboration/pkg/config/wopiapp.go
Normal file
7
services/collaboration/pkg/config/wopiapp.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// WopiApp defines the available configuration in order to connect to a WOPI app.
|
||||
type WopiApp struct {
|
||||
Addr string `yaml:"addr" env:"COLLABORATION_WOPIAPP_ADDR" desc:"The URL where the WOPI app is located, such as https://127.0.0.1:8080."`
|
||||
Insecure bool `yaml:"insecure" env:"COLLABORATION_WOPIAPP_INSECURE" desc:"Connect insecurely"`
|
||||
}
|
||||
42
services/collaboration/pkg/cs3wopiserver/start.go
Normal file
42
services/collaboration/pkg/cs3wopiserver/start.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cs3wopiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
|
||||
)
|
||||
|
||||
func Start(cfg *config.Config) (*app.DemoApp, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
app, err := app.New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := app.RegisterOcisService(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := app.WopiDiscovery(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := app.GetCS3apiClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := app.RegisterDemoApp(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// GRPC and HTTP server are started using the standard
|
||||
// `ocis collaboration server` command through the usual means
|
||||
|
||||
// TODO:
|
||||
// "app" initialization needs to be moved
|
||||
|
||||
return app, nil
|
||||
}
|
||||
109
services/collaboration/pkg/internal/app/app.go
Normal file
109
services/collaboration/pkg/internal/app/app.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config/defaults"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/logging"
|
||||
|
||||
registryv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/mime"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type DemoApp struct {
|
||||
gwc gatewayv1beta1.GatewayAPIClient
|
||||
grpcServer *grpc.Server
|
||||
|
||||
AppURLs map[string]map[string]string
|
||||
|
||||
Config *config.Config
|
||||
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) (*DemoApp, error) {
|
||||
app := &DemoApp{
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
err := envdecode.Decode(app)
|
||||
if err != nil {
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
app.Logger = logging.Configure("wopiserver", defaults.FullDefaultConfig().Log)
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *DemoApp) GetCS3apiClient() error {
|
||||
// establish a connection to the cs3 api endpoint
|
||||
// in this case a REVA gateway, started by oCIS
|
||||
gwc, err := pool.GetGatewayServiceClient(app.Config.CS3Api.Gateway.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.gwc = gwc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DemoApp) RegisterOcisService(ctx context.Context) error {
|
||||
svc := registry.BuildGRPCService(app.Config.Service.Name, uuid.Must(uuid.NewV4()).String(), app.Config.GRPC.Addr, "0.0.0")
|
||||
return registry.RegisterService(ctx, svc, app.Logger)
|
||||
}
|
||||
|
||||
func (app *DemoApp) RegisterDemoApp(ctx context.Context) error {
|
||||
mimeTypesMap := make(map[string]bool)
|
||||
for _, extensions := range app.AppURLs {
|
||||
for ext := range extensions {
|
||||
m := mime.Detect(false, ext)
|
||||
mimeTypesMap[m] = true
|
||||
}
|
||||
}
|
||||
|
||||
mimeTypes := make([]string, 0, len(mimeTypesMap))
|
||||
for m := range mimeTypesMap {
|
||||
mimeTypes = append(mimeTypes, m)
|
||||
}
|
||||
|
||||
// TODO: REVA has way to filter supported mimetypes (do we need to implement it here or is it in the registry?)
|
||||
|
||||
// TODO: an added app provider shouldn't last forever. Instead the registry should use a TTL
|
||||
// and delete providers that didn't register again. If an app provider dies or get's disconnected,
|
||||
// the users will be no longer available to choose to open a file with it (currently, opening a file just fails)
|
||||
req := ®istryv1beta1.AddAppProviderRequest{
|
||||
Provider: ®istryv1beta1.ProviderInfo{
|
||||
Name: app.Config.App.Name,
|
||||
Description: app.Config.App.Description,
|
||||
Icon: app.Config.App.Icon,
|
||||
Address: app.Config.Service.Name,
|
||||
MimeTypes: mimeTypes,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := app.gwc.AddAppProvider(ctx, req)
|
||||
if err != nil {
|
||||
app.Logger.Error().Err(err).Msg("AddAppProvider failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
app.Logger.Error().Str("status_code", resp.Status.Code.String()).Msg("AddAppProvider failed")
|
||||
return errors.New("status code != CODE_OK")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
services/collaboration/pkg/internal/app/claims.go
Normal file
8
services/collaboration/pkg/internal/app/claims.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package app
|
||||
|
||||
import "github.com/golang-jwt/jwt/v4"
|
||||
|
||||
type Claims struct {
|
||||
WopiContext WopiContext `json:"WopiContext"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
74
services/collaboration/pkg/internal/app/crypto.go
Normal file
74
services/collaboration/pkg/internal/app/crypto.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
func keyPadding(key []byte) []byte {
|
||||
switch length := len(key); {
|
||||
case length < 16:
|
||||
return append(key, make([]byte, 16-length)...)
|
||||
case length == 16:
|
||||
return key
|
||||
case length < 24:
|
||||
return append(key, make([]byte, 24-length)...)
|
||||
case length == 24:
|
||||
return key
|
||||
case length < 32:
|
||||
return append(key, make([]byte, 32-length)...)
|
||||
case length == 32:
|
||||
return key
|
||||
case length > 32:
|
||||
return key[:32]
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
func EncryptAES(key []byte, plainText string) (string, error) {
|
||||
src := []byte(plainText)
|
||||
|
||||
block, err := aes.NewCipher(keyPadding(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipherText := make([]byte, aes.BlockSize+len(src))
|
||||
iv := cipherText[:aes.BlockSize]
|
||||
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(cipherText[aes.BlockSize:], src)
|
||||
|
||||
return base64.URLEncoding.EncodeToString(cipherText), nil
|
||||
}
|
||||
|
||||
func DecryptAES(key []byte, securemess string) (string, error) {
|
||||
cipherText, err := base64.URLEncoding.DecodeString(securemess)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(keyPadding(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherText) < aes.BlockSize {
|
||||
return "", errors.New("ciphertext block size is too short")
|
||||
}
|
||||
|
||||
iv := cipherText[:aes.BlockSize]
|
||||
cipherText = cipherText[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(cipherText, cipherText)
|
||||
|
||||
return string(cipherText), nil
|
||||
}
|
||||
141
services/collaboration/pkg/internal/app/fileinfo.go
Normal file
141
services/collaboration/pkg/internal/app/fileinfo.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package app
|
||||
|
||||
type FileInfo struct {
|
||||
// ------------
|
||||
// Microsoft WOPI check file info specification:
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo
|
||||
// ------------
|
||||
|
||||
// The string name of the file, including extension, without a path. Used for display in user interface (UI), and determining the extension of the file.
|
||||
BaseFileName string `json:"BaseFileName,omitempty"`
|
||||
//A string that uniquely identifies the owner of the file. In most cases, the user who uploaded or created the file should be considered the owner.
|
||||
OwnerID string `json:"OwnerId,omitempty"`
|
||||
// A string value uniquely identifying the user currently accessing the file.
|
||||
UserID string `json:"UserId,omitempty"`
|
||||
// The size of the file in bytes, expressed as a long, a 64-bit signed integer.
|
||||
Size int64 `json:"Size,omitempty"`
|
||||
// The current version of the file based on the server’s file version schema, as a string. This value must change when the file changes, and version values must never repeat for a given file.
|
||||
Version string `json:"Version,omitempty"`
|
||||
// A string that is the name of the user, suitable for displaying in UI.
|
||||
UserFriendlyName string `json:"UserFriendlyName,omitempty"`
|
||||
// A 256 bit SHA-2-encoded [FIPS 180-2] hash of the file contents, as a Base64-encoded string. Used for caching purposes in WOPI clients.
|
||||
SHA256 string `json:"SHA256,omitempty"`
|
||||
// A string value representing the file extension for the file. This value must begin with a .. If provided, WOPI clients will use this value as the file extension. Otherwise the extension will be parsed from the BaseFileName.
|
||||
FileExtension string `json:"FileExtension,omitempty"`
|
||||
// An integer value that indicates the maximum length for file names that the WOPI host supports, excluding the file extension. The default value is 250. Note that WOPI clients will use this default value if the property is omitted or if it is explicitly set to 0.
|
||||
FileNameMaxLength int `json:"FileNameMaxLength,omitempty"`
|
||||
// A string that represents the last time that the file was modified. This time must always be a must be a UTC time, and must be formatted in ISO 8601 round-trip format. For example, "2009-06-15T13:45:30.0000000Z".
|
||||
LastModifiedTime string `json:"LastModifiedTime,omitempty"`
|
||||
// A string value containing information about the user. This string can be passed from a WOPI client to the host by means of a PutUserInfo operation. If the host has a UserInfo string for the user, they must include it in this property. See the PutUserInfo documentation for more details.
|
||||
UserInfo string `json:"UserInfo,omitempty"`
|
||||
// A Boolean value that indicates that, for this user, the file cannot be changed.
|
||||
ReadOnly bool `json:"ReadOnly"`
|
||||
// A Boolean value that indicates that if host is temporarily unable to process writes on a file
|
||||
TemporarilyNotWritable bool `json:"TemporarilyNotWritable,omitempty"`
|
||||
// A string value indicating whether the current document is shared with other users. The value can change upon adding or removing permissions to other users. Clients should use this value to help decide when to enable collaboration features as a document must be Shared in order to multi-user collaboration on the document.
|
||||
SharingStatus string `json:"SharingStatus,omitempty"`
|
||||
// A Boolean value that indicates that the WOPI client should restrict what actions the user can perform on the file. The behavior of this property is dependent on the WOPI client.
|
||||
RestrictedWebViewOnly bool `json:"RestrictedWebViewOnly"`
|
||||
// A Boolean value that indicates that the user has permission to view a broadcast of this file.
|
||||
UserCanAttend bool `json:"UserCanAttend"`
|
||||
// A Boolean value that indicates that the user has permission to broadcast this file to a set of users who have permission to broadcast or view a broadcast of the current file.
|
||||
UserCanPresent bool `json:"UserCanPresent"`
|
||||
// A Boolean value that indicates the user does not have sufficient permission to create new files on the WOPI server. Setting this to true tells the WOPI client that calls to PutRelativeFile will fail for this user on the current file.
|
||||
UserCanNotWriteRelative bool `json:"UserCanNotWriteRelative"`
|
||||
// A Boolean value that indicates the user has permission to rename the current file.
|
||||
UserCanRename bool `json:"UserCanRename"`
|
||||
// A Boolean value that indicates that the user has permission to alter the file. Setting this to true tells the WOPI client that it can call PutFile on behalf of the user.
|
||||
UserCanWrite bool `json:"UserCanWrite"`
|
||||
// A Boolean value that indicates the WOPI client should close the window or tab when the user activates any Close UI in the WOPI client.
|
||||
CloseButtonClosesWindow bool `json:"CloseButtonClosesWindow"`
|
||||
// A string value indicating whether the WOPI client should disable Copy and Paste functionality within the application. The default is to permit all Copy and Paste functionality, i.e. the setting has no effect.
|
||||
CopyPasteRestrictions string `json:"CopyPasteRestrictions,omitempty"`
|
||||
// A Boolean value that indicates the WOPI client should disable all print functionality.
|
||||
DisablePrint bool `json:"DisablePrint"`
|
||||
// A Boolean value that indicates the WOPI client should disable all machine translation functionality.
|
||||
DisableTranslation bool `json:"DisableTranslation"`
|
||||
// A URI to a web page that the WOPI client should navigate to when the application closes, or in the event of an unrecoverable error.
|
||||
CloseUrl string `json:"CloseUrl,omitempty"`
|
||||
// A user-accessible URI to the file intended to allow the user to download a copy of the file.
|
||||
DownloadUrl string `json:"DownloadUrl,omitempty"`
|
||||
// A URI to a location that allows the user to create an embeddable URI to the file.
|
||||
FileEmbedCommandUrl string `json:"FileEmbedCommandUrl,omitempty"`
|
||||
// A URI to a location that allows the user to share the file.
|
||||
FileSharingUrl string `json:"FileSharingUrl,omitempty"`
|
||||
// A URI to the file location that the WOPI client uses to get the file. If this is provided, the WOPI client may use this URI to get the file instead of a GetFile request. A host might set this property if it is easier or provides better performance to serve files from a different domain than the one handling standard WOPI requests. WOPI clients must not add or remove parameters from the URL; no other parameters, including the access token, should be appended to the FileUrl before it is used.
|
||||
FileUrl string `json:"FileUrl,omitempty"`
|
||||
// A URI to a location that allows the user to view the version history for the file.
|
||||
FileVersionUrl string `json:"FileVersionUrl,omitempty"`
|
||||
// A URI to a host page that loads the edit WOPI action.
|
||||
HostEditUrl string `json:"HostEditUrl,omitempty"`
|
||||
// A URI to a web page that provides access to a viewing experience for the file that can be embedded in another HTML page. This is typically a URI to a host page that loads the embedview WOPI action.
|
||||
HostEmbeddedViewUrl string `json:"HostEmbeddedViewUrl,omitempty"`
|
||||
// A URI to a host page that loads the view WOPI action. This URL is used by Office Online to navigate between view and edit mode.
|
||||
HostViewUrl string `json:"HostViewUrl,omitempty"`
|
||||
// A URI that will sign the current user out of the host’s authentication system.
|
||||
SignoutUrl string `json:"SignoutUrl,omitempty"`
|
||||
// A string that indicates the brand name of the host.
|
||||
BreadcrumbBrandName string `json:"BreadcrumbBrandName,omitempty"`
|
||||
// A URI to a web page that the WOPI client should navigate to when the user clicks on UI that displays BreadcrumbBrandName.
|
||||
BreadcrumbBrandUrl string `json:"BreadcrumbBrandUrl,omitempty"`
|
||||
// A string that indicates the name of the file. If this is not provided, WOPI clients may use the BaseFileName value.
|
||||
BreadcrumbDocName string `json:"BreadcrumbDocName,omitempty"`
|
||||
// A string that indicates the name of the container that contains the file.
|
||||
BreadcrumbFolderName string `json:"BreadcrumbFolderName,omitempty"`
|
||||
// A URI to a web page that the WOPI client should navigate to when the user clicks on UI that displays BreadcrumbFolderName.
|
||||
BreadcrumbFolderUrl string `json:"BreadcrumbFolderUrl,omitempty"`
|
||||
// A Boolean value that indicates a WOPI client may connect to Microsoft services to provide end-user functionality.
|
||||
AllowAdditionalMicrosoftServices bool `json:"AllowAdditionalMicrosoftServices"`
|
||||
// A Boolean value that indicates that in the event of an error, the WOPI client is permitted to prompt the user for permission to collect a detailed report about their specific error. The information gathered could include the user’s file and other session-specific state.
|
||||
AllowErrorReportPrompt bool `json:"AllowErrorReportPrompt"`
|
||||
// A Boolean value that indicates a WOPI client may allow connections to external services referenced in the file (for example, a marketplace of embeddable JavaScript apps).
|
||||
AllowExternalMarketplace bool `json:"AllowExternalMarketplace"`
|
||||
// A string value offering guidance to the WOPI client as to how to differentiate client throttling behaviors between the user and documents combinations from the WOPI host.
|
||||
ClientThrottlingProtection string `json:"ClientThrottlingProtection,omitempty"`
|
||||
// A string value indicating whether the WOPI host is experiencing capacity problems and would like to reduce the frequency at which the WOPI clients make calls to the host
|
||||
RequestedCallThrottling string `json:"RequestedCallThrottling,omitempty"`
|
||||
// An array of strings containing the Share URL types supported by the host.
|
||||
SupportedShareUrlTypes []string `json:"SupportedShareUrlTypes,omitempty"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: ExecuteCellStorageRequest, ExecuteCellStorageRelativeRequest
|
||||
SupportsCobalt bool `json:"SupportsCobalt"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: CheckContainerInfo, CreateChildContainer, CreateChildFile, DeleteContainer, DeleteFile, EnumerateAncestors (containers), EnumerateAncestors (files), EnumerateChildren (containers), GetEcosystem (containers), RenameContainer
|
||||
SupportsContainers bool `json:"SupportsContainers"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: CheckEcosystem, GetEcosystem (containers), GetEcosystem (files), GetRootContainer (ecosystem)
|
||||
SupportsEcosystem bool `json:"SupportsEcosystem"`
|
||||
// A Boolean value that indicates that the host supports lock IDs up to 1024 ASCII characters long. If not provided, WOPI clients will assume that lock IDs are limited to 256 ASCII characters.
|
||||
SupportsExtendedLockLength bool `json:"SupportsExtendedLockLength"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: CheckFolderInfo, EnumerateChildren (folders), DeleteFile
|
||||
SupportsFolders bool `json:"SupportsFolders"`
|
||||
// A Boolean value that indicates that the host supports the GetFileWopiSrc (ecosystem) operation.
|
||||
SupportsGetFileWopiSrc bool `json:"SupportsGetFileWopiSrc"`
|
||||
// A Boolean value that indicates that the host supports the GetLock operation.
|
||||
SupportsGetLock bool `json:"SupportsGetLock"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: Lock, Unlock, RefreshLock, UnlockAndRelock operations for this file.
|
||||
SupportsLocks bool `json:"SupportsLocks"`
|
||||
// A Boolean value that indicates that the host supports the RenameFile operation.
|
||||
SupportsRename bool `json:"SupportsRename"`
|
||||
// A Boolean value that indicates that the host supports the following WOPI operations: PutFile, PutRelativeFile
|
||||
SupportsUpdate bool `json:"SupportsUpdate"` // whether "Putfile" and "PutRelativeFile" work
|
||||
// A Boolean value that indicates that the host supports the DeleteFile operation.
|
||||
SupportsDeleteFile bool `json:"SupportsDeleteFile"`
|
||||
// A Boolean value that indicates that the host supports the PutUserInfo operation.
|
||||
SupportsUserInfo bool `json:"SupportsUserInfo"`
|
||||
// A Boolean value indicating whether the user is authenticated with the host or not. Hosts should always set this to true for unauthenticated users, so that clients are aware that the user is anonymous. When setting this to true, hosts can choose to omit the UserId property, but must still set the OwnerId property.
|
||||
IsAnonymousUser bool `json:"IsAnonymousUser,omitempty"`
|
||||
// A Boolean value indicating whether the user is an education user or not.
|
||||
IsEduUser bool `json:"IsEduUser,omitempty"`
|
||||
// A Boolean value indicating whether the user is a business user or not.
|
||||
LicenseCheckForEditIsEnabled bool `json:"LicenseCheckForEditIsEnabled,omitempty"`
|
||||
|
||||
// ------------
|
||||
// Collabora WOPI check file info specification:
|
||||
// https://sdk.collaboraonline.com/docs/advanced_integration.html
|
||||
// ------------
|
||||
|
||||
// If set to true, it allows the document owner (the one with OwnerId =UserId) to send a closedocument message (see protocol.txt)
|
||||
EnableOwnerTermination bool `json:"EnableOwnerTermination,omitempty"`
|
||||
// Disables export functionality in backend. If set to true, HideExportOption is assumed to be true
|
||||
DisableExport bool `json:"DisableExport,omitempty"`
|
||||
// Disables copying from the document in libreoffice online backend. Pasting into the document would still be possible. However, it is still possible to do an “internal” cut/copy/paste.
|
||||
DisableCopy bool `json:"DisableCopy,omitempty"`
|
||||
}
|
||||
5
services/collaboration/pkg/internal/app/headers.go
Normal file
5
services/collaboration/pkg/internal/app/headers.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package app
|
||||
|
||||
const (
|
||||
HeaderWopiLock string = "X-WOPI-Lock"
|
||||
)
|
||||
86
services/collaboration/pkg/internal/app/wopicontext.go
Normal file
86
services/collaboration/pkg/internal/app/wopicontext.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
wopiContextKey key = iota
|
||||
)
|
||||
|
||||
type WopiContext struct {
|
||||
AccessToken string
|
||||
FileReference providerv1beta1.Reference
|
||||
User *userv1beta1.User
|
||||
ViewMode appproviderv1beta1.ViewMode
|
||||
EditAppUrl string
|
||||
ViewAppUrl string
|
||||
}
|
||||
|
||||
func WopiContextAuthMiddleware(app *DemoApp, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := r.URL.Query().Get("access_token")
|
||||
if accessToken == "" {
|
||||
fmt.Println("wopicontext", "accesstoken empty")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
_, err := jwt.ParseWithClaims(accessToken, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(app.Config.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("wopicontext", err)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := claims.Valid(); err != nil {
|
||||
fmt.Println("wopicontext", err)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
wopiContextAccessToken, err := DecryptAES([]byte(app.Config.Secret), claims.WopiContext.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Println("wopicontext", err)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
claims.WopiContext.AccessToken = wopiContextAccessToken
|
||||
|
||||
ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext)
|
||||
// authentication for the CS3 api
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func WopiContextFromCtx(ctx context.Context) (WopiContext, error) {
|
||||
if wopiContext, ok := ctx.Value(wopiContextKey).(WopiContext); ok {
|
||||
return wopiContext, nil
|
||||
}
|
||||
return WopiContext{}, errors.New("no wopi context found")
|
||||
}
|
||||
123
services/collaboration/pkg/internal/app/wopidiscovery.go
Normal file
123
services/collaboration/pkg/internal/app/wopidiscovery.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (app *DemoApp) WopiDiscovery(ctx context.Context) error {
|
||||
res, err := getAppURLs(app.Config.WopiApp.Addr, app.Config.WopiApp.Insecure, app.Logger)
|
||||
if err != nil {
|
||||
// logging is already covered inside the `getAppURLs` function
|
||||
return err
|
||||
}
|
||||
|
||||
app.AppURLs = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAppURLs(wopiAppUrl string, insecure bool, logger log.Logger) (map[string]map[string]string, error) {
|
||||
|
||||
wopiAppUrl = wopiAppUrl + "/hosting/discovery"
|
||||
|
||||
httpClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
httpResp, err := httpClient.Get(wopiAppUrl)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("WopiAppUrl", wopiAppUrl).
|
||||
Msg("WopiDiscovery: failed to access wopi app url")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
logger.Error().
|
||||
Str("WopiAppUrl", wopiAppUrl).
|
||||
Int("HttpCode", httpResp.StatusCode).
|
||||
Msg("WopiDiscovery: wopi app url failed with unexpected code")
|
||||
return nil, errors.New("status code was not 200")
|
||||
}
|
||||
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
var appURLs map[string]map[string]string
|
||||
|
||||
appURLs, err = parseWopiDiscovery(httpResp.Body)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("WopiAppUrl", wopiAppUrl).
|
||||
Msg("WopiDiscovery: failed to parse wopi discovery response")
|
||||
return nil, errors.Wrap(err, "error parsing wopi discovery response")
|
||||
}
|
||||
|
||||
// TODO: Log appUrls? not easy with the format
|
||||
// It's also a one-time call during service setup, so it's pointless
|
||||
// to use an "all-is-good" debug log
|
||||
return appURLs, nil
|
||||
}
|
||||
|
||||
func parseWopiDiscovery(body io.Reader) (map[string]map[string]string, error) {
|
||||
appURLs := make(map[string]map[string]string)
|
||||
|
||||
doc := etree.NewDocument()
|
||||
if _, err := doc.ReadFrom(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root := doc.SelectElement("wopi-discovery")
|
||||
|
||||
for _, netzone := range root.SelectElements("net-zone") {
|
||||
|
||||
if strings.Contains(netzone.SelectAttrValue("name", ""), "external") {
|
||||
for _, app := range netzone.SelectElements("app") {
|
||||
for _, action := range app.SelectElements("action") {
|
||||
access := action.SelectAttrValue("name", "")
|
||||
if access == "view" || access == "edit" {
|
||||
ext := action.SelectAttrValue("ext", "")
|
||||
urlString := action.SelectAttrValue("urlsrc", "")
|
||||
|
||||
if ext == "" || urlString == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// remove any malformed query parameter from discovery urls
|
||||
q := u.Query()
|
||||
for k := range q {
|
||||
if strings.Contains(k, "<") || strings.Contains(k, ">") {
|
||||
q.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
if _, ok := appURLs[access]; !ok {
|
||||
appURLs[access] = make(map[string]string)
|
||||
}
|
||||
appURLs[access]["."+ext] = u.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return appURLs, nil
|
||||
}
|
||||
97
services/collaboration/pkg/internal/app/wopifilecontents.go
Normal file
97
services/collaboration/pkg/internal/app/wopifilecontents.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/helpers"
|
||||
)
|
||||
|
||||
// GetFile downloads the file from the storage
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getfile
|
||||
func GetFile(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
// download the file
|
||||
resp, err := helpers.DownloadFile(
|
||||
ctx,
|
||||
&wopiContext.FileReference,
|
||||
app.gwc,
|
||||
wopiContext.AccessToken,
|
||||
app.Config.CS3Api.DataGateway.Insecure,
|
||||
app.Logger,
|
||||
)
|
||||
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Int("HttpCode", resp.StatusCode).
|
||||
Msg("GetFile: downloading the file failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// read the file from the body
|
||||
defer resp.Body.Close()
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("GetFile: copying the file content to the response body failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("GetFile: success")
|
||||
http.Error(w, "", http.StatusOK)
|
||||
}
|
||||
|
||||
// PutFile uploads the file to the storage
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putfile
|
||||
func PutFile(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
// read the file from the body
|
||||
defer r.Body.Close()
|
||||
|
||||
// upload the file
|
||||
err := helpers.UploadFile(
|
||||
ctx,
|
||||
r.Body,
|
||||
&wopiContext.FileReference,
|
||||
app.gwc,
|
||||
wopiContext.AccessToken,
|
||||
r.Header.Get(HeaderWopiLock),
|
||||
app.Config.CS3Api.DataGateway.Insecure,
|
||||
app.Logger,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("PutFile: uploading the file failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("PutFile: success")
|
||||
http.Error(w, "", http.StatusOK)
|
||||
}
|
||||
136
services/collaboration/pkg/internal/app/wopiinfo.go
Normal file
136
services/collaboration/pkg/internal/app/wopiinfo.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func WopiInfoHandler(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
// Logs for this endpoint will be covered by the access log. We can't extract
|
||||
// more info
|
||||
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
||||
}
|
||||
|
||||
// CheckFileInfo returns information about the requested file and capabilities of the wopi server
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo
|
||||
func CheckFileInfo(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
statRes, err := app.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: &wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("CheckFileInfo: stat failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("StatusCode", statRes.Status.Code.String()).
|
||||
Str("StatusMsg", statRes.Status.Message).
|
||||
Msg("CheckFileInfo: stat failed with unexpected status")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fileInfo := FileInfo{
|
||||
OwnerID: statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp,
|
||||
Size: int64(statRes.Info.Size),
|
||||
Version: statRes.Info.Mtime.String(),
|
||||
BaseFileName: path.Base(statRes.Info.Path),
|
||||
BreadcrumbDocName: path.Base(statRes.Info.Path),
|
||||
// to get the folder we actually need to do a GetPath() request
|
||||
//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
|
||||
|
||||
UserCanNotWriteRelative: true,
|
||||
|
||||
HostViewUrl: wopiContext.ViewAppUrl,
|
||||
HostEditUrl: wopiContext.EditAppUrl,
|
||||
|
||||
EnableOwnerTermination: true,
|
||||
|
||||
SupportsExtendedLockLength: true,
|
||||
|
||||
SupportsGetLock: true,
|
||||
SupportsLocks: true,
|
||||
}
|
||||
|
||||
switch wopiContext.ViewMode {
|
||||
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE:
|
||||
fileInfo.SupportsUpdate = true
|
||||
fileInfo.UserCanWrite = true
|
||||
|
||||
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_ONLY:
|
||||
// nothing special to do here for now
|
||||
|
||||
case appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY:
|
||||
fileInfo.DisableExport = true
|
||||
fileInfo.DisableCopy = true
|
||||
fileInfo.DisablePrint = true
|
||||
}
|
||||
|
||||
// user logic from reva wopi driver #TODO: refactor
|
||||
var isPublicShare bool = false
|
||||
if wopiContext.User != nil {
|
||||
if wopiContext.User.Id.Type == userv1beta1.UserType_USER_TYPE_LIGHTWEIGHT {
|
||||
fileInfo.UserID = statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp
|
||||
} else {
|
||||
fileInfo.UserID = wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp
|
||||
}
|
||||
|
||||
if wopiContext.User.Opaque != nil {
|
||||
if _, ok := wopiContext.User.Opaque.Map["public-share-role"]; ok {
|
||||
isPublicShare = true
|
||||
}
|
||||
}
|
||||
if !isPublicShare {
|
||||
fileInfo.UserFriendlyName = wopiContext.User.Username
|
||||
fileInfo.UserID = wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp
|
||||
}
|
||||
}
|
||||
if wopiContext.User == nil || isPublicShare {
|
||||
randomID, _ := uuid.NewUUID()
|
||||
fileInfo.UserID = "guest-" + randomID.String()
|
||||
fileInfo.UserFriendlyName = "Guest " + randomID.String()
|
||||
fileInfo.IsAnonymousUser = true
|
||||
}
|
||||
|
||||
jsonFileInfo, err := json.Marshal(fileInfo)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("CheckFileInfo: failed to marshal fileinfo")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("CheckFileInfo: success")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonFileInfo)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
280
services/collaboration/pkg/internal/app/wopilocking.go
Normal file
280
services/collaboration/pkg/internal/app/wopilocking.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
// WOPI Locks generally have a lock duration of 30 minutes and will be refreshed before expiration if needed
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#lock
|
||||
lockDuration time.Duration = 30 * time.Minute
|
||||
)
|
||||
|
||||
// GetLock returns a lock or an empty string if no lock exists
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getlock
|
||||
func GetLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
req := &providerv1beta1.GetLockRequest{
|
||||
Ref: &wopiContext.FileReference,
|
||||
}
|
||||
|
||||
resp, err := app.gwc.GetLock(ctx, req)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("GetLock failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("GetLock failed with unexpected status")
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
lockID := ""
|
||||
if resp.Lock != nil {
|
||||
lockID = resp.Lock.LockId
|
||||
}
|
||||
|
||||
// log the success at debug level
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("LockID", lockID).
|
||||
Msg("GetLock success")
|
||||
|
||||
w.Header().Set(HeaderWopiLock, lockID)
|
||||
http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
// Lock returns a WOPI lock or performs an unlock and relock
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/lock
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlockandrelock
|
||||
func Lock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
// TODO: handle un- and relock
|
||||
|
||||
lockID := r.Header.Get(HeaderWopiLock)
|
||||
if lockID == "" {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("Lock failed due to empty lockID")
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req := &providerv1beta1.SetLockRequest{
|
||||
Ref: &wopiContext.FileReference,
|
||||
Lock: &providerv1beta1.Lock{
|
||||
LockId: lockID,
|
||||
AppName: app.Config.App.LockName,
|
||||
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
|
||||
Expiration: &typesv1beta1.Timestamp{
|
||||
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("Performing SetLock")
|
||||
|
||||
resp, err := app.gwc.SetLock(ctx, req)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("SetLock failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch resp.Status.Code {
|
||||
case rpcv1beta1.Code_CODE_OK:
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("SetLock successful")
|
||||
http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
return
|
||||
|
||||
case rpcv1beta1.Code_CODE_FAILED_PRECONDITION:
|
||||
// already locked
|
||||
req := &providerv1beta1.GetLockRequest{
|
||||
Ref: &wopiContext.FileReference,
|
||||
}
|
||||
|
||||
resp, err := app.gwc.GetLock(ctx, req)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("SetLock failed, fallback to GetLock failed too")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("SetLock failed, fallback to GetLock failed with unexpected status")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if resp.Lock != nil {
|
||||
if resp.Lock.LockId != lockID {
|
||||
app.Logger.Warn().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("LockID", resp.Lock.LockId).
|
||||
Msg("SetLock conflict")
|
||||
w.Header().Set(HeaderWopiLock, resp.Lock.LockId)
|
||||
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: according to the spec we need to treat this as a RefreshLock
|
||||
|
||||
app.Logger.Warn().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("LockID", resp.Lock.LockId).
|
||||
Msg("SetLock lock refreshed instead")
|
||||
http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("SetLock failed and could not refresh")
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
|
||||
default:
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("SetLock failed with unexpected status")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// RefreshLock refreshes a provided lock for 30 minutes
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock
|
||||
func RefreshLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: implement
|
||||
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// UnLock removes a given lock from a file
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlock
|
||||
func UnLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
wopiContext, _ := WopiContextFromCtx(ctx)
|
||||
|
||||
lockID := r.Header.Get(HeaderWopiLock)
|
||||
if lockID == "" {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Msg("Unlock failed due to empty lockID")
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req := &providerv1beta1.UnlockRequest{
|
||||
Ref: &wopiContext.FileReference,
|
||||
Lock: &providerv1beta1.Lock{
|
||||
LockId: lockID,
|
||||
AppName: app.Config.App.LockName,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := app.gwc.Unlock(ctx, req)
|
||||
if err != nil {
|
||||
app.Logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("Unlock failed")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
app.Logger.Error().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("Unlock failed with unexpected status")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Logger.Debug().
|
||||
Str("FileReference", wopiContext.FileReference.String()).
|
||||
Str("ViewMode", wopiContext.ViewMode.String()).
|
||||
Str("Requester", wopiContext.User.GetId().String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("Unlock successful")
|
||||
http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
106
services/collaboration/pkg/internal/helpers/download.go
Normal file
106
services/collaboration/pkg/internal/helpers/download.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
func DownloadFile(
|
||||
ctx context.Context,
|
||||
ref *providerv1beta1.Reference,
|
||||
gwc gatewayv1beta1.GatewayAPIClient,
|
||||
token string,
|
||||
insecure bool,
|
||||
logger log.Logger,
|
||||
) (http.Response, error) {
|
||||
|
||||
req := &providerv1beta1.InitiateFileDownloadRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
|
||||
resp, err := gwc.InitiateFileDownload(ctx, req)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Msg("DownloadHelper: InitiateFileDownload failed")
|
||||
return http.Response{}, err
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("FileReference", ref.String()).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("DownloadHelper: InitiateFileDownload failed with wrong status")
|
||||
return http.Response{}, errors.New("InitiateFileDownload failed with status " + resp.Status.Code.String())
|
||||
}
|
||||
|
||||
downloadEndpoint := ""
|
||||
downloadToken := ""
|
||||
hasDownloadToken := false
|
||||
|
||||
for _, proto := range resp.Protocols {
|
||||
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
|
||||
downloadEndpoint = proto.DownloadEndpoint
|
||||
downloadToken = proto.Token
|
||||
hasDownloadToken = proto.Token != ""
|
||||
}
|
||||
}
|
||||
|
||||
if downloadEndpoint == "" {
|
||||
logger.Error().
|
||||
Str("FileReference", ref.String()).
|
||||
Str("Endpoint", downloadEndpoint).
|
||||
Bool("HasDownloadToken", hasDownloadToken).
|
||||
Msg("DownloadHelper: Download endpoint or token is missing")
|
||||
return http.Response{}, errors.New("download endpoint is missing")
|
||||
}
|
||||
|
||||
httpClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadEndpoint, bytes.NewReader([]byte("")))
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Str("Endpoint", downloadEndpoint).
|
||||
Bool("HasDownloadToken", hasDownloadToken).
|
||||
Msg("DownloadHelper: Could not create the request to the endpoint")
|
||||
return http.Response{}, err
|
||||
}
|
||||
if downloadToken != "" {
|
||||
// public link downloads have the token in the download endpoint
|
||||
httpReq.Header.Add("X-Reva-Transfer", downloadToken)
|
||||
}
|
||||
// TODO: the access token shouldn't be needed
|
||||
httpReq.Header.Add("x-access-token", token)
|
||||
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Str("Endpoint", downloadEndpoint).
|
||||
Bool("HasDownloadToken", hasDownloadToken).
|
||||
Msg("DownloadHelper: Get request to the download endpoint failed")
|
||||
return http.Response{}, err
|
||||
}
|
||||
|
||||
return *httpResp, nil
|
||||
}
|
||||
127
services/collaboration/pkg/internal/helpers/upload.go
Normal file
127
services/collaboration/pkg/internal/helpers/upload.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
func UploadFile(ctx context.Context, content io.ReadCloser, ref *providerv1beta1.Reference, gwc gatewayv1beta1.GatewayAPIClient, token string, lockID string, insecure bool, logger log.Logger) error {
|
||||
|
||||
req := &providerv1beta1.InitiateFileUploadRequest{
|
||||
Ref: ref,
|
||||
LockId: lockID,
|
||||
// TODO: if-match
|
||||
//Options: &providerv1beta1.InitiateFileUploadRequest_IfMatch{
|
||||
// IfMatch: "",
|
||||
//},
|
||||
}
|
||||
|
||||
resp, err := gwc.InitiateFileUpload(ctx, req)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Msg("UploadHelper: InitiateFileUpload failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("StatusCode", resp.Status.Code.String()).
|
||||
Str("StatusMsg", resp.Status.Message).
|
||||
Msg("UploadHelper: InitiateFileUpload failed with wrong status")
|
||||
return errors.New("InitiateFileUpload failed with status " + resp.Status.Code.String())
|
||||
}
|
||||
|
||||
uploadEndpoint := ""
|
||||
uploadToken := ""
|
||||
hasUploadToken := false
|
||||
|
||||
for _, proto := range resp.Protocols {
|
||||
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
|
||||
uploadEndpoint = proto.UploadEndpoint
|
||||
uploadToken = proto.Token
|
||||
hasUploadToken = proto.Token != ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if uploadEndpoint == "" {
|
||||
logger.Error().
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("Endpoint", uploadEndpoint).
|
||||
Bool("HasUploadToken", hasUploadToken).
|
||||
Msg("UploadHelper: Upload endpoint or token is missing")
|
||||
return errors.New("upload endpoint or token is missing")
|
||||
}
|
||||
|
||||
httpClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadEndpoint, content)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("Endpoint", uploadEndpoint).
|
||||
Bool("HasUploadToken", hasUploadToken).
|
||||
Msg("UploadHelper: Could not create the request to the endpoint")
|
||||
return err
|
||||
}
|
||||
|
||||
if uploadToken != "" {
|
||||
// public link uploads have the token in the upload endpoint
|
||||
httpReq.Header.Add("X-Reva-Transfer", uploadToken)
|
||||
}
|
||||
// TODO: the access token shouldn't be needed
|
||||
httpReq.Header.Add("x-access-token", token)
|
||||
|
||||
// TODO: better mechanism for the upload while locked, relies on patch in REVA
|
||||
//if lockID, ok := ctxpkg.ContextGetLockID(ctx); ok {
|
||||
// httpReq.Header.Add("X-Lock-Id", lockID)
|
||||
//}
|
||||
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("Endpoint", uploadEndpoint).
|
||||
Bool("HasUploadToken", hasUploadToken).
|
||||
Msg("UploadHelper: Put request to the upload endpoint failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
logger.Error().
|
||||
Str("FileReference", ref.String()).
|
||||
Str("RequestedLockID", lockID).
|
||||
Str("Endpoint", uploadEndpoint).
|
||||
Bool("HasUploadToken", hasUploadToken).
|
||||
Int("HttpCode", httpResp.StatusCode).
|
||||
Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
|
||||
return errors.New("Put request failed with status " + strconv.Itoa(httpResp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
services/collaboration/pkg/internal/logging/logging.go
Normal file
17
services/collaboration/pkg/internal/logging/logging.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
32
services/collaboration/pkg/internal/middleware/accesslog.go
Normal file
32
services/collaboration/pkg/internal/middleware/accesslog.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// AccessLog is a middleware to log http requests at info level logging.
|
||||
func AccessLog(logger log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
wrap := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
next.ServeHTTP(wrap, r)
|
||||
|
||||
logger.Info().
|
||||
Str("proto", r.Proto).
|
||||
Str("request", chimiddleware.GetReqID(r.Context())).
|
||||
Str("remote-addr", r.RemoteAddr).
|
||||
Str("method", r.Method).
|
||||
Int("status", wrap.Status()).
|
||||
Str("path", r.URL.Path).
|
||||
Dur("duration", time.Since(start)).
|
||||
Int("bytes", wrap.BytesWritten()).
|
||||
Msg("access-log")
|
||||
})
|
||||
}
|
||||
}
|
||||
76
services/collaboration/pkg/server/grpc/option.go
Normal file
76
services/collaboration/pkg/server/grpc/option.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
App *app.DemoApp
|
||||
Name string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
TraceProvider trace.TracerProvider
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// App provides a function to set the logger option.
|
||||
func App(val *app.DemoApp) Option {
|
||||
return func(o *Options) {
|
||||
o.App = val
|
||||
}
|
||||
}
|
||||
|
||||
// Name provides a name for the service.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the trace provider option.
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TraceProvider = val
|
||||
}
|
||||
}
|
||||
32
services/collaboration/pkg/server/grpc/server.go
Normal file
32
services/collaboration/pkg/server/grpc/server.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
svc "github.com/owncloud/ocis/v2/services/collaboration/pkg/service/grpc/v0"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Server initializes a new grpc service ready to run
|
||||
// THIS SERVICE IS REGISTERED AGAINST REVA, NOT GO-MICRO
|
||||
func Server(opts ...Option) (*grpc.Server, func(), error) {
|
||||
grpcOpts := []grpc.ServerOption{}
|
||||
options := newOptions(opts...)
|
||||
grpcServer := grpc.NewServer(grpcOpts...)
|
||||
|
||||
handle, teardown, err := svc.NewHandler(
|
||||
svc.Config(options.Config),
|
||||
svc.Logger(options.Logger),
|
||||
svc.AppURLs(options.App.AppURLs),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing collaboration service")
|
||||
return grpcServer, teardown, err
|
||||
}
|
||||
|
||||
// register the app provider interface / OpenInApp call
|
||||
appproviderv1beta1.RegisterProviderAPIServer(grpcServer, handle)
|
||||
|
||||
return grpcServer, teardown, nil
|
||||
}
|
||||
68
services/collaboration/pkg/server/http/option.go
Normal file
68
services/collaboration/pkg/server/http/option.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
App *app.DemoApp
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
TracerProvider trace.TracerProvider
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// App provides a function to set the logger option.
|
||||
func App(val *app.DemoApp) Option {
|
||||
return func(o *Options) {
|
||||
o.App = val
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// TracerProvider provides a function to set the TracerProvider option
|
||||
func TracerProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TracerProvider = val
|
||||
}
|
||||
}
|
||||
157
services/collaboration/pkg/server/http/server.go
Normal file
157
services/collaboration/pkg/server/http/server.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
stdhttp "net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/account"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
|
||||
"github.com/riandyrn/otelchi"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service, err := http.NewService(
|
||||
http.TLSConfig(options.Config.HTTP.TLS),
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.GetString()),
|
||||
http.Address(options.Config.HTTP.BindAddr),
|
||||
http.Context(options.Context),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing http service")
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
|
||||
chimiddleware.RequestID,
|
||||
middleware.Version(
|
||||
"userlog",
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
middleware.ExtractAccountUUID(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.Secret), // previously, secret came from Config.TokenManager.JWTSecret
|
||||
),
|
||||
/*
|
||||
// Need CORS? not in the original server
|
||||
// Also, CORS isn't part of the config right now
|
||||
middleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
),
|
||||
*/
|
||||
middleware.Secure,
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
mux.Use(middlewares...)
|
||||
|
||||
mux.Use(
|
||||
otelchi.Middleware(
|
||||
"collaboration",
|
||||
otelchi.WithChiRoutes(mux),
|
||||
otelchi.WithTracerProvider(options.TracerProvider),
|
||||
otelchi.WithPropagators(tracing.GetPropagator()),
|
||||
),
|
||||
)
|
||||
|
||||
prepareRoutes(mux, options.App)
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), mux); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func prepareRoutes(r *chi.Mux, demoapp *app.DemoApp) {
|
||||
r.Route("/wopi", func(r chi.Router) {
|
||||
|
||||
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
app.WopiInfoHandler(demoapp, w, r)
|
||||
})
|
||||
|
||||
r.Route("/files/{fileid}", func(r chi.Router) {
|
||||
|
||||
r.Use(func(h stdhttp.Handler) stdhttp.Handler {
|
||||
// authentication and wopi context
|
||||
return app.WopiContextAuthMiddleware(demoapp, h)
|
||||
},
|
||||
)
|
||||
|
||||
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
app.CheckFileInfo(demoapp, w, r)
|
||||
})
|
||||
|
||||
r.Post("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
action := r.Header.Get("X-WOPI-Override")
|
||||
switch action {
|
||||
|
||||
case "LOCK":
|
||||
app.Lock(demoapp, w, r)
|
||||
case "GET_LOCK":
|
||||
app.GetLock(demoapp, w, r)
|
||||
case "REFRESH_LOCK":
|
||||
app.RefreshLock(demoapp, w, r)
|
||||
case "UNLOCK":
|
||||
app.UnLock(demoapp, w, r)
|
||||
|
||||
case "PUT_USER_INFO":
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putuserinfo
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusNotImplemented), stdhttp.StatusNotImplemented)
|
||||
case "PUT_RELATIVE":
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putrelativefile
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusNotImplemented), stdhttp.StatusNotImplemented)
|
||||
case "RENAME_FILE":
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/renamefile
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusNotImplemented), stdhttp.StatusNotImplemented)
|
||||
case "DELETE":
|
||||
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/deletefile
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusNotImplemented), stdhttp.StatusNotImplemented)
|
||||
|
||||
default:
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusInternalServerError), stdhttp.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
r.Route("/contents", func(r chi.Router) {
|
||||
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
app.GetFile(demoapp, w, r)
|
||||
})
|
||||
|
||||
r.Post("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
action := r.Header.Get("X-WOPI-Override")
|
||||
switch action {
|
||||
|
||||
case "PUT":
|
||||
app.PutFile(demoapp, w, r)
|
||||
|
||||
default:
|
||||
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusInternalServerError), stdhttp.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
47
services/collaboration/pkg/service/grpc/v0/option.go
Normal file
47
services/collaboration/pkg/service/grpc/v0/option.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
AppURLs map[string]map[string]string
|
||||
}
|
||||
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the Logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the Config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// ViewUrl provides a function to set the ViewUrl option.
|
||||
func AppURLs(val map[string]map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.AppURLs = val
|
||||
}
|
||||
}
|
||||
227
services/collaboration/pkg/service/grpc/v0/service.go
Normal file
227
services/collaboration/pkg/service/grpc/v0/service.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
|
||||
)
|
||||
|
||||
func NewHandler(opts ...Option) (*Service, func(), error) {
|
||||
teardown := func() {}
|
||||
options := newOptions(opts...)
|
||||
|
||||
gwc, err := pool.GetGatewayServiceClient(options.Config.CS3Api.Gateway.Name)
|
||||
if err != nil {
|
||||
return nil, teardown, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
id: options.Config.GRPC.Namespace + "." + options.Config.Service.Name,
|
||||
appURLs: options.AppURLs,
|
||||
logger: options.Logger,
|
||||
config: options.Config,
|
||||
gwc: gwc,
|
||||
}, teardown, nil
|
||||
}
|
||||
|
||||
// Service implements the searchServiceHandler interface
|
||||
type Service struct {
|
||||
id string
|
||||
appURLs map[string]map[string]string
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
gwc gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
func (s *Service) OpenInApp(
|
||||
ctx context.Context,
|
||||
req *appproviderv1beta1.OpenInAppRequest,
|
||||
) (*appproviderv1beta1.OpenInAppResponse, error) {
|
||||
|
||||
// get the current user
|
||||
var user *userv1beta1.User = nil
|
||||
meReq := &gatewayv1beta1.WhoAmIRequest{
|
||||
Token: req.AccessToken,
|
||||
}
|
||||
meResp, err := s.gwc.WhoAmI(ctx, meReq)
|
||||
if err == nil {
|
||||
if meResp.Status.Code == rpcv1beta1.Code_CODE_OK {
|
||||
user = meResp.User
|
||||
}
|
||||
}
|
||||
|
||||
// required for the response, it will be used also for logs
|
||||
providerFileRef := providerv1beta1.Reference{
|
||||
ResourceId: req.GetResourceInfo().GetId(),
|
||||
Path: ".",
|
||||
}
|
||||
|
||||
// build a urlsafe and stable file reference that can be used for proxy routing,
|
||||
// so that all sessions on one file end on the same office server
|
||||
|
||||
c := sha256.New()
|
||||
c.Write([]byte(req.ResourceInfo.Id.StorageId + "$" + req.ResourceInfo.Id.SpaceId + "!" + req.ResourceInfo.Id.OpaqueId))
|
||||
fileRef := hex.EncodeToString(c.Sum(nil))
|
||||
|
||||
// get the file extension to use the right wopi app url
|
||||
fileExt := path.Ext(req.GetResourceInfo().Path)
|
||||
|
||||
var viewAppURL string
|
||||
var editAppURL string
|
||||
if viewAppURLs, ok := s.appURLs["view"]; ok {
|
||||
if url := viewAppURLs[fileExt]; ok {
|
||||
viewAppURL = url
|
||||
}
|
||||
}
|
||||
if editAppURLs, ok := s.appURLs["edit"]; ok {
|
||||
if url, ok := editAppURLs[fileExt]; ok {
|
||||
editAppURL = url
|
||||
}
|
||||
}
|
||||
|
||||
if editAppURL == "" {
|
||||
// assuming that an view action is always available in the /hosting/discovery manifest
|
||||
// eg. Collabora does support viewing jpgs but no editing
|
||||
// eg. OnlyOffice does support viewing pdfs but no editing
|
||||
// there is no known case of supporting edit only without view
|
||||
editAppURL = viewAppURL
|
||||
}
|
||||
|
||||
wopiSrcURL := url.URL{
|
||||
Scheme: s.config.HTTP.Scheme,
|
||||
Host: s.config.HTTP.Addr,
|
||||
Path: path.Join("wopi", "files", fileRef),
|
||||
}
|
||||
|
||||
addWopiSrcQueryParam := func(baseURL string) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Add("WOPISrc", wopiSrcURL.String())
|
||||
qs := q.Encode()
|
||||
u.RawQuery = qs
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
viewAppURL, err = addWopiSrcQueryParam(viewAppURL)
|
||||
if err != nil {
|
||||
s.logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: error parsing viewAppUrl")
|
||||
return nil, err
|
||||
}
|
||||
editAppURL, err = addWopiSrcQueryParam(editAppURL)
|
||||
if err != nil {
|
||||
s.logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: error parsing editAppUrl")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appURL := viewAppURL
|
||||
if req.ViewMode == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
|
||||
appURL = editAppURL
|
||||
}
|
||||
|
||||
cryptedReqAccessToken, err := app.EncryptAES([]byte(s.config.Secret), req.AccessToken)
|
||||
if err != nil {
|
||||
s.logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: error encrypting access token")
|
||||
return &appproviderv1beta1.OpenInAppResponse{
|
||||
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
|
||||
}, err
|
||||
}
|
||||
|
||||
wopiContext := app.WopiContext{
|
||||
AccessToken: cryptedReqAccessToken,
|
||||
FileReference: providerFileRef,
|
||||
User: user,
|
||||
ViewMode: req.ViewMode,
|
||||
EditAppUrl: editAppURL,
|
||||
ViewAppUrl: viewAppURL,
|
||||
}
|
||||
|
||||
cs3Claims := &jwt.StandardClaims{}
|
||||
cs3JWTparser := jwt.Parser{}
|
||||
_, _, err = cs3JWTparser.ParseUnverified(req.AccessToken, cs3Claims)
|
||||
if err != nil {
|
||||
s.logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: error parsing JWT token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims := &app.Claims{
|
||||
WopiContext: wopiContext,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: cs3Claims.ExpiresAt,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
accessToken, err := token.SignedString([]byte(s.config.Secret))
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error().
|
||||
Err(err).
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: error signing access token")
|
||||
return &appproviderv1beta1.OpenInAppResponse{
|
||||
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
|
||||
}, err
|
||||
}
|
||||
|
||||
s.logger.Debug().
|
||||
Str("FileReference", providerFileRef.String()).
|
||||
Str("ViewMode", req.ViewMode.String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Msg("OpenInApp: success")
|
||||
|
||||
return &appproviderv1beta1.OpenInAppResponse{
|
||||
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
|
||||
AppUrl: &appproviderv1beta1.OpenInAppURL{
|
||||
AppUrl: appURL,
|
||||
Method: "POST",
|
||||
FormParameters: map[string]string{
|
||||
// these parameters will be passed to the web server by the app provider application
|
||||
"access_token": accessToken,
|
||||
// milliseconds since Jan 1, 1970 UTC as required in https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#access_token_ttl
|
||||
"access_token_ttl": strconv.FormatInt(claims.ExpiresAt*1000, 10),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
121
vendor/github.com/dchest/uniuri/COPYING
generated
vendored
Normal file
121
vendor/github.com/dchest/uniuri/COPYING
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
95
vendor/github.com/dchest/uniuri/README.md
generated
vendored
Normal file
95
vendor/github.com/dchest/uniuri/README.md
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
Package uniuri
|
||||
=====================
|
||||
|
||||
```go
|
||||
import "github.com/dchest/uniuri"
|
||||
```
|
||||
|
||||
Package uniuri generates random strings good for use in URIs to identify
|
||||
unique objects.
|
||||
|
||||
Example usage:
|
||||
|
||||
```go
|
||||
s := uniuri.New() // s is now "apHCJBl7L1OmC57n"
|
||||
```
|
||||
|
||||
A standard string created by New() is 16 bytes in length and consists of
|
||||
Latin upper and lowercase letters, and numbers (from the set of 62 allowed
|
||||
characters), which means that it has ~95 bits of entropy. To get more
|
||||
entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving
|
||||
~119 bits of entropy, or any other desired length.
|
||||
|
||||
Functions read from crypto/rand random source, and panic if they fail to
|
||||
read from it.
|
||||
|
||||
|
||||
Constants
|
||||
---------
|
||||
|
||||
```go
|
||||
const (
|
||||
// StdLen is a standard length of uniuri string to achive ~95 bits of entropy.
|
||||
StdLen = 16
|
||||
// UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest
|
||||
// to what can be losslessly converted to UUIDv4 (122 bits).
|
||||
UUIDLen = 20
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
Variables
|
||||
---------
|
||||
|
||||
```go
|
||||
var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
```
|
||||
|
||||
|
||||
StdChars is a set of standard characters allowed in uniuri string.
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
### func New
|
||||
|
||||
```go
|
||||
func New() string
|
||||
```
|
||||
|
||||
New returns a new random string of the standard length, consisting of
|
||||
standard characters.
|
||||
|
||||
### func NewLen
|
||||
|
||||
```go
|
||||
func NewLen(length int) string
|
||||
```
|
||||
|
||||
NewLen returns a new random string of the provided length, consisting of
|
||||
standard characters.
|
||||
|
||||
### func NewLenChars
|
||||
|
||||
```go
|
||||
func NewLenChars(length int, chars []byte) string
|
||||
```
|
||||
|
||||
NewLenChars returns a new random string of the provided length, consisting
|
||||
of the provided byte slice of allowed characters (maximum 256).
|
||||
|
||||
|
||||
|
||||
Public domain dedication
|
||||
------------------------
|
||||
|
||||
Written in 2011-2014 by Dmitry Chestnykh
|
||||
|
||||
The author(s) have dedicated all copyright and related and
|
||||
neighboring rights to this software to the public domain
|
||||
worldwide. Distributed without any warranty.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
120
vendor/github.com/dchest/uniuri/uniuri.go
generated
vendored
Normal file
120
vendor/github.com/dchest/uniuri/uniuri.go
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
// Written in 2011-2014 by Dmitry Chestnykh
|
||||
//
|
||||
// The author(s) have dedicated all copyright and related and
|
||||
// neighboring rights to this software to the public domain
|
||||
// worldwide. Distributed without any warranty.
|
||||
// http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
// Package uniuri generates random strings good for use in URIs to identify
|
||||
// unique objects.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// s := uniuri.New() // s is now "apHCJBl7L1OmC57n"
|
||||
//
|
||||
// A standard string created by New() is 16 bytes in length and consists of
|
||||
// Latin upper and lowercase letters, and numbers (from the set of 62 allowed
|
||||
// characters), which means that it has ~95 bits of entropy. To get more
|
||||
// entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving
|
||||
// ~119 bits of entropy, or any other desired length.
|
||||
//
|
||||
// Functions read from crypto/rand random source, and panic if they fail to
|
||||
// read from it.
|
||||
package uniuri
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
// StdLen is a standard length of uniuri string to achive ~95 bits of entropy.
|
||||
StdLen = 16
|
||||
// UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest
|
||||
// to what can be losslessly converted to UUIDv4 (122 bits).
|
||||
UUIDLen = 20
|
||||
)
|
||||
|
||||
// StdChars is a set of standard characters allowed in uniuri string.
|
||||
var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
// New returns a new random string of the standard length, consisting of
|
||||
// standard characters.
|
||||
func New() string {
|
||||
return NewLenChars(StdLen, StdChars)
|
||||
}
|
||||
|
||||
// NewLen returns a new random string of the provided length, consisting of
|
||||
// standard characters.
|
||||
func NewLen(length int) string {
|
||||
return NewLenChars(length, StdChars)
|
||||
}
|
||||
|
||||
// maxBufLen is the maximum length of a temporary buffer for random bytes.
|
||||
const maxBufLen = 2048
|
||||
|
||||
// minRegenBufLen is the minimum length of temporary buffer for random bytes
|
||||
// to fill after the first rand.Read request didn't produce the full result.
|
||||
// If the initial buffer is smaller, this value is ignored.
|
||||
// Rationale: for performance, assume it's pointless to request fewer bytes from rand.Read.
|
||||
const minRegenBufLen = 16
|
||||
|
||||
// estimatedBufLen returns the estimated number of random bytes to request
|
||||
// given that byte values greater than maxByte will be rejected.
|
||||
func estimatedBufLen(need, maxByte int) int {
|
||||
return int(math.Ceil(float64(need) * (255 / float64(maxByte))))
|
||||
}
|
||||
|
||||
// NewLenCharsBytes returns a new random byte slice of the provided length, consisting
|
||||
// of the provided byte slice of allowed characters (maximum 256).
|
||||
func NewLenCharsBytes(length int, chars []byte) []byte {
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
clen := len(chars)
|
||||
if clen < 2 || clen > 256 {
|
||||
panic("uniuri: wrong charset length for NewLenChars")
|
||||
}
|
||||
maxrb := 255 - (256 % clen)
|
||||
buflen := estimatedBufLen(length, maxrb)
|
||||
if buflen < length {
|
||||
buflen = length
|
||||
}
|
||||
if buflen > maxBufLen {
|
||||
buflen = maxBufLen
|
||||
}
|
||||
buf := make([]byte, buflen) // storage for random bytes
|
||||
out := make([]byte, length) // storage for result
|
||||
i := 0
|
||||
for {
|
||||
if _, err := rand.Read(buf[:buflen]); err != nil {
|
||||
panic("uniuri: error reading random bytes: " + err.Error())
|
||||
}
|
||||
for _, rb := range buf[:buflen] {
|
||||
c := int(rb)
|
||||
if c > maxrb {
|
||||
// Skip this number to avoid modulo bias.
|
||||
continue
|
||||
}
|
||||
out[i] = chars[c%clen]
|
||||
i++
|
||||
if i == length {
|
||||
return out
|
||||
}
|
||||
}
|
||||
// Adjust new requested length, but no smaller than minRegenBufLen.
|
||||
buflen = estimatedBufLen(length-i, maxrb)
|
||||
if buflen < minRegenBufLen && minRegenBufLen < cap(buf) {
|
||||
buflen = minRegenBufLen
|
||||
}
|
||||
if buflen > maxBufLen {
|
||||
buflen = maxBufLen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewLenChars returns a new random string of the provided length, consisting
|
||||
// of the provided byte slice of allowed characters (maximum 256).
|
||||
func NewLenChars(length int, chars []byte) string {
|
||||
return string(NewLenCharsBytes(length, chars))
|
||||
}
|
||||
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@@ -737,6 +737,9 @@ github.com/cyphar/filepath-securejoin
|
||||
# github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
## explicit
|
||||
github.com/davecgh/go-spew/spew
|
||||
# github.com/dchest/uniuri v1.2.0
|
||||
## explicit; go 1.19
|
||||
github.com/dchest/uniuri
|
||||
# github.com/deckarep/golang-set v1.8.0
|
||||
## explicit; go 1.17
|
||||
github.com/deckarep/golang-set
|
||||
|
||||
Reference in New Issue
Block a user