Skip to content

Commit

Permalink
Merge pull request #7 from film42/gt/better_config
Browse files Browse the repository at this point in the history
Gt/better config
  • Loading branch information
film42 authored May 7, 2017
2 parents af4528b + da9ee82 commit c391f53
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 39 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Turbluence only allows the listen port, proxy username, and proxy password to be

```
Usage of ./turbulence:
-config string
config file
-password string
password for proxy authentication
-port int
Expand All @@ -36,6 +38,28 @@ Usage of ./turbulence:

Turbulence supports basic authentication. Other massive proxies like Squid support complex authentication, whitelisting and custom headers, but Turbulence doesn't try to solve these other problems. It's built to do as little as possible. If you _need_ authentication or the ability to modify headers, then you should use a different proxy, but if whitelisting is good enough, use `iptables` instead. Even if you use Squid's whitelisting, the proxy is still publicly accessible and will respond to proxy connections with an unauthorized error. If you're using a cloud provider like amazon or google, be sure to use their firewall tools.

If you want to use a sample config instead of passing your options as command line arguments, you may do so. Here's an
example config:

```
{
"port": 9000,
"strip_proxy_headers": true,
"credentials": [
{
"username": "ron.swanson",
"password": "g0ld5topsTheGovt"
}
]
}
```

Save and run:

```
./turbulence --config config_for_ron.json
```

### Purpose

After using large proxies like Squid, I realized I was only using a very small subset of its features and when something would go wrong, I wasn't sure what why. After a few hours of trying to solve those problems, I looked up the proxy server RFC and realized how easy it would be to start from scratch and only add the most basic components required to proxy http and https requests. After first writing a functional proxy in ruby with nio4r, I realized go would be a much easier runtime to work with. Turbulence is the result of a few iterations of writing a toy proxy. Turbulence isn't nearly as battle tested as Squid, but it is very tiny and can fit into your head. I've hit Turbulence with pings and full proxy requests continuously for over two weeks (still running) and the memory utilization has been about 1/3 of Squid, but YMMV.
Expand Down
64 changes: 64 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"encoding/json"
"errors"
"io"
)

var InvalidCredentials = errors.New("Invalid credentials provided. Must have a username/ password or none at all.")

type Credential struct {
Username string
Password string
}

type Config struct {
Credentials []Credential
StripProxyHeaders bool `json:"strip_proxy_headers"`
Port int
}

func (config *Config) AuthenticationRequired() bool {
return len(config.Credentials) > 0
}

func validCredentials(username, password string) bool {
if username == "" && password == "" {
return true
}
if username != "" && password != "" {
return true
}
return false
}

func (config *Config) Validate() error {
for _, credential := range config.Credentials {
if !validCredentials(credential.Username, credential.Password) {
return InvalidCredentials
}
}

return nil
}

func (config *Config) IsAuthenticated(username, password string) bool {
for _, credential := range config.Credentials {
if credential.Username == username && credential.Password == password {
return true
}
}

return false
}

func NewConfigFromReader(reader io.Reader) (*Config, error) {
config := new(Config)
decoder := json.NewDecoder(reader)
err := decoder.Decode(&config)
if err != nil {
return nil, err
}
return config, nil
}
72 changes: 72 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"os"
"strings"
"testing"
)

func TestLoadingConfigFromFile(t *testing.T) {
sampleConfigFile, _ := os.Open("test/config.json")
defer sampleConfigFile.Close()

sampleConfig, _ := NewConfigFromReader(sampleConfigFile)
if err := sampleConfig.Validate(); err != nil {
t.Fatal("Expected config to be valid but found: ", err)
}

if sampleConfig.Port != 26000 {
t.Fatal("Expected port to be 26000 but found:", sampleConfig.Port)
}

if !sampleConfig.StripProxyHeaders {
t.Fatal("Expected sampleConfig.StripProxyHeaders to be true")
}

if !sampleConfig.AuthenticationRequired() {
t.Fatal("Expected sample config to require authentication")
}

credentialsLen := len(sampleConfig.Credentials)
if credentialsLen != 2 {
t.Fatal("Expected config to have 2 credentials but found:", credentialsLen)
}

credential := Credential{Username: "login", Password: "yolo"}
if sampleConfig.Credentials[0] != credential {
t.Fatal("Expected", credential, "but found", sampleConfig.Credentials[0])
}

credential = Credential{Username: "ron.swanson", Password: "g0ld5topsTheGovt"}
if sampleConfig.Credentials[1] != credential {
t.Fatal("Expected", credential, "but found", sampleConfig.Credentials[1])
}
}

func TestInvalidConfigFormat(t *testing.T) {
invalidFormatReader := strings.NewReader(`{"testing":"ok...`)
_, err := NewConfigFromReader(invalidFormatReader)
if err == nil {
t.Fatal("Config should have returned an error for being invalid json")
}
}

func TestConfigInavlidUsername(t *testing.T) {
sampleConfig := &Config{
Credentials: []Credential{Credential{Username: "", Password: "test"}},
}

if sampleConfig.Validate() != InvalidCredentials {
t.Fatal("Expected username to be invalid")
}
}

