Skip to content

Commit b0cec52

Browse files
author
Charles-Antoine Mathieu
committed
src-d#203 src-d#210 Support different root path
1 parent 053396d commit b0cec52

File tree

7 files changed

+147
-71
lines changed

7 files changed

+147
-71
lines changed

server/common/config.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@ import (
4141

4242
// Configuration object
4343
type Configuration struct {
44-
LogLevel string `json:"-"`
45-
ListenAddress string `json:"-"`
46-
ListenPort int `json:"-"`
47-
MaxFileSize int64 `json:"maxFileSize"`
48-
MaxFilePerUpload int `json:"maxFilePerUpload"`
44+
LogLevel string `json:"-"`
45+
46+
ListenAddress string `json:"-"`
47+
ListenPort int `json:"-"`
48+
Path string `json:"-"`
49+
50+
MaxFileSize int64 `json:"maxFileSize"`
51+
MaxFilePerUpload int `json:"maxFilePerUpload"`
4952

5053
DefaultTTL int `json:"defaultTTL"`
5154
MaxTTL int `json:"maxTTL"`
@@ -103,8 +106,6 @@ func NewConfiguration() (config *Configuration) {
103106
config.DefaultTTL = 2592000 // 30 days
104107
config.MaxTTL = 0
105108
config.SslEnabled = false
106-
config.SslCert = ""
107-
config.SslKey = ""
108109
config.StreamMode = true
109110
return
110111
}
@@ -114,6 +115,7 @@ func NewConfiguration() (config *Configuration) {
114115
// override default params
115116
func LoadConfiguration(file string) {
116117
Config = NewConfiguration()
118+
117119
if _, err := toml.DecodeFile(file, Config); err != nil {
118120
Logger().Fatalf("Unable to load config file %s : %s", file, err)
119121
}
@@ -125,6 +127,8 @@ func LoadConfiguration(file string) {
125127
Logger().SetFlags(logger.Fdate | logger.Flevel | logger.FfixedSizeLevel)
126128
}
127129

130+
Config.Path = strings.TrimSuffix(Config.Path, "/")
131+
128132
// Do user specified a ApiKey and ApiSecret for Yubikey
129133
if Config.YubikeyEnabled {
130134
yubiAuth, err := yubigo.NewYubiAuth(Config.YubikeyAPIKey, Config.YubikeyAPISecret)

server/common/context.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,36 @@ func Fail(ctx *juliet.Context, req *http.Request, resp http.ResponseWriter, mess
145145
}
146146
}
147147
if redirect {
148-
http.Redirect(resp, req, fmt.Sprintf("/#!/?err=%s&errcode=%d&uri=%s", message, status, req.RequestURI), 301)
148+
http.Redirect(resp, req, fmt.Sprintf("%s/#!/?err=%s&errcode=%d&uri=%s", Config.Path, message, status, req.RequestURI), 301)
149149
return
150150
}
151151
}
152152

153153
http.Error(resp, NewResult(message, nil).ToJSONString(), status)
154154
}
155+
156+
// StripPrefix returns a handler that serves HTTP requests
157+
// removing the given prefix from the request URL's Path
158+
// It differs from http.StripPrefix by defaulting to "/" and not ""
159+
func StripPrefix(prefix string, handler http.Handler) http.Handler {
160+
if prefix == "" || prefix == "/" {
161+
return handler
162+
}
163+
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
164+
// Relative paths to javascript, css, ... imports won't work without a tailing slash
165+
if req.URL.Path == prefix {
166+
http.Redirect(resp, req, prefix+"/", 301)
167+
return
168+
}
169+
if p := strings.TrimPrefix(req.URL.Path, prefix); len(p) < len(req.URL.Path) {
170+
req.URL.Path = p
171+
} else {
172+
http.NotFound(resp, req)
173+
return
174+
}
175+
if !strings.HasPrefix(req.URL.Path, "/") {
176+
req.URL.Path = "/" + req.URL.Path
177+
}
178+
handler.ServeHTTP(resp, req)
179+
})
180+
}

