Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
cyfdecyf committed Jul 1, 2013
2 parents 429c6cf + 3bffc23 commit ace634c
Show file tree
Hide file tree
Showing 18 changed files with 311 additions and 114 deletions.
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
language: go
go:
- 1.1
env:
- TRAVIS="yes"
install:
- go get github.com/shadowsocks/shadowsocks-go/shadowsocks
- go get github.com/cyfdecyf/bufio
- go get github.com/cyfdecyf/leakybuf
- go get code.google.com/p/go.crypto/blowfish
- go get code.google.com/p/go.crypto/cast5
script:
- cd $TRAVIS_BUILD_DIR
- pushd $TRAVIS_BUILD_DIR
- go test -v
- ./script/test.sh
- popd
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
0.7.2 (2013-07-01)
* Close idle server connections earlier: avoid opening too many sockets
* Support authenticating multiple users (can limit port for each user)

0.7.1 (2013-06-08)
* Fix parent proxy fallback bug

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

COW 是一个利用二级代理帮助自动化翻墙的 HTTP 代理服务器。它能自动检测被墙网站,且仅对被墙网站使用二级代理。

当前版本:0.7.0 [CHANGELOG](CHANGELOG)
当前版本:0.7.2 [CHANGELOG](CHANGELOG)
[![Build Status](https://travis-ci.org/cyfdecyf/cow.png?branch=master)](https://travis-ci.org/cyfdecyf/cow)

**如果要发 pull request,请在最新的 develop branch 上进行开发。**
Expand Down
158 changes: 122 additions & 36 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package main

import (
"bytes"
"errors"
"fmt"
"github.com/cyfdecyf/bufio"
"net"
"os"
"strconv"
"strings"
"text/template"
Expand All @@ -29,25 +32,61 @@ type netAddr struct {
mask net.IPMask
}

type authUser struct {
// user name is the key to auth.user, no need to store here
passwd string
ha1 string // used in request digest, initialized ondemand
port uint16 // 0 means any port
}

var auth struct {
required bool

user string
passwd string
ha1 string // used in request digest
user map[string]*authUser

allowedClient []netAddr

authed *TimeoutSet // cache authentication based on client ip
authed *TimeoutSet // cache authenticated users based on ip

template *template.Template
}

func (au *authUser) initHA1(user string) {
if au.ha1 == "" {
au.ha1 = md5sum(user + ":" + authRealm + ":" + au.passwd)
}
}

func parseUserPasswd(userPasswd string) (user string, au *authUser, err error) {
arr := strings.Split(userPasswd, ":")
n := len(arr)
if n == 1 || n > 3 {
err = errors.New("user password: " + userPasswd +
" syntax wrong, should be username:password[:port]")
return
}
user, passwd := arr[0], arr[1]
if user == "" || passwd == "" {
err = errors.New("user password " + userPasswd +
" should not contain empty user name or password")
return "", nil, err
}
var port int
if n == 3 && arr[2] != "" {
port, err = strconv.Atoi(arr[2])
if err != nil || port <= 0 || port > 0xffff {
err = errors.New("user password: " + userPasswd + " invalid port")
return "", nil, err
}
}
au = &authUser{passwd, "", uint16(port)}
return user, au, nil
}

func parseAllowedClient(val string) {
if val == "" {
return
}
auth.required = true
arr := strings.Split(val, ",")
auth.allowedClient = make([]netAddr, len(arr))
for i, v := range arr {
Expand All @@ -58,13 +97,13 @@ func parseAllowedClient(val string) {
}
ip := net.ParseIP(ipAndMask[0])
if ip == nil {
Fatal("allowedClient syntax error %s: ip address not valid\n", s)
Fatalf("allowedClient syntax error %s: ip address not valid\n", s)
}
var mask net.IPMask
if len(ipAndMask) == 2 {
nbit, err := strconv.Atoi(ipAndMask[1])
if err != nil {
Fatal("allowedClient syntax error %s: %v\n", s, err)
Fatalf("allowedClient syntax error %s: %v\n", s, err)
}
if nbit > 32 {
Fatal("allowedClient error: mask number should <= 32")
Expand All @@ -77,38 +116,63 @@ func parseAllowedClient(val string) {
}
}

func parseUserPasswd(val string) {
func addUserPasswd(val string) {
if val == "" {
return
}
auth.required = true
// password format checking is done in checkConfig in config.go
arr := strings.SplitN(val, ":", 2)
auth.user, auth.passwd = arr[0], arr[1]
user, au, err := parseUserPasswd(val)
debug.Println("user:", user, "port:", au.port)
if err != nil {
Fatal(err)
}
if _, ok := auth.user[user]; ok {
Fatal("duplicate user:", user)
}
auth.user[user] = au
}

func initAuth() {
parseUserPasswd(config.UserPasswd)
parseAllowedClient(config.AllowedClient)

if !auth.required {
func loadUserPasswdFile(file string) {
if file == "" {
return
}
f, err := os.Open(file)
if err != nil {
Fatal("error opening user passwd fle:", err)
}

auth.authed = NewTimeoutSet(time.Duration(config.AuthTimeout) * time.Hour)
r := bufio.NewReader(f)
s := bufio.NewScanner(r)
for s.Scan() {
addUserPasswd(s.Text())
}
f.Close()
}

if auth.user == "" {
func initAuth() {
if config.UserPasswd != "" ||
config.UserPasswdFile != "" ||
config.AllowedClient != "" {
auth.required = true
} else {
return
}
auth.ha1 = md5sum(auth.user + ":" + authRealm + ":" + auth.passwd)

auth.user = make(map[string]*authUser)

addUserPasswd(config.UserPasswd)
loadUserPasswdFile(config.UserPasswdFile)
parseAllowedClient(config.AllowedClient)

auth.authed = NewTimeoutSet(time.Duration(config.AuthTimeout) * time.Hour)

rawTemplate := "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: Digest realm=\"" + authRealm + "\", nonce=\"{{.Nonce}}\", qop=\"auth\"\r\n" +
"Content-Type: text/html\r\n" +
"Cache-Control: no-cache\r\n" +
"Content-Length: " + fmt.Sprintf("%d", len(authRawBodyTmpl)) + "\r\n\r\n" + authRawBodyTmpl
var err error
if auth.template, err = template.New("auth").Parse(rawTemplate); err != nil {
Fatal("Internal error generating auth template:", err)
Fatal("internal error generating auth template:", err)
}
}

Expand All @@ -123,11 +187,14 @@ func Authenticate(conn *clientConn, r *Request) (err error) {
if authIP(clientIP) { // IP is allowed
return
}
// No user specified
if auth.user == "" {
sendErrorPage(conn, "403 Forbidden", "Access forbidden", "You are not allowed to use the proxy.")
return errShouldClose
}
/*
// No user specified
if auth.user == "" {
sendErrorPage(conn, "403 Forbidden", "Access forbidden",
"You are not allowed to use the proxy.")
return errShouldClose
}
*/
err = authUserPasswd(conn, r)
if err == nil {
auth.authed.add(clientIP)
Expand Down Expand Up @@ -175,7 +242,7 @@ func calcRequestDigest(kv map[string]string, ha1, method string) string {
return md5sum(buf.String())
}

func checkProxyAuthorization(r *Request) error {
func checkProxyAuthorization(conn *clientConn, r *Request) error {
debug.Println("authorization:", r.ProxyAuthorization)
arr := strings.SplitN(r.ProxyAuthorization, " ", 2)
if len(arr) != 2 {
Expand All @@ -200,21 +267,39 @@ func checkProxyAuthorization(r *Request) error {
if time.Now().Sub(time.Unix(nonceTime, 0)) > time.Minute {
return errAuthRequired
}
if authHeader["username"] != auth.user {
errl.Println("auth: username mismatch:", authHeader["username"])

user := authHeader["username"]
au, ok := auth.user[user]
if !ok {
errl.Println("auth: no such user:", authHeader["username"])
return errAuthRequired
}

if au.port != 0 {
// check port
_, portStr := splitHostPort(conn.LocalAddr().String())
port, _ := strconv.Atoi(portStr)
if uint16(port) != au.port {
errl.Println("auth: user", user, "port not match")
return errAuthRequired
}
}

if authHeader["qop"] != "auth" {
errl.Println("auth: qop wrong:", authHeader["qop"])
return errBadRequest
msg := "auth: qop wrong: " + authHeader["qop"]
errl.Println(msg)
return errors.New(msg)
}

response, ok := authHeader["response"]
if !ok {
errl.Println("auth: no request-digest")
return errBadRequest
msg := "auth: no request-digest"
errl.Println(msg)
return errors.New(msg)
}

digest := calcRequestDigest(authHeader, auth.ha1, r.Method)
au.initHA1(user)
digest := calcRequestDigest(authHeader, au.ha1, r.Method)
if response == digest {
return nil
}
Expand All @@ -225,13 +310,14 @@ func checkProxyAuthorization(r *Request) error {
func authUserPasswd(conn *clientConn, r *Request) (err error) {
if r.ProxyAuthorization != "" {
// client has sent authorization header
err = checkProxyAuthorization(r)
err = checkProxyAuthorization(conn, r)
if err == nil {
return
} else if err != errAuthRequired {
sendErrorPage(conn, errCodeBadReq, "Bad authorization request", "")
sendErrorPage(conn, errCodeBadReq, "Bad authorization request", err.Error())
return
}
// auth required to through the following
}

nonce := genNonce()
Expand Down
34 changes: 34 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ import (
"testing"
)

func TestParseUserPasswd(t *testing.T) {
testData := []struct {
val string
user string
au *authUser
}{
{"foo:bar", "foo", &authUser{"bar", "", 0}},
{"foo:bar:-1", "", nil},
{"hello:world:", "hello", &authUser{"world", "", 0}},
{"hello:world:0", "", nil},
{"hello:world:1024", "hello", &authUser{"world", "", 1024}},
{"hello:world:65535", "hello", &authUser{"world", "", 65535}},
}

for _, td := range testData {
user, au, err := parseUserPasswd(td.val)
if td.au == nil {
if err == nil {
t.Error(td.val, "should return error")
}
continue
}
if td.user != user {
t.Error(td.val, "user should be:", td.user, "got:", user)
}
if td.au.passwd != au.passwd {
t.Error(td.val, "passwd should be:", td.au.passwd, "got:", au.passwd)
}
if td.au.port != au.port {
t.Error(td.val, "port should be:", td.au.port, "got:", au.port)
}
}
}

func TestCalcDigest(t *testing.T) {
a1 := md5sum("cyf" + ":" + authRealm + ":" + "wlx")
auth := map[string]string{
Expand Down
Loading

0 comments on commit ace634c

Please sign in to comment.