Commit b4037dcf authored by Jacob Vosmaer's avatar Jacob Vosmaer

Stop passing gitRequests when not needed

parent 0592ff1c
package main
func (u *upstream) artifactsAuthorizeHandler(handleFunc serviceHandleFunc) serviceHandleFunc {
return u.preAuthorizeHandler(handleFunc, "/authorize")
import (
"net/http"
)
func (u *upstream) artifactsAuthorizeHandler(h handleFunc) handleFunc {
return u.preAuthorizeHandler(func(w http.ResponseWriter, r *gitRequest) {
req := r.Request
req.Header.Set("Gitlab-Workhorse-Temp-Path", r.TempPath)
h(w, req)
}, "/authorize")
}
......@@ -51,9 +51,9 @@ func (u *upstream) newUpstreamRequest(r *http.Request, body io.Reader, suffix st
return authReq, nil
}
func (u *upstream) preAuthorizeHandler(handleFunc serviceHandleFunc, suffix string) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
authReq, err := u.newUpstreamRequest(r.Request, nil, suffix)
func (u *upstream) preAuthorizeHandler(h serviceHandleFunc, suffix string) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
authReq, err := u.newUpstreamRequest(r, nil, suffix)
if err != nil {
fail500(w, fmt.Errorf("preAuthorizeHandler: newUpstreamRequest: %v", err))
return
......@@ -85,10 +85,11 @@ func (u *upstream) preAuthorizeHandler(handleFunc serviceHandleFunc, suffix stri
return
}
g := &gitRequest{Request: r}
// The auth backend validated the client request and told us additional
// request metadata. We must extract this information from the auth
// response body.
if err := json.NewDecoder(authResponse.Body).Decode(&r.authorizationResponse); err != nil {
if err := json.NewDecoder(authResponse.Body).Decode(&g.authorizationResponse); err != nil {
fail500(w, fmt.Errorf("preAuthorizeHandler: decode authorization response: %v", err))
return
}
......@@ -104,6 +105,6 @@ func (u *upstream) preAuthorizeHandler(handleFunc serviceHandleFunc, suffix stri
}
}
handleFunc(w, r)
h(w, g)
}
}
......@@ -24,12 +24,9 @@ func runPreAuthorizeHandler(t *testing.T, suffix string, url *regexp.Regexp, aut
t.Fatal(err)
}
u := newUpstream(ts.URL, nil)
request := gitRequest{
Request: httpRequest,
}
response := httptest.NewRecorder()
u.preAuthorizeHandler(okHandler, suffix)(response, &request)
u.preAuthorizeHandler(okHandler, suffix)(response, httpRequest)
assertResponseCode(t, response, expectedCode)
return response
}
......
......@@ -6,8 +6,8 @@ import (
"path/filepath"
)
func handleDeployPage(documentRoot *string, handler serviceHandleFunc) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
func handleDeployPage(documentRoot *string, handler handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
deployPage := filepath.Join(*documentRoot, "index.html")
data, err := ioutil.ReadFile(deployPage)
if err != nil {
......
......@@ -19,7 +19,7 @@ func TestIfNoDeployPageExist(t *testing.T) {
w := httptest.NewRecorder()
executed := false
handleDeployPage(&dir, func(w http.ResponseWriter, r *gitRequest) {
handleDeployPage(&dir, func(w http.ResponseWriter, r *http.Request) {
executed = true
})(w, nil)
if !executed {
......@@ -40,7 +40,7 @@ func TestIfDeployPageExist(t *testing.T) {
w := httptest.NewRecorder()
executed := false
handleDeployPage(&dir, func(w http.ResponseWriter, r *gitRequest) {
handleDeployPage(&dir, func(_ http.ResponseWriter, _ *http.Request) {
executed = true
})(w, nil)
if executed {
......
......@@ -2,10 +2,10 @@ package main
import "net/http"
func handleDevelopmentMode(developmentMode *bool, handler serviceHandleFunc) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
func handleDevelopmentMode(developmentMode *bool, handler handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !*developmentMode {
http.NotFound(w, r.Request)
http.NotFound(w, r)
return
}
......
......@@ -13,9 +13,9 @@ func TestDevelopmentModeEnabled(t *testing.T) {
w := httptest.NewRecorder()
executed := false
handleDevelopmentMode(&developmentMode, func(w http.ResponseWriter, r *gitRequest) {
handleDevelopmentMode(&developmentMode, func(_ http.ResponseWriter, _ *http.Request) {
executed = true
})(w, &gitRequest{Request: r})
})(w, r)
if !executed {
t.Error("The handler should get executed")
}
......@@ -28,9 +28,9 @@ func TestDevelopmentModeDisabled(t *testing.T) {
w := httptest.NewRecorder()
executed := false
handleDevelopmentMode(&developmentMode, func(w http.ResponseWriter, r *gitRequest) {
handleDevelopmentMode(&developmentMode, func(_ http.ResponseWriter, _ *http.Request) {
executed = true
})(w, &gitRequest{Request: r})
})(w, r)
if executed {
t.Error("The handler should not get executed")
}
......
......@@ -59,8 +59,8 @@ func (s *errorPageResponseWriter) Flush() {
s.WriteHeader(http.StatusOK)
}
func handleRailsError(documentRoot *string, handler serviceHandleFunc) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
func handleRailsError(documentRoot *string, handler handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
rw := errorPageResponseWriter{
rw: w,
path: documentRoot,
......
......@@ -22,7 +22,7 @@ func TestIfErrorPageIsPresented(t *testing.T) {
w := httptest.NewRecorder()
handleRailsError(&dir, func(w http.ResponseWriter, r *gitRequest) {
handleRailsError(&dir, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(404)
fmt.Fprint(w, "Not Found")
})(w, nil)
......@@ -42,7 +42,7 @@ func TestIfErrorPassedIfNoErrorPageIsFound(t *testing.T) {
w := httptest.NewRecorder()
errorResponse := "ERROR"
handleRailsError(&dir, func(w http.ResponseWriter, r *gitRequest) {
handleRailsError(&dir, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(404)
fmt.Fprint(w, errorResponse)
})(w, nil)
......
......@@ -26,7 +26,7 @@ func looksLikeRepo(p string) bool {
return true
}
func (u *upstream) repoPreAuthorizeHandler(handleFunc serviceHandleFunc) serviceHandleFunc {
func (u *upstream) repoPreAuthorizeHandler(handleFunc serviceHandleFunc) handleFunc {
return u.preAuthorizeHandler(func(w http.ResponseWriter, r *gitRequest) {
if r.RepoPath == "" {
fail500(w, errors.New("repoPreAuthorizeHandler: RepoPath empty"))
......
......@@ -7,8 +7,8 @@ import (
"net/http"
)
func contentEncodingHandler(handleFunc serviceHandleFunc) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
func contentEncodingHandler(h handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
var body io.ReadCloser
var err error
......@@ -32,6 +32,6 @@ func contentEncodingHandler(handleFunc serviceHandleFunc) serviceHandleFunc {
r.Body = body
r.Header.Del("Content-Encoding")
handleFunc(w, r)
h(w, r)
}
}
......@@ -27,15 +27,14 @@ func TestGzipEncoding(t *testing.T) {
}
req.Header.Set("Content-Encoding", "gzip")
request := gitRequest{Request: req}
contentEncodingHandler(func(w http.ResponseWriter, r *gitRequest) {
contentEncodingHandler(func(w http.ResponseWriter, r *http.Request) {
if _, ok := r.Body.(*gzip.Reader); !ok {
t.Fatal("Expected gzip reader for body, but it's:", reflect.TypeOf(r.Body))
}
if r.Header.Get("Content-Encoding") != "" {
t.Fatal("Content-Encoding should be deleted")
}
})(resp, &request)
})(resp, req)
assertResponseCode(t, resp, 200)
}
......@@ -52,15 +51,14 @@ func TestNoEncoding(t *testing.T) {
}
req.Header.Set("Content-Encoding", "")
request := gitRequest{Request: req}
contentEncodingHandler(func(w http.ResponseWriter, r *gitRequest) {
contentEncodingHandler(func(_ http.ResponseWriter, r *http.Request) {
if r.Body != body {
t.Fatal("Expected the same body")
}
if r.Header.Get("Content-Encoding") != "" {
t.Fatal("Content-Encoding should be deleted")
}
})(resp, &request)
})(resp, req)
assertResponseCode(t, resp, 200)
}
......@@ -74,10 +72,9 @@ func TestInvalidEncoding(t *testing.T) {
}
req.Header.Set("Content-Encoding", "application/unknown")
request := gitRequest{Request: req}
contentEncodingHandler(func(w http.ResponseWriter, r *gitRequest) {
contentEncodingHandler(func(_ http.ResponseWriter, _ *http.Request) {
t.Fatal("it shouldn't be executed")
})(resp, &request)
})(resp, req)
assertResponseCode(t, resp, 500)
}
......@@ -17,7 +17,7 @@ import (
"path/filepath"
)
func (u *upstream) lfsAuthorizeHandler(handleFunc serviceHandleFunc) serviceHandleFunc {
func (u *upstream) lfsAuthorizeHandler(handleFunc serviceHandleFunc) handleFunc {
return u.preAuthorizeHandler(func(w http.ResponseWriter, r *gitRequest) {
if r.StoreLFSPath == "" {
......@@ -75,5 +75,5 @@ func (u *upstream) handleStoreLfsObject(w http.ResponseWriter, r *gitRequest) {
r.ContentLength = 0
// And proxy the request
u.proxyRequest(w, r)
u.proxyRequest(w, r.Request)
}
......@@ -43,9 +43,11 @@ var developmentMode = flag.Bool("developmentMode", false, "Allow to serve assets
type httpRoute struct {
method string
regex *regexp.Regexp
handleFunc serviceHandleFunc
handleFunc handleFunc
}
type handleFunc func(http.ResponseWriter, *http.Request)
const projectPattern = `^/[^/]+/[^/]+/`
const gitProjectPattern = `^/[^/]+/[^/]+\.git/`
......@@ -63,8 +65,8 @@ func compileRoutes(u *upstream) {
httpRoutes = []httpRoute{
// Git Clone
httpRoute{"GET", regexp.MustCompile(gitProjectPattern + `info/refs\z`), u.repoPreAuthorizeHandler(handleGetInfoRefs)},
httpRoute{"POST", regexp.MustCompile(gitProjectPattern + `git-upload-pack\z`), u.repoPreAuthorizeHandler(contentEncodingHandler(handlePostRPC))},
httpRoute{"POST", regexp.MustCompile(gitProjectPattern + `git-receive-pack\z`), u.repoPreAuthorizeHandler(contentEncodingHandler(handlePostRPC))},
httpRoute{"POST", regexp.MustCompile(gitProjectPattern + `git-upload-pack\z`), contentEncodingHandler(u.repoPreAuthorizeHandler(handlePostRPC))},
httpRoute{"POST", regexp.MustCompile(gitProjectPattern + `git-receive-pack\z`), contentEncodingHandler(u.repoPreAuthorizeHandler(handlePostRPC))},
httpRoute{"PUT", regexp.MustCompile(gitProjectPattern + `gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`), u.lfsAuthorizeHandler(u.handleStoreLfsObject)},
// Repository Archive
......@@ -82,7 +84,7 @@ func compileRoutes(u *upstream) {
httpRoute{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive.tar.bz2\z`), u.repoPreAuthorizeHandler(handleGetArchive)},
// CI Artifacts API
httpRoute{"POST", regexp.MustCompile(ciAPIPattern + `v1/builds/[0-9]+/artifacts\z`), u.artifactsAuthorizeHandler(contentEncodingHandler(u.handleFileUploads))},
httpRoute{"POST", regexp.MustCompile(ciAPIPattern + `v1/builds/[0-9]+/artifacts\z`), contentEncodingHandler(u.artifactsAuthorizeHandler(u.handleFileUploads))},
// Explicitly proxy API requests
httpRoute{"", regexp.MustCompile(apiPattern), u.proxyRequest},
......
......@@ -51,15 +51,15 @@ func headerClone(h http.Header) http.Header {
return h2
}
func (u *upstream) proxyRequest(w http.ResponseWriter, r *gitRequest) {
func (u *upstream) proxyRequest(w http.ResponseWriter, r *http.Request) {
// Clone request
req := *r.Request
req := r
req.Header = headerClone(r.Header)
// Set Workhorse version
req.Header.Set("Gitlab-Workhorse", Version)
rw := newSendFileResponseWriter(w, &req)
rw := newSendFileResponseWriter(w, req)
defer rw.Flush()
u.httpProxy.ServeHTTP(&rw, &req)
u.httpProxy.ServeHTTP(&rw, req)
}
......@@ -39,13 +39,9 @@ func TestProxyRequest(t *testing.T) {
}
httpRequest.Header.Set("Custom-Header", "test")
request := gitRequest{
Request: httpRequest,
}
u := newUpstream(ts.URL, nil)
w := httptest.NewRecorder()
u.proxyRequest(w, &request)
u.proxyRequest(w, httpRequest)
assertResponseCode(t, w, 202)
assertResponseBody(t, w, "RESPONSE")
......@@ -65,13 +61,9 @@ func TestProxyError(t *testing.T) {
transport: http.DefaultTransport,
}
request := gitRequest{
Request: httpRequest,
}
u := newUpstream("http://localhost:655575/", &transport)
w := httptest.NewRecorder()
u.proxyRequest(w, &request)
u.proxyRequest(w, httpRequest)
assertResponseCode(t, w, 502)
assertResponseBody(t, w, "dial tcp: invalid port 655575")
}
......@@ -98,13 +90,10 @@ func TestProxyReadTimeout(t *testing.T) {
},
}
request := gitRequest{
Request: httpRequest,
}
u := newUpstream(ts.URL, transport)
w := httptest.NewRecorder()
u.proxyRequest(w, &request)
u.proxyRequest(w, httpRequest)
assertResponseCode(t, w, 502)
assertResponseBody(t, w, "net/http: timeout awaiting response headers")
}
......@@ -125,13 +114,10 @@ func TestProxyHandlerTimeout(t *testing.T) {
transport: http.DefaultTransport,
}
request := gitRequest{
Request: httpRequest,
}
u := newUpstream(ts.URL, transport)
w := httptest.NewRecorder()
u.proxyRequest(w, &request)
u.proxyRequest(w, httpRequest)
assertResponseCode(t, w, 503)
assertResponseBody(t, w, "Request took too long")
}
......@@ -16,8 +16,8 @@ const (
CacheExpireMax
)
func (u *upstream) handleServeFile(documentRoot *string, cache CacheMode, notFoundHandler serviceHandleFunc) serviceHandleFunc {
return func(w http.ResponseWriter, r *gitRequest) {
func (u *upstream) handleServeFile(documentRoot *string, cache CacheMode, notFoundHandler handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
file := filepath.Join(*documentRoot, u.relativeURIPath(cleanURIPath(r.URL.Path)))
// The filepath.Join does Clean traversing directories up
......@@ -50,7 +50,7 @@ func (u *upstream) handleServeFile(documentRoot *string, cache CacheMode, notFou
if notFoundHandler != nil {
notFoundHandler(w, r)
} else {
http.NotFound(w, r.Request)
http.NotFound(w, r)
}
return
}
......@@ -65,6 +65,6 @@ func (u *upstream) handleServeFile(documentRoot *string, cache CacheMode, notFou
}
log.Printf("Send static file %q (%q) for %s %q", file, w.Header().Get("Content-Encoding"), r.Method, r.RequestURI)
http.ServeContent(w, r.Request, filepath.Base(file), fi.ModTime(), content)
http.ServeContent(w, r, filepath.Base(file), fi.ModTime(), content)
}
}
......@@ -14,12 +14,9 @@ import (
func TestServingNonExistingFile(t *testing.T) {
dir := "/path/to/non/existing/directory"
httpRequest, _ := http.NewRequest("GET", "/file", nil)
request := &gitRequest{
Request: httpRequest,
}
w := httptest.NewRecorder()
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, httpRequest)
assertResponseCode(t, w, 404)
}
......@@ -31,38 +28,28 @@ func TestServingDirectory(t *testing.T) {
defer os.RemoveAll(dir)
httpRequest, _ := http.NewRequest("GET", "/file", nil)
request := &gitRequest{
Request: httpRequest,
}
w := httptest.NewRecorder()
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, httpRequest)
assertResponseCode(t, w, 404)
}
func TestServingMalformedUri(t *testing.T) {
dir := "/path/to/non/existing/directory"
httpRequest, _ := http.NewRequest("GET", "/../../../static/file", nil)
request := &gitRequest{
Request: httpRequest,
}
w := httptest.NewRecorder()
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, httpRequest)
assertResponseCode(t, w, 404)
}
func TestExecutingHandlerWhenNoFileFound(t *testing.T) {
dir := "/path/to/non/existing/directory"
httpRequest, _ := http.NewRequest("GET", "/file", nil)
request := &gitRequest{
Request: httpRequest,
}
executed := false
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, func(w http.ResponseWriter, r *gitRequest) {
executed = (r == request)
})(nil, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, func(_ http.ResponseWriter, r *http.Request) {
executed = (r == httpRequest)
})(nil, httpRequest)
if !executed {
t.Error("The handler should get executed")
}
......@@ -76,15 +63,12 @@ func TestServingTheActualFile(t *testing.T) {
defer os.RemoveAll(dir)
httpRequest, _ := http.NewRequest("GET", "/file", nil)
request := &gitRequest{
Request: httpRequest,
}
fileContent := "STATIC"
ioutil.WriteFile(filepath.Join(dir, "file"), []byte(fileContent), 0600)
w := httptest.NewRecorder()
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, httpRequest)
assertResponseCode(t, w, 200)
if w.Body.String() != fileContent {
t.Error("We should serve the file: ", w.Body.String())
......@@ -99,9 +83,6 @@ func testServingThePregzippedFile(t *testing.T, enableGzip bool) {
defer os.RemoveAll(dir)
httpRequest, _ := http.NewRequest("GET", "/file", nil)
request := &gitRequest{
Request: httpRequest,
}
if enableGzip {
httpRequest.Header.Set("Accept-Encoding", "gzip, deflate")
......@@ -118,7 +99,7 @@ func testServingThePregzippedFile(t *testing.T, enableGzip bool) {
ioutil.WriteFile(filepath.Join(dir, "file"), []byte(fileContent), 0600)
w := httptest.NewRecorder()
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, request)
newUpstream("http://localhost", nil).handleServeFile(&dir, CacheDisabled, nil)(w, httpRequest)
assertResponseCode(t, w, 200)
if enableGzip {
assertResponseHeader(t, w, "Content-Encoding", "gzip")
......
......@@ -11,7 +11,7 @@ import (
"os"
)
func rewriteFormFilesFromMultipart(r *gitRequest, writer *multipart.Writer) (cleanup func(), err error) {
func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, tempPath string) (cleanup func(), err error) {
// Create multipart reader
reader, err := r.MultipartReader()
if err != nil {
......@@ -47,12 +47,12 @@ func rewriteFormFilesFromMultipart(r *gitRequest, writer *multipart.Writer) (cle
// Copy form field
if filename := p.FileName(); filename != "" {
// Create temporary directory where the uploaded file will be stored
if err := os.MkdirAll(r.TempPath, 0700); err != nil {
if err := os.MkdirAll(tempPath, 0700); err != nil {
return cleanup, err
}
// Create temporary file in path returned by Authorization filter
file, err := ioutil.TempFile(r.TempPath, "upload_")
file, err := ioutil.TempFile(tempPath, "upload_")
if err != nil {
return cleanup, err
}
......@@ -83,8 +83,9 @@ func rewriteFormFilesFromMultipart(r *gitRequest, writer *multipart.Writer) (cle
return cleanup, nil
}
func (u *upstream) handleFileUploads(w http.ResponseWriter, r *gitRequest) {
if r.TempPath == "" {
func (u *upstream) handleFileUploads(w http.ResponseWriter, r *http.Request) {
tempPath := r.Header.Get("Gitlab-Workhorse-Temp-Path")
if tempPath == "" {
fail500(w, errors.New("handleFileUploads: TempPath empty"))
return
}
......@@ -94,7 +95,7 @@ func (u *upstream) handleFileUploads(w http.ResponseWriter, r *gitRequest) {
defer writer.Close()
// Rewrite multipart form data
cleanup, err := rewriteFormFilesFromMultipart(r, writer)
cleanup, err := rewriteFormFilesFromMultipart(r, writer, tempPath)
if err != nil {
if err == http.ErrNotMultipart {
u.proxyRequest(w, r)
......
......@@ -16,12 +16,8 @@ import (
func TestUploadTempPathRequirement(t *testing.T) {
response := httptest.NewRecorder()
request := gitRequest{
authorizationResponse: authorizationResponse{
TempPath: "",
},
}
newUpstream("http://localhost", nil).handleFileUploads(response, &request)
request := &http.Request{}
newUpstream("http://localhost", nil).handleFileUploads(response, request)
assertResponseCode(t, response, 500)
}
......@@ -53,15 +49,11 @@ func TestUploadHandlerForwardingRawData(t *testing.T) {
defer os.RemoveAll(tempPath)
response := httptest.NewRecorder()
request := gitRequest{
Request: httpRequest,
authorizationResponse: authorizationResponse{
TempPath: tempPath,
},
}
httpRequest.Header.Set("Gitlab-Workhorse-Temp-Path", tempPath)
u := newUpstream(ts.URL, nil)
u.handleFileUploads(response, &request)
u.handleFileUploads(response, httpRequest)
assertResponseCode(t, response, 202)
if response.Body.String() != "RESPONSE" {
t.Fatal("Expected RESPONSE in response body")
......@@ -132,17 +124,11 @@ func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
httpRequest.Body = ioutil.NopCloser(&buffer)
httpRequest.ContentLength = int64(buffer.Len())
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
httpRequest.Header.Set("Gitlab-Workhorse-Temp-Path", tempPath)
response := httptest.NewRecorder()
request := gitRequest{
Request: httpRequest,
authorizationResponse: authorizationResponse{
TempPath: tempPath,
},
}
u := newUpstream(ts.URL, nil)
u.handleFileUploads(response, &request)
u.handleFileUploads(response, httpRequest)
assertResponseCode(t, response, 202)
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
......
......@@ -132,9 +132,5 @@ func (u *upstream) ServeHTTP(ow http.ResponseWriter, r *http.Request) {
return
}
request := gitRequest{
Request: r,
}
g.handleFunc(&w, &request)
g.handleFunc(&w, r)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment