Feat: Per-user vhosts (user.sometilde.org)

Limitations:
- Doesn't care about `extra.stuff.here.user.host.name`
- No custom hostname for user subdomains
- Returns not-found for `user.host.name` where `/home/user/userdir/`
  doesn't exist
This commit is contained in:
hedy 2022-03-12 15:45:18 +08:00
parent efbf17cfd6
commit 2dfaed4fa3
Signed by: hedy
GPG Key ID: B51B5A8D1B176372
3 changed files with 47 additions and 13 deletions

View File

@ -86,6 +86,7 @@ Here are the config options and their default values
* `userdirEnable=true`: enable serving `/~user/*` requests * `userdirEnable=true`: enable serving `/~user/*` requests
* `userdir="public_spartan"`: root directory for users. This should not have trailing slashes, and it is relative to `/home/user/` * `userdir="public_spartan"`: root directory for users. This should not have trailing slashes, and it is relative to `/home/user/`
* `userSubdomains=false`: User vhosts. Whether to allow `user.host.name/foo.txt` being the same as `host.name/~user/foo.txt` (When `hostname="host.name"`). **NOTE**: This only works when `hostname` option is set.
**CGI** **CGI**
@ -143,11 +144,19 @@ Keep in mind that CGI scripts (as of now) are run by the same user as the server
- [x] user homedir - [x] user homedir
- [x] hostname, port - [x] hostname, port
- [x] public dir - [x] public dir
- [ ] dirlist title - [x] dirlist title
- [x] user vhost
- [ ] userdir slug - [ ] userdir slug
- [ ] redirects - [ ] redirects
- [x] CGI - [x] CGI
- [x] pipe data block - [x] pipe data block
- [ ] user cgi config and change uid to user - [ ] user cgi config and change uid to user
- [ ] regex in cgi paths - [ ] regex in cgi paths
- [ ] SCGI
- [ ] Multiple servers with each of their own confs
README:
- [ ] Add example confs
- [ ] Add example .service files
``` ```

View File

@ -15,6 +15,7 @@ type Config struct {
RootDir string RootDir string
UserDirEnable bool UserDirEnable bool
UserDir string UserDir string
UserSubdomains bool
DirlistEnable bool DirlistEnable bool
DirlistReverse bool DirlistReverse bool
DirlistSort string DirlistSort string
@ -33,6 +34,7 @@ var defaultConf = &Config{
DirlistTitles: true, DirlistTitles: true,
UserDirEnable: true, UserDirEnable: true,
UserDir: "public_spartan", UserDir: "public_spartan",
UserSubdomains: false,
CGIPaths: []string{"cgi/"}, CGIPaths: []string{"cgi/"},
UserCGIEnable: false, // Turned off by default because scripts are run by server user as of now UserCGIEnable: false, // Turned off by default because scripts are run by server user as of now
} }

View File

@ -21,6 +21,7 @@ import (
type Request struct { type Request struct {
conn io.ReadWriteCloser conn io.ReadWriteCloser
netConn *net.Conn netConn *net.Conn
vhost string
user string user string
path string // Requested path path string // Requested path
filePath string // Actual file path that does not include the content dir name filePath string // Actual file path that does not include the content dir name
@ -155,13 +156,19 @@ func handleConnection(netConn net.Conn, conf *Config) {
sendResponseHeader(conn, statusClientError, "Bad request") sendResponseHeader(conn, statusClientError, "Bad request")
return return
} }
userSubdomainReq := false
if conf.Hostname != "" { if conf.Hostname != "" {
if conf.Hostname != host { if conf.Hostname != host {
if conf.UserDirEnable && conf.UserSubdomains && strings.HasSuffix(host, conf.Hostname) {
userSubdomainReq = true
}
if !userSubdomainReq {
log.Println("Request host does not match config value Hostname, returning client error.") log.Println("Request host does not match config value Hostname, returning client error.")
sendResponseHeader(conn, statusClientError, "No proxying to other hosts!") sendResponseHeader(conn, statusClientError, "No proxying to other hosts!")
return return
} }
} }
}
if strings.Contains(reqPath, "..") { if strings.Contains(reqPath, "..") {
log.Println("Returning client error (directory traversal)") log.Println("Returning client error (directory traversal)")
sendResponseHeader(conn, statusClientError, "Stop it with your directory traversal technique!") sendResponseHeader(conn, statusClientError, "Stop it with your directory traversal technique!")
@ -185,7 +192,13 @@ func handleConnection(netConn net.Conn, conf *Config) {
data += newData data += newData
} }
} }
req := &Request{path: reqPath, netConn: &netConn, conn: conn, data: data, dataLen: dataLen}
var vhost string
if userSubdomainReq {
// TODO: Handle extra dots like a.b.host.name?
vhost = strings.TrimSuffix(host, "."+conf.Hostname)
}
req := &Request{vhost: vhost, path: reqPath, netConn: &netConn, conn: conn, data: data, dataLen: dataLen}
// Time to fetch the files! // Time to fetch the files!
path := resolvePath(reqPath, conf, req) path := resolvePath(reqPath, conf, req)
@ -219,25 +232,35 @@ func handleConnection(netConn net.Conn, conf *Config) {
// resolvePath takes in teh request path and returns the cleaned filepath that needs to be fetched. // resolvePath takes in teh request path and returns the cleaned filepath that needs to be fetched.
// It also handles user directories paths /~user/ and /~user if user directories is enabled in the config. // It also handles user directories paths /~user/ and /~user if user directories is enabled in the config.
func resolvePath(reqPath string, conf *Config, req *Request) (path string) { func resolvePath(reqPath string, conf *Config, req *Request) (path string) {
var user string
// Handle user subdomains
if req.vhost != "" {
user = req.vhost
path = reqPath
} else if conf.UserDirEnable && strings.HasPrefix(reqPath, "/~") {
// Handle tildes // Handle tildes
if conf.UserDirEnable && strings.HasPrefix(reqPath, "/~") { // Note that user.host.name/~user/ would treat it as a literal folder named /~user/
// (hence using `else if`)
bits := strings.Split(reqPath, "/") bits := strings.Split(reqPath, "/")
username := bits[1][1:] user = bits[1][1:]
// /~user to /~user/ is somehow able to be handled together with any other /folder to /foler/ redirects // /~user to /~user/ is somehow able to be handled together with any other /folder to /folder/ redirects
// So I won't worry about that nor handle it specifically // So I won't worry about that nor handle it specifically
req.filePath = strings.TrimPrefix(filepath.Clean(strings.TrimPrefix(reqPath, "/~"+username)), "/") req.filePath = strings.TrimPrefix(filepath.Clean(strings.TrimPrefix(reqPath, "/~"+user)), "/")
path = req.filePath
}
new_prefix := filepath.Join("/home/", username, conf.UserDir) if user != "" {
req.user = username path = filepath.Join("/home/", user, conf.UserDir, path)
path = filepath.Clean(strings.Replace(reqPath, bits[1], new_prefix, 1)) req.user = user
if strings.HasSuffix(reqPath, "/") { if strings.HasSuffix(reqPath, "/") {
path = filepath.Join(path, "index.gmi") path = filepath.Join(path, "index.gmi")
} }
return return
} }
path = reqPath path = reqPath
// TODO: [config] default index file for a directory is index.gmi // TODO: [config] default index file for a directory is index.gmi
if strings.HasSuffix(reqPath, "/") || reqPath == "" { if strings.HasSuffix(reqPath, "/") || reqPath == "" {