Skip to content

Commit 1ca65d8

Browse files
author
Moriyoshi Koizumi
committed
FastCGI upstream support
1 parent ec17112 commit 1ca65d8

5 files changed

Lines changed: 453 additions & 2 deletions

File tree

example.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ hosts:
3030
# you can add arbitrary headers to every request that matches the pattern
3131
headers:
3232
X-Forwarded-Proto: https
33+
http://fastcgi.example.com:
34+
- ^(((?:/.*)*/[^/]+\.php)(/.*|$)): fastcgi://localhost$1
35+
headers:
36+
X-Cgi-Script-Filename: /document_root$2
37+
X-Cgi-Script-Name: $2
38+
X-Cgi-Path-Info: $3

fastcgi.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2016 Moriyoshi Koizumi
3+
*
4+
* All rights reserved.
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are
8+
* met:
9+
*
10+
* * Redistributions of source code must retain the above copyright
11+
* notice, this list of conditions and the following disclaimer.
12+
* * Redistributions in binary form must reproduce the above
13+
* copyright notice, this list of conditions and the following disclaimer
14+
* in the documentation and/or other materials provided with the
15+
* distribution.
16+
* * Neither the name of Elazar Leibovich. nor the names of its
17+
* contributors may be used to endorse or promote products derived from
18+
* this software without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package main
33+
34+
import (
35+
"bufio"
36+
"bytes"
37+
"fmt"
38+
"github.com/Sirupsen/logrus"
39+
"github.com/moriyoshi/devproxy/fcgiclient"
40+
"io/ioutil"
41+
"net"
42+
"net/http"
43+
"net/textproto"
44+
)
45+
46+
type fastCGIRoundTripper struct {
47+
Logger *logrus.Logger
48+
reqId uint16
49+
}
50+
51+
func pop(h http.Header, k string) string {
52+
v, ok := h[k]
53+
if !ok {
54+
return ""
55+
}
56+
delete(h, k)
57+
return v[0]
58+
}
59+
60+
func (rt *fastCGIRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
61+
env := map[string][]byte{
62+
"REQUEST_METHOD": []byte(req.Method),
63+
"REQUEST_URI": []byte(req.URL.Path),
64+
"HTTP_HOST": []byte(req.Host),
65+
"HTTP_COOKIE": []byte(req.Header.Get("Cookie")),
66+
"HTTP_REFERER": []byte(req.Header.Get("Referer")),
67+
"HTTP_USER_AGENT": []byte(req.Header.Get("User-Agent")),
68+
"SCRIPT_FILENAME": []byte(pop(req.Header, "X-Cgi-Script-Filename")),
69+
"SCRIPT_NAME": []byte(pop(req.Header, "X-Cgi-Script-Name")),
70+
"PATH_INFO": []byte(pop(req.Header, "X-Cgi-Path-Info")),
71+
"SERVER_PROTOCOL": []byte(req.Proto),
72+
"REMOTE_ADDR": []byte(req.Header.Get("X-Forwarded-For")),
73+
"QUERY_STRING": []byte(req.URL.RawQuery),
74+
}
75+
if req.Header.Get("X-Forwarded-Proto") == "https" {
76+
env["HTTPS"] = []byte("on")
77+
}
78+
pair := splitHostPort(req.URL.Host)
79+
unix := len(pair.Host) > 0 && pair.Host[0] == '/'
80+
if !unix && pair.Port == "" {
81+
pair.Port = "9000"
82+
}
83+
var proto string
84+
if unix {
85+
proto = "unix"
86+
} else {
87+
proto = "tcp"
88+
}
89+
conn, err := net.Dial(proto, pair.String())
90+
if err != nil {
91+
return nil, err
92+
}
93+
defer conn.Close()
94+
fcgi, err := fcgiclient.New(conn)
95+
if err != nil {
96+
return nil, err
97+
}
98+
var reqBody []byte
99+
if req.Body != nil {
100+
reqBody, err = ioutil.ReadAll(req.Body)
101+
if err != nil {
102+
return nil, err
103+
}
104+
}
105+
respBytes, errBytes, err := fcgi.Request(env, reqBody, rt.reqId)
106+
rt.reqId += 1
107+
if err != nil {
108+
return nil, err
109+
}
110+
if errBytes != nil {
111+
return nil, fmt.Errorf("Upstream returned error: %s", string(errBytes))
112+
}
113+
headers, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(respBytes))).ReadMIMEHeader()
114+
if err != nil {
115+
return nil, err
116+
}
117+
status := headers.Get("Status")
118+
if status == "" {
119+
status = "200 OK"
120+
}
121+
_respBytes := make([]byte, 0, 9+len(status)+2+len(respBytes))
122+
_respBytes = append(_respBytes, 'H', 'T', 'T', 'P', '/', '1', '.', '1', ' ')
123+
_respBytes = append(_respBytes, []byte(status)...)
124+
_respBytes = append(_respBytes, '\r', '\n')
125+
_respBytes = append(_respBytes, respBytes...)
126+
return http.ReadResponse(bufio.NewReader(bytes.NewReader(_respBytes)), req)
127+
}

0 commit comments

Comments
 (0)