Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Łukasz Nowak
caddy
Commits
9e12c45d
Commit
9e12c45d
authored
Apr 25, 2015
by
Thomas Hansen
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
https://github.com/mholt/caddy
parents
96985fb3
24d9d237
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
269 additions
and
91 deletions
+269
-91
config/config.go
config/config.go
+1
-4
config/directives.go
config/directives.go
+0
-43
config/dispenser.go
config/dispenser.go
+1
-1
config/middleware.go
config/middleware.go
+2
-0
config/parser.go
config/parser.go
+1
-0
config/parser_test.go
config/parser_test.go
+47
-0
config/parsing.go
config/parsing.go
+23
-6
main.go
main.go
+57
-3
middleware/basicauth/basicauth.go
middleware/basicauth/basicauth.go
+101
-0
middleware/browse/browse.go
middleware/browse/browse.go
+6
-4
middleware/browse/template.go
middleware/browse/template.go
+7
-7
middleware/log/log.go
middleware/log/log.go
+2
-0
middleware/markdown/markdown.go
middleware/markdown/markdown.go
+1
-1
middleware/recorder.go
middleware/recorder.go
+6
-1
middleware/replacer.go
middleware/replacer.go
+3
-2
middleware/templates/templates.go
middleware/templates/templates.go
+4
-1
server/server.go
server/server.go
+6
-17
server/virtualhost.go
server/virtualhost.go
+1
-1
No files found.
config/config.go
View file @
9e12c45d
...
...
@@ -12,7 +12,7 @@ import (
const
(
defaultHost
=
"localhost"
defaultPort
=
"
8080
"
defaultPort
=
"
2015
"
defaultRoot
=
"."
// The default configuration file to load if none is specified
...
...
@@ -47,9 +47,6 @@ type Config struct {
// these are executed in response to SIGINT and are blocking
Shutdown
[]
func
()
error
// MaxCPU is the maximum number of cores for the whole process to use
MaxCPU
int
// The path to the configuration file from which this was loaded
ConfigFile
string
}
...
...
config/directives.go
View file @
9e12c45d
...
...
@@ -3,9 +3,6 @@ package config
import
(
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
)
...
...
@@ -74,46 +71,6 @@ func init() {
p
.
cfg
.
TLS
=
tls
return
nil
},
"cpu"
:
func
(
p
*
parser
)
error
{
sysCores
:=
runtime
.
NumCPU
()
if
!
p
.
nextArg
()
{
return
p
.
argErr
()
}
strNum
:=
p
.
tkn
()
setCPU
:=
func
(
val
int
)
{
if
val
<
1
{
val
=
1
}
if
val
>
sysCores
{
val
=
sysCores
}
if
val
>
p
.
cfg
.
MaxCPU
{
p
.
cfg
.
MaxCPU
=
val
}
}
if
strings
.
HasSuffix
(
strNum
,
"%"
)
{
// Percent
var
percent
float32
pctStr
:=
strNum
[
:
len
(
strNum
)
-
1
]
pctInt
,
err
:=
strconv
.
Atoi
(
pctStr
)
if
err
!=
nil
||
pctInt
<
1
||
pctInt
>
100
{
return
p
.
err
(
"Parse"
,
"Invalid number '"
+
strNum
+
"' (must be a positive percentage between 1 and 100)"
)
}
percent
=
float32
(
pctInt
)
/
100
setCPU
(
int
(
float32
(
sysCores
)
*
percent
))
}
else
{
// Number
num
,
err
:=
strconv
.
Atoi
(
strNum
)
if
err
!=
nil
||
num
<
0
{
return
p
.
err
(
"Parse"
,
"Invalid number '"
+
strNum
+
"' (requires positive integer or percent)"
)
}
setCPU
(
num
)
}
return
nil
},
"startup"
:
func
(
p
*
parser
)
error
{
// TODO: This code is duplicated with the shutdown directive below
...
...
config/dispenser.go
View file @
9e12c45d
...
...
@@ -149,7 +149,7 @@ func (d *dispenser) ArgErr() error {
if
d
.
Val
()
==
"{"
{
return
d
.
Err
(
"Unexpected token '{', expecting argument"
)
}
return
d
.
Err
(
"
Unexpected line ending after '"
+
d
.
Val
()
+
"' (missing arguments?)
"
)
return
d
.
Err
(
"
Wrong argument count or unexpected line ending after '"
+
d
.
Val
()
+
"'
"
)
}
// Err generates a custom parse error with a message of msg.
...
...
config/middleware.go
View file @
9e12c45d
...
...
@@ -2,6 +2,7 @@ package config
import
(
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy/middleware/extensions"
...
...
@@ -45,6 +46,7 @@ func init() {
register
(
"rewrite"
,
rewrite
.
New
)
register
(
"redir"
,
redirect
.
New
)
register
(
"ext"
,
extensions
.
New
)
register
(
"basicauth"
,
basicauth
.
New
)
register
(
"proxy"
,
proxy
.
New
)
register
(
"fastcgi"
,
fastcgi
.
New
)
register
(
"websocket"
,
websockets
.
New
)
...
...
config/parser.go
View file @
9e12c45d
...
...
@@ -19,6 +19,7 @@ type (
other
[]
locationContext
// tokens to be 'parsed' later by middleware generators
scope
*
locationContext
// the current location context (path scope) being populated
unused
*
token
// sometimes a token will be read but not immediately consumed
eof
bool
// if we encounter a valid EOF in a hard place
}
// locationContext represents a location context
...
...
config/parser_test.go
View file @
9e12c45d
...
...
@@ -211,6 +211,53 @@ func TestParserBasicWithAlternateAddressStyles(t *testing.T) {
t
.
Fatalf
(
"Expected root for conf of %s to be '/test/www', but got: %s"
,
conf
.
Address
(),
conf
.
Root
)
}
}
p
=
&
parser
{
filename
:
"test"
}
input
=
`host:port, http://host:port, http://host, https://host:port, host`
p
.
lexer
.
load
(
strings
.
NewReader
(
input
))
confs
,
err
=
p
.
parse
()
if
err
!=
nil
{
t
.
Fatalf
(
"Expected no errors, but got '%s'"
,
err
)
}
if
len
(
confs
)
!=
5
{
t
.
Fatalf
(
"Expected 5 configurations, but got %d: %#v"
,
len
(
confs
),
confs
)
}
if
confs
[
0
]
.
Host
!=
"host"
{
t
.
Errorf
(
"Expected conf[0] Host='host', got '%#v'"
,
confs
[
0
])
}
if
confs
[
0
]
.
Port
!=
"port"
{
t
.
Errorf
(
"Expected conf[0] Port='port', got '%#v'"
,
confs
[
0
])
}
if
confs
[
1
]
.
Host
!=
"host"
{
t
.
Errorf
(
"Expected conf[1] Host='host', got '%#v'"
,
confs
[
1
])
}
if
confs
[
1
]
.
Port
!=
"port"
{
t
.
Errorf
(
"Expected conf[1] Port='port', got '%#v'"
,
confs
[
1
])
}
if
confs
[
2
]
.
Host
!=
"host"
{
t
.
Errorf
(
"Expected conf[2] Host='host', got '%#v'"
,
confs
[
2
])
}
if
confs
[
2
]
.
Port
!=
"http"
{
t
.
Errorf
(
"Expected conf[2] Port='http', got '%#v'"
,
confs
[
2
])
}
if
confs
[
3
]
.
Host
!=
"host"
{
t
.
Errorf
(
"Expected conf[3] Host='host', got '%#v'"
,
confs
[
3
])
}
if
confs
[
3
]
.
Port
!=
"port"
{
t
.
Errorf
(
"Expected conf[3] Port='port', got '%#v'"
,
confs
[
3
])
}
if
confs
[
4
]
.
Host
!=
"host"
{
t
.
Errorf
(
"Expected conf[4] Host='host', got '%#v'"
,
confs
[
4
])
}
if
confs
[
4
]
.
Port
!=
defaultPort
{
t
.
Errorf
(
"Expected conf[4] Port='%s', got '%#v'"
,
defaultPort
,
confs
[
4
]
.
Port
)
}
}
func
TestParserImport
(
t
*
testing
.
T
)
{
...
...
config/parsing.go
View file @
9e12c45d
...
...
@@ -38,18 +38,25 @@ func (p *parser) addresses() error {
// address gets host and port in a format accepted by net.Dial
address
:=
func
(
str
string
)
(
host
,
port
string
,
err
error
)
{
var
schemePort
string
if
strings
.
HasPrefix
(
str
,
"https://"
)
{
port
=
"https"
host
=
str
[
8
:
]
return
schemePort
=
"https"
str
=
str
[
8
:
]
}
else
if
strings
.
HasPrefix
(
str
,
"http://"
)
{
port
=
"http"
host
=
str
[
7
:
]
return
schemePort
=
"http"
str
=
str
[
7
:
]
}
else
if
!
strings
.
Contains
(
str
,
":"
)
{
str
+=
":"
+
defaultPort
}
host
,
port
,
err
=
net
.
SplitHostPort
(
str
)
if
err
!=
nil
&&
schemePort
!=
""
{
host
=
str
port
=
schemePort
// assume port from scheme
err
=
nil
}
return
}
...
...
@@ -88,6 +95,10 @@ func (p *parser) addresses() error {
if
!
expectingAnother
&&
p
.
line
()
>
startLine
{
break
}
if
!
hasNext
{
p
.
eof
=
true
break
// EOF
}
}
return
nil
...
...
@@ -115,6 +126,12 @@ func (p *parser) addressBlock() error {
})
p
.
scope
=
&
p
.
other
[
0
]
if
p
.
eof
{
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return
nil
}
err
:=
p
.
directives
()
if
err
!=
nil
{
return
err
...
...
main.go
View file @
9e12c45d
package
main
import
(
"errors"
"flag"
"fmt"
"log"
"net"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/config"
...
...
@@ -13,18 +17,27 @@ import (
var
(
conf
string
http2
bool
http2
bool
// TODO: temporary flag until http2 is standard
quiet
bool
cpu
string
)
func
init
()
{
flag
.
StringVar
(
&
conf
,
"conf"
,
config
.
DefaultConfigFile
,
"the configuration file to use"
)
flag
.
BoolVar
(
&
http2
,
"http2"
,
true
,
"enable HTTP/2 support"
)
// temporary flag until http2 merged into std lib
flag
.
BoolVar
(
&
http2
,
"http2"
,
true
,
"enable HTTP/2 support"
)
// TODO: temporary flag until http2 merged into std lib
flag
.
BoolVar
(
&
quiet
,
"quiet"
,
false
,
"quiet mode (no initialization output)"
)
flag
.
StringVar
(
&
cpu
,
"cpu"
,
"100%"
,
"CPU cap"
)
flag
.
Parse
()
}
func
main
()
{
var
wg
sync
.
WaitGroup
flag
.
Parse
()
// Set CPU cap
err
:=
setCPU
(
cpu
)
if
err
!=
nil
{
log
.
Fatal
(
err
)
}
// Load config from file
allConfigs
,
err
:=
config
.
Load
(
conf
)
...
...
@@ -60,6 +73,12 @@ func main() {
log
.
Println
(
err
)
}
}(
s
)
if
!
quiet
{
for
_
,
config
:=
range
configs
{
fmt
.
Println
(
config
.
Address
())
}
}
}
wg
.
Wait
()
...
...
@@ -102,3 +121,38 @@ func arrangeBindings(allConfigs []config.Config) (map[string][]config.Config, er
return
addresses
,
nil
}
// setCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%).
func
setCPU
(
cpu
string
)
error
{
var
numCPU
int
availCPU
:=
runtime
.
NumCPU
()
if
strings
.
HasSuffix
(
cpu
,
"%"
)
{
// Percent
var
percent
float32
pctStr
:=
cpu
[
:
len
(
cpu
)
-
1
]
pctInt
,
err
:=
strconv
.
Atoi
(
pctStr
)
if
err
!=
nil
||
pctInt
<
1
||
pctInt
>
100
{
return
errors
.
New
(
"Invalid CPU value: percentage must be between 1-100"
)
}
percent
=
float32
(
pctInt
)
/
100
numCPU
=
int
(
float32
(
availCPU
)
*
percent
)
}
else
{
// Number
num
,
err
:=
strconv
.
Atoi
(
cpu
)
if
err
!=
nil
||
num
<
1
{
return
errors
.
New
(
"Invalid CPU value: provide a number or percent greater than 0"
)
}
numCPU
=
num
}
if
numCPU
>
availCPU
{
numCPU
=
availCPU
}
runtime
.
GOMAXPROCS
(
numCPU
)
return
nil
}
middleware/basicauth/basicauth.go
0 → 100644
View file @
9e12c45d
package
basicauth
import
(
"net/http"
"github.com/mholt/caddy/middleware"
)
// New constructs a new BasicAuth middleware instance.
func
New
(
c
middleware
.
Controller
)
(
middleware
.
Middleware
,
error
)
{
rules
,
err
:=
parse
(
c
)
if
err
!=
nil
{
return
nil
,
err
}
basic
:=
BasicAuth
{
Rules
:
rules
,
}
return
func
(
next
middleware
.
Handler
)
middleware
.
Handler
{
basic
.
Next
=
next
return
basic
},
nil
}
// ServeHTTP implements the middleware.Handler interface.
func
(
a
BasicAuth
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
for
_
,
rule
:=
range
a
.
Rules
{
for
_
,
res
:=
range
rule
.
Resources
{
if
!
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
res
)
{
continue
}
// Path matches; parse auth header
username
,
password
,
ok
:=
r
.
BasicAuth
()
// Check credentials
if
!
ok
||
username
!=
rule
.
Username
||
password
!=
rule
.
Password
{
w
.
Header
()
.
Set
(
"WWW-Authenticate"
,
"Basic"
)
return
http
.
StatusUnauthorized
,
nil
}
// "It's an older code, sir, but it checks out. I was about to clear them."
return
a
.
Next
.
ServeHTTP
(
w
,
r
)
}
}
// Pass-thru when no paths match
return
a
.
Next
.
ServeHTTP
(
w
,
r
)
}
func
parse
(
c
middleware
.
Controller
)
([]
Rule
,
error
)
{
var
rules
[]
Rule
for
c
.
Next
()
{
var
rule
Rule
args
:=
c
.
RemainingArgs
()
switch
len
(
args
)
{
case
2
:
rule
.
Username
=
args
[
0
]
rule
.
Password
=
args
[
1
]
for
c
.
NextBlock
()
{
rule
.
Resources
=
append
(
rule
.
Resources
,
c
.
Val
())
if
c
.
NextArg
()
{
return
rules
,
c
.
Err
(
"Expecting only one resource per line (extra '"
+
c
.
Val
()
+
"')"
)
}
}
case
3
:
rule
.
Resources
=
append
(
rule
.
Resources
,
args
[
0
])
rule
.
Username
=
args
[
1
]
rule
.
Password
=
args
[
2
]
default
:
return
rules
,
c
.
ArgErr
()
}
rules
=
append
(
rules
,
rule
)
}
return
rules
,
nil
}
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type
BasicAuth
struct
{
Next
middleware
.
Handler
Rules
[]
Rule
}
// Rule represents a BasicAuth rule. A username and password
// combination protect the associated resources, which are
// file or directory paths.
type
Rule
struct
{
Username
string
Password
string
Resources
[]
string
}
middleware/browse/browse.go
View file @
9e12c45d
...
...
@@ -3,6 +3,7 @@
package
browse
import
(
"bytes"
"fmt"
"html/template"
"io/ioutil"
...
...
@@ -122,8 +123,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
}
defer
file
.
Close
()
w
.
Header
()
.
Set
(
"Content-Type"
,
"text/html; charset=utf-8"
)
files
,
err
:=
file
.
Readdir
(
-
1
)
if
err
!=
nil
{
return
http
.
StatusForbidden
,
err
...
...
@@ -182,12 +181,15 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
Items
:
fileinfos
,
}
// TODO: Don't write to w until we know there wasn't an erro
r
err
=
bc
.
Template
.
Execute
(
w
,
listing
)
var
buf
bytes
.
Buffe
r
err
=
bc
.
Template
.
Execute
(
&
buf
,
listing
)
if
err
!=
nil
{
return
http
.
StatusInternalServerError
,
err
}
w
.
Header
()
.
Set
(
"Content-Type"
,
"text/html; charset=utf-8"
)
buf
.
WriteTo
(
w
)
return
http
.
StatusOK
,
nil
}
...
...
middleware/browse/template.go
View file @
9e12c45d
...
...
@@ -11,7 +11,7 @@ const defaultTemplate = `<!DOCTYPE html>
body {
padding: 1% 2%;
font: 16px
sans-serif
;
font: 16px
Arial
;
}
header {
...
...
@@ -60,7 +60,7 @@ th {
text-align: left;
}
@media (max-width:
65
0px) {
@media (max-width:
70
0px) {
.hideable {
display: none;
}
...
...
@@ -71,7 +71,7 @@ th {
header,
header h1 {
font-size: 1
4
px;
font-size: 1
6
px;
}
header {
...
...
@@ -80,7 +80,7 @@ th {
width: 100%;
background: #333;
color: #FFF;
padding: 1
0
px;
padding: 1
5
px;
text-align: center;
}
...
...
@@ -95,8 +95,8 @@ th {
position: absolute;
left: 0;
top: 0;
width:
35
px;
height:
2
8px;
width:
40
px;
height:
4
8px;
font-size: 35px;
}
...
...
@@ -105,7 +105,7 @@ th {
}
main {
margin-top:
5
0px;
margin-top:
7
0px;
}
}
</style>
...
...
middleware/log/log.go
View file @
9e12c45d
...
...
@@ -88,6 +88,8 @@ func parse(c middleware.Controller) ([]LogRule, error) {
format
=
commonLogFormat
case
"{combined}"
:
format
=
combinedLogFormat
default
:
format
=
args
[
2
]
}
}
...
...
middleware/markdown/markdown.go
View file @
9e12c45d
...
...
@@ -135,7 +135,7 @@ func parse(c middleware.Controller) ([]MarkdownConfig, error) {
}
// Get the path scope
if
!
c
.
NextArg
()
{
if
!
c
.
NextArg
()
||
c
.
Val
()
==
"{"
{
return
mdconfigs
,
c
.
ArgErr
()
}
md
.
PathScope
=
c
.
Val
()
...
...
middleware/recorder.go
View file @
9e12c45d
package
middleware
import
"net/http"
import
(
"net/http"
"time"
)
// responseRecorder is a type of ResponseWriter that captures
// the status code written to it and also the size of the body
...
...
@@ -12,6 +15,7 @@ type responseRecorder struct {
http
.
ResponseWriter
status
int
size
int
start
time
.
Time
}
// NewResponseRecorder makes and returns a new responseRecorder,
...
...
@@ -24,6 +28,7 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder {
return
&
responseRecorder
{
ResponseWriter
:
w
,
status
:
http
.
StatusOK
,
start
:
time
.
Now
(),
}
}
...
...
middleware/replacer.go
View file @
9e12c45d
...
...
@@ -52,6 +52,7 @@ func NewReplacer(r *http.Request, rr *responseRecorder) replacer {
}(),
"{status}"
:
strconv
.
Itoa
(
rr
.
status
),
"{size}"
:
strconv
.
Itoa
(
rr
.
size
),
"{latency}"
:
time
.
Since
(
rr
.
start
)
.
String
(),
}
// Header placeholders
...
...
middleware/templates/templates.go
View file @
9e12c45d
package
templates
import
(
"bytes"
"net/http"
"path"
"text/template"
...
...
@@ -47,10 +48,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
}
// Execute it
err
=
tpl
.
Execute
(
w
,
ctx
)
var
buf
bytes
.
Buffer
err
=
tpl
.
Execute
(
&
buf
,
ctx
)
if
err
!=
nil
{
return
http
.
StatusInternalServerError
,
err
}
buf
.
WriteTo
(
w
)
return
http
.
StatusOK
,
nil
}
...
...
server/server.go
View file @
9e12c45d
...
...
@@ -11,7 +11,6 @@ import (
"net/http"
"os"
"os/signal"
"runtime"
"github.com/bradfitz/http2"
"github.com/mholt/caddy/config"
...
...
@@ -21,9 +20,9 @@ import (
// static content at a particular address (host and port).
type
Server
struct
{
HTTP2
bool
// temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
address
string
tls
bool
vhosts
map
[
string
]
virtualHost
address
string
// the actual address for net.Listen to listen on
tls
bool
// whether this server is serving all HTTPS hosts or not
vhosts
map
[
string
]
virtualHost
// virtual hosts keyed by their address
}
// New creates a new Server which will bind to addr and serve
...
...
@@ -41,11 +40,6 @@ func New(addr string, configs []config.Config, tls bool) (*Server, error) {
return
nil
,
fmt
.
Errorf
(
"Cannot serve %s - host already defined for address %s"
,
conf
.
Address
(),
s
.
address
)
}
// Use all CPUs (if needed) by default
if
conf
.
MaxCPU
==
0
{
conf
.
MaxCPU
=
runtime
.
NumCPU
()
}
vh
:=
virtualHost
{
config
:
conf
}
// Build middleware stack
...
...
@@ -73,7 +67,7 @@ func (s *Server) Serve() error {
}
for
_
,
vh
:=
range
s
.
vhosts
{
// Execute startup functions
// Execute startup functions
now
for
_
,
start
:=
range
vh
.
config
.
Startup
{
err
:=
start
()
if
err
!=
nil
{
...
...
@@ -81,13 +75,8 @@ func (s *Server) Serve() error {
}
}
// Use highest procs value across all configurations
if
vh
.
config
.
MaxCPU
>
0
&&
vh
.
config
.
MaxCPU
>
runtime
.
GOMAXPROCS
(
0
)
{
runtime
.
GOMAXPROCS
(
vh
.
config
.
MaxCPU
)
}
if
len
(
vh
.
config
.
Shutdown
)
>
0
{
// Execute shutdown commands on exit
if
len
(
vh
.
config
.
Shutdown
)
>
0
{
go
func
()
{
interrupt
:=
make
(
chan
os
.
Signal
,
1
)
signal
.
Notify
(
interrupt
,
os
.
Interrupt
,
os
.
Kill
)
// TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only)
...
...
server/virtualhost.go
View file @
9e12c45d
...
...
@@ -9,7 +9,7 @@ import (
// virtualHost represents a virtual host/server. While a Server
// is what actually binds to the address, a user may want to serve
// multiple sites on a single address, and
what
is what a
// multiple sites on a single address, and
this
is what a
// virtualHost allows us to do.
type
virtualHost
struct
{
config
config
.
Config
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment