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
cd53ec9b
Commit
cd53ec9b
authored
Apr 25, 2015
by
Matt Holt
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1 from thomas4019/master
adding support for php including clean urls and wordpress permalinks
parents
24d9d237
1ac32a52
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
225 additions
and
47 deletions
+225
-47
middleware/fastcgi/fastcgi.go
middleware/fastcgi/fastcgi.go
+213
-47
middleware/fastcgi/fcgiclient.go
middleware/fastcgi/fcgiclient.go
+12
-0
No files found.
middleware/fastcgi/fastcgi.go
View file @
cd53ec9b
...
...
@@ -4,10 +4,14 @@
package
fastcgi
import
(
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
...
...
@@ -15,7 +19,10 @@ import (
// New generates a new FastCGI middleware.
func
New
(
c
middleware
.
Controller
)
(
middleware
.
Middleware
,
error
)
{
root
:=
c
.
Root
()
root
,
err
:=
filepath
.
Abs
(
c
.
Root
())
if
err
!=
nil
{
return
nil
,
err
}
rules
,
err
:=
parse
(
c
)
if
err
!=
nil
{
...
...
@@ -23,91 +30,180 @@ func New(c middleware.Controller) (middleware.Middleware, error) {
}
return
func
(
next
middleware
.
Handler
)
middleware
.
Handler
{
return
Handler
{
Next
:
next
,
Rules
:
rules
,
Root
:
root
}
return
Handler
{
Next
:
next
,
Rules
:
rules
,
Root
:
root
,
SoftwareName
:
"Caddy"
,
// TODO: Once generators are not in the same pkg as handler, obtain this from some global const
SoftwareVersion
:
""
,
// TODO: Get this from some global const too
// TODO: Set ServerName and ServerPort to correct values... (as user defined in config)
}
},
nil
}
// Handler is a middleware type that can handle requests as a FastCGI client.
type
Handler
struct
{
Next
middleware
.
Handler
Root
string
Root
string
// must be absolute path to site root
Rules
[]
Rule
// These are sent to CGI scripts in env variables
SoftwareName
string
SoftwareVersion
string
ServerName
string
ServerPort
string
}
// ServeHTTP satisfies the middleware.Handler interface.
func
(
h
Handler
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
servedFcgi
:=
false
for
_
,
rule
:=
range
h
.
Rules
{
if
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
rule
.
Path
)
{
servedFcgi
=
true
// In addition to matching the path, a request must meet some
// other criteria before being proxied as FastCGI. For example,
// we probably want to exclude static assets (CSS, JS, images...)
// but we also want to be flexible for the script we proxy to.
// Get absolute file path
s
absPath
,
err
:=
filepath
.
Abs
(
h
.
Root
+
r
.
URL
.
Path
)
if
err
!=
nil
{
return
http
.
StatusInternalServerError
,
err
}
// These criteria work well in this order for PHP site
s
if
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
rule
.
Path
)
&&
(
r
.
URL
.
Path
[
len
(
r
.
URL
.
Path
)
-
1
]
==
'/'
||
strings
.
HasSuffix
(
r
.
URL
.
Path
,
rule
.
Ext
)
||
!
h
.
exists
(
r
.
URL
.
Path
))
{
//
Get absolute file path to website roo
t
absRootPath
,
err
:=
filepath
.
Abs
(
h
.
Root
)
//
Create environment for CGI scrip
t
env
,
err
:=
h
.
buildEnv
(
r
,
rule
)
if
err
!=
nil
{
return
http
.
StatusInternalServerError
,
err
}
// Separate remote IP and port
var
ip
,
port
string
if
idx
:=
strings
.
Index
(
r
.
RemoteAddr
,
":"
);
idx
>
-
1
{
ip
=
r
.
RemoteAddr
[
idx
:
]
port
=
r
.
RemoteAddr
[
:
idx
]
}
else
{
ip
=
r
.
RemoteAddr
}
// TODO: Do we really have to make this map from scratch for each request?
// TODO: We have quite a few more to map, too.
env
:=
make
(
map
[
string
]
string
)
env
[
"SERVER_SOFTWARE"
]
=
"caddy"
// TODO: Obtain version info...
env
[
"SERVER_PROTOCOL"
]
=
r
.
Proto
env
[
"SCRIPT_FILENAME"
]
=
absPath
env
[
"REMOTE_ADDR"
]
=
ip
env
[
"REMOTE_PORT"
]
=
port
env
[
"REQUEST_METHOD"
]
=
r
.
Method
env
[
"QUERY_STRING"
]
=
r
.
URL
.
RawQuery
env
[
"DOCUMENT_URI"
]
=
r
.
URL
.
Path
env
[
"DOCUMENT_ROOT"
]
=
absRootPath
// Connect to FastCGI gateway
fcgi
,
err
:=
Dial
(
"tcp"
,
rule
.
Address
)
if
err
!=
nil
{
return
http
.
StatusBadGateway
,
err
}
resp
,
err
:=
fcgi
.
Get
(
env
)
if
err
!=
nil
&&
err
!=
io
.
EOF
{
return
http
.
StatusBadGateway
,
err
// TODO: Allow more methods (requires refactoring fcgiclient first...)
var
resp
*
http
.
Response
switch
r
.
Method
{
case
"GET"
:
resp
,
err
=
fcgi
.
Get
(
env
)
case
"POST"
:
l
,
_
:=
strconv
.
Atoi
(
r
.
Header
.
Get
(
"Content-Length"
))
resp
,
err
=
fcgi
.
Post
(
env
,
r
.
Header
.
Get
(
"Content-Type"
),
r
.
Body
,
l
)
default
:
return
http
.
StatusMethodNotAllowed
,
nil
}
defer
resp
.
Body
.
Close
()
body
,
err
:=
ioutil
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
if
err
!=
nil
&&
err
!=
io
.
EOF
{
return
http
.
StatusBadGateway
,
err
}
// Write the response header
for
key
,
vals
:=
range
resp
.
Header
{
for
_
,
val
:=
range
vals
{
w
.
Header
()
.
Add
(
key
,
val
)
}
}
w
.
WriteHeader
(
resp
.
StatusCode
)
body
,
err
:=
ioutil
.
ReadAll
(
resp
.
Body
)
fmt
.
Printf
(
"%s"
,
body
)
fmt
.
Printf
(
"%d
\n
"
,
resp
.
StatusCode
)
fmt
.
Printf
(
"%d
\n
"
,
len
(
body
))
w
.
Write
(
body
)
break
return
resp
.
StatusCode
,
nil
}
}
if
!
servedFcgi
{
return
h
.
Next
.
ServeHTTP
(
w
,
r
)
}
func
(
h
Handler
)
exists
(
path
string
)
bool
{
if
_
,
err
:=
os
.
Stat
(
h
.
Root
+
path
);
err
==
nil
{
return
true
}
return
false
}
func
(
h
Handler
)
buildEnv
(
r
*
http
.
Request
,
rule
Rule
)
(
map
[
string
]
string
,
error
)
{
var
env
map
[
string
]
string
// Get absolute path of requested resource
absPath
,
err
:=
filepath
.
Abs
(
h
.
Root
+
r
.
URL
.
Path
)
if
err
!=
nil
{
return
env
,
err
}
// Separate remote IP and port; more lenient than net.SplitHostPort
var
ip
,
port
string
if
idx
:=
strings
.
Index
(
r
.
RemoteAddr
,
":"
);
idx
>
-
1
{
ip
=
r
.
RemoteAddr
[
:
idx
]
port
=
r
.
RemoteAddr
[
idx
+
1
:
]
}
else
{
ip
=
r
.
RemoteAddr
}
// Split path in preparation for env variables
splitPos
:=
strings
.
Index
(
r
.
URL
.
Path
,
rule
.
SplitPath
)
var
docURI
,
scriptName
,
scriptFilename
,
pathInfo
string
if
splitPos
==
-
1
{
// Request doesn't have the extension, so assume index file
docURI
=
"/"
+
rule
.
IndexFile
scriptName
=
"/"
+
rule
.
IndexFile
scriptFilename
=
h
.
Root
+
"/"
+
rule
.
IndexFile
pathInfo
=
r
.
URL
.
Path
}
else
{
// Request has the extension; path was split successfully
docURI
=
r
.
URL
.
Path
[
:
splitPos
+
len
(
rule
.
SplitPath
)]
pathInfo
=
r
.
URL
.
Path
[
splitPos
+
len
(
rule
.
SplitPath
)
:
]
scriptName
=
r
.
URL
.
Path
scriptFilename
=
absPath
}
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
env
=
map
[
string
]
string
{
// Variables defined in CGI 1.1 spec
"AUTH_TYPE"
:
""
,
// Not used
"CONTENT_LENGTH"
:
r
.
Header
.
Get
(
"Content-Length"
),
"CONTENT_TYPE"
:
r
.
Header
.
Get
(
"Content-Type"
),
"GATEWAY_INTERFACE"
:
"CGI/1.1"
,
"PATH_INFO"
:
pathInfo
,
"PATH_TRANSLATED"
:
h
.
Root
+
"/"
+
pathInfo
,
// Source for path_translated: http://www.oreilly.com/openbook/cgi/ch02_04.html
"QUERY_STRING"
:
r
.
URL
.
RawQuery
,
"REMOTE_ADDR"
:
ip
,
"REMOTE_HOST"
:
ip
,
// For speed, remote host lookups disabled
"REMOTE_PORT"
:
port
,
"REMOTE_IDENT"
:
""
,
// Not used
"REMOTE_USER"
:
""
,
// Not used
"REQUEST_METHOD"
:
r
.
Method
,
"SERVER_NAME"
:
h
.
ServerName
,
"SERVER_PORT"
:
h
.
ServerPort
,
"SERVER_PROTOCOL"
:
r
.
Proto
,
"SERVER_SOFTWARE"
:
h
.
SoftwareName
+
"/"
+
h
.
SoftwareVersion
,
// Other variables
"DOCUMENT_ROOT"
:
h
.
Root
,
"DOCUMENT_URI"
:
docURI
,
"HTTP_HOST"
:
r
.
Host
,
// added here, since not always part of headers
"REQUEST_URI"
:
r
.
URL
.
RequestURI
(),
"SCRIPT_FILENAME"
:
scriptFilename
,
"SCRIPT_NAME"
:
scriptName
,
}
return
0
,
nil
// Add all HTTP headers to env variables
for
field
,
val
:=
range
r
.
Header
{
header
:=
strings
.
ToUpper
(
field
)
header
=
headerNameReplacer
.
Replace
(
header
)
// We don't want to pass the encoding header to prevent the fastcgi server from gzipping
// TODO: is there a better way.
if
header
!=
"ACCEPT_ENCODING"
{
env
[
"HTTP_"
+
header
]
=
strings
.
Join
(
val
,
", "
)
}
}
return
env
,
nil
}
func
parse
(
c
middleware
.
Controller
)
([]
Rule
,
error
)
{
...
...
@@ -115,16 +211,86 @@ func parse(c middleware.Controller) ([]Rule, error) {
for
c
.
Next
()
{
var
rule
Rule
if
!
c
.
Args
(
&
rule
.
Path
,
&
rule
.
Address
)
{
args
:=
c
.
RemainingArgs
()
switch
len
(
args
)
{
case
0
:
return
rules
,
c
.
ArgErr
()
case
1
:
rule
.
Path
=
"/"
rule
.
Address
=
args
[
0
]
case
2
:
rule
.
Path
=
args
[
0
]
rule
.
Address
=
args
[
1
]
case
3
:
rule
.
Path
=
args
[
0
]
rule
.
Address
=
args
[
1
]
err
:=
preset
(
args
[
2
],
&
rule
)
if
err
!=
nil
{
return
rules
,
c
.
Err
(
"Invalid fastcgi rule preset '"
+
args
[
2
]
+
"'"
)
}
}
for
c
.
NextBlock
()
{
switch
c
.
Val
()
{
case
"ext"
:
if
!
c
.
NextArg
()
{
return
rules
,
c
.
ArgErr
()
}
rule
.
Ext
=
c
.
Val
()
case
"split"
:
if
!
c
.
NextArg
()
{
return
rules
,
c
.
ArgErr
()
}
rule
.
SplitPath
=
c
.
Val
()
case
"index"
:
if
!
c
.
NextArg
()
{
return
rules
,
c
.
ArgErr
()
}
rule
.
IndexFile
=
c
.
Val
()
}
}
rules
=
append
(
rules
,
rule
)
}
return
rules
,
nil
}
// preset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func
preset
(
name
string
,
rule
*
Rule
)
error
{
switch
name
{
case
"php"
:
rule
.
Ext
=
".php"
rule
.
SplitPath
=
".php"
rule
.
IndexFile
=
"index.php"
default
:
return
errors
.
New
(
name
+
" is not a valid preset name"
)
}
return
nil
}
// Rule represents a FastCGI handling rule.
type
Rule
struct
{
Path
,
Address
string
// The base path to match. Required.
Path
string
// The address of the FastCGI server. Required.
Address
string
// Always process files with this extension with fastcgi.
Ext
string
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
SplitPath
string
// If the URL does not indicate a file, an index file with this name will be assumed.
IndexFile
string
}
var
headerNameReplacer
=
strings
.
NewReplacer
(
" "
,
"_"
,
"-"
,
"_"
)
middleware/fastcgi/fcgiclient.go
View file @
cd53ec9b
...
...
@@ -26,6 +26,7 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
)
...
...
@@ -373,6 +374,17 @@ func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.
}
resp
.
Header
=
http
.
Header
(
mimeHeader
)
if
resp
.
Header
.
Get
(
"Status"
)
!=
""
{
statusParts
:=
strings
.
SplitN
(
resp
.
Header
.
Get
(
"Status"
),
" "
,
2
)
resp
.
StatusCode
,
err
=
strconv
.
Atoi
(
statusParts
[
0
])
if
err
!=
nil
{
return
}
resp
.
Status
=
statusParts
[
1
]
}
else
{
resp
.
StatusCode
=
http
.
StatusOK
}
// TODO: fixTransferEncoding ?
resp
.
TransferEncoding
=
resp
.
Header
[
"Transfer-Encoding"
]
resp
.
ContentLength
,
_
=
strconv
.
ParseInt
(
resp
.
Header
.
Get
(
"Content-Length"
),
10
,
64
)
...
...
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