Tobias Begalke 7 years ago
commit
c0560ceda7
4 changed files with 432 additions and 0 deletions
  1. 168 0
      ajp13.go
  2. 113 0
      ajp13_test.go
  3. 56 0
      header.go
  4. 95 0
      method.go

+ 168 - 0
ajp13.go

@@ -0,0 +1,168 @@
+package ajp13
+
+import (
+	"encoding/binary"
+	"fmt"
+	"net"
+	"reflect"
+)
+
+// ForwardRequest marks that this request is an actual request
+const ForwardRequest = 2
+
+// Shutdown marks that this is a shutdown request
+const Shutdown = 7
+
+// Ping marks that this is a ping request
+const Ping = 8
+
+// CPing marks that this is a cping request
+const CPing = 10
+
+// AJP13 represents the apache java protocol
+type AJP13 struct {
+	len        uint16
+	Type       uint8
+	method     HTTPMethod
+	Version    string // e.g. HTTP/1.1
+	URI        string
+	RemoteAddr net.IP
+	RemoteHost string
+	Server     string
+	Port       uint16
+	SSL        bool
+	headers    map[string]string
+}
+
+// Parse takes a []byte payload and builds
+func Parse(payload []byte) (*AJP13, error) {
+	if !reflect.DeepEqual(payload[0:2], []byte{0x12, 0x34}) {
+		return nil, fmt.Errorf("The magic signature is not 0x12 0x34 but %v", payload[0:2])
+	}
+
+	a := &AJP13{}
+
+	// the data length
+	a.len = binary.BigEndian.Uint16(payload[2:4])
+
+	data := payload[4 : a.len+4]
+
+	// the request type
+	a.Type = uint8(data[0])
+
+	switch a.Type {
+	case ForwardRequest:
+		a.parseForwardRequest(data)
+	}
+
+	return a, nil
+}
+
+func (a *AJP13) parseForwardRequest(data []byte) {
+
+	// Method
+	a.method = HTTPMethod(uint8(data[1]))
+
+	offset := uint16(2)
+
+	// Version
+	if !reflect.DeepEqual(data[offset:offset+2], []byte{0xff, 0xff}) {
+		versionLen := binary.BigEndian.Uint16(data[offset : offset+2])
+		a.Version = string(data[offset+2 : offset+2+versionLen])
+		offset += versionLen + 1
+	}
+	offset += 2
+
+	// URI
+	if !reflect.DeepEqual(data[offset:offset+2], []byte{0xff, 0xff}) {
+		uriLen := binary.BigEndian.Uint16(data[offset : offset+2])
+		a.URI = string(data[offset+2 : offset+2+uriLen])
+		offset += uriLen + 1
+	}
+	offset += 2
+
+	// RemoteAddr
+	if !reflect.DeepEqual(data[offset:offset+2], []byte{0xff, 0xff}) {
+		raddrLen := binary.BigEndian.Uint16(data[offset : offset+2])
+		a.RemoteAddr = net.ParseIP(string(data[offset+2 : offset+2+raddrLen]))
+		offset += raddrLen + 1
+	}
+	offset += 2
+
+	// RemoteHost
+	if !reflect.DeepEqual(data[offset:offset+2], []byte{0xff, 0xff}) {
+		rhostLen := binary.BigEndian.Uint16(data[offset : offset+2])
+		a.RemoteHost = string(data[offset+2 : offset+2+rhostLen])
+		offset += rhostLen + 1
+	}
+	offset += 2
+
+	// Server
+	if !reflect.DeepEqual(data[offset:offset+2], []byte{0xff, 0xff}) {
+		len := binary.BigEndian.Uint16(data[offset : offset+2])
+		a.Server = string(data[offset+2 : offset+2+len])
+		offset += len + 1
+	}
+	offset += 2
+
+	// Port
+	a.Port = binary.BigEndian.Uint16(data[offset : offset+2])
+
+	offset += 2
+
+	// SSL
+	if data[offset] == 0x1 {
+		a.SSL = true
+	}
+
+	offset++
+
+	// Headers
+	numHeaders := binary.BigEndian.Uint16(data[offset : offset+2])
+	offset += 2
+
+	headers := make(map[string]string)
+
+	for idx := uint16(0); idx < numHeaders; idx++ {
+		if data[offset] == 0xa0 { // encoded header name
+			len := binary.BigEndian.Uint16(data[offset+2 : offset+4])
+			hdrName := HTTPHeader(binary.BigEndian.Uint16(data[offset : offset+2])).String()
+			if hdrName != "UNKNOWN" {
+				headers[hdrName] = string(data[offset+4 : offset+4+len])
+			}
+			offset = offset + 5 + len
+		} else { // plain text header name
+			length := binary.BigEndian.Uint16(data[offset : offset+2])
+			key := string(data[offset+2 : offset+2+length])
+
+			offset = offset + 3 + length
+
+			length = binary.BigEndian.Uint16(data[offset : offset+2])
+			val := string(data[offset+2 : offset+2+length])
+			headers[key] = val
+
+			offset = offset + 3 + length
+		}
+	}
+
+	a.headers = headers
+
+	// don't need Attributes for the time being
+
+}
+
+// Length returns the length of the data this AJP13 request consists of
+func (a *AJP13) Length() uint16 {
+	return a.len
+}
+
+// Method returns the name of the method used
+func (a *AJP13) Method() string {
+	return a.method.String()
+}
+
+// Header returns the header value refered to by key
+func (a *AJP13) Header(key string) (string, bool) {
+	val, ok := a.headers[key]
+	return val, ok
+}

