// postUsersJSON creates a user from a JSON body, returning a client string/server error pair func postUsersJSON(body []byte) (string, error) { // Unmarshal JSON from body var jsonUser data.UserRecord if err := json.Unmarshal(body, &jsonUser); err != nil { return "Malformed request JSON", nil } // Check for valid input if jsonUser.Username == "" || jsonUser.Password == "" || jsonUser.TorrentLimit == 0 { return "Missing required parameters: username, password, torrentLimit", nil } // Create user from input user := new(data.UserRecord) if err := user.Create(jsonUser.Username, jsonUser.Password, jsonUser.TorrentLimit); err != nil { return "", err } // Save user to database if err := user.Save(); err != nil { return "", err } return "", nil }
// getUsersJSON returns a JSON representation of one or more data.UserRecords func getUsersJSON(ID int) ([]byte, error) { // Check for a valid integer ID if ID > 0 { // Load user user, err := new(data.UserRecord).Load(ID, "id") if err != nil { return nil, err } // Create JSON represenation jsonUser, err := user.ToJSON() if err != nil { return nil, err } // Marshal into JSON res, err := json.Marshal(jsonUser) if err != nil { return nil, err } return res, nil } // Load all users users, err := new(data.UserRecordRepository).All() if err != nil { return nil, err } // Convert all users to JSON representation jsonUsers := make([]data.JSONUserRecord, 0) for _, u := range users { j, err := u.ToJSON() if err != nil { return nil, err } jsonUsers = append(jsonUsers[:], j) } // Marshal into JSON res, err := json.Marshal(jsonUsers) if err != nil { return nil, err } return res, err }
// TestPostLogin verifies that /api/login returns proper JSON output func TestPostLogin(t *testing.T) { log.Println("TestPostLogin()") // Load config config, err := common.LoadConfig() if err != nil { t.Fatalf("Could not load configuration: %s", err.Error()) } common.Static.Config = config // Generate mock data.UserRecord mockUser := new(data.UserRecord) if err := mockUser.Create("test", "test", 100); err != nil { t.Fatalf("Failed to create mock user: %s", err.Error()) } // Save mock user if err := mockUser.Save(); err != nil { t.Fatalf("Failed to save mock user: %s", err.Error()) } // Load mock user to fetch ID user, err := mockUser.Load(mockUser.Username, "username") if user == (data.UserRecord{}) || err != nil { t.Fatalf("Failed to load mock user: %s", err.Error()) } // Perform login request for this user res, err := postLogin(user) if err != nil { t.Fatalf("Failed to retrieve login JSON: %s", err.Error()) } log.Println(string(res)) // Unmarshal output JSON var key data.JSONAPIKey err = json.Unmarshal(res, &key) if err != nil { t.Fatalf("Failed to unmarshal login JSON: %s", err.Error()) } // Verify same user ID from API if user.ID != key.UserID { t.Fatalf("Mismatched user IDs, got %d, expected %d", key.UserID, user.ID) } // Delete mock user if err := user.Delete(); err != nil { t.Fatalf("Failed to delete mock user: %s", err.Error()) } }
// TestBasicAuthenticator verifies that BasicAuthenticator.Auth works properly func TestBasicAuthenticator(t *testing.T) { log.Println("TestBasicAuthenticator()") // Load config config := common.LoadConfig() common.Static.Config = config // Generate mock user user := data.UserRecord{ Username: "******", Passkey: "abcdef0123456789", TorrentLimit: 10, } // Save mock user if !user.Save() { t.Fatalf("Failed to save mock user") } // Load mock user to fetch ID user = user.Load(user.Username, "username") if user == (data.UserRecord{}) { t.Fatalf("Failed to load mock user") } // Generate an API key and salt pass := "******" salt := "test" sha := sha1.New() sha.Write([]byte(pass + salt)) hash := fmt.Sprintf("%x", sha.Sum(nil)) // Generate mock API key key := data.APIKey{ UserID: user.ID, Key: hash, Salt: salt, } // Save mock API key if !key.Save() { t.Fatalf("Failed to save mock data.APIKey") } // Load mock data.APIKey to fetch ID key = key.Load(key.Key, "key") if key == (data.APIKey{}) { t.Fatalf("Failed to load mock data.APIKey") } // Generate mock HTTP request r := http.Request{} headers := map[string][]string{ "Authorization": {"Basic " + base64.URLEncoding.EncodeToString([]byte(user.Username+":"+pass))}, } r.Header = headers // Perform authentication request auth := new(BasicAuthenticator).Auth(&r) if !auth { t.Fatalf("Failed to authenticate using BasicAuthenticator") } // Delete mock user if !user.Delete() { t.Fatalf("Failed to delete mock user") } // Delete mock API key if !key.Delete() { t.Fatalf("Failed to delete mock data.APIKey") } }
// TestGetUsersJSON verifies that /api/users returns proper JSON output func TestGetUsersJSON(t *testing.T) { log.Println("TestGetUsersJSON()") // Load config config, err := common.LoadConfig() if err != nil { t.Fatalf("Could not load configuration: %s", err.Error()) } common.Static.Config = config // Generate mock data.UserRecord mockUser := new(data.UserRecord) if err := mockUser.Create("test", "test", 100); err != nil { t.Fatalf("Failed to create mock user: %s", err.Error()) } // Save mock user if err := mockUser.Save(); err != nil { t.Fatalf("Failed to save mock user: %s", err.Error()) } // Load mock user to fetch ID user, err := mockUser.Load(mockUser.Username, "username") if user == (data.UserRecord{}) || err != nil { t.Fatalf("Failed to load mock user: %s", err.Error()) } // Request output JSON from API for this user res, err := getUsersJSON(user.ID) if err != nil { t.Fatalf("Failed to retrieve users JSON: %s", err.Error()) } // Unmarshal output JSON var user2 data.UserRecord err = json.Unmarshal(res, &user2) if err != nil { t.Fatalf("Failed to unmarshal result JSON for single user: %s", err.Error()) } // Verify objects are the same if user.ID != user2.ID { t.Fatalf("ID, expected %d, got %d", user.ID, user2.ID) } // Request output JSON from API for all users res, err = getUsersJSON(-1) if err != nil { t.Fatalf("Failed to retrieve all users JSON: %s", err.Error()) } // Unmarshal all output JSON var allUsers []data.JSONUserRecord err = json.Unmarshal(res, &allUsers) if err != nil { t.Fatalf("Failed to unmarshal result JSON for all users: %s", err.Error()) } // Verify known user is in result set found := false for _, f := range allUsers { if f.ID == user.ID { found = true } } if !found { t.Fatalf("Expected user not found in all users result set") } // Delete mock user if err := user.Delete(); err != nil { t.Fatalf("Failed to delete mock user: %s", err.Error()) } }
// Parse incoming HTTP connections before making tracker calls func parseHTTP(w http.ResponseWriter, r *http.Request) { // Create a tracker to handle this client httpTracker := tracker.HTTPTracker{} // Count incoming connections atomic.AddInt64(&common.Static.HTTP.Current, 1) atomic.AddInt64(&common.Static.HTTP.Total, 1) // Add header to identify goat w.Header().Add("Server", fmt.Sprintf("%s/%s", App, Version)) // Store current URL path url := r.URL.Path // Split URL into segments urlArr := strings.Split(url, "/") // If configured, Detect if client is making an API call url = urlArr[1] if url == "api" { // Output JSON w.Header().Add("Content-Type", "application/json") // API enabled if common.Static.Config.API { // API authentication auth := new(api.BasicAuthenticator).Auth(r) if !auth { http.Error(w, api.ErrorResponse("Authentication failed"), 401) return } // Handle API calls, output JSON api.Router(w, r) return } http.Error(w, api.ErrorResponse("API is currently disabled"), 503) return } // Check for maintenance mode if common.Static.Maintenance { // Return tracker error with maintenance message if _, err := w.Write(httpTracker.Error("Maintenance: " + common.Static.StatusMessage)); err != nil { log.Println(err.Error()) } return } // Detect if passkey present in URL var passkey string if len(urlArr) == 3 { passkey = urlArr[1] url = urlArr[2] } // Make sure URL is valid torrent function if url != "announce" && url != "scrape" { if _, err := w.Write(httpTracker.Error("Malformed announce")); err != nil { log.Println(err.Error()) } return } // Verify that torrent client is advertising its User-Agent, so we can use a whitelist if r.Header.Get("User-Agent") == "" { if _, err := w.Write(httpTracker.Error("Your client is not identifying itself")); err != nil { log.Println(err.Error()) } return } client := r.Header.Get("User-Agent") // If configured, verify that torrent client is on whitelist if common.Static.Config.Whitelist { whitelist := new(data.WhitelistRecord).Load(client, "client") if whitelist == (data.WhitelistRecord{}) || !whitelist.Approved { if _, err := w.Write(httpTracker.Error("Your client is not whitelisted")); err != nil { log.Println(err.Error()) } // Block things like browsers and web crawlers, because they will just clutter up the table if strings.Contains(client, "Mozilla") || strings.Contains(client, "Opera") { return } // Insert unknown clients into list for later approval if whitelist == (data.WhitelistRecord{}) { whitelist.Client = client whitelist.Approved = false log.Printf("whitelist: detected new client '%s', awaiting manual approval", client) go whitelist.Save() } return } } // Parse querystring into a Values map query := r.URL.Query() // Check if IP was previously set if query.Get("ip") == "" { // If no IP set, detect and store it in query map query.Set("ip", strings.Split(r.RemoteAddr, ":")[0]) } // Put client in query map query.Set("client", client) // Check if server is configured for passkey announce if common.Static.Config.Passkey && passkey == "" { if _, err := w.Write(httpTracker.Error("No passkey found in announce URL")); err != nil { log.Println(err.Error()) } return } // Validate passkey if needed user := new(data.UserRecord).Load(passkey, "passkey") if common.Static.Config.Passkey && user == (data.UserRecord{}) { if _, err := w.Write(httpTracker.Error("Invalid passkey")); err != nil { log.Println(err.Error()) } return } // Put passkey in query map query.Set("passkey", user.Passkey) // Mark client as HTTP query.Set("udp", "0") // Get user's total number of active torrents seeding := user.Seeding() leeching := user.Leeching() if seeding == -1 || leeching == -1 { if _, err := w.Write(httpTracker.Error("Failed to calculate active torrents")); err != nil { log.Println(err.Error()) } return } // Verify that client has not exceeded this user's torrent limit activeSum := seeding + leeching if user.TorrentLimit < activeSum { msg := fmt.Sprintf("Exceeded active torrent limit: %d > %d", activeSum, user.TorrentLimit) if _, err := w.Write(httpTracker.Error(msg)); err != nil { log.Println(err.Error()) } return } // Tracker announce if url == "announce" { // Validate required parameter input required := []string{"info_hash", "ip", "port", "uploaded", "downloaded", "left"} // Validate required integer input reqInt := []string{"port", "uploaded", "downloaded", "left"} // Check for required parameters for _, r := range required { if query.Get(r) == "" { if _, err := w.Write(httpTracker.Error("Missing required parameter: " + r)); err != nil { log.Println(err.Error()) } return } } // Check for all valid integers for _, r := range reqInt { if query.Get(r) != "" { _, err := strconv.Atoi(query.Get(r)) if err != nil { if _, err := w.Write(httpTracker.Error("Invalid integer parameter: " + r)); err != nil { log.Println(err.Error()) } return } } } // Only allow compact announce if query.Get("compact") == "" || query.Get("compact") != "1" { if _, err := w.Write(httpTracker.Error("Your client does not support compact announce")); err != nil { log.Println(err.Error()) } return } // NOTE: currently, we do not bother using gzip to compress the tracker announce response // This is done for two reasons: // 1) Clients may or may not support gzip in the first place // 2) gzip may actually make announce response larger, as per testing in What.CD's ocelot // Perform tracker announce if _, err := w.Write(tracker.Announce(httpTracker, user, query)); err != nil { log.Println(err.Error()) } return } // Tracker scrape if url == "scrape" { // Check for required parameter info_hash if query.Get("info_hash") == "" { if _, err := w.Write(httpTracker.Error("Missing required parameter: info_hash")); err != nil { log.Println(err.Error()) } return } if _, err := w.Write(tracker.Scrape(httpTracker, query)); err != nil { log.Println(err.Error()) } return } return }
// TestAuthenticator verifies that the Basic and HMAC authenticators work properly func TestAuthenticator(t *testing.T) { log.Println("TestBasicAuthenticator()") // Load config config, err := common.LoadConfig() if err != nil { t.Fatalf("Could not load configuration: %s", err.Error()) } common.Static.Config = config // Generate mock user user := new(data.UserRecord) if err := user.Create("test", "test", 10); err != nil { t.Fatalf("Failed to create mock user: %s", err.Error()) } // Save mock user if err := user.Save(); err != nil { t.Fatalf("Failed to save mock user: %s", err.Error()) } // Load user to get ID user2, err := user.Load("test", "username") if err != nil || (user2 == data.UserRecord{}) { t.Fatalf("Failed to load mock user: %s", err.Error()) } // Generate mock HTTP request r, err := http.NewRequest("POST", "http://localhost:8080/api/login", nil) if err != nil { t.Fatalf("Failed to generate HTTP request: %s", err.Error()) } headers := map[string][]string{ "Authorization": {"Basic " + base64.URLEncoding.EncodeToString([]byte("test:test"))}, } r.Header = headers // Capture HTTP response with recorder w := httptest.NewRecorder() // Perform HTTP Basic authentication var apiAuth APIAuthenticator apiAuth = new(BasicAuthenticator) // Attempt Basic authentication clientErr, serverErr := apiAuth.Auth(r) if clientErr != nil { t.Fatalf("Failed to authenticate: client: %s", clientErr.Error()) } if serverErr != nil { t.Fatalf("Failed to authenticate: server: %s", serverErr.Error()) } // Invoke API router Router(w, r, user2) // Read HTTP response body body, err := ioutil.ReadAll(w.Body) if err != nil { t.Fatalf("Failed to read HTTP response body: %s", err.Error()) } // Unmarshal result JSON var login data.JSONAPIKey if err := json.Unmarshal(body, &login); err != nil { t.Fatalf("Failed to unmarshal login JSON: %s", err.Error()) } // Generate API signature nonce := "abcdef" method := "GET" resource := "/api/status" signature, err := apiSignature(login.UserID, nonce, method, resource, login.Secret) if err != nil { t.Fatalf("Failed to generate API signature: %s", err.Error()) } log.Println("signature:", signature) // Generate mock HTTP request r, err = http.NewRequest(method, "http://localhost:8080"+resource, nil) if err != nil { t.Fatalf("Failed to generate HTTP request: %s", err.Error()) } headers = map[string][]string{ "Authorization": {"Basic " + base64.URLEncoding.EncodeToString([]byte(login.Pubkey+":"+nonce+"/"+signature))}, } r.Header = headers // Capture HTTP response with recorder w = httptest.NewRecorder() // Attempt HMAC authentication apiAuth = new(HMACAuthenticator) clientErr, serverErr = apiAuth.Auth(r) if clientErr != nil { t.Fatalf("Failed to authenticate: client: %s", clientErr.Error()) } if serverErr != nil { t.Fatalf("Failed to authenticate: server: %s", serverErr.Error()) } // Invoke API router Router(w, r, user2) // Read HTTP response body body, err = ioutil.ReadAll(w.Body) if err != nil { t.Fatalf("Failed to read HTTP response body: %s", err.Error()) } log.Println(string(body)) // Delete mock user if err := user2.Delete(); err != nil { t.Fatalf("Failed to delete mock user: %s", err.Error()) } }