func TestConfigInavlidPassword(t *testing.T) {
sampleConfig := &Config{
Credentials: []Credential{Credential{Username: "test", Password: ""}},
}

if sampleConfig.Validate() != InvalidCredentials {
t.Fatal("Expected password to be invalid")
}
}
19 changes: 9 additions & 10 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ func (c *connection) Handle() {

defer request.Body.Close()

if AuthenticationRequired && !credentialsAreValid(request) {
if !isAuthenticated(request) {
logger.Fatal.Println(c.id, "Invalid credentials.")
c.incoming.Write([]byte(ProxyAuthenticationRequired))
return
}

// Delete the auth and proxy headers.
if AuthenticationRequired {
if config.AuthenticationRequired() {
request.Header.Del("Proxy-Authorization")
}

// Delete any other proxy related thing if enabled.
if StripProxyHeaders {
if config.StripProxyHeaders {
request.Header.Del("Forwarded")
request.Header.Del("Proxy-Connection")
request.Header.Del("Via")
Expand Down Expand Up @@ -117,23 +117,22 @@ func parseBasicAuth(auth string) (username, password string, ok bool) {
return cs[:s], cs[s+1:], true
}

func credentialsAreValid(request *http.Request) bool {
func isAuthenticated(request *http.Request) bool {
if !config.AuthenticationRequired() {
return true
}

proxyAuthHeader := request.Header.Get("Proxy-Authorization")
if proxyAuthHeader == "" {
return false
}

username, password, ok := parseBasicAuth(proxyAuthHeader)

if !ok {
return false
}

if username == Username && password == Password {
return true
}

return false
return config.IsAuthenticated(username, password)
}

func newConnectionId() string {
Expand Down
8 changes: 3 additions & 5 deletions connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@ func createHttpServer(address, payload string, code int) *http.Server {
}

func resetCredentials() {
setCredentials("", "")
AuthenticationRequired = false
config.Credentials = []Credential{}
}

func setCredentials(user, pass string) {
AuthenticationRequired = true
Username = user
Password = pass
config.Credentials = []Credential{Credential{Username: user, Password: pass}}
}

func basicHttpProxyRequest() string {
Expand All @@ -50,6 +47,7 @@ func readMessage(reader io.Reader) string {
}

func TestMain(m *testing.M) {
config = &Config{}
InitNullLogger()
m.Run()
}
Expand Down
59 changes: 35 additions & 24 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import (
"strconv"
)

var AuthenticationRequired = false
var Username = ""
var Password = ""
var StripProxyHeaders = true
var config *Config

func handleConnection(conn net.Conn) {
connection := NewConnection(conn)
Expand All @@ -34,17 +31,6 @@ func acceptedConnsChannel(listener net.Listener) chan net.Conn {
return channel
}

func validCredentials(username, password string) bool {
if username == "" && password == "" {
return true
}
if username != "" && password != "" {
AuthenticationRequired = true
return true
}
return false
}

func listenAndServe(port int) {
listenOn := ":" + strconv.Itoa(port)
server, err := net.Listen("tcp", listenOn)
Expand All @@ -64,25 +50,50 @@ func listenAndServe(port int) {
func main() {
InitLogger()

configPtr := flag.String("config", "", "config file")
portPtr := flag.Int("port", 25000, "listen port")
stripProxyHeadersPtr := flag.Bool("strip-proxy-headers", true, "strip proxy headers from http requests")
usernamePtr := flag.String("username", "", "username for proxy authentication")
passwordPtr := flag.String("password", "", "password for proxy authentication")
stripProxyHeadersPtr := flag.Bool("strip-proxy-headers", true, "strip proxy headers from http requests")
flag.Parse()

if !validCredentials(*usernamePtr, *passwordPtr) {
logger.Fatal.Println("Invalid credentials provided. Must have a username/password or none at all.")
if *configPtr != "" {
configFile, err := os.Open(*configPtr)
if err != nil {
logger.Fatal.Println("Could not open config file", err)
os.Exit(1)
}

config, err = NewConfigFromReader(configFile)
if err != nil {
logger.Fatal.Println("Could not parse config file", err)
os.Exit(1)
}

configFile.Close()
} else {
config = &Config{
Port: *portPtr,
StripProxyHeaders: *stripProxyHeadersPtr,
}

if *usernamePtr != "" {
config.Credentials = []Credential{
Credential{Username: *usernamePtr, Password: *passwordPtr},
}
}
}

err := config.Validate()
if err != nil {
logger.Fatal.Println("Config is not valid:", err)
os.Exit(1)
}

if AuthenticationRequired {
if config.AuthenticationRequired() {
logger.Info.Println("Credentials provided. Proxy authentication will be required for all connections.")
Username = *usernamePtr
Password = *passwordPtr
}

StripProxyHeaders = *stripProxyHeadersPtr

logger.Info.Println("Prepare for takeoff...")
listenAndServe(*portPtr)
listenAndServe(config.Port)
}
14 changes: 14 additions & 0 deletions test/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"port": 26000,
"strip_proxy_headers": true,
"credentials": [
{
"username": "login",
"password": "yolo"
},
{
"username": "ron.swanson",
"password": "g0ld5topsTheGovt"
}
]
}

0 comments on commit c391f53

Please sign in to comment.