📜 ⬆️ ⬇️

BitTorrent Tracker on C #

For a long time, I searched the net for an example of the simplest tracker in C #, but unfortunately, my searches were not successful. Therefore, I decided to try writing tracker on C #, and after getting a more or less working version, I would like to share the experience of its creation with everyone. And at the same time and get as many tips on how to improve it.

But let's start from the beginning ...

The first thing I did was open the BitTorrent protocol specification:.

It says that the client sends a GET request with the following data to the server: info_hash, peer_id, ip, port, uploaded, downloaded, left and event.
')
Accordingly, on the database server, I created the following table:

CREATE TABLE [dbo].[psy_trance_fm_bittorrent_announces] ( [id] [int] IDENTITY(1,1) NOT NULL, [info_hash] [char](40) NOT NULL, [peer_id] [char](40) NOT NULL, [ip] [varchar](512) NOT NULL, [port] [int] NOT NULL, [uploaded] [int] NOT NULL, [downloaded] [int] NOT NULL, [left] [int] NOT NULL, [event] [varchar](512) NULL ) 


Also, the specification says that the server sends data to the client in text / plain format in the form of a bencoded dictionary.

The Bencoded dictionary can contain four data types: string, int, list, and dictionary.

I wrote four functions for encoding in bencode — one for each data type:

  public string encode(string _string) { StringBuilder string_builder = new StringBuilder(); string_builder.Append(_string.Length); string_builder.Append(":"); string_builder.Append(_string); return string_builder.ToString(); } 


  public string encode(int _int) { StringBuilder string_builder = new StringBuilder(); string_builder.Append("i"); string_builder.Append(_int); string_builder.Append("e"); return string_builder.ToString(); } 


  public string encode(List<object> list) { StringBuilder string_builder = new StringBuilder(); string_builder.Append("l"); foreach (object _object in list) { if (_object.GetType() == typeof(string)) { string_builder.Append(encode((string)_object)); } if (_object.GetType() == typeof(int)) { string_builder.Append(encode((int)_object)); } if (_object.GetType() == typeof(List<object>)) { string_builder.Append(encode((List<object>)_object)); } if (_object.GetType() == typeof(SortedDictionary<string, object>)) { string_builder.Append(encode((SortedDictionary<string, object>)_object)); } } string_builder.Append("e"); return string_builder.ToString(); } 


  public string encode(SortedDictionary<string, object> sorted_dictionary) { StringBuilder string_builder = new StringBuilder(); string_builder.Append("d"); foreach (KeyValuePair<string, object> key_value_pair in sorted_dictionary) { string_builder.Append(encode((string)key_value_pair.Key)); if (key_value_pair.Value.GetType() == typeof(string)) { string_builder.Append(encode((string)key_value_pair.Value)); } if (key_value_pair.Value.GetType() == typeof(int)) { string_builder.Append(encode((int)key_value_pair.Value)); } if (key_value_pair.Value.GetType() == typeof(List<object>)) { string_builder.Append(encode((List<object>)key_value_pair.Value)); } if (key_value_pair.Value.GetType() == typeof(SortedDictionary<string, object>)) { string_builder.Append(encode((SortedDictionary<string, object>)key_value_pair.Value)); } } string_builder.Append("e"); return string_builder.ToString(); } 


I draw your attention to the fact that for dictionaries I used the type SortedDictionary <string, object>, and not Dictionary <string, object>. This is because, according to the specification, the keys in the dictionaries must be sorted.

Well, then the fun began ...

It would seem that in order to get info_hash and peer_id from a GET request, it is enough to use Request.QueryString ["info_hash"] and Request.QueryString ["peer_id"], respectively, but these methods returned utter nonsense. For a long time I could not understand what was the matter ...

And the thing was this: info_hash transmitted from client to server looks like this:% 124Vx% 9A% BC% DE% F1% 23Eg% 89% AB% CD% EF% 124Vx% 9A. Request.QueryString ["info_hash"] considers this to be a UTF-8 string and decodes it.

This can be seen by looking at the Reflector function FillFromString, for example.

In order to get around this moment, I decided to work with Request.Url.Query, returning the "raw" string.

