From 3bc0b4ca000209766620ee29fd94814cfcc5d1e8 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte <39946305+gmgigi96@users.noreply.github.com> Date: Wed, 13 Jul 2022 18:14:25 +0200 Subject: [PATCH] Allow http service to expose prefixes containing slashes (/) (#3070) --- .../unreleased/ext-http-service-prefix.md | 3 + pkg/rhttp/rhttp.go | 75 ++++++++++++++++--- pkg/rhttp/rhttp_test.go | 74 ++++++++++++++++++ 3 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 changelog/unreleased/ext-http-service-prefix.md create mode 100644 pkg/rhttp/rhttp_test.go diff --git a/changelog/unreleased/ext-http-service-prefix.md b/changelog/unreleased/ext-http-service-prefix.md new file mode 100644 index 0000000000..6555c1fb35 --- /dev/null +++ b/changelog/unreleased/ext-http-service-prefix.md @@ -0,0 +1,3 @@ +Enhancement: Allow http service to expose prefixes containing / + +https://github.com/cs3org/reva/pull/3070 diff --git a/pkg/rhttp/rhttp.go b/pkg/rhttp/rhttp.go index 4ed8297c60..ba852572f1 100644 --- a/pkg/rhttp/rhttp.go +++ b/pkg/rhttp/rhttp.go @@ -25,6 +25,7 @@ import ( "net/http" "path" "sort" + "strings" "time" "github.com/cs3org/reva/internal/http/interceptors/appctx" @@ -32,7 +33,6 @@ import ( "github.com/cs3org/reva/internal/http/interceptors/log" "github.com/cs3org/reva/internal/http/interceptors/providerauthorizer" "github.com/cs3org/reva/pkg/rhttp/global" - "github.com/cs3org/reva/pkg/rhttp/router" rtrace "github.com/cs3org/reva/pkg/trace" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -234,25 +234,80 @@ func getUnprotected(prefix string, unprotected []string) []string { return unprotected } +// clean the url putting a slash (/) at the beginning if it does not have it +// and removing the slashes at the end +// if the url is "/", the output is "" +func cleanURL(url string) string { + if len(url) > 0 { + if url[0] != '/' { + url = "/" + url + } + url = strings.TrimRight(url, "/") + } + return url +} + +func urlHasPrefix(url, prefix string) bool { + url = cleanURL(url) + prefix = cleanURL(prefix) + + partsURL := strings.Split(url, "/") + partsPrefix := strings.Split(prefix, "/") + + if len(partsPrefix) > len(partsURL) { + return false + } + + for i, p := range partsPrefix { + u := partsURL[i] + if p != u { + return false + } + } + + return true +} + +func (s *Server) getHandlerLongestCommongURL(url string) (http.Handler, string, bool) { + var match string + + for k := range s.handlers { + if urlHasPrefix(url, k) && len(k) > len(match) { + match = k + } + } + + h, ok := s.handlers[match] + return h, match, ok +} + +func getSubURL(url, prefix string) string { + // pre cond: prefix is a prefix for url + // example: url = "/api/v0/", prefix = "/api", res = "/v0" + url = cleanURL(url) + prefix = cleanURL(prefix) + + return url[len(prefix):] +} + func (s *Server) getHandler() (http.Handler, error) { h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - head, tail := router.ShiftPath(r.URL.Path) - if h, ok := s.handlers[head]; ok { - r.URL.Path = tail - s.log.Debug().Msgf("http routing: head=%s tail=%s svc=%s", head, r.URL.Path, head) + if h, ok := s.handlers[r.URL.Path]; ok { + s.log.Debug().Msgf("http routing: url=%s", r.URL.Path) + r.URL.Path = "/" h.ServeHTTP(w, r) return } - // when a service is exposed at the root. - if h, ok := s.handlers[""]; ok { - r.URL.Path = "/" + head + tail - s.log.Debug().Msgf("http routing: head= tail=%s svc=root", r.URL.Path) + // find by longest common path + if h, url, ok := s.getHandlerLongestCommongURL(r.URL.Path); ok { + s.log.Debug().Msgf("http routing: url=%s", url) + r.URL.Path = getSubURL(r.URL.Path, url) h.ServeHTTP(w, r) return } - s.log.Debug().Msgf("http routing: head=%s tail=%s svc=not-found", head, tail) + s.log.Debug().Msgf("http routing: url=%s svc=not-found", r.URL.Path) w.WriteHeader(http.StatusNotFound) }) diff --git a/pkg/rhttp/rhttp_test.go b/pkg/rhttp/rhttp_test.go new file mode 100644 index 0000000000..eb68a0c22d --- /dev/null +++ b/pkg/rhttp/rhttp_test.go @@ -0,0 +1,74 @@ +// Copyright 2018-2022 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package rhttp + +import "testing" + +func TestURLHasPrefix(t *testing.T) { + tests := map[string]struct { + url string + prefix string + expected bool + }{ + "root": { + url: "/", + prefix: "/", + expected: true, + }, + "suburl_root": { + url: "/api/v0", + prefix: "/", + expected: true, + }, + "suburl_root_slash_end": { + url: "/api/v0/", + prefix: "/", + expected: true, + }, + "suburl_root_no_slash": { + url: "/api/v0", + prefix: "", + expected: true, + }, + "no_common_prefix": { + url: "/api/v0/project", + prefix: "/api/v0/p", + expected: false, + }, + "long_url_prefix": { + url: "/api/v0/project/test", + prefix: "/api/v0", + expected: true, + }, + "prefix_end_slash": { + url: "/api/v0/project/test", + prefix: "/api/v0/", + expected: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := urlHasPrefix(test.url, test.prefix) + if res != test.expected { + t.Fatalf("%s got an unexpected result: %+v instead of %+v", t.Name(), res, test.expected) + } + }) + } +}