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,11 +156,17 @@ 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 {
log.Println("Request host does not match config value Hostname, returning client error.") if conf.UserDirEnable && conf.UserSubdomains && strings.HasSuffix(host, conf.Hostname) {
sendResponseHeader(conn, statusClientError, "No proxying to other hosts!") userSubdomainReq = true
return }
if !userSubdomainReq {
log.Println("Request host does not match config value Hostname, returning client error.")
sendResponseHeader(conn, statusClientError, "No proxying to other hosts!")
return
}
} }
} }
if strings.Contains(reqPath, "..") { if strings.Contains(reqPath, "..") {
@ -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) {
// Handle tildes var user string
if conf.UserDirEnable && strings.HasPrefix(reqPath, "/~") { // Handle user subdomains
if req.vhost != "" {
user = req.vhost
path = reqPath
} else if conf.UserDirEnable && strings.HasPrefix(reqPath, "/~") {
// Handle tildes
// 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 == "" {