+ 113 - 0
ajp13_test.go

@@ -0,0 +1,113 @@
+package ajp13
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"testing"
+
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/pcap"
+)
+
+func TestParser(t *testing.T) {
+	dataEncoded := "1MOyoQIABAAAAAAAAAAAAP//AAABAAAAEap4WQGTAwBJAgAASQIAAAAAAAAAAAAAAAAAAIbdYAAAAAITBkAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAZUYH0lJ90Z8dtQzgYAYAgACGwAAAQEICj+Ftv4/hbb+EjQB7wICAAhIVFRQLzEuMQAAHi95cC93d3cvaGVpenVuZy1zYW5pdGFlci9tYXllbgAADTk1LjkxLjIyMC4xNTMA//8AEnd3dy5nZWxiZXNlaXRlbi5kZQABuwEACKAGAAprZWVwLWFsaXZlAKABAD90ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSwqLyo7cT0wLjgAoA4Ah01vemlsbGEvNS4wIChpUGhvbmU7IENQVSBpUGhvbmUgT1MgMTBfM18yIGxpa2UgTWFjIE9TIFgpIEFwcGxlV2ViS2l0LzYwMy4yLjQgKEtIVE1MLCBsaWtlIEdlY2tvKSBWZXJzaW9uLzEwLjAgTW9iaWxlLzE0Rjg5IFNhZmFyaS82MDIuMQCgBAAFZGUtZGUAoA0AFmh0dHBzOi8vd3d3Lmdvb2dsZS5kZS8AoAMADWd6aXAsIGRlZmxhdGUAABBYLUZvcndhcmRlZC1Ib3N0AAASd3d3LmdlbGJlc2VpdGVuLmRlAKALABRnc2Ntcy5nZWxiZXNlaXRlbi5kZQAIABtFQ0RIRS1SU0EtQUVTMjU2LUdDTS1TSEEzODQACwEACgAPQUpQX1JFTU9URV9QT1JUAAAFMjU0MDQA/w=="
+	decoded, err := base64.StdEncoding.DecodeString(dataEncoded)
+	if err != nil {
+		t.Errorf("Failed to decode base64 packet data: %s\n", err)
+	}
+	tmpFile, err := ioutil.TempFile("", "ajp13")
+	if err != nil {
+		fmt.Printf("Failed to create temp file: %s\n", err)
+		os.Exit(1)
+	}
+	defer os.Remove(tmpFile.Name())
+
+	if _, err := tmpFile.Write(decoded); err != nil {
+		fmt.Printf("Failed to write to temp file: %s\n", err)
+		os.Exit(2)
+	}
+
+	if err := tmpFile.Close(); err != nil {
+		fmt.Printf("Failed to close temp file: %s\n", err)
+		os.Exit(4)
+	}
+
+	handle, err := pcap.OpenOffline(tmpFile.Name())
+	if err != nil {
+		fmt.Printf("Failed to open %s: %s\n", tmpFile.Name(), err)
+		os.Exit(3)
+	}
+
+	err = handle.SetBPFFilter("tcp")
+	if err != nil {
+		log.Fatalf("Failed to set BPF Filter: %s\n", err)
+	}
+
+	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
+
+	for packet := range packetSource.Packets() {
+		app := packet.ApplicationLayer()
+		if app == nil {
+			continue
+		}
+		data := app.Payload()
+
+		a, err := Parse(data)
+		if err != nil {
+			t.Errorf("Failed to parse data: %s\n", err)
+		}
+
+		if a.Length() != 495 {
+			t.Errorf("The AJP13 packet should announce a length of 495 bytes (%d)\n", a.len)
+		}
+
+		if a.Type != ForwardRequest {
+			t.Errorf("The request type should be 2 (forward request): %d\n", a.Type)
+		}
+
+		if a.Method() != "GET" {
+			t.Errorf("The method should be GET (2) but is %d\n", a.method)
+		}
+
+		if a.Version != "HTTP/1.1" {
+			t.Errorf("The version should be HTTP/1.1 but is %s\n", a.Version)
+		}
+
+		if a.URI != "/yp/www/heizung-sanitaer/mayen" {
+			t.Errorf("The URI should be /yp/www/heizung-sanitaer/mayen but is %s\n", a.URI)
+		}
+
+		if a.RemoteAddr.String() != "95.91.220.153" {
+			t.Errorf("RemoteAddr should be 95.91.220.153 but is %s\n", a.RemoteAddr)
+		}
+
+		if a.RemoteHost != "" {
+			t.Errorf("RemoteHost should be '' but is '%s'\n", a.RemoteHost)
+		}
+
+		if a.Server != "www.gelbeseiten.de" {
+			t.Errorf("Server should be www.gelbeseiten.de but is %s", a.Server)
+		}
+
+		if a.Port != 443 {
+			t.Errorf("Port should be 443 but is %d\n", a.Port)
+		}
+
+		if a.SSL != true {
+			t.Errorf("SSL should be true but is false\n")
+		}
+
+		if val, _ := a.Header("X-Forwarded-Host"); val != "www.gelbeseiten.de" {
+			t.Errorf("Header field X-Forwarded-Host should be 'www.gelbeseiten.de' but is '%s'\n", val)
+		}
+
+		if val, _ := a.Header("User-Agent"); val != "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1" {
+			t.Errorf("Header field User-Agent should be 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1' but is '%s'", val)
+		}
+
+		break
+	}
+}

