Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: new openInApp logic for WOPI #149

Merged
merged 1 commit into from
Jun 28, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 39 additions & 121 deletions pkg/app/provider/wopi/wopi.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@
package demo

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"

"github.com/cs3org/reva/pkg/app"
Expand All @@ -37,7 +33,6 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/app/provider/registry"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/cs3org/reva/pkg/user"
"github.com/mitchellh/mapstructure"
Expand All @@ -49,9 +44,13 @@ func init() {
}

type config struct {
IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."`
WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."`
WopiBridgeURL string `mapstructure:"wopi_bridge_url" docs:";The wopibridge's URL."`
IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."`
WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."`
MSOOURL string `mapstructure:"msoo_url" docs:";The MS Office Online URL."`
CodeURL string `mapstructure:"code_url" docs:";The Collabora Online URL."`
CodiMDURL string `mapstructure:"codimd_url" docs:";The CodiMD URL."`
CodiMDIntURL string `mapstructure:"codimd_int_url" docs:";The CodiMD internal URL."`
CodiMDApiKey string `mapstructure:"codimd_url" docs:";The CodiMD URL."`
}

func parseConfig(m map[string]interface{}) (*config, error) {
Expand All @@ -63,45 +62,8 @@ func parseConfig(m map[string]interface{}) (*config, error) {
}

type wopiProvider struct {
conf *config
client *http.Client
wopiBridgeClient *http.Client
}

func (p *wopiProvider) getWopiAppEndpoints(ctx context.Context) (map[string]interface{}, error) {
// TODO this query will eventually be served by Reva.
// For the time being it is a remnant of the CERNBox-specific WOPI server, which justifies the /cbox path in the URL.
wopiurl, err := url.Parse(p.conf.WopiURL)
if err != nil {
return nil, err
}
wopiurl.Path = path.Join(wopiurl.Path, "/wopi/cbox/endpoints")
appsReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
if err != nil {
return nil, err
}
appsRes, err := p.client.Do(appsReq)
if err != nil {
return nil, err
}
defer appsRes.Body.Close()
if appsRes.StatusCode != http.StatusOK {
return nil, errtypes.InternalError(fmt.Sprintf("Request to WOPI server returned %d", appsRes.StatusCode))
}
appsBody, err := ioutil.ReadAll(appsRes.Body)
if err != nil {
return nil, err
}

appsURLMap := make(map[string]interface{})
err = json.Unmarshal(appsBody, &appsURLMap)
if err != nil {
return nil, err
}

log := appctx.GetLogger(ctx)
log.Info().Msg(fmt.Sprintf("Successfully retrieved %d WOPI app endpoints", len(appsURLMap)))
return appsURLMap, nil
conf *config
wopiClient *http.Client
}

func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, app, token string) (string, error) {
Expand All @@ -111,7 +73,7 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
if err != nil {
return "", err
}
wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/open")
wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/openinapp")
httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
if err != nil {
return "", err
Expand All @@ -123,12 +85,30 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
q.Add("viewmode", viewMode.String())
// TODO the folder URL should be resolved as e.g. `'https://cernbox.cern.ch/index.php/apps/files/?dir=' + filepath.Dir(req.Ref.GetPath())`
// or should be deprecated/removed altogether, needs discussion and decision.
q.Add("folderurl", "undefined")
// q.Add("folderurl", "...")
u, ok := user.ContextGetUser(ctx)
if ok {
q.Add("username", u.Username)
}
// else defaults to "Anonymous Guest"
if app == "" {
// Default behavior: look for the default app for this file's mimetype
// XXX TODO
app = "Collabora Online"
}
q.Add("appname", app)
if app == "CodiMD" {
// This is served by the WOPI bridge extensions
q.Add("appediturl", p.conf.CodiMDURL)
if p.conf.CodiMDIntURL != "" {
q.Add("appinturl", p.conf.CodiMDIntURL)
}
httpReq.Header.Set("ApiKey", p.conf.CodiMDApiKey)
} else {
// TODO get AppRegistry
//q.Add("appediturl", AppRegistry.get(app).getEditUrl())
//q.Add("appviewurl", AppRegistry.get(app).getViewUrl())
}

if p.conf.IOPSecret == "" {
p.conf.IOPSecret = os.Getenv("REVA_APPPROVIDER_IOPSECRET")
Expand All @@ -138,100 +118,38 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
httpReq.Header.Set("TokenHeader", token)

httpReq.URL.RawQuery = q.Encode()

openRes, err := p.client.Do(httpReq)
openRes, err := p.wopiClient.Do(httpReq)
if err != nil {
return "", errors.Wrap(err, "wopi: error performing open request to WOPI server")
}
defer openRes.Body.Close()

if openRes.StatusCode != http.StatusOK {
if openRes.StatusCode != http.StatusFound {
return "", errors.Wrap(err, "wopi: error performing open request to WOPI server, status: "+openRes.Status)
}
appURL := openRes.Header.Get("Location")

buf := new(bytes.Buffer)
_, err = buf.ReadFrom(openRes.Body)
if err != nil {
return "", err
}
openResBody := buf.String()

var viewModeStr string
if viewMode == appprovider.OpenInAppRequest_VIEW_MODE_READ_WRITE {
viewModeStr = "edit"
} else {
viewModeStr = "view"
}

var appProviderURL string
if app == "" {
// Default behavior: work out the application URL to be used for this file
// TODO call this e.g. once a day or a week, and cache the content in a shared map protected by a multi-reader Lock
appsURLMap, err := p.getWopiAppEndpoints(ctx)
if err != nil {
return "", errors.Wrap(err, "wopi: getWopiAppEndpoints failed")
}
viewOptions := appsURLMap[path.Ext(resource.GetPath())]
viewOptionsMap, ok := viewOptions.(map[string]interface{})
if !ok {
return "", errtypes.InternalError("wopi: incorrect parsing of the App URLs map from the WOPI server")
}

appProviderURL = fmt.Sprintf("%v", viewOptionsMap[viewModeStr])
if strings.Contains(appProviderURL, "?") {
appProviderURL += "&"
} else {
appProviderURL += "?"
}
appProviderURL = fmt.Sprintf("%sWOPISrc=%s", appProviderURL, openResBody)
} else {
// User specified the application to use, generate the URL out of that
// TODO map the given req.App to the URL via config. For now assume it's a URL!
appProviderURL = fmt.Sprintf("%sWOPISrc=%s", app, openResBody)
}

// In case of applications served by the WOPI bridge, resolve the URL and go to the app
// Note that URL matching is performed via string matching, not via IP resolution: may need to fix this
if len(p.conf.WopiBridgeURL) > 0 && strings.Contains(appProviderURL, p.conf.WopiBridgeURL) {
bridgeReq, err := rhttp.NewRequest(ctx, "GET", appProviderURL, nil)
if err != nil {
return "", err
}
bridgeRes, err := p.wopiBridgeClient.Do(bridgeReq)
if err != nil {
return "", err
}
defer bridgeRes.Body.Close()
if bridgeRes.StatusCode != http.StatusFound {
return "", errtypes.InternalError(fmt.Sprintf("Request to WOPI bridge returned %d", bridgeRes.StatusCode))
}
appProviderURL = bridgeRes.Header.Get("Location")
}

log.Info().Msg(fmt.Sprintf("wopi: returning app provider URL %s", appProviderURL))
return appProviderURL, nil
log.Info().Msg(fmt.Sprintf("wopi: returning app URL %s", appURL))
return appURL, nil
}

// New returns an implementation to of the app.Provider interface that
// New returns an implementation of the app.Provider interface that
// connects to an application in the backend.
func New(m map[string]interface{}) (app.Provider, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}

wopiBridgeClient := rhttp.GetHTTPClient(
wopiClient := rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(5 * int64(time.Second))),
)
wopiBridgeClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
wopiClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}

return &wopiProvider{
conf: c,
client: rhttp.GetHTTPClient(
rhttp.Timeout(5 * time.Second),
),
wopiBridgeClient: wopiBridgeClient,
conf: c,
wopiClient: wopiClient,
}, nil
}