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

Configure directory listing #136

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
7 changes: 5 additions & 2 deletions cmd/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd
import (
"errors"

"github.com/loophole/cli/config"
"github.com/loophole/cli/internal/app/loophole"
lm "github.com/loophole/cli/internal/app/loophole/models"
"github.com/loophole/cli/internal/pkg/communication"
Expand Down Expand Up @@ -32,8 +33,9 @@ To expose local directory (e.g. /data/my-data) simply use 'loophole path /data/m
quitChannel := make(chan bool)

exposeConfig := lm.ExposeDirectoryConfig{
Local: dirEndpointSpecs,
Remote: remoteEndpointSpecs,
Local: dirEndpointSpecs,
Remote: remoteEndpointSpecs,
DisableDirectoryListing: config.Config.Display.DisableDirectoryListing,
}

authMethod, err := loophole.RegisterTunnel(&exposeConfig.Remote)
Expand All @@ -55,6 +57,7 @@ To expose local directory (e.g. /data/my-data) simply use 'loophole path /data/m
}

func init() {
dirCmd.PersistentFlags().BoolVar(&config.Config.Display.DisableDirectoryListing, "disable-directory-listing", false, "Disable showing all files when navigating to directories without index.html")
initServeCommand(dirCmd)
rootCmd.AddCommand(dirCmd)
}
5 changes: 3 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ type OAuthConfig struct {

// DisplayConfig defines the display switches shape
type DisplayConfig struct {
Verbose bool `json:"verbose"`
QR bool `json:"qr"`
DisableDirectoryListing bool `json:"disabledirectorylisting"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use camelCase syntax

Suggested change
DisableDirectoryListing bool `json:"disabledirectorylisting"`
DisableDirectoryListing bool `json:"disableDirectoryListing"`

Verbose bool `json:"verbose"`
QR bool `json:"qr"`
}

// ApplicationConfig defines the application config shape
Expand Down
5 changes: 3 additions & 2 deletions internal/app/loophole/models/ExposeDirectoryConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models

// ExposeDirectoryConfig represents loophole configuration when directory is exposed
type ExposeDirectoryConfig struct {
Local LocalDirectorySpecs `json:"local"`
Remote RemoteEndpointSpecs `json:"remote"`
Local LocalDirectorySpecs `json:"local"`
Remote RemoteEndpointSpecs `json:"remote"`
DisableDirectoryListing bool `json:"deactivatedirectorylisting"`
}
75 changes: 75 additions & 0 deletions internal/pkg/customfilesystem/customfilesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//necessary bits and pieces for being able to serve e.g. the custom notification html
// without needing to create files in the users folders
package customfilesystem

import (
"bytes"
"errors"
"io/fs"
"net/http"
"path/filepath"
)

var DirectoryListingDisabledPage = []byte("<!DOCTYPE html><html><body><img src=\"https://raw.githubusercontent.com/loophole/website/master/static/img/logo.png\" alt=\"https://raw.githubusercontent.com/loophole/website/master/static/img/logo.png\" class=\"transparent shrinkToFit\" width=\"400\" height=\"88\"><p>Directory index listing has been disabled. Please enter the path of a file.</p></body></html>")

type CustomFileSystem struct {
FS http.FileSystem
}

//the file cannot be reused since it's io.Reader can only be read from once,
// so we need a reusable way to create it
func writeDirectoryListingDisabledPageFile(pageFile *MyFile) {
*pageFile = MyFile{
Reader: bytes.NewReader(DirectoryListingDisabledPage),
mif: myFileInfo{
name: "customIndex.html",
data: DirectoryListingDisabledPage,
},
}
}

func (cfs CustomFileSystem) Open(path string) (http.File, error) {
f, err := _Open(path, cfs)

if err != nil {
var pathErrorInstance error = &fs.PathError{
Err: errors.New(""),
}
if errors.As(err, &pathErrorInstance) {
return nil, err
}
var pageFile *MyFile = &MyFile{}
writeDirectoryListingDisabledPageFile(pageFile)
return pageFile, nil
}
return f, nil
}

//if there is an elegant way to integrate the following into the function above without
// using labeled breaks or adding even more control structures let me know
func _Open(path string, cfs CustomFileSystem) (http.File, error) {
f, err := cfs.FS.Open(path)
if err != nil {
if path == "/" {
var pageFile *MyFile = &MyFile{}
writeDirectoryListingDisabledPageFile(pageFile)
return pageFile, nil
}
return nil, err
}

s, err := f.Stat()
if err != nil {
return nil, err
}

if s.IsDir() {
index := filepath.Join(path, "index.html")
if _, err := cfs.FS.Open(index); err != nil {
var pageFile *MyFile = &MyFile{}
writeDirectoryListingDisabledPageFile(pageFile)
return pageFile, nil
}
}
return f, nil
}
44 changes: 44 additions & 0 deletions internal/pkg/customfilesystem/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//taken from https://stackoverflow.com/questions/52697277/simples-way-to-make-a-byte-into-a-virtual-file-object-in-golang
package customfilesystem

import (
"bytes"
"io"
"os"
"time"
)

type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}

type myFileInfo struct {
name string
data []byte
}

func (mif myFileInfo) Name() string { return mif.name }
func (mif myFileInfo) Size() int64 { return int64(len(mif.data)) }
func (mif myFileInfo) Mode() os.FileMode { return 0444 } // Read for all
func (mif myFileInfo) ModTime() time.Time { return time.Time{} } // Return anything
func (mif myFileInfo) IsDir() bool { return false }
func (mif myFileInfo) Sys() interface{} { return nil }

type MyFile struct {
*bytes.Reader
mif myFileInfo
}

func (mf *MyFile) Close() error { return nil } // Noop, nothing to do

func (mf *MyFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, nil // We are not a directory but a single file
}

func (mf *MyFile) Stat() (os.FileInfo, error) {
return mf.mif, nil
}
36 changes: 33 additions & 3 deletions internal/pkg/httpserver/httpserver.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package httpserver

import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"

auth "github.com/abbot/go-http-auth"
"github.com/loophole/cli/config"
lm "github.com/loophole/cli/internal/app/loophole/models"
cfs "github.com/loophole/cli/internal/pkg/customfilesystem"
"github.com/loophole/cli/internal/pkg/urlmaker"
"golang.org/x/crypto/bcrypt"
"golang.org/x/net/webdav"
Expand Down Expand Up @@ -188,13 +192,39 @@ func (ssb *staticServerBuilder) WithBasicAuth(username string, password string)
return ssb
}

func serveDirectoryListingNotification(w http.ResponseWriter, req *http.Request) {
customPageSeeker := bytes.NewReader(cfs.DirectoryListingDisabledPage)
http.ServeContent(w, req, "name", time.Now(), customPageSeeker)
}

//this could break desktop but I haven't been able to figure out another way yet
var fsHandler http.Handler = nil
var fsHandlerIsCustom = false

//handler functions may only take these arguments, so we need variables outside of it to make it's behaviour conditional
func conditionalHandler(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/" && fsHandlerIsCustom {
serveDirectoryListingNotification(w, req)
} else {
fsHandler.ServeHTTP(w, req)
}
}

func (ssb *staticServerBuilder) Build() (*http.Server, error) {
fs := http.FileServer(http.Dir(ssb.directory))
if config.Config.Display.DisableDirectoryListing {
fsHandler = http.FileServer(cfs.CustomFileSystem{
FS: http.Dir(ssb.directory),
})
fsHandlerIsCustom = true
} else {
fsHandler = http.FileServer(http.Dir(ssb.directory))
fsHandlerIsCustom = false
}

var server *http.Server

if ssb.basicAuthEnabled {
handler, err := getBasicAuthHandler(ssb.serverBuilder.siteID, ssb.serverBuilder.domain, ssb.basicAuthUsername, ssb.basicAuthPassword, fs.ServeHTTP)
handler, err := getBasicAuthHandler(ssb.serverBuilder.siteID, ssb.serverBuilder.domain, ssb.basicAuthUsername, ssb.basicAuthPassword, conditionalHandler)
if err != nil {
return nil, err
}
Expand All @@ -205,7 +235,7 @@ func (ssb *staticServerBuilder) Build() (*http.Server, error) {
}
} else {
server = &http.Server{
Handler: fs,
Handler: http.HandlerFunc(conditionalHandler),
TLSConfig: getTLSConfig(ssb.serverBuilder.siteID, ssb.serverBuilder.domain, ssb.serverBuilder.disableOldCiphers),
}
}
Expand Down
31 changes: 31 additions & 0 deletions ui/desktop/src/components/form/DirectorySettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

interface DirectorySettingsProps {
usingValue: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why using? I would name it value and changeCallback

usingChangeCallback: Function;
}

const DirectorySettings = (props: DirectorySettingsProps): JSX.Element => {
const disableDirectoryListing = props.usingValue;
const setDisableDirectoryListing = props.usingChangeCallback;

return (
<div>
<div className="field">
<div className="control">
<label className="checkbox">
<input
type="checkbox"
onChange={(e) => {
setDisableDirectoryListing(!disableDirectoryListing);
}}
/>{" "}
I want to disable Directory Listing
</label>
</div>
</div>
</div>
);
};

export default DirectorySettings;
1 change: 1 addition & 0 deletions ui/desktop/src/interfaces/ExposeDirectoryMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import RemoteEndpointSpecs from './RemoteEndpointSpecs';
export default interface ExposeDirectoryMessage {
local: LocalDirectorySpecs;
remote: RemoteEndpointSpecs;
deactivatedirectorylisting: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deactivatedirectorylisting: boolean;
disableDirectoryListing: boolean;

}
10 changes: 10 additions & 0 deletions ui/desktop/src/pages/Directory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
isLocalPathValid,
isLoopholeHostnameValid,
} from "../features/validator/validators";
import DirectorySettings from "../components/form/DirectorySettings";

const DirectoryPage = () => {
const dispatch = useDispatch();
Expand All @@ -31,6 +32,7 @@ const DirectoryPage = () => {
const [basicAuthUsername, setBasicAuthUsername] = useState("");
const [basicAuthPassword, setBasicAuthPassword] = useState("");
const [disableOldCiphers, setDisableOldCiphers] = useState(false);
const [disableDirectoryListing, setDisableDirectoryListing] = useState(false);

const areInputsValid = (): boolean => {
if (!isLocalPathValid(path)) return false;
Expand All @@ -55,6 +57,7 @@ const DirectoryPage = () => {
disableOldCiphers: false,
tunnelId: uuidv4(),
},
deactivatedirectorylisting: disableDirectoryListing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deactivatedirectorylisting: disableDirectoryListing
disableDirectoryListing,

as in other places?

};
if (usingCustomHostname) {
options.remote.siteId = customHostname;
Expand Down Expand Up @@ -122,6 +125,13 @@ const DirectoryPage = () => {
</div>
</div>
</div>
<div className="column is-12">
<h5 className="title is-5">Directory Listing</h5>
<DirectorySettings
usingValue={disableDirectoryListing}
usingChangeCallback={setDisableDirectoryListing}
/>
</div>
<div className="column is-12">
<div className="field is-grouped is-pulled-right">
<div className="control">
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/pages/WebDav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const WebDav = () => {
disableOldCiphers: false,
tunnelId: uuidv4(),
},
deactivatedirectorylisting: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deactivatedirectorylisting: false,
disableDirectoryListing: false,

as in other places?

};
if (usingCustomHostname) {
options.remote.siteId = customHostname;
Expand Down
2 changes: 2 additions & 0 deletions ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/gorilla/websocket"

"github.com/loophole/cli/config"
"github.com/loophole/cli/internal/app/loophole"
lm "github.com/loophole/cli/internal/app/loophole/models"
"github.com/loophole/cli/internal/pkg/cache"
Expand Down Expand Up @@ -98,6 +99,7 @@ func websocketHandler(w http.ResponseWriter, r *http.Request) {
communication.Warn(err.Error())
}

config.Config.Display.DisableDirectoryListing = exposeDirectoryConfig.DisableDirectoryListing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would not use Display for that, they are not really for it. It's rather remoteEndpointSpecs the same way as here: https://github.com/loophole/cli/blob/master/cmd/http.go#L72

tunnelQuitChannel := make(chan bool)
go func() {
sshDir := cache.GetLocalStorageDir(".ssh") //getting our sshDir and creating it, if it doesn't exist
Expand Down