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

Bugfix: Fixed broken support for GUI.base_path #3862

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
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
5 changes: 3 additions & 2 deletions api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ func NewInterceptingResponseWriter(
return &interceptingResponseWriter{
ResponseWriter: w,
from: "url(/app/assets/",
to: fmt.Sprintf("url(/%v/app/assets/", base_path),
br_writer: brotli.NewWriter(w),
to: fmt.Sprintf("url(/%v/app/assets/",
strings.TrimPrefix(base_path, "/")),
br_writer: brotli.NewWriter(w),
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions api/authenticators/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package authenticators
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -290,14 +289,14 @@ func authenticateUserHandle(
users := services.GetUserManager()
user_record, err := users.GetUser(r.Context(), username, username)
if err != nil {
reject_cb(w, r, errors.New("Invalid user"), username)
reject_cb(w, r, fmt.Errorf("Invalid user: %v", err), username)
return
}

// Does the user have access to the specified org?
err = CheckOrgAccess(config_obj, r, user_record)
if err != nil {
reject_cb(w, r, errors.New("Insufficient permissions"), user_record.Name)
reject_cb(w, r, fmt.Errorf("Insufficient permissions: %v", err), user_record.Name)
return
}

Expand Down Expand Up @@ -349,7 +348,7 @@ func reject_with_username(
w.WriteHeader(http.StatusOK)

renderRejectionMessage(config_obj,
w, username, []velociraptor.AuthenticatorInfo{
r, w, err, username, []velociraptor.AuthenticatorInfo{
{
LoginURL: login_url,
ProviderName: provider,
Expand Down
4 changes: 3 additions & 1 deletion api/authenticators/multiple.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func (self *MultiAuthenticator) reject_with_username(
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)

renderRejectionMessage(self.config_obj, w, username, self.delegate_info)
renderRejectionMessage(
self.config_obj, r, w, err,
username, self.delegate_info)
}

func (self *MultiAuthenticator) AuthenticateUserHandler(
Expand Down
12 changes: 10 additions & 2 deletions api/authenticators/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package authenticators

import (
"net/http"
"strings"
"text/template"

utils "www.velocidex.com/golang/velociraptor/api/utils"
Expand All @@ -13,8 +14,15 @@ import (

func renderRejectionMessage(
config_obj *config_proto.Config,
w http.ResponseWriter, username string,
authenticators []velociraptor.AuthenticatorInfo) {
r *http.Request, w http.ResponseWriter, err error,
username string, authenticators []velociraptor.AuthenticatorInfo) {

// For API calls we render the error as JSON
base_path := config_obj.GUI.BasePath + "/api/"
if strings.HasPrefix(r.URL.Path, base_path) {
w.Write([]byte(json.Format(`{"message": %q}`, err.Error())))
return
}

data, err := gui_assets.ReadFile("/index.html")
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion artifacts/definitions/Server/Monitor/Health.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ reports:

## Current Orgs
{{ define "OrgsTable" }}
LET ColumnTypes <= dict(ClientConfig='url_internal')
LET ColumnTypes <= dict(ClientConfig='url')
LET OrgsTable = SELECT Name, OrgId,
upload(accessor='data', file=_client_config,
name='client.'+OrgId+'.config.yaml') AS _Upload
Expand Down
11 changes: 11 additions & 0 deletions gui/velociraptor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gui/velociraptor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"moment-timezone": "0.5.45",
"npm-watch": "^0.13.0",
"patch-package": "8.0.0",
"path-browserify": "1.0.1",
"prop-types": "^15.8.1",
"qs": "6.13.0",
"react": "^16.14.0",
Expand Down
22 changes: 11 additions & 11 deletions gui/velociraptor/src/components/core/api-service.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios, {isCancel} from 'axios';
import path from 'path-browserify';

import _ from 'lodash';
import qs from 'qs';
Expand Down Expand Up @@ -259,9 +260,10 @@ const upload = function(url, files, params) {
}).catch(handle_error);
};

// Internal Routes declared in api/proxy.go
const internal_links = new RegExp("^/notebooks|downloads|hunts|clients/");
const api_links = new RegExp("^/api/");
// Internal Routes declared in api/proxy.go Assume base_path is regex
// safe due to the sanitation in the sanitation service.
const internal_links = new RegExp(
"^" + base_path + "/api|app|notebooks|downloads|hunts|clients/");

// Prepare a suitable href link for <a>
// This function accepts a number of options:
Expand Down Expand Up @@ -290,13 +292,10 @@ const href = function(url, params, options) {

// All internal links must point to the same page since this
// is a SPA
if (options && options.internal) {
if (internal_links.test(parsed.pathname) ||
api_links.test(parsed.pathname)) {
parsed.pathname = src_of(parsed.pathname);
} else {
parsed.pathname = window.location.pathname;
}
if (internal_links.test(parsed.pathname)) {
parsed.pathname = src_of(parsed.pathname);
} else {
parsed.pathname = window.location.pathname;
}
}

Expand Down Expand Up @@ -330,11 +329,12 @@ const delete_req = function(url, params, cancel_token) {

var hooks = [];

// Returns a url corrected for base_path. Handles data URLs properly.
const src_of = function (url) {
if (url && url.match(/^data/)) {
return url;
}
return window.base_path + url;
return path.join(window.base_path, url);
};

const error = function(msg) {
Expand Down
5 changes: 3 additions & 2 deletions gui/velociraptor/src/components/users/user-label.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,9 @@ export default class UserLabel extends React.Component {
api.post("v1/SetGUIOptions", params,
this.source.token).then((response) => {
if (response.status === 200) {
// Check for redirect from the server - this is normally
// set by the authenticator to redirect to a better server.
// Check for redirect from the server - this
// is normally set by the authenticator to
// redirect to a better server.
if (response.data && response.data.redirect_url &&
response.data.redirect_url !== "") {
window.location.assign(response.data.redirect_url);
Expand Down
2 changes: 1 addition & 1 deletion gui/velociraptor/src/components/welcome/login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class LoginPage extends Component {
enforceFocus={true}
scrollable={true}
onHide={() => {
window.location = '/';
window.location = api.href("/app/index.html");
}}>
<Modal.Header closeButton>
<Modal.Title>Velociraptor Login</Modal.Title>
Expand Down
2 changes: 1 addition & 1 deletion gui/velociraptor/src/components/welcome/logoff.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class LogoffPage extends Component {
enforceFocus={true}
scrollable={true}
onHide={() => {
window.location = '/';
window.location = api.href("/app/index.html");
}}>
<Modal.Header closeButton>
<Modal.Title>Velociraptor Login</Modal.Title>
Expand Down
3 changes: 2 additions & 1 deletion gui/velociraptor/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
window.base_path = "{{.BasePath}}";
/// Support development
if (window.base_path.substring(0,2) === "\{\{") {
window.base_path = "";
window.base_path = "/";
window.globals.base_path = window.base_path;
}

// Set the OrgId from the URL if possible.
Expand Down
16 changes: 13 additions & 3 deletions services/notebook/notebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"encoding/base64"
"errors"
"net/url"
"os"
"path"
"strings"
"sync"

Expand Down Expand Up @@ -186,9 +186,19 @@ func (self *NotebookManager) UploadNotebookAttachment(
return nil, err
}

public_url, err := utils.GetBaseURL(self.config_obj)
if err != nil {
return nil, err
}

// Calculate the URL to the resource
public_url.Path = path.Join(public_url.Path, full_path.AsClientPath())
values := public_url.Query()
values.Set("org_id", utils.NormalizedOrgId(self.config_obj.OrgId))
public_url.RawQuery = values.Encode()

result := &api_proto.NotebookFileUploadResponse{
Url: full_path.AsClientPath() + "?org_id=" +
url.QueryEscape(utils.NormalizedOrgId(self.config_obj.OrgId)),
Url: public_url.String(),
Filename: filename,
}

Expand Down
18 changes: 18 additions & 0 deletions services/sanity/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package sanity
import (
"fmt"
"net"
"regexp"
"strings"

config_proto "www.velocidex.com/golang/velociraptor/config/proto"
"www.velocidex.com/golang/velociraptor/logging"
)

var (
basePathRegEx = regexp.MustCompile("^/[^/][a-zA-Z0-9_-]+[^/]$")
)

func (self *SanityChecks) CheckFrontendSettings(
config_obj *config_proto.Config) error {

Expand All @@ -22,6 +28,18 @@ func (self *SanityChecks) CheckFrontendSettings(
}
logger.Info("GUI Will only accept conections from <green>%v</>", cidr_net)
}

if config_obj.GUI.BasePath != "" {
if !basePathRegEx.MatchString(config_obj.GUI.BasePath) {
return fmt.Errorf("Invalid GUI.base_path. This must start with a / and end without a /. For example '/velociraptor' . Only a-z0-9 characters are allowed in the path name.")
}

if !strings.HasSuffix(config_obj.GUI.PublicUrl,
config_obj.GUI.BasePath+"/app/index.html") {
return fmt.Errorf("Invalid GUI.public_url. When setting a base_url the public_url must be adjusted accordingly. For example `https://www.example.com/velociraptor/app/index.html` for a base_path of `/velociraptor` .")
}
}

}
return nil
}
36 changes: 36 additions & 0 deletions utils/urls.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package utils

import (
"fmt"
"net/url"
"path"
"strings"

config_proto "www.velocidex.com/golang/velociraptor/config/proto"
)

// Work around issues with https://github.com/golang/go/issues/4013
Expand All @@ -12,3 +16,35 @@ func QueryEscape(in string) string {
res := url.QueryEscape(in)
return strings.Replace(res, "+", "%20", -1)
}

// The URL to the App.html itself
func GetPublicUrl(config_obj *config_proto.Config) (res *url.URL, err error) {
res = &url.URL{Path: "/"}

if config_obj.GUI != nil && config_obj.GUI.PublicUrl != "" {
res, err = url.Parse(config_obj.GUI.PublicUrl)
if err != nil {
return nil, fmt.Errorf(
"Invalid configuration! GUI.public_url must be the public URL over which the GUI is served!: %w", err)
}
}
res.RawQuery = ""
res.Fragment = ""
res.RawFragment = ""

return res, nil
}

// Calculates the Base URL to the top of the app
func GetBaseURL(config_obj *config_proto.Config) (res *url.URL, err error) {
res, err = GetPublicUrl(config_obj)
if err != nil {
return nil, err
}
res.Path = "/"
if config_obj.GUI != nil && config_obj.GUI.BasePath != "" {
res.Path = path.Join(res.Path, config_obj.GUI.BasePath)
}

return res, nil
}
21 changes: 13 additions & 8 deletions vql/server/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"fmt"
"net/url"
"path"
"strings"

"github.com/Velocidex/ordereddict"
"www.velocidex.com/golang/velociraptor/services"
"www.velocidex.com/golang/velociraptor/utils"
"www.velocidex.com/golang/velociraptor/vql"
vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
"www.velocidex.com/golang/vfilter"
Expand Down Expand Up @@ -48,13 +50,10 @@ func (self *LinkToFunction) Call(ctx context.Context,
return vfilter.Null{}
}

url := &url.URL{Path: "/"}
if config_obj.GUI.PublicUrl != "" {
url, err = url.Parse(config_obj.GUI.PublicUrl)
if err != nil {
scope.Log("link_to: Invalid configuration! GUI.public_url must be the public URL over which the GUI is served!: %v", err)
return vfilter.Null{}
}
url, err := utils.GetPublicUrl(config_obj)
if err != nil {
scope.Log("link_to: %v", err)
return vfilter.Null{}
}

org := arg.OrgId
Expand Down Expand Up @@ -98,7 +97,13 @@ func (self *LinkToFunction) Call(ctx context.Context,
}

// The link is an API call to VFSDownloadInfo
url.Path = "/api/v1/DownloadVFSFile"
url, err = utils.GetBaseURL(config_obj)
if err != nil {
scope.Log("link_to: %v", err)
return vfilter.Null{}
}

url.Path = path.Join(url.Path, "/api/v1/DownloadVFSFile")
query.Add("vfs_path", vfs_name)
for _, c := range components {
query.Add("fs_components", c)
Expand Down
Loading