Initial QSfera import
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package data
|
||||
|
||||
// Meta holds response metadata
|
||||
type Meta struct {
|
||||
Status string `json:"status" xml:"status"`
|
||||
StatusCode int `json:"statuscode" xml:"statuscode"`
|
||||
Message string `json:"message" xml:"message"`
|
||||
TotalItems string `json:"totalitems,omitempty" xml:"totalitems,omitempty"`
|
||||
ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"`
|
||||
}
|
||||
|
||||
// MetaOK is the default ok response with code 100
|
||||
var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}
|
||||
|
||||
// MetaFailure is a failure response with code 101
|
||||
var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
|
||||
|
||||
// MetaInvalidInput is an error response with code 102
|
||||
var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
|
||||
|
||||
// MetaForbidden is an error response with code 104
|
||||
var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
|
||||
|
||||
// MetaBadRequest is used for unknown errors
|
||||
var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}
|
||||
|
||||
// MetaServerError is returned on server errors
|
||||
var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"}
|
||||
|
||||
// MetaUnauthorized is returned on unauthorized requests
|
||||
var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"}
|
||||
|
||||
// MetaNotFound is returned when trying to access not existing resources
|
||||
var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"}
|
||||
|
||||
// MetaUnknownError is used for unknown errors
|
||||
var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"}
|
||||
|
||||
// MessageUserNotFound is used when a user can not be found
|
||||
var MessageUserNotFound = "The requested user could not be found"
|
||||
|
||||
// MessageGroupNotFound is used when a group can not be found
|
||||
var MessageGroupNotFound = "The requested group could not be found"
|
||||
@@ -0,0 +1,7 @@
|
||||
package data
|
||||
|
||||
// SigningKey holds the Payload for a GetSigningKey response
|
||||
type SigningKey struct {
|
||||
User string `json:"user" xml:"user"`
|
||||
SigningKey string `json:"signing-key" xml:"signing-key"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/services/ocs/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLogging(next Service, logger log.Logger) Service {
|
||||
return logging{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type logging struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/ocs/pkg/config"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// 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
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware provides a function to set the middleware option.
|
||||
func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Middleware = val
|
||||
}
|
||||
}
|
||||
|
||||
// Store provides a function to set the store option.
|
||||
func Store(s store.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
|
||||
)
|
||||
|
||||
// Response is the top level response structure
|
||||
type Response struct {
|
||||
OCS *Payload `json:"ocs" xml:"ocs"`
|
||||
}
|
||||
|
||||
var (
|
||||
elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
|
||||
metaStartElement = xml.StartElement{Name: xml.Name{Local: "meta"}}
|
||||
ocsName = xml.Name{Local: "ocs"}
|
||||
dataName = xml.Name{Local: "data"}
|
||||
)
|
||||
|
||||
// Payload combines response metadata and data
|
||||
type Payload struct {
|
||||
Meta data.Meta `json:"meta" xml:"meta"`
|
||||
Data any `json:"data,omitempty" xml:"data,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
|
||||
func (rsp Response) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
// first the easy part
|
||||
// use ocs as the surrounding tag
|
||||
start.Name = ocsName
|
||||
if err = e.EncodeToken(start); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// encode the meta tag
|
||||
if err = e.EncodeElement(rsp.OCS.Meta, metaStartElement); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we need to use reflection to determine if p.Data is an array or a slice
|
||||
rt := reflect.TypeOf(rsp.OCS.Data)
|
||||
if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
|
||||
// this is how to wrap the data elements in their own <element> tag
|
||||
v := reflect.ValueOf(rsp.OCS.Data)
|
||||
if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
} else if err = e.EncodeElement(rsp.OCS.Data, xml.StartElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// write the closing <ocs> tag
|
||||
if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Render sets the status code of the http response, taking the ocs version into account
|
||||
func (rsp *Response) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
version := APIVersion(r.Context())
|
||||
m := statusCodeMapper(version)
|
||||
statusCode := m(rsp.OCS.Meta)
|
||||
render.Status(r, statusCode)
|
||||
if version == ocsVersion2 && statusCode == http.StatusOK {
|
||||
rsp.OCS.Meta.StatusCode = statusCode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DataRender creates an OK Payload for the given data
|
||||
func DataRender(d any) render.Renderer {
|
||||
return &Response{
|
||||
&Payload{
|
||||
Meta: data.MetaOK,
|
||||
Data: d,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ErrRender creates an Error Payload with the given OCS error code and message
|
||||
// The httpcode will be determined using the API version stored in the context
|
||||
func ErrRender(c int, m string) render.Renderer {
|
||||
return &Response{
|
||||
&Payload{
|
||||
Meta: data.Meta{Status: "error", StatusCode: c, Message: m},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func statusCodeMapper(version string) func(data.Meta) int {
|
||||
var mapper func(data.Meta) int
|
||||
switch version {
|
||||
case ocsVersion1:
|
||||
mapper = OcsV1StatusCodes
|
||||
case ocsVersion2:
|
||||
mapper = OcsV2StatusCodes
|
||||
default:
|
||||
mapper = defaultStatusCodeMapper
|
||||
}
|
||||
return mapper
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
apiVersionKey key = iota
|
||||
ocsVersion1 = "1"
|
||||
ocsVersion2 = "2"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultStatusCodeMapper = OcsV2StatusCodes
|
||||
)
|
||||
|
||||
// APIVersion retrieves the api version from the context.
|
||||
func APIVersion(ctx context.Context) string {
|
||||
value := ctx.Value(apiVersionKey)
|
||||
if value != nil {
|
||||
return value.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
|
||||
func OcsV1StatusCodes(meta data.Meta) int {
|
||||
if meta.StatusCode == data.MetaUnauthorized.StatusCode {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
|
||||
// see https://github.com/owncloud/core/blob/c08baf580927ecb8ec179028dda255fdd85b4568/lib/private/legacy/api.php#L528
|
||||
// also HTTP status codes for apps are the same as OCS codes
|
||||
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
|
||||
// I think this is a bug in the ocs v2 api, but since we are going to mimic bugs in КуСфера ... here goes
|
||||
func OcsV2StatusCodes(meta data.Meta) int {
|
||||
sc := meta.StatusCode
|
||||
switch sc {
|
||||
case data.MetaNotFound.StatusCode:
|
||||
return http.StatusNotFound
|
||||
case data.MetaUnknownError.StatusCode:
|
||||
fallthrough
|
||||
case data.MetaServerError.StatusCode:
|
||||
return http.StatusInternalServerError
|
||||
case data.MetaUnauthorized.StatusCode:
|
||||
return http.StatusUnauthorized
|
||||
case data.MetaOK.StatusCode:
|
||||
// TODO mustn't data.Meta be a pointer so this assignment has an effect
|
||||
meta.StatusCode = http.StatusOK
|
||||
return http.StatusOK
|
||||
}
|
||||
// any 2xx, 4xx and 5xx will be used as is
|
||||
if sc >= 200 && sc < 600 {
|
||||
return sc
|
||||
}
|
||||
|
||||
// any error codes > 100 are treated as client errors
|
||||
if sc > 100 && sc < 200 {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
// TODO change this status code? yes, align with oc10 core mapStatusCodes
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
// VersionCtx middleware is used to determine the response mapper from
|
||||
// the URL parameters passed through as the request. In case
|
||||
// the Version is unknown, we stop here and return a 404.
|
||||
func VersionCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
version := chi.URLParam(r, "version")
|
||||
if version == "" {
|
||||
_ = render.Render(w, r, ErrRender(data.MetaBadRequest.StatusCode, "unknown ocs api version"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Ocs-Api-Version", version)
|
||||
|
||||
// store version in context so handlers can access it
|
||||
ctx := context.WithValue(r.Context(), apiVersionKey, version)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/qsfera/server/pkg/account"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
opkgm "github.com/qsfera/server/pkg/middleware"
|
||||
"github.com/qsfera/server/services/ocs/pkg/config"
|
||||
ocsm "github.com/qsfera/server/services/ocs/pkg/middleware"
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/response"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// Service defines the service handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) Service {
|
||||
options := newOptions(opts...)
|
||||
|
||||
m := chi.NewMux()
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
svc := Ocs{
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: options.Logger,
|
||||
store: options.Store,
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
r.NotFound(svc.NotFound)
|
||||
r.Use(middleware.StripSlashes)
|
||||
r.Use(opkgm.ExtractAccountUUID(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
|
||||
)
|
||||
r.Use(ocsm.OCSFormatCtx) // updates request Accept header according to format=(json|xml) query parameter
|
||||
r.Route("/v{version:(1|2)}.php", func(r chi.Router) {
|
||||
r.Use(response.VersionCtx) // stores version in context
|
||||
r.Get("/cloud/user/signing-key", svc.GetSigningKey)
|
||||
})
|
||||
})
|
||||
|
||||
_ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// Ocs defines implements the business logic for Service.
|
||||
type Ocs struct {
|
||||
config *config.Config
|
||||
logger log.Logger
|
||||
mux *chi.Mux
|
||||
store microstore.Store
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (o Ocs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
o.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// NotFound uses ErrRender to always return a proper OCS payload
|
||||
func (o Ocs) NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "not found"))
|
||||
}
|
||||
|
||||
func (o Ocs) mustRender(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
|
||||
if err := render.Render(w, r, renderer); err != nil {
|
||||
o.logger.Err(err).Msgf("failed to write response for ocs request %s on %s", r.Method, r.URL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
)
|
||||
|
||||
// NewTracing returns a service that instruments traces.
|
||||
func NewTracing(next Service) Service {
|
||||
return tracing{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
type tracing struct {
|
||||
next Service
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.TraceContext(t.next).ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
|
||||
"github.com/qsfera/server/services/ocs/pkg/service/v0/response"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// GetSigningKey returns the signing key for the current user. It will create it on the fly if it does not exist
|
||||
// The signing key is part of the user settings and is used by the proxy to authenticate requests
|
||||
// Currently, the username is used as the OC-Credential
|
||||
func (o Ocs) GetSigningKey(w http.ResponseWriter, r *http.Request) {
|
||||
u, ok := revactx.ContextGetUser(r.Context())
|
||||
if !ok {
|
||||
//o.logger.Error().Msg("missing user in context")
|
||||
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
|
||||
return
|
||||
}
|
||||
|
||||
// use the user's UUID
|
||||
userID := u.Id.OpaqueId
|
||||
|
||||
res, err := o.store.Read(userID)
|
||||
if err == nil && len(res) > 0 {
|
||||
o.mustRender(w, r, response.DataRender(&data.SigningKey{
|
||||
User: userID,
|
||||
SigningKey: string(res[0].Value),
|
||||
}))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
// not found is ok, so we can continue and generate the key on the fly
|
||||
} else {
|
||||
o.logger.Error().Err(err).Msg("error reading from server")
|
||||
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "error reading from store"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try creating it
|
||||
key := make([]byte, 64)
|
||||
_, err = rand.Read(key[:])
|
||||
if err != nil {
|
||||
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not generate signing key"))
|
||||
return
|
||||
}
|
||||
signingKey := hex.EncodeToString(key)
|
||||
|
||||
err = o.store.Write(&store.Record{Key: userID, Value: []byte(signingKey)})
|
||||
if err != nil {
|
||||
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not persist signing key"))
|
||||
return
|
||||
}
|
||||
|
||||
o.mustRender(w, r, response.DataRender(&data.SigningKey{
|
||||
User: userID,
|
||||
SigningKey: signingKey,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user