+ 56 - 0
header.go

@@ -0,0 +1,56 @@
+package ajp13
+
+// HTTPHeader encodes which HTTP header is being used
+type HTTPHeader uint16
+
+const (
+	Accept         = HTTPHeader(iota + 0xA001)
+	AcceptCharset  = HTTPHeader(iota + 0xA001)
+	AcceptEncoding = HTTPHeader(iota + 0xA001)
+	AcceptLanguage = HTTPHeader(iota + 0xA001)
+	Authorization  = HTTPHeader(iota + 0xA001)
+	Connection     = HTTPHeader(iota + 0xA001)
+	ContentType    = HTTPHeader(iota + 0xA001)
+	ContentLength  = HTTPHeader(iota + 0xA001)
+	Cookie         = HTTPHeader(iota + 0xA001)
+	Cookie2        = HTTPHeader(iota + 0xA001)
+	Host           = HTTPHeader(iota + 0xA001)
+	Pragma         = HTTPHeader(iota + 0xA001)
+	Referer        = HTTPHeader(iota + 0xA001)
+	UserAgent      = HTTPHeader(iota + 0xA001)
+)
+
+func (h HTTPHeader) String() string {
+	switch h {
+	case Accept:
+		return "Accept"
+	case AcceptCharset:
+		return "Accept-Charset"
+	case AcceptEncoding:
+		return "Accept-Encoding"
+	case AcceptLanguage:
+		return "Accept-Language"
+	case Authorization:
+		return "Authorization"
+	case Connection:
+		return "Connection"
+	case ContentType:
+		return "Content-Type"
+	case ContentLength:
+		return "Content-Length"
+	case Cookie:
+		return "Cookie"
+	case Cookie2:
+		return "Cookie2"
+	case Host:
+		return "Host"
+	case Pragma:
+		return "Pragma"
+	case Referer:
+		return "Referer"
+	case UserAgent:
+		return "User-Agent"
+	}
+
+	return "UNKNOWN"
+}

