Skip to content

Commit

Permalink
feat: office 365 proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
micbar committed Aug 5, 2024
1 parent 38c1473 commit 7768e86
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 106 deletions.
3 changes: 2 additions & 1 deletion services/collaboration/pkg/config/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ type App struct {
Addr string `yaml:"addr" env:"COLLABORATION_APP_ADDR" desc:"The URL where the WOPI app is located, such as https://127.0.0.1:8080." introductionVersion:"6.0.0"`
Insecure bool `yaml:"insecure" env:"COLLABORATION_APP_INSECURE" desc:"Skip TLS certificate verification when connecting to the WOPI app" introductionVersion:"6.0.0"`

ProofKeys ProofKeys `yaml:"proofkeys"`
ProofKeys ProofKeys `yaml:"proofkeys"`
LicenseCheckEnable bool `yaml:"licensecheckenable" env:"COLLABORATION_APP_LICENSE_CHECK_ENABLE" desc:"Enable license check for edit" introductionVersion:"%%NEXT%%"`
}

type ProofKeys struct {
Expand Down
2 changes: 2 additions & 0 deletions services/collaboration/pkg/config/wopi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ type Wopi struct {
WopiSrc string `yaml:"wopisrc" env:"COLLABORATION_WOPI_SRC" desc:"The WOPISrc base URL containing schema, host and port. Set this to the schema and domain where the collaboration service is reachable for the wopi app, such as https://office.owncloud.test." introductionVersion:"6.0.0"`
Secret string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"`
DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." introductionVersion:"%%NEXT%%"`
ProxyURL string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy." introductionVersion:"%%NEXT%%"`
ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"The secret to authenticate against the ownCloud Office365 WOPI proxy." introductionVersion:"%%NEXT%%"`
}
66 changes: 48 additions & 18 deletions services/collaboration/pkg/connector/contentconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
"github.com/rs/zerolog"
)
Expand All @@ -27,12 +28,12 @@ import (
// Target file is within the WOPI context
type ContentConnectorService interface {
// GetFile downloads the file and write its contents in the provider writer
GetFile(ctx context.Context, writer io.Writer) error
GetFile(ctx context.Context, w http.ResponseWriter) error
// PutFile uploads the stream up to the stream length. The file should be
// locked beforehand, so the lockID needs to be provided.
// The current lockID will be returned ONLY if a conflict happens (the file is
// locked with a different lockID)
PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error)
PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (*types.Timestamp, string, error)
}

// ContentConnector implements the "File contents" endpoint.
Expand Down Expand Up @@ -61,7 +62,7 @@ func NewContentConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config
//
// The contents of the file will be written directly into the writer passed as
// parameter.
func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error {
func (c *ContentConnector) GetFile(ctx context.Context, w http.ResponseWriter) error {
wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil {
return err
Expand All @@ -72,6 +73,16 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
Logger()
logger.Debug().Msg("GetFile: start")

sResp, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: wopiContext.FileReference,
})
if err != nil {
logger.Error().Err(err).Msg("GetFile: Stat Request failed")
return err
}
if sResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
return NewConnectorError(500, sResp.GetStatus().GetCode().String()+" "+sResp.GetStatus().GetMessage())
}
// Initiate download request
req := &providerv1beta1.InitiateFileDownloadRequest{
Ref: wopiContext.FileReference,
Expand Down Expand Up @@ -164,13 +175,14 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
return NewConnectorError(500, "GetFile: Downloading the file failed")
}

helpers.SetVersionHeader(w, sResp.GetInfo().GetMtime())

// Copy the download into the writer
_, err = io.Copy(writer, httpResp.Body)
_, err = io.Copy(w, httpResp.Body)
if err != nil {
logger.Error().Msg("GetFile: copying the file content to the response body failed")
return err
}

logger.Debug().Msg("GetFile: success")
return nil
}
Expand All @@ -195,10 +207,10 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
// lock ID that should be used in the X-WOPI-Lock header. In other error
// cases or if the method is successful, an empty string will be returned
// (check for err != nil to know if something went wrong)
func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error) {
func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (*types.Timestamp, string, error) {
wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil {
return "", err
return nil, "", err
}

logger := zerolog.Ctx(ctx).With().
Expand All @@ -215,24 +227,25 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
})
if err != nil {
logger.Error().Err(err).Msg("PutFile: stat failed")
return "", err
return nil, "", err
}

if statRes.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK && statRes.GetStatus().GetCode() != rpcv1beta1.Code_CODE_NOT_FOUND {
logger.Error().
Str("StatusCode", statRes.GetStatus().GetCode().String()).
Str("StatusMsg", statRes.GetStatus().GetMessage()).
Msg("PutFile: stat failed with unexpected status")
return "", NewConnectorError(500, statRes.GetStatus().GetCode().String()+" "+statRes.GetStatus().GetMessage())
return nil, "", NewConnectorError(500, statRes.GetStatus().GetCode().String()+" "+statRes.GetStatus().GetMessage())
}

mtime := statRes.GetInfo().GetMtime()
// If there is a lock and it mismatches, return 409
if statRes.GetInfo().GetLock() != nil && statRes.GetInfo().GetLock().GetLockId() != lockID {
logger.Error().
Str("LockID", statRes.GetInfo().GetLock().GetLockId()).
Msg("PutFile: wrong lock")
// onlyoffice says it's required to send the current lockId, MS doesn't say anything
return statRes.GetInfo().GetLock().GetLockId(), NewConnectorError(409, "Wrong lock")
return nil, statRes.GetInfo().GetLock().GetLockId(), NewConnectorError(409, "Wrong lock")
}

// only unlocked uploads can go through if the target file is empty,
Expand All @@ -242,7 +255,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
if lockID == "" && statRes.GetInfo().GetLock() == nil && statRes.GetInfo().GetSize() > 0 {
logger.Error().Msg("PutFile: file must be locked first")
// onlyoffice says to send an empty string if the file is unlocked, MS doesn't say anything
return "", NewConnectorError(409, "File must be locked first")
return nil, "", NewConnectorError(409, "File must be locked first")
}

// Prepare the data to initiate the upload
Expand Down Expand Up @@ -270,15 +283,15 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
resp, err := c.gwc.InitiateFileUpload(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("UploadHelper: InitiateFileUpload failed")
return "", err
return nil, "", err
}

if resp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.GetStatus().GetCode().String()).
Str("StatusMsg", resp.GetStatus().GetMessage()).
Msg("UploadHelper: InitiateFileUpload failed with wrong status")
return "", NewConnectorError(500, resp.GetStatus().GetCode().String()+" "+resp.GetStatus().GetMessage())
return nil, "", NewConnectorError(500, resp.GetStatus().GetCode().String()+" "+resp.GetStatus().GetMessage())
}

// if the content length is greater than 0, we need to upload the content to the
Expand All @@ -303,7 +316,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Upload endpoint or token is missing")
return "", NewConnectorError(500, "upload endpoint or token is missing")
return nil, "", NewConnectorError(500, "upload endpoint or token is missing")
}

httpClient := http.Client{
Expand All @@ -323,7 +336,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Could not create the request to the endpoint")
return "", err
return nil, "", err
}
// "stream" is an *http.body and doesn't fill the httpReq.ContentLength automatically
// we need to fill the ContentLength ourselves, and must match the stream length in order
Expand All @@ -349,7 +362,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Put request to the upload endpoint failed")
return "", err
return nil, "", err
}
defer httpResp.Body.Close()

Expand All @@ -359,10 +372,27 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
Bool("HasUploadToken", hasUploadToken).
Int("HttpCode", httpResp.StatusCode).
Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
return "", NewConnectorError(500, "PutFile: Uploading the file failed")
return nil, "", NewConnectorError(500, "PutFile: Uploading the file failed")
}
// We need a stat call on the target file after the upload to get the
// new mtime
statResAfter, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: wopiContext.FileReference,
})
if err != nil {
logger.Error().Err(err).Msg("PutFile: stat after upload failed")
return nil, "", err
}
if statResAfter.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", statRes.GetStatus().GetCode().String()).
Str("StatusMsg", statRes.GetStatus().GetMessage()).
Msg("PutFile: stat after upload failed with unexpected status")
return nil, "", NewConnectorError(500, statResAfter.GetStatus().GetCode().String()+" "+statResAfter.GetStatus().GetMessage())
}
mtime = statResAfter.GetInfo().GetMtime()
}

logger.Debug().Msg("PutFile: success")
return "", nil
return mtime, "", nil
}
Loading

0 comments on commit 7768e86

Please sign in to comment.