Actually, I took the code for the FillFromString function from Reflector and removed from it a couple of lines responsible for decoding:

  string s = Request.Url.Query.Substring(1); SortedDictionary<string, object> parameters = new SortedDictionary<string, object>(StringComparer.Ordinal); int num = (s != null) ? s.Length : 0; for (int i = 0; i < num; i++) { int startIndex = i; int num4 = -1; while (i < num) { char ch = s[i]; if (ch == '=') { if (num4 < 0) { num4 = i; } } else if (ch == '&') { break; } i++; } string str = null; string str2 = null; if (num4 >= 0) { str = s.Substring(startIndex, num4 - startIndex); str2 = s.Substring(num4 + 1, (i - num4) - 1); } else { str2 = s.Substring(startIndex, i - startIndex); } parameters.Add("@" + str, str2); } 


Well, in order to return info_hash and peer_id to their original hex format, I wrote two more lines of code:

  parameters["@info_hash"] = BitConverter.ToString(HttpUtility.UrlDecodeToBytes((string)parameters["@info_hash"])).Replace("-", "").ToLower(); parameters["@peer_id"] = BitConverter.ToString(HttpUtility.UrlDecodeToBytes((string)parameters["@peer_id"])).Replace("-", "").ToLower(); 


According to the specification, ip and event are optional parameters. Ip does not transfer most clients to the server, and event transfers only in three cases: started, completed and stopped.

So I decided to check if they are in the parameter collection, but if not, then add them:

  if (parameters.ContainsKey("@ip") == false) { parameters.Add("@ip", Request.UserHostAddress); } if (parameters.ContainsKey("@event") == false) { parameters.Add("@event", DBNull.Value); } 


Then everything is simple. Check if there is a distribution in the database corresponding to the transferred info_hash and peer_id, if not, add it, if yes, then just update the distribution data.

psy_trance_fm.execute_scalar and psy_trance_fm.execute_non_query are the functions for working with the database, they are very typical and I don’t see any reason to bring them here.

  psy_trance_fm psy_trance_fm = new psy_trance_fm(); if (psy_trance_fm.execute_scalar("SELECT * FROM [dbo].[psy_trance_fm_bittorrent_announces] WHERE [info_hash] = @info_hash AND [peer_id] = @peer_id", parameters, CommandType.Text) == null) { psy_trance_fm.execute_non_query("INSERT INTO [dbo].[psy_trance_fm_bittorrent_announces] ([info_hash], [peer_id], [ip], [port], [uploaded], [downloaded], [left], [event]) VALUES (@info_hash, @peer_id, @ip, @port, @uploaded, @downloaded, @left, @event)", parameters, CommandType.Text); } else { psy_trance_fm.execute_non_query("UPDATE [dbo].[psy_trance_fm_bittorrent_announces] SET [ip] = @ip, [port] = @port, [uploaded] = @uploaded, [downloaded] = @downloaded, [left] = @left, [event] = @event WHERE [info_hash] = @info_hash AND [peer_id] = @peer_id", parameters, CommandType.Text); } 


After we write the data to the database, we need to return the bencoded dictionary to the client.

This is done as follows:

  SortedDictionary<string, object> sorted_dictionary = new SortedDictionary<string, object>(StringComparer.Ordinal); sorted_dictionary.Add("interval", 60); List<object> peers = new List<object>(); DataTable data_table = psy_trance_fm.fill("SELECT * FROM [dbo].[psy_trance_fm_bittorrent_announces] WHERE [info_hash] = @info_hash", parameters, CommandType.Text); foreach (DataRow data_row in data_table.Rows) { SortedDictionary<string, object> peer = new SortedDictionary<string, object>(StringComparer.Ordinal); peer.Add("peer id", data_row["peer_id"]); peer.Add("ip", data_row["ip"]); peer.Add("port", data_row["port"]); peers.Add(peer); } sorted_dictionary.Add("peers", peers); bencode bencode = new bencode(); Response.Write(bencode.encode(sorted_dictionary)); 


Well that's all! The simplest C # BitTorrent Tracker is ready. Yes, it does not contain any error handling, statistics, and so on and so forth. But it works!

I really hope that people who are knowledgeable will tell you how to improve it, what mistakes it has and generally give more advice.

Thanks for reading!

Source: https://habr.com/ru/post/187544/


All Articles