+ 95 - 0
method.go

@@ -0,0 +1,95 @@
+package ajp13
+
+// HTTPMethod denotes the HTTP method used
+type HTTPMethod uint8
+
+const (
+	Options         = HTTPMethod(iota + 1)
+	Get             = HTTPMethod(iota + 1)
+	Head            = HTTPMethod(iota + 1)
+	Post            = HTTPMethod(iota + 1)
+	Put             = HTTPMethod(iota + 1)
+	Delete          = HTTPMethod(iota + 1)
+	Trace           = HTTPMethod(iota + 1)
+	PropFind        = HTTPMethod(iota + 1)
+	PropPatch       = HTTPMethod(iota + 1)
+	MkCol           = HTTPMethod(iota + 1)
+	Copy            = HTTPMethod(iota + 1)
+	Move            = HTTPMethod(iota + 1)
+	Lock            = HTTPMethod(iota + 1)
+	Unlock          = HTTPMethod(iota + 1)
+	Acl             = HTTPMethod(iota + 1)
+	Report          = HTTPMethod(iota + 1)
+	VersionControl  = HTTPMethod(iota + 1)
+	CheckIn         = HTTPMethod(iota + 1)
+	CheckOut        = HTTPMethod(iota + 1)
+	UnCheckOut      = HTTPMethod(iota + 1)
+	Search          = HTTPMethod(iota + 1)
+	MkWorkspace     = HTTPMethod(iota + 1)
+	Update          = HTTPMethod(iota + 1)
+	Label           = HTTPMethod(iota + 1)
+	Merge           = HTTPMethod(iota + 1)
+	BaselineControl = HTTPMethod(iota + 1)
+	MkActivity      = HTTPMethod(iota + 1)
+)
+
+func (method HTTPMethod) String() string {
+	switch method {
+	case Options:
+		return "OPTIONS"
+	case Get:
+		return "GET"
+	case Head:
+		return "HEAD"
+	case Post:
+		return "POST"
+	case Put:
+		return "PUT"
+	case Delete:
+		return "DELETE"
+	case Trace:
+		return "TRACE"
+	case PropFind:
+		return "PROPFIND"
+	case PropPatch:
+		return "PROPPATCH"
+	case MkCol:
+		return "MKCOL"
+	case Copy:
+		return "COPY"
+	case Move:
+		return "MOVE"
+	case Lock:
+		return "LOCK"
+	case Unlock:
+		return "UNLOCK"
+	case Acl:
+		return "ACL"
+	case Report:
+		return "REPORT"
+	case VersionControl:
+		return "VERSIONCONTROL"
+	case CheckIn:
+		return "CHECKIN"
+	case CheckOut:
+		return "CHECKOUT"
+	case UnCheckOut:
+		return "UNCHECKOUT"
+	case Search:
+		return "SEARCH"
+	case MkWorkspace:
+		return "MKWORKSPACE"
+	case Update:
+		return "UPDATE"
+	case Label:
+		return "LABEL"
+	case Merge:
+		return "MERGE"
+	case BaselineControl:
+		return "BASELINECONTROL"
+	case MkActivity:
+		return "MKACTIVITY"
+	}
+
+	return "UNKNOWN"
+}