server/handlers/google.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func GoogleLogin(ctx *juliet.Context, resp http.ResponseWriter, req *http.Reques
6363
origin := req.Header.Get("referer")
6464
if origin == "" {
6565
log.Warning("Missing referer header")
66-
common.Fail(ctx, req, resp, "Missing referer herader", 400)
66+
common.Fail(ctx, req, resp, "Missing referer header", 400)
6767
return
6868
}
6969

@@ -263,5 +263,5 @@ func GoogleCallback(ctx *juliet.Context, resp http.ResponseWriter, req *http.Req
263263
xsrfCookie.Path = "/"
264264
http.SetCookie(resp, xsrfCookie)
265265

266-
http.Redirect(resp, req, "/#!/login", 301)
266+
http.Redirect(resp, req, common.Config.Path+"/#!/login", 301)
267267
}

server/handlers/ovh.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func OvhLogin(ctx *juliet.Context, resp http.ResponseWriter, req *http.Request)
107107
origin := req.Header.Get("referer")
108108
if origin == "" {
109109
log.Warning("Missing referer header")
110-
common.Fail(ctx, req, resp, "Missing referer herader", 400)
110+
common.Fail(ctx, req, resp, "Missing referer header", 400)
111111
return
112112
}
113113

@@ -377,5 +377,5 @@ func OvhCallback(ctx *juliet.Context, resp http.ResponseWriter, req *http.Reques
377377
xsrfCookie.Path = "/"
378378
http.SetCookie(resp, xsrfCookie)
379379

380-
http.Redirect(resp, req, "/#!/login", 301)
380+
http.Redirect(resp, req, common.Config.Path+"/#!/login", 301)
381381
}

server/plik.go

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,39 +97,42 @@ func main() {
9797
getFileChain := juliet.NewChain(middleware.Upload, middleware.Yubikey, middleware.File)
9898

9999
// HTTP Api routes configuration
100-
r := mux.NewRouter()
101-
r.Handle("/config", stdChain.Then(handlers.GetConfiguration)).Methods("GET")
102-
r.Handle("/version", stdChain.Then(handlers.GetVersion)).Methods("GET")
103-
r.Handle("/upload", tokenChain.Then(handlers.CreateUpload)).Methods("POST")
104-
r.Handle("/upload/{uploadID}", authChain.Append(middleware.Upload).Then(handlers.GetUpload)).Methods("GET")
105-
r.Handle("/upload/{uploadID}", authChain.Append(middleware.Upload).Then(handlers.RemoveUpload)).Methods("DELETE")
106-
r.Handle("/file/{uploadID}", tokenChain.Append(middleware.Upload).Then(handlers.AddFile)).Methods("POST")
107-
r.Handle("/file/{uploadID}/{fileID}/{filename}", tokenChain.Append(middleware.Upload, middleware.File).Then(handlers.AddFile)).Methods("POST")
108-
r.Handle("/file/{uploadID}/{fileID}/{filename}", authChain.Append(middleware.Upload, middleware.File).Then(handlers.RemoveFile)).Methods("DELETE")
109-
r.Handle("/file/{uploadID}/{fileID}/{filename}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
110-
r.Handle("/file/{uploadID}/{fileID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
111-
r.Handle("/stream/{uploadID}/{fileID}/{filename}", tokenChain.Append(middleware.Upload, middleware.File).Then(handlers.AddFile)).Methods("POST")
112-
r.Handle("/stream/{uploadID}/{fileID}/{filename}", authChain.Append(middleware.Upload, middleware.File).Then(handlers.RemoveFile)).Methods("DELETE")
113-
r.Handle("/stream/{uploadID}/{fileID}/{filename}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
114-
r.Handle("/stream/{uploadID}/{fileID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
115-
r.Handle("/archive/{uploadID}/{filename}", authChainWithRedirect.Append(middleware.Upload, middleware.Yubikey).Then(handlers.GetArchive)).Methods("HEAD", "GET")
116-
r.Handle("/archive/{uploadID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.Append(middleware.Upload, middleware.Yubikey).Then(handlers.GetArchive)).Methods("HEAD", "GET")
117-
r.Handle("/auth/google/login", authChain.Then(handlers.GoogleLogin)).Methods("GET")
118-
r.Handle("/auth/google/callback", stdChainWithRedirect.Then(handlers.GoogleCallback)).Methods("GET")
119-
r.Handle("/auth/ovh/login", authChain.Then(handlers.OvhLogin)).Methods("GET")
120-
r.Handle("/auth/ovh/callback", stdChainWithRedirect.Then(handlers.OvhCallback)).Methods("GET")
121-
r.Handle("/auth/logout", authChain.Then(handlers.Logout)).Methods("GET")
122-
r.Handle("/me", authChain.Then(handlers.UserInfo)).Methods("GET")
123-
r.Handle("/me", authChain.Then(handlers.DeleteAccount)).Methods("DELETE")
124-
r.Handle("/me/token", authChain.Then(handlers.CreateToken)).Methods("POST")
125-
r.Handle("/me/token/{token}", authChain.Then(handlers.RevokeToken)).Methods("DELETE")
126-
r.Handle("/me/uploads", authChain.Then(handlers.GetUserUploads)).Methods("GET")
127-
r.Handle("/me/uploads", authChain.Then(handlers.RemoveUserUploads)).Methods("DELETE")
128-
r.Handle("/qrcode", stdChain.Then(handlers.GetQrCode)).Methods("GET")
129-
r.PathPrefix("/clients/").Handler(http.StripPrefix("/clients/", http.FileServer(http.Dir("../clients"))))
130-
r.PathPrefix("/changelog/").Handler(http.StripPrefix("/changelog/", http.FileServer(http.Dir("../changelog"))))
131-
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/")))
132-
http.Handle("/", r)
100+
router := mux.NewRouter()
101+
router.Handle("/config", stdChain.Then(handlers.GetConfiguration)).Methods("GET")
102+
router.Handle("/version", stdChain.Then(handlers.GetVersion)).Methods("GET")
103+
router.Handle("/upload", tokenChain.Then(handlers.CreateUpload)).Methods("POST")
104+
router.Handle("/upload/{uploadID}", authChain.Append(middleware.Upload).Then(handlers.GetUpload)).Methods("GET")
105+
router.Handle("/upload/{uploadID}", authChain.Append(middleware.Upload).Then(handlers.RemoveUpload)).Methods("DELETE")
106+
router.Handle("/file/{uploadID}", tokenChain.Append(middleware.Upload).Then(handlers.AddFile)).Methods("POST")
107+
router.Handle("/file/{uploadID}/{fileID}/{filename}", tokenChain.Append(middleware.Upload, middleware.File).Then(handlers.AddFile)).Methods("POST")
108+
router.Handle("/file/{uploadID}/{fileID}/{filename}", authChain.Append(middleware.Upload, middleware.File).Then(handlers.RemoveFile)).Methods("DELETE")
109+
router.Handle("/file/{uploadID}/{fileID}/{filename}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
110+
router.Handle("/file/{uploadID}/{fileID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
111+
router.Handle("/stream/{uploadID}/{fileID}/{filename}", tokenChain.Append(middleware.Upload, middleware.File).Then(handlers.AddFile)).Methods("POST")
112+
router.Handle("/stream/{uploadID}/{fileID}/{filename}", authChain.Append(middleware.Upload, middleware.File).Then(handlers.RemoveFile)).Methods("DELETE")
113+
router.Handle("/stream/{uploadID}/{fileID}/{filename}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
114+
router.Handle("/stream/{uploadID}/{fileID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.AppendChain(getFileChain).Then(handlers.GetFile)).Methods("HEAD", "GET")
115+
router.Handle("/archive/{uploadID}/{filename}", authChainWithRedirect.Append(middleware.Upload, middleware.Yubikey).Then(handlers.GetArchive)).Methods("HEAD", "GET")
116+
router.Handle("/archive/{uploadID}/{filename}/yubikey/{yubikey}", authChainWithRedirect.Append(middleware.Upload, middleware.Yubikey).Then(handlers.GetArchive)).Methods("HEAD", "GET")
117+
router.Handle("/auth/google/login", authChain.Then(handlers.GoogleLogin)).Methods("GET")
118+
router.Handle("/auth/google/callback", stdChainWithRedirect.Then(handlers.GoogleCallback)).Methods("GET")
119+
router.Handle("/auth/ovh/login", authChain.Then(handlers.OvhLogin)).Methods("GET")
120+
router.Handle("/auth/ovh/callback", stdChainWithRedirect.Then(handlers.OvhCallback)).Methods("GET")
121+
router.Handle("/auth/logout", authChain.Then(handlers.Logout)).Methods("GET")
122+
router.Handle("/me", authChain.Then(handlers.UserInfo)).Methods("GET")
123+
router.Handle("/me", authChain.Then(handlers.DeleteAccount)).Methods("DELETE")
124+
router.Handle("/me/token", authChain.Then(handlers.CreateToken)).Methods("POST")
125+
router.Handle("/me/token/{token}", authChain.Then(handlers.RevokeToken)).Methods("DELETE")
126+
router.Handle("/me/uploads", authChain.Then(handlers.GetUserUploads)).Methods("GET")
127+
router.Handle("/me/uploads", authChain.Then(handlers.RemoveUserUploads)).Methods("DELETE")
128+
router.Handle("/qrcode", stdChain.Then(handlers.GetQrCode)).Methods("GET")
129+
router.PathPrefix("/clients/").Handler(http.StripPrefix("/clients/", http.FileServer(http.Dir("../clients"))))
130+
router.PathPrefix("/changelog/").Handler(http.StripPrefix("/changelog/", http.FileServer(http.Dir("../changelog"))))
131+
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/")))
132+
133+
handler := common.StripPrefix(common.Config.Path, router)
134+
135+
http.Handle("/", handler)
133136

134137
go UploadsCleaningRoutine()
135138

@@ -150,10 +153,10 @@ func main() {
150153
}
151154

152155
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS10, Certificates: []tls.Certificate{cert}}
153-
server = &http.Server{Addr: address, Handler: r, TLSConfig: tlsConfig}
156+
server = &http.Server{Addr: address, Handler: handler, TLSConfig: tlsConfig}
154157
} else {
155158
proto = "http"
156-
server = &http.Server{Addr: address, Handler: r}
159+
server = &http.Server{Addr: address, Handler: handler}
157160
}
158161

159162
log.Infof("Starting http server at %s://%s", proto, address)

server/plikd.cfg

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
#
99

1010
LogLevel = "INFO" # Other levels : DEBUG, INFO, WARNING, CRITICAL, FATAL
11-
ListenPort = 8080
12-
ListenAddress = "0.0.0.0"
11+
12+
ListenPort = 8080 # Port the HTTP server will listen on
13+
ListenAddress = "0.0.0.0" # Address the HTTP server will bind on
14+
Path = "/" # Root path for the HTTP server
15+
1316
MaxFileSize = 10737418240 # 10GB
1417
MaxFilePerUpload = 1000
1518

server/public/js/app.js

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ angular.module('dialog', ['ui.bootstrap']).
126126
// API Service
127127
angular.module('api', ['ngFileUpload']).
128128
factory('$api', function ($http, $q, Upload) {
129-
var api = {base: ''};
129+
var api = {base: window.location.origin + window.location.pathname.replace(/\/$/, '') };
130130

131131
// Make the actual HTTP call and return a promise
132132
api.call = function (url, method, params, data, uploadToken) {
@@ -427,9 +427,42 @@ plik.controller('MainCtrl', ['$scope', '$api', '$config', '$route', '$location',
427427
var err = $location.search().err;
428428
if (!_.isUndefined(err)) {
429429
if (err == "Invalid yubikey token" && $location.search().uri) {
430-
var uri = $location.search().uri.split("/");
431-
$scope.load(uri[2]);
432-
$scope.downloadWithYubikey(location.origin + "/file/" + uri[2] + "/" + uri[3] + "/" + uri[4]);
430+
var uri = $location.search().uri;
431+
if ( !uri ) {
432+
$dialog.alert({status: 0, message: "Unable to get uri from yubikey redirect"});
433+
$location.search({});
434+
$location.hash("");
435+
return;
436+
}
437+
438+
// Parse URI
439+
var url = new URL(window.location.origin + uri);
440+
441+
var regex = /^.*\/(file|stream|archive)\/(.*?)\/(.*?)\/(.*)$/;
442+
var match = regex.exec(url.pathname);
443+
if ( !match && match.length != 5 ) {
444+
$dialog.alert({status: 0, message: "Unable to get uploadId from yubikey redirect"});
445+
$location.search({});
446+
$location.hash("");
447+
return;
448+
}
449+
450+
var mode = match[1];
451+
var uploadId = match[2];
452+
var fileId = match[3];
453+
var fileName = match[4];
454+
455+
var download = false;
456+
if (url.searchParams.get("dl") == 1) {
457+
download = true;
458+
}
459+
460+
// For now there is nothing preventing us to load the upload at this point.
461+
// But I think that upload metadata should be also be protected by the token.
462+
// And I don't want the user to be asked for two tokens.
463+
// https://github.com/root-gg/plik/issues/215.
464+
465+
$scope.downloadWithYubikey(mode, uploadId, fileId, fileName, download);
433466
} else {
434467
var code = $location.search().errcode;
435468
$dialog.alert({status: code, message: err});
@@ -742,12 +775,13 @@ plik.controller('MainCtrl', ['$scope', '$api', '$config', '$route', '$location',
742775
return filesize(size, {base: 2});
743776
};
744777

745-
// Return file download URL
746-
$scope.getFileUrl = function (file, dl) {
747-
if (!file || !file.metadata) return;
748-
var mode = $scope.upload.stream ? "stream" : "file";
749-
var domain = $scope.config.downloadDomain ? $scope.config.downloadDomain : location.origin;
750-
var url = domain + '/' + mode + '/' + $scope.upload.id + '/' + file.metadata.id + '/' + file.metadata.fileName;
778+
// Build file download URL
779+
var getFileUrl = function(mode, uploadID, fileID, fileName, yubikeyToken, dl) {
780+
var domain = $scope.config.downloadDomain ? $scope.config.downloadDomain : $api.base;
781+
var url = domain + '/' + mode + '/' + uploadID + '/' + fileID + '/' + fileName;
782+
if (yubikeyToken) {
783+
url += "/yubikey/" + yubikeyToken
784+
}
751785
if (dl) {
752786
// Force file download
753787
url += "?dl=1";
@@ -756,22 +790,23 @@ plik.controller('MainCtrl', ['$scope', '$api', '$config', '$route', '$location',
756790
return encodeURI(url);
757791
};
758792

793+
// Return file download URL
794+
$scope.getFileUrl = function (file, dl) {
795+
if (!file || !file.metadata) return;
796+
var mode = $scope.upload.stream ? "stream" : "file";
797+
return getFileUrl(mode, $scope.upload.id, file.metadata.id, file.metadata.fileName, null, dl);
798+
};
799+
759800
// Return zip archive download URL
760801
$scope.getZipArchiveUrl = function (dl) {
761802
if (!$scope.upload.id) return;
762-
var domain = $scope.config.downloadDomain ? $scope.config.downloadDomain : location.origin;
763-
var url = domain + '/archive/' + $scope.upload.id + '/archive.zip';
764-
if (dl) {
765-
// Force file download
766-
url += "?dl=1";
767-
}
768-
return encodeURI(url);
803+
return getFileUrl("archive", $scope.upload.id, file.metadata.id, "archive.zip", null, dl);
769804
};
770805

771806
// Return QR Code image url
772807
$scope.getQrCodeUrl = function (url, size) {
773808
if (!url) return;
774-
return location.origin + "/qrcode?url=" + encodeURIComponent(url) + "&size=" + size;
809+
return $api.base + "/qrcode?url=" + encodeURIComponent(url) + "&size=" + size;
775810
};
776811

777812
// Return QR Code image url for current upload
@@ -852,7 +887,7 @@ plik.controller('MainCtrl', ['$scope', '$api', '$config', '$route', '$location',
852887
};
853888

854889
// Yubikey OTP download dialog
855-
$scope.downloadWithYubikey = function (url) {
890+
$scope.downloadWithYubikey = function (mode, uploadID, fileId, fileName, dl) {
856891
$dialog.openDialog({
857892
backdrop: true,
858893
backdropClick: true,
@@ -861,7 +896,8 @@ plik.controller('MainCtrl', ['$scope', '$api', '$config', '$route', '$location',
861896
}).result.then(
862897
function (token) {
863898
// Redirect to file download URL with yubikey token
864-
window.location.replace(url + '/yubikey/' + token);
899+
var url = getFileUrl(mode, uploadID, fileId, fileName, token, dl);
900+
window.location.replace(url);
865901
}, function () {
866902
// Avoid "Possibly unhandled rejection"
867903
});
@@ -993,6 +1029,10 @@ plik.controller('ClientListCtrl', ['$scope', '$api', '$dialog',
9931029
.then(null, function (error) {
9941030
$dialog.alert(error);
9951031
});
1032+
1033+
$scope.getClientPath = function(client) {
1034+
return $api.base + client.path;
1035+
}
9961036
}]);
9971037

9981038
// Login controller
@@ -1213,12 +1253,12 @@ plik.controller('HomeCtrl', ['$scope', '$api', '$config', '$dialog', '$location'
12131253

12141254
// Get upload url
12151255
$scope.getUploadUrl = function (upload) {
1216-
return location.origin + '/#/?id=' + upload.id;
1256+
return $api.base + '/#/?id=' + upload.id;
12171257
};
12181258

12191259
// Get file url
12201260
$scope.getFileUrl = function (upload, file) {
1221-
return location.origin + '/file/' + upload.id + '/' + file.id + '/' + file.fileName;
1261+
return $api.base + '/file/' + upload.id + '/' + file.id + '/' + file.fileName;
12221262
};
12231263

12241264
// Compute human readable size

0 commit comments

Comments
 (0)