- Bug fix pressing faq multiple times
- Add tx stats
- Make C# lib (more changes to follow)
This commit is contained in:
Kieran 2018-12-06 00:02:17 +08:00
parent 7c476d0487
commit 06581ffa26
39 changed files with 1451 additions and 728 deletions

View File

@ -115,6 +115,8 @@
</div> </div>
</div> </div>
<div id="page-stats"> <div id="page-stats">
<h3>7 days tx graph</h3>
<canvas id="weektxgraph" width="1024" height="200"></canvas>
</div> </div>
<div id="page-faq"> <div id="page-faq">
<div class="faq-section"> <div class="faq-section">
@ -139,7 +141,7 @@
I have more questions I have more questions
</div> </div>
<div class="faq-content"> <div class="faq-content">
Feel free to join <a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a> or send a mail to <a href="mailto:admin@void.cat">admin@void.cat</a> Feel free to join <a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a> and ask there.
</div> </div>
</div> </div>
</div> </div>
@ -149,7 +151,9 @@
<div><b>24Hr Transfer:</b><span>0 B</span></div> <div><b>24Hr Transfer:</b><span>0 B</span></div>
</div> </div>
<div id="footer"> <div id="footer">
<a href="javascript:(function() { window.location.href='/#faq'; App.Init(); })()">FAQ</a> | <a href="http://rv6omygg3ksi3dys.onion/" target="_blank">Tor</a> <a href="javascript:(function() { App.ShowStats(); })()">Stats</a> |
<a href="javascript:(function() { App.ShowFAQ(); })()">FAQ</a> |
<a href="http://rv6omygg3ksi3dys.onion/" target="_blank">Tor</a>
<br> <br>
</div> </div>
@ -159,14 +163,6 @@
(adsbygoogle = window.adsbygoogle || []).push({}); (adsbygoogle = window.adsbygoogle || []).push({});
</script> </script>
</div> </div>
<!--<script type="text/javascript" src="src/js/Const.js" async></script>
<script type="text/javascript" src="src/js/Util.js" async></script>
<script type="text/javascript" src="src/js/DropzoneManager.js" async></script>
<script type="text/javascript" src="src/js/FileDownloader.js" async></script>
<script type="text/javascript" src="src/js/FileUpload.js" async></script>
<script type="text/javascript" src="src/js/VBF.js" async></script>
<script type="text/javascript" src="src/js/ViewManager.js" async></script>
<script type="text/javascript" src="src/js/App.js" async></script>-->
<script type="text/javascript" src="dist/script.min.js" async></script> <script type="text/javascript" src="dist/script.min.js" async></script>
</body> </body>

View File

@ -173,6 +173,7 @@ a:hover { text-decoration: underline; }
#page-stats { #page-stats {
display: none; display: none;
margin: 10px;
} }
#page-faq { #page-faq {

View File

@ -2,12 +2,16 @@
* @constant {Object} * @constant {Object}
*/ */
const App = { const App = {
Loaded: false,
CharJsLoaded: false,
Elements: { Elements: {
get Dropzone() { return $('#dropzone') }, get Dropzone() { return $('#dropzone') },
get Uploads() { return $('#uploads') }, get Uploads() { return $('#uploads') },
get PageView() { return $('#page-view') }, get PageView() { return $('#page-view') },
get PageUpload() { return $('#page-upload') }, get PageUpload() { return $('#page-upload') },
get PageFaq() { return $('#page-faq') } get PageFaq() { return $('#page-faq') },
get PageStats() { return $('#page-stats') }
}, },
Templates: { Templates: {
@ -43,6 +47,61 @@ const App = {
await Promise.all(proc_files); await Promise.all(proc_files);
}, },
ResetView: function () {
App.Elements.PageView.style.display = "none";
App.Elements.PageUpload.style.display = "none";
App.Elements.PageFaq.style.display = "none";
App.Elements.PageStats.style.display = "none";
},
ShowStats: async function () {
location.hash = "#stats";
App.ResetView();
if (!App.CharJsLoaded) {
await App.InsertScript("//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js", () => {
return typeof moment !== "undefined";
});
await App.InsertScript("//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js", () => {
return typeof Chart !== "undefined";
});
}
let api_rsp = await Api.GetTxChart();
if (api_rsp.ok) {
let ctx = $('#weektxgraph').getContext('2d');
new Chart(ctx, {
type: 'line',
data: api_rsp.data,
options: {
scales: {
xAxes: [{
type: 'time',
time: {
source: 'data'
}
}],
yAxes: [{
ticks: {
beginAtZero: true,
callback: function(label, index, labels) {
return Utils.FormatBytes(label);
}
}
}]
}
}
});
}
App.Elements.PageStats.style.display = "block";
},
ShowFAQ: function () {
location.hash = "#faq";
App.ResetView();
App.Elements.PageFaq.style.display = "block";
},
/** /**
* Sets up the page * Sets up the page
*/ */
@ -50,26 +109,20 @@ const App = {
App.CheckBrowserSupport(); App.CheckBrowserSupport();
App.MakePolyfills(); App.MakePolyfills();
window.site_info = await Api.GetSiteInfo(); App.ResetView();
App.Elements.PageView.style.display = "none";
App.Elements.PageUpload.style.display = "none";
App.Elements.PageFaq.style.display = "none";
if (location.hash !== "") { if (location.hash !== "") {
if (location.hash == "#faq") { if (location.hash == "#faq") {
let faq_headers = document.querySelectorAll('#page-faq .faq-header'); App.ShowFAQ();
for (let x = 0; x < faq_headers.length; x++) { } else if (location.hash == "#stats") {
faq_headers[x].addEventListener('click', function () { App.ShowStats();
this.nextElementSibling.classList.toggle("show");
}.bind(faq_headers[x]));
}
App.Elements.PageFaq.style.display = "block";
} else { } else {
App.Elements.PageView.style.display = "block"; App.Elements.PageView.style.display = "block";
new ViewManager(); new ViewManager();
} }
window.site_info = await Api.GetSiteInfo();
} else { } else {
window.site_info = await Api.GetSiteInfo();
App.Elements.PageUpload.style.display = "block"; App.Elements.PageUpload.style.display = "block";
$('#dropzone').innerHTML = `Click me!<br><small>(${Utils.FormatBytes(window.site_info.data.max_upload_size)} max)</small>`; $('#dropzone').innerHTML = `Click me!<br><small>(${Utils.FormatBytes(window.site_info.data.max_upload_size)} max)</small>`;
new DropzoneManager(App.Elements.Dropzone); new DropzoneManager(App.Elements.Dropzone);
@ -81,6 +134,15 @@ const App = {
elms[1].textContent = Utils.FormatBytes(window.site_info.data.basic_stats.Size, 2); elms[1].textContent = Utils.FormatBytes(window.site_info.data.basic_stats.Size, 2);
elms[2].textContent = Utils.FormatBytes(window.site_info.data.basic_stats.Transfer_24h, 2); elms[2].textContent = Utils.FormatBytes(window.site_info.data.basic_stats.Transfer_24h, 2);
} }
let faq_headers = document.querySelectorAll('#page-faq .faq-header');
for (let x = 0; x < faq_headers.length; x++) {
faq_headers[x].addEventListener('click', function () {
this.nextElementSibling.classList.toggle("show");
}.bind(faq_headers[x]));
}
App.Loaded = true;
}, },
/** /**
@ -96,12 +158,24 @@ const App = {
/** /**
* Adds a script tag at the top of the header * Adds a script tag at the top of the header
* @param {string} src - The script src url * @param {string} src - The script src url
* @param {function} fnWait - Function to use in promise to test if the script is loaded
*/ */
InsertScript: function (src) { InsertScript: async function (src, fnWait) {
var before = document.head.getElementsByTagName('script')[0]; var before = document.head.getElementsByTagName('script')[0];
var newlink = document.createElement('script'); var newlink = document.createElement('script');
newlink.src = src; newlink.src = src;
document.head.insertBefore(newlink, before); document.head.insertBefore(newlink, before);
if (typeof fnWait === "function") {
await new Promise((resolve, reject) => {
let timer = setInterval(() => {
if (fnWait()) {
clearInterval(timer);
resolve();
}
}, 100);
});
}
}, },
/** /**

View File

@ -81,6 +81,12 @@ const Api = {
return JSON.parse((await JsonXHR('POST', '/api', req)).response); return JSON.parse((await JsonXHR('POST', '/api', req)).response);
}, },
GetTxChart: async function (id) {
return await Api.DoRequest({
cmd: '7_day_tx_graph'
});
},
GetSiteInfo: async function (id) { GetSiteInfo: async function (id) {
return await Api.DoRequest({ return await Api.DoRequest({
cmd: 'site_info' cmd: 'site_info'

View File

@ -57,6 +57,27 @@
} }
break; break;
} }
case "7_day_tx_graph": {
$stats = Stats::GetTxStats(24 * 7);
$data = array();
foreach($stats as $time => $bytes){
$data[] = array(
"t" => date_timestamp_get(date_create_from_format("YmdH", $time)) * 1000,
"y" => $bytes
);
}
$rsp->data = array(
"datasets" => array(
array(
"label" => "bytes",
"data" => $data
)
)
);
$rsp->ok = true;
break;
}
} }
header('Content-Type: application/json'); header('Content-Type: application/json');

View File

@ -7,18 +7,31 @@
private static $AllTransferStatsKey = REDIS_PREFIX . "stats-transfer-all:"; private static $AllTransferStatsKey = REDIS_PREFIX . "stats-transfer-all:";
private static $GeneralStatsKey = REDIS_PREFIX . "stats-general"; private static $GeneralStatsKey = REDIS_PREFIX . "stats-general";
public static function GetTxStats($nHours) : array {
$redis = StaticRedis::ReadOp();
$ret = array();
$now = time();
for($x = 0; $x < $nHours; $x += 1) {
$stat_key = date("YmdH", $now - (60 * 60 * $x));
$val = $redis->get(self::$AllTransferStatsKey . $stat_key);
if($val != False){
$ret[$stat_key] = intval($val);
} else {
$ret[$stat_key] = 0;
}
}
return $ret;
}
public static function Get() : Stats { public static function Get() : Stats {
$redis = StaticRedis::ReadOp(); $redis = StaticRedis::ReadOp();
//calculate 24hr transfer stats //calculate 24hr transfer stats
$tx_24h = 0; $tx_24h = 0;
$now = time(); foreach(self::GetTxStats(24) as $time => $bytes) {
for($x = 0; $x < 24; $x += 1) { $tx_24h += $bytes;
$stat_key = date("YmdH", $now - (60 * 60 * $x));
$val = $redis->get(self::$AllTransferStatsKey . $stat_key);
if($val != False){
$tx_24h += intval($val);
}
} }
//get general stats //get general stats

View File

@ -56,7 +56,7 @@
)); ));
//this should be sent to the slave node if we are connected on a slave //this should be sent to the slave node if we are connected on a slave
StaticRedis::ReadOp()->publish('ga-page-view-matomo', $msg); StaticRedis::ReadOp()->publish(StaticRedis::$IsConnectedToSlave ? 'v3-matomo' : 'v3-matomo-master', $msg);
} }
} }
?> ?>

View File

@ -26,6 +26,9 @@
} }
public function HandleRequest() : void { public function HandleRequest() : void {
header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]);
header("Access-Control-Allow-Method: POST");
$rsp = new UploadResponse(); $rsp = new UploadResponse();
$file_size = $_SERVER["CONTENT_LENGTH"]; $file_size = $_SERVER["CONTENT_LENGTH"];

View File

@ -4,7 +4,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace void_util namespace void_lib
{ {
public class ChunkStream : Stream, IDisposable public class ChunkStream : Stream, IDisposable
{ {

View File

@ -0,0 +1,152 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace void_lib
{
public class DownloadResult
{
public string Filepath { get; set; }
public FileHeader Header { get; set; }
}
public class Download : Progress<VoidProgress>
{
private Guid Id { get; set; } = Guid.NewGuid();
private string UserAgent { get; set; }
public Download(string ua = "VoidUtil/1.0")
{
UserAgent = ua;
}
/// <summary>
/// Downloads a file and returns the temp file name and the FileHeader associated with the file
/// </summary>
/// <param name="url">Full url including key and iv</param>
/// <returns></returns>
public async Task<DownloadResult> DownloadFileAsync(string url)
{
var url_base = new Uri(url);
var hash_frag = url_base.Fragment.Substring(1).Split(':');
var key = hash_frag[1].FromHex();
var iv = hash_frag[2].FromHex();
base.OnReport(VoidProgress.Create(Id, label: $"Starting..."));
base.OnReport(VoidProgress.Create(Id, log: $"Starting download for: {hash_frag[0]}"));
var req = (HttpWebRequest)WebRequest.Create($"{url_base.Scheme}://{url_base.Host}/{hash_frag[0]}");
req.UserAgent = UserAgent;
var rsp = await req.GetResponseAsync();
var file_length = rsp.ContentLength;
using (var rsp_stream = rsp.GetResponseStream())
{
var version = rsp_stream.ReadByte();
var hmac_data = new byte[32];
var ts = new byte[4];
await rsp_stream.ReadAsync(hmac_data, 0, hmac_data.Length);
await rsp_stream.ReadAsync(ts, 0, ts.Length);
base.OnReport(VoidProgress.Create(Id, log: $"Blob version is {version}, HMAC is {hmac_data.ToHex()}"));
var tmp_name = Path.GetTempFileName();
FileHeader header_obj = null;
using (var tmp_file = new FileStream(tmp_name, FileMode.Open, FileAccess.ReadWrite))
{
using (var aes = new AesManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
using (var ds = aes.CreateDecryptor(key, iv))
{
var buf = new byte[ds.InputBlockSize * 1024];
var out_buf = new byte[ds.OutputBlockSize * 1024];
bool first_block = true;
int read_offset = 0;
long t_len = 0;
while (true)
{
var rlen = await rsp_stream.ReadAsync(buf, read_offset, buf.Length - read_offset);
//end do final block
if (rlen == 0)
{
var last_buf = ds.TransformFinalBlock(buf, 0, read_offset);
await tmp_file.WriteAsync(last_buf, 0, last_buf.Length);
break;
}
else
{
if ((read_offset + rlen) % ds.InputBlockSize != 0)
{
read_offset += rlen;
continue;
}
else
{
rlen += read_offset;
read_offset = 0;
}
}
var clen = ds.TransformBlock(buf, 0, rlen, out_buf, 0);
if (first_block)
{
first_block = false;
var hlen = BitConverter.ToUInt16(out_buf, 0);
var header = Encoding.UTF8.GetString(out_buf, 2, hlen);
base.OnReport(VoidProgress.Create(Id, log: $"Header is: {header}"));
header_obj = JsonConvert.DeserializeObject<FileHeader>(header);
var file_start = 2 + hlen;
await tmp_file.WriteAsync(out_buf, file_start, clen - file_start);
}
else
{
await tmp_file.WriteAsync(out_buf, 0, clen);
}
t_len += rlen;
base.OnReport(VoidProgress.Create(Id, percentage: t_len / (decimal)file_length));
}
}
}
tmp_file.Seek(0, SeekOrigin.Begin);
using (var hmac = HMAC.Create("HMACSHA256"))
{
hmac.Key = key;
var hmac_test = hmac.ComputeHash(tmp_file);
if (hmac_test.ToHex() == hmac_data.ToHex())
{
base.OnReport(VoidProgress.Create(Id, log: "HMAC verified!"));
}
else
{
throw new Exception($"HMAC verify failed.. {hmac_test.ToHex()} != {hmac_data.ToHex()}");
}
}
}
//file is downloaded to temp path, move it now
return new DownloadResult()
{
Filepath = tmp_name,
Header = header_obj
};
}
}
}
}

View File

@ -1,13 +1,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace void_util namespace void_lib
{ {
public static class Ext public static class Ext
{ {
public static byte[] FromHex(this string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
public static string ToHex(this byte[] data)
{
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
}
public static async Task CopyToAsync(this Stream in_stream, Stream out_stream, Action<long> progress) public static async Task CopyToAsync(this Stream in_stream, Stream out_stream, Action<long> progress)
{ {
long total = 0; long total = 0;

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace void_lib
{
public class FileHeader
{
public string name { get; set; }
public string mime { get; set; }
public ulong len { get; set; }
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace void_lib
{
public abstract class VoidProgress
{
public Guid Id { get; set; }
public static VoidProgress Create(Guid id, string label = null, string log = null, decimal? percentage = null)
{
if (label != null)
{
return new LabelVoidProgress()
{
Id = id,
Label = label
};
}
else if (log != null)
{
return new LogVoidProgress()
{
Id = id,
Log = log
};
}
else if (percentage != null)
{
return new PercentageVoidProgress()
{
Id = id,
Percentage = percentage.Value
};
}
return null;
}
}
public class LabelVoidProgress : VoidProgress
{
public string Label { get; set; }
}
public class LogVoidProgress : VoidProgress
{
public string Log { get; set; }
}
public class PercentageVoidProgress : VoidProgress
{
public decimal Percentage { get; set; }
}
}

View File

@ -0,0 +1,221 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace void_lib
{
public class UploadResponse
{
public int status { get; set; }
public string msg { get; set; }
public string id { get; set; }
public int[] sync { get; set; }
}
public class Upload : Progress<VoidProgress>
{
private Guid Id { get; set; } = Guid.NewGuid();
private string UserAgent { get; set; }
private string BaseHostname { get; set; }
public Upload(string host = "v3.void.cat", string ua = "VoidLib/1.0")
{
BaseHostname = host;
UserAgent = ua;
}
public Task<UploadResponse> UploadFileAsync(string file, byte[] key, byte[] iv)
{
var fi = new FileInfo(file);
return UploadFileAsync(fi.OpenRead(), fi.Name, key, iv);
}
public async Task<UploadResponse> UploadFileAsync(Stream in_stream, string filename, byte[] key, byte[] iv)
{
base.OnReport(VoidProgress.Create(Id, label: "Starting.."));
var site_info = await new VoidApi(BaseHostname).GetUploadHostAsync();
base.OnReport(VoidProgress.Create(Id, log: $"Starting upload for: {filename} => {site_info.upload_host}\nUsing key: {key.ToHex()} and IV: {iv.ToHex()}"));
var file_length = in_stream.Length;
var header = JsonConvert.SerializeObject(new FileHeader()
{
name = filename,
mime = "", // idk what to do with this haha, its not really important anyway since we dont preview in browser
len = (ulong)file_length
});
base.OnReport(VoidProgress.Create(Id, log: $"Using header: {header}"));
//unforutnatly we need to use a raw socket here because HttpWebRequest just bufferes forever
//sad.. no good for large uploads
var hosts = await Dns.GetHostAddressesAsync(site_info.upload_host);
if (hosts.Length > 0)
{
var sock = new Socket(SocketType.Stream, ProtocolType.Tcp);
var tcs = new TaskCompletionSource<bool>();
var sae = new SocketAsyncEventArgs()
{
RemoteEndPoint = new IPEndPoint(hosts[0], 443)
};
sae.Completed += (s, e) =>
{
tcs.SetResult(true);
};
if (sock.ConnectAsync(sae))
{
await tcs.Task;
}
using (var ssl_stream = new SslStream(new NetworkStream(sock)))
{
await ssl_stream.AuthenticateAsClientAsync(site_info.upload_host);
var http_header = $"POST /upload HTTP/1.1\r\nHost: {site_info.upload_host}\r\nConnection: close\r\nContent-Type: application/octet-stream\r\nTransfer-Encoding: chunked\r\nUser-Agent: {UserAgent}\r\nTrailer: \r\nAccept-Encoding: 0\r\n\r\n";
var http_header_bytes = Encoding.UTF8.GetBytes(http_header);
await ssl_stream.WriteAsync(http_header_bytes, 0, http_header_bytes.Length);
await ssl_stream.FlushAsync();
using (var cs = new ChunkStream(ssl_stream, 16384, true))
{
//send the file data
byte[] hash;
//create hmac
base.OnReport(VoidProgress.Create(Id, label: "Hashing..."));
using (var hmac = HMAC.Create("HMACSHA256"))
{
hmac.Key = key;
hash = hmac.ComputeHash(in_stream);
}
base.OnReport(VoidProgress.Create(Id, log: $"HMAC is: {hash.ToHex()}"));
in_stream.Seek(0, SeekOrigin.Begin);
//write header to request stream
var vbf_buf = new byte[37];
vbf_buf[0] = 1;
Array.Copy(hash, 0, vbf_buf, 1, hash.Length);
var ts_buf = BitConverter.GetBytes((UInt32)DateTimeOffset.Now.ToUnixTimeSeconds());
Array.Copy(ts_buf, 0, vbf_buf, 33, ts_buf.Length);
await cs.WriteAsync(vbf_buf, 0, vbf_buf.Length);
base.OnReport(VoidProgress.Create(Id, label: "Uploading..."));
using (var aes = new AesManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
using (var ds = aes.CreateEncryptor(key, iv))
{
var buf = new byte[ds.InputBlockSize * 1000];
var out_buf = new byte[ds.OutputBlockSize * 1000];
var header_bytes = Encoding.UTF8.GetBytes(header);
var hlb = BitConverter.GetBytes((UInt16)header_bytes.Length);
Array.Copy(hlb, buf, hlb.Length);
Array.Copy(header_bytes, 0, buf, 2, header_bytes.Length);
var init_offset = hlb.Length + header_bytes.Length;
long frlen = 0;
long tlen = 0;
while ((frlen = await in_stream.ReadAsync(buf, init_offset, buf.Length - init_offset)) > 0)
{
var actual_rlen = (int)(init_offset + frlen);
if (actual_rlen % ds.InputBlockSize != 0)
{
var last_block = ds.TransformFinalBlock(buf, 0, actual_rlen);
await cs.WriteAsync(last_block, 0, last_block.Length);
}
else
{
var clen = ds.TransformBlock(buf, 0, actual_rlen, out_buf, 0);
await cs.WriteAsync(out_buf, 0, clen);
}
//offset should always be 0 from after the first block
if (init_offset != 0)
{
init_offset = 0;
}
tlen += frlen;
base.OnReport(VoidProgress.Create(Id, percentage: tlen / (decimal)file_length));
}
}
}
//write end chunk
await cs.WriteAsync(new byte[0], 0, 0);
await cs.FlushAsync();
}
//fuck my life why am i doing this to mysefl..
var crlf = new byte[] { 13, 10, 13, 10 };
var sb_headers = new StringBuilder();
var rlen = 0;
var header_buff = new byte[256];
var header_end = 0;
while ((rlen = await ssl_stream.ReadAsync(header_buff, 0, header_buff.Length)) != 0)
{
if ((header_end = header_buff.IndexOf(crlf)) != -1)
{
sb_headers.Append(Encoding.UTF8.GetString(header_buff, 0, header_end + 4));
break;
}
else
{
sb_headers.Append(Encoding.UTF8.GetString(header_buff, 0, rlen));
}
}
var header_dict = sb_headers.ToString().Split('\n').Select(a =>
{
var i = a.IndexOf(":");
return i == -1 ? null : new string[] { a.Substring(0, i), a.Substring(i + 2) };
}).Where(a => a != null).ToDictionary(a => a[0].Trim(), b => b.Length > 1 ? b[1].Trim() : null);
if (header_dict.ContainsKey("Content-Length"))
{
using (var msb = new MemoryStream())
{
msb.Write(header_buff, header_end + 4, rlen - header_end - 4);
await ssl_stream.CopyToAsync(msb);
return JsonConvert.DeserializeObject<UploadResponse>(Encoding.UTF8.GetString(msb.ToArray()));
}
}
else
{
if (header_dict.ContainsKey("Transfer-Encoding") && header_dict["Transfer-Encoding"] == "chunked")
{
using (var msb = new MemoryStream())
{
using (var cr = new ChunkStream(ssl_stream))
{
cr.PreLoadBuffer(header_buff, header_end + 4, rlen - header_end - 4);
await cr.CopyToAsync(msb);
}
return JsonConvert.DeserializeObject<UploadResponse>(Encoding.UTF8.GetString(msb.ToArray()));
}
}
}
}
}
return null;
}
}
}

View File

@ -1,12 +1,10 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace void_util namespace void_lib
{ {
public class BasicStats public class BasicStats
{ {
@ -38,12 +36,21 @@ namespace void_util
public class VoidApi public class VoidApi
{ {
public static async Task<string> CallApiAsync(string cmd) public static string BaseHostname { get; set; }
public static string UserAgent { get; set; }
public VoidApi(string hostname, string ua = "VoidApi/1.0")
{ {
var req = (HttpWebRequest)WebRequest.Create($"https://{Program.BaseHostname}/api"); BaseHostname = hostname;
UserAgent = ua;
}
public async Task<string> CallApiAsync(string cmd)
{
var req = (HttpWebRequest)WebRequest.Create($"https://{BaseHostname}/api");
req.Method = "POST"; req.Method = "POST";
req.ContentType = "application/json"; req.ContentType = "application/json";
req.UserAgent = Program.UserAgent; req.UserAgent = UserAgent;
var cmd_data = Encoding.UTF8.GetBytes(cmd); var cmd_data = Encoding.UTF8.GetBytes(cmd);
await (await req.GetRequestStreamAsync()).WriteAsync(cmd_data, 0, cmd_data.Length); await (await req.GetRequestStreamAsync()).WriteAsync(cmd_data, 0, cmd_data.Length);
@ -55,7 +62,7 @@ namespace void_util
} }
} }
public static async Task<SiteInfo> GetUploadHostAsync() public async Task<SiteInfo> GetUploadHostAsync()
{ {
return JsonConvert.DeserializeObject<ApiResponse<SiteInfo>>(await CallApiAsync(JsonConvert.SerializeObject(new Cmd() return JsonConvert.DeserializeObject<ApiResponse<SiteInfo>>(await CallApiAsync(JsonConvert.SerializeObject(new Cmd()
{ {

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net472</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
</Project>

View File

@ -5,6 +5,10 @@ VisualStudioVersion = 15.0.27703.2042
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "void_util", "void_util\void_util.csproj", "{6AE35BCD-015B-4316-8783-7AF6D75DE7E9}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "void_util", "void_util\void_util.csproj", "{6AE35BCD-015B-4316-8783-7AF6D75DE7E9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "void_util_form", "void_util_form\void_util_form.csproj", "{35F4300A-A50E-4D66-96D4-F9D380B21728}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "void_lib", "void_lib\void_lib.csproj", "{AF65F8E1-D7A7-4EE1-B6C2-31FC6047B8B0}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -15,6 +19,14 @@ Global
{6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Release|Any CPU.Build.0 = Release|Any CPU {6AE35BCD-015B-4316-8783-7AF6D75DE7E9}.Release|Any CPU.Build.0 = Release|Any CPU
{35F4300A-A50E-4D66-96D4-F9D380B21728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35F4300A-A50E-4D66-96D4-F9D380B21728}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35F4300A-A50E-4D66-96D4-F9D380B21728}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35F4300A-A50E-4D66-96D4-F9D380B21728}.Release|Any CPU.Build.0 = Release|Any CPU
{AF65F8E1-D7A7-4EE1-B6C2-31FC6047B8B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF65F8E1-D7A7-4EE1-B6C2-31FC6047B8B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF65F8E1-D7A7-4EE1-B6C2-31FC6047B8B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF65F8E1-D7A7-4EE1-B6C2-31FC6047B8B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,13 +1,8 @@
using Newtonsoft.Json; using System;
using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using void_lib;
namespace void_util namespace void_util
{ {
@ -75,361 +70,61 @@ Modes:
} }
} }
public static byte[] FromHex(string hex) private static void VoidProgress(object sender, VoidProgress vp)
{ {
return Enumerable.Range(0, hex.Length) switch (vp)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
private static string ToHex(byte[] data)
{
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
}
private static async Task UploadFileAsync(string filename, byte[] key, byte[] iv)
{
if (File.Exists(filename))
{ {
var file_info = new FileInfo(filename); case LogVoidProgress l:
var site_info = await VoidApi.GetUploadHostAsync();
Console.WriteLine($"Starting upload for: {file_info.Name} => {site_info.upload_host}\nUsing key: {ToHex(key)} and IV: {ToHex(iv)}");
var file_length = file_info.Length;
var header = JsonConvert.SerializeObject(new FileHeader()
{
name = file_info.Name,
mime = "", // idk what to do with this haha, its not really important anyway since we dont preview in browser
len = (ulong)file_length
});
Console.WriteLine($"Using header: {header}");
// unforutnatly we need to use a raw socket here because HttpWebRequest just bufferes forever
//sad.. no good for large uploads
var hosts = await Dns.GetHostAddressesAsync(site_info.upload_host);
if (hosts.Length > 0)
{
var sock = new Socket(SocketType.Stream, ProtocolType.Tcp);
var tcs = new TaskCompletionSource<bool>();
var sae = new SocketAsyncEventArgs()
{ {
RemoteEndPoint = new IPEndPoint(hosts[0], 443) Console.WriteLine(l.Log);
}; break;
sae.Completed += (s, e) =>
{
tcs.SetResult(true);
};
if (sock.ConnectAsync(sae))
{
await tcs.Task;
} }
case LabelVoidProgress l:
using (var ssl_stream = new SslStream(new NetworkStream(sock)))
{ {
await ssl_stream.AuthenticateAsClientAsync(site_info.upload_host); Console.WriteLine(l.Label);
break;
var http_header = $"POST /upload HTTP/1.1\r\nHost: {site_info.upload_host}\r\nConnection: close\r\nContent-Type: application/octet-stream\r\nTransfer-Encoding: chunked\r\nUser-Agent: {UserAgent}\r\nTrailer: \r\nAccept-Encoding: 0\r\n\r\n";
var http_header_bytes = Encoding.UTF8.GetBytes(http_header);
await ssl_stream.WriteAsync(http_header_bytes, 0, http_header_bytes.Length);
await ssl_stream.FlushAsync();
using (var cs = new ChunkStream(ssl_stream, 16384, true))
{
//send the file data
using (var fs = file_info.OpenRead())
{
byte[] hash;
//create hmac
Console.WriteLine("Hashing...");
using (var hmac = HMAC.Create("HMACSHA256"))
{
hmac.Key = key;
hash = hmac.ComputeHash(fs);
}
Console.WriteLine($"Hash is {ToHex(hash)}");
fs.Seek(0, SeekOrigin.Begin);
//write header to request stream
var vbf_buf = new byte[37];
vbf_buf[0] = 1;
Array.Copy(hash, 0, vbf_buf, 1, hash.Length);
var ts_buf = BitConverter.GetBytes((UInt32)DateTimeOffset.Now.ToUnixTimeSeconds());
Array.Copy(ts_buf, 0, vbf_buf, 33, ts_buf.Length);
await cs.WriteAsync(vbf_buf, 0, vbf_buf.Length);
Console.WriteLine("Encrypting and Uploading...");
using (var aes = new AesManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
using (var ds = aes.CreateEncryptor(key, iv))
{
var buf = new byte[ds.InputBlockSize * 1000];
var out_buf = new byte[ds.OutputBlockSize * 1000];
var header_bytes = Encoding.UTF8.GetBytes(header);
var hlb = BitConverter.GetBytes((UInt16)header_bytes.Length);
Array.Copy(hlb, buf, hlb.Length);
Array.Copy(header_bytes, 0, buf, 2, header_bytes.Length);
var init_offset = hlb.Length + header_bytes.Length;
long frlen = 0;
long tlen = 0;
while ((frlen = await fs.ReadAsync(buf, init_offset, buf.Length - init_offset)) > 0)
{
var actual_rlen = (int)(init_offset + frlen);
if (actual_rlen % ds.InputBlockSize != 0)
{
var last_block = ds.TransformFinalBlock(buf, 0, actual_rlen);
await cs.WriteAsync(last_block, 0, last_block.Length);
}
else
{
var clen = ds.TransformBlock(buf, 0, actual_rlen, out_buf, 0);
await cs.WriteAsync(out_buf, 0, clen);
}
//offset should always be 0 from after the first block
if (init_offset != 0)
{
init_offset = 0;
}
tlen += frlen;
Console.Write($"\r{(100 * (tlen / (decimal)file_length)).ToString("000.0")}%");
}
}
}
}
//write end chunk
await cs.WriteAsync(new byte[0], 0, 0);
await cs.FlushAsync();
}
//fuck my life why am i doing this to mysefl..
var crlf = new byte[] { 13, 10, 13, 10 };
var sb_headers = new StringBuilder();
var rlen = 0;
var header_buff = new byte[256];
var header_end = 0;
while ((rlen = await ssl_stream.ReadAsync(header_buff, 0, header_buff.Length)) != 0)
{
if ((header_end = header_buff.IndexOf(crlf)) != -1)
{
sb_headers.Append(Encoding.UTF8.GetString(header_buff, 0, header_end + 4));
break;
}
else
{
sb_headers.Append(Encoding.UTF8.GetString(header_buff, 0, rlen));
}
}
var header_dict = sb_headers.ToString().Split('\n').Select(a =>
{
var i = a.IndexOf(":");
return i == -1 ? null : new string[] { a.Substring(0, i), a.Substring(i + 2) };
}).Where(a => a != null).ToDictionary(a => a[0].Trim(), b => b.Length > 1 ? b[1].Trim() : null);
if (header_dict.ContainsKey("Content-Length"))
{
using (var msb = new MemoryStream())
{
msb.Write(header_buff, header_end + 4, rlen - header_end - 4);
await ssl_stream.CopyToAsync(msb);
PrintUploadResult(msb.ToArray(), key, iv);
}
}
else
{
if (header_dict.ContainsKey("Transfer-Encoding") && header_dict["Transfer-Encoding"] == "chunked")
{
using (var msb = new MemoryStream())
{
using (var cr = new ChunkStream(ssl_stream))
{
cr.PreLoadBuffer(header_buff, header_end + 4, rlen - header_end - 4);
await cr.CopyToAsync(msb);
}
PrintUploadResult(msb.ToArray(), key, iv);
}
}
}
} }
} case PercentageVoidProgress p:
}
else
{
Console.WriteLine("\nError: file not found!");
}
}
private static void PrintUploadResult(byte[] data, byte[] key, byte[] iv)
{
var json_data = Encoding.UTF8.GetString(data);
try
{
var rsp = JsonConvert.DeserializeObject<UploadResponse>(json_data);
if (rsp != null)
{
if (rsp.status == 200)
{ {
Console.WriteLine($"\nUpload complete!\nUrl: https://{BaseHostname}/#{rsp.id}:{ToHex(key)}:{ToHex(iv)}"); Console.Write($"\r{(100 * p.Percentage).ToString("000.00")}%");
break;
} }
else
{
Console.WriteLine($"\nUpload error: {rsp.msg}");
}
}
else
{
Console.WriteLine($"\nGot invalid response: {json_data}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Got unknown response: \n{json_data}");
} }
} }
private static async Task DownloadFileAsync(string url) private static async Task DownloadFileAsync(string url)
{ {
var url_base = new Uri(url); var dl = new Download(ua: UserAgent);
var hash_frag = url_base.Fragment.Substring(1).Split(':'); dl.ProgressChanged += VoidProgress;
Console.WriteLine($"Starting download for: {hash_frag[0]}"); var res = await dl.DownloadFileAsync(url);
var req = (HttpWebRequest)WebRequest.Create($"{url_base.Scheme}://{url_base.Host}/{hash_frag[0]}"); if(res != null)
req.UserAgent = UserAgent;
var rsp = await req.GetResponseAsync();
var file_length = rsp.ContentLength;
using (var rsp_stream = rsp.GetResponseStream())
{ {
var version = rsp_stream.ReadByte(); var out_file = Path.Combine(Directory.GetCurrentDirectory(), res.Header.name);
var hmac_data = new byte[32];
var ts = new byte[4];
await rsp_stream.ReadAsync(hmac_data, 0, hmac_data.Length);
await rsp_stream.ReadAsync(ts, 0, ts.Length);
Console.WriteLine($"Blob version is {version}, HMAC is {ToHex(hmac_data)}");
var tmp_name = Path.GetTempFileName();
string real_name = null;
using (var tmp_file = new FileStream(tmp_name, FileMode.Open, FileAccess.ReadWrite))
{
using (var aes = new AesManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
using (var ds = aes.CreateDecryptor(FromHex(hash_frag[1]), FromHex(hash_frag[2])))
{
var buf = new byte[ds.InputBlockSize * 1024];
var out_buf = new byte[ds.OutputBlockSize * 1024];
bool first_block = true;
int read_offset = 0;
int last_rlen = 0;
long t_len = 0;
while (true)
{
var rlen = await rsp_stream.ReadAsync(buf, read_offset, buf.Length - read_offset);
//end do final block
if (rlen == 0)
{
var last_buf = ds.TransformFinalBlock(buf, 0, last_rlen);
await tmp_file.WriteAsync(last_buf, 0, last_buf.Length);
break;
}
else
{
if ((read_offset + rlen) % ds.InputBlockSize != 0)
{
read_offset += rlen;
continue;
}
else
{
rlen += read_offset;
last_rlen = rlen;
read_offset = 0;
}
}
var clen = ds.TransformBlock(buf, 0, rlen, out_buf, 0);
if (first_block)
{
first_block = false;
var hlen = BitConverter.ToUInt16(out_buf, 0);
var header = Encoding.UTF8.GetString(out_buf, 2, hlen);
Console.WriteLine($"Header is: {header}");
var header_obj = JsonConvert.DeserializeObject<FileHeader>(header);
real_name = header_obj.name;
var file_start = 2 + hlen;
await tmp_file.WriteAsync(out_buf, file_start, clen - file_start);
}
else
{
await tmp_file.WriteAsync(out_buf, 0, clen);
}
t_len += rlen;
Console.Write($"\r{(100 * (t_len / (decimal)file_length)).ToString("000.0")}%");
}
}
}
tmp_file.Seek(0, SeekOrigin.Begin);
using (var hmac = HMAC.Create("HMACSHA256"))
{
var hmac_test = hmac.ComputeHash(tmp_file);
if (ToHex(hmac_test) == ToHex(hmac_data))
{
Console.WriteLine("HMAC verified!");
}
else
{
throw new Exception($"HMAC verify failed.. {ToHex(hmac_test)} != {ToHex(hmac_data)}");
}
}
}
//file is downloaded to temp path, move it now
var out_file = Path.Combine(Directory.GetCurrentDirectory(), real_name);
Console.WriteLine($"\nMoving file to {out_file}"); Console.WriteLine($"\nMoving file to {out_file}");
File.Move(tmp_name, out_file); File.Move(res.Filepath, out_file);
} }
Console.WriteLine("\nDone!");
} }
}
internal class FileHeader private static async Task UploadFileAsync(string filename, byte[] key, byte[] iv)
{ {
public string name { get; set; } var up = new Upload(host: BaseHostname, ua: UserAgent);
public string mime { get; set; } up.ProgressChanged += VoidProgress;
public ulong len { get; set; }
var rsp = await up.UploadFileAsync(filename, key, iv);
if (rsp != null)
{
if (rsp.status == 200)
{
Console.WriteLine($"\nUpload complete!\nUrl: https://{BaseHostname}/#{rsp.id}:{key.ToHex()}:{iv.ToHex()}");
}
else
{
Console.WriteLine($"\nUpload error: {rsp.msg}");
}
}
}
} }
internal class FileData internal class FileData
@ -440,12 +135,4 @@ Modes:
public DateTime Uploaded { get; set; } public DateTime Uploaded { get; set; }
public byte[] EncryptedPayload { get; set; } public byte[] EncryptedPayload { get; set; }
} }
internal class UploadResponse
{
public int status { get; set; }
public string msg { get; set; }
public string id { get; set; }
public int[] sync { get; set; }
}
} }

View File

@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks> <TargetFrameworks>netcoreapp2.1;net472</TargetFrameworks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <ProjectReference Include="..\void_lib\void_lib.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@ -0,0 +1,197 @@
namespace void_util_form
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.listView1 = new System.Windows.Forms.ListView();
this.button1 = new System.Windows.Forms.Button();
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.listView2 = new System.Windows.Forms.ListView();
this.columnHeader4 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader5 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader6 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.button2 = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.textBox1 = new System.Windows.Forms.TextBox();
this.groupBox1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.SuspendLayout();
//
// groupBox1
//
this.groupBox1.Controls.Add(this.button1);
this.groupBox1.Controls.Add(this.listView1);
this.groupBox1.Location = new System.Drawing.Point(12, 12);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(504, 246);
this.groupBox1.TabIndex = 0;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Upload";
//
// listView1
//
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2,
this.columnHeader3});
this.listView1.Location = new System.Drawing.Point(6, 19);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(409, 221);
this.listView1.TabIndex = 0;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details;
//
// button1
//
this.button1.Location = new System.Drawing.Point(421, 19);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(83, 23);
this.button1.TabIndex = 1;
this.button1.Text = "Upload Files";
this.button1.UseVisualStyleBackColor = true;
//
// columnHeader1
//
this.columnHeader1.Text = "ID";
this.columnHeader1.Width = 146;
//
// columnHeader2
//
this.columnHeader2.Text = "Name";
this.columnHeader2.Width = 187;
//
// columnHeader3
//
this.columnHeader3.Text = "Progress";
//
// groupBox2
//
this.groupBox2.Controls.Add(this.textBox1);
this.groupBox2.Controls.Add(this.label1);
this.groupBox2.Controls.Add(this.button2);
this.groupBox2.Controls.Add(this.listView2);
this.groupBox2.Location = new System.Drawing.Point(12, 264);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(504, 251);
this.groupBox2.TabIndex = 1;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Download";
//
// listView2
//
this.listView2.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader4,
this.columnHeader5,
this.columnHeader6});
this.listView2.Location = new System.Drawing.Point(6, 39);
this.listView2.Name = "listView2";
this.listView2.Size = new System.Drawing.Size(409, 201);
this.listView2.TabIndex = 2;
this.listView2.UseCompatibleStateImageBehavior = false;
this.listView2.View = System.Windows.Forms.View.Details;
//
// columnHeader4
//
this.columnHeader4.Text = "ID";
this.columnHeader4.Width = 146;
//
// columnHeader5
//
this.columnHeader5.Text = "Name";
this.columnHeader5.Width = 187;
//
// columnHeader6
//
this.columnHeader6.Text = "Progress";
//
// button2
//
this.button2.Location = new System.Drawing.Point(421, 11);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(83, 23);
this.button2.TabIndex = 3;
this.button2.Text = "Download";
this.button2.UseVisualStyleBackColor = true;
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(6, 16);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(20, 13);
this.label1.TabIndex = 4;
this.label1.Text = "Url";
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(32, 13);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(383, 20);
this.textBox1.TabIndex = 5;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(528, 527);
this.Controls.Add(this.groupBox2);
this.Controls.Add(this.groupBox1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Name = "Form1";
this.Text = "VoidApp";
this.groupBox1.ResumeLayout(false);
this.groupBox2.ResumeLayout(false);
this.groupBox2.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.ListView listView1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.ColumnHeader columnHeader3;
private System.Windows.Forms.GroupBox groupBox2;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.ListView listView2;
private System.Windows.Forms.ColumnHeader columnHeader4;
private System.Windows.Forms.ColumnHeader columnHeader5;
private System.Windows.Forms.ColumnHeader columnHeader6;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Label label1;
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using void_lib;
namespace void_util_form
{
public partial class MainForm : Form
{
private static ObservableCollection<DownloadView> Downloads { get; set; } = new ObservableCollection<DownloadView>();
public MainForm()
{
InitializeComponent();
listView2.DataBindings = Downloads;
}
private void button2_Click(object sender, EventArgs e)
{
}
}
internal class DownloadView
{
public DownloadView(Download dl)
{
dl.ProgressChanged += (s, e) =>
{
switch (e)
{
case LogVoidProgress l:
{
Console.WriteLine(l.Log);
break;
}
case LabelVoidProgress l:
{
Label = l.Label;
break;
}
case PercentageVoidProgress p:
{
ProgressInner = p.Percentage;
break;
}
}
};
}
private decimal ProgressInner { get; set; }
public string Progress => $"{(100 * ProgressInner).ToString("0.0")}%";
public string Label { get; set; }
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace void_util_form
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("void_util_form")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("void_util_form")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("35f4300a-a50e-4d66-96d4-f9d380b21728")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace void_util_form.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("void_util_form.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace void_util_form.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{35F4300A-A50E-4D66-96D4-F9D380B21728}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>void_util_form</RootNamespace>
<AssemblyName>void_util_form</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\void_lib\void_lib.csproj">
<Project>{af65f8e1-d7a7-4ee1-b6c2-31fc6047b8b0}</Project>
<Name>void_lib</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -13,24 +13,27 @@ namespace ga_page_view
static ConnectionMultiplexer c { get; set; } static ConnectionMultiplexer c { get; set; }
static BatchBlock<string> _queue = new BatchBlock<string>(20); static BatchBlock<string> _queue = new BatchBlock<string>(20);
static string Token { get; set; } static string Token { get; set; }
static string Channel { get; set; }
static Task Main(string[] args) static Task Main(string[] args)
{ {
if (args.Length != 1) if (args.Length != 2)
{ {
Console.WriteLine("Required args: token_auth"); Console.WriteLine("Required args: channel token_auth");
return Task.CompletedTask; return Task.CompletedTask;
} }
Token = args[0]; Channel = args[0];
Console.WriteLine($"Token is: {Token}"); Token = args[1];
Console.WriteLine($"Token is: {Token}\nChannel is: {Channel}");
return startSvc(); return startSvc();
} }
private static async Task startSvc() private static async Task startSvc()
{ {
c = await ConnectionMultiplexer.ConnectAsync("localhost"); c = await ConnectionMultiplexer.ConnectAsync("localhost");
await c.GetSubscriber().SubscribeAsync("ga-page-view-matomo", queueMsg); await c.GetSubscriber().SubscribeAsync(Channel, queueMsg);
Console.WriteLine("Connected to redis"); Console.WriteLine("Connected to redis");

View File

@ -8,9 +8,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<TargetFramework>netcoreapp2.1</TargetFramework> <TargetFramework>netcoreapp2.1</TargetFramework>
<PublishDir>bin\Release\netcoreapp2.1\publish\</PublishDir> <PublishDir>bin\publish\</PublishDir>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier> <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained> <SelfContained>true</SelfContained>
<_IsPortable>true</_IsPortable> <_IsPortable>false</_IsPortable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,3 +0,0 @@
/target/
**/*.rs.bk

View File

@ -1,7 +0,0 @@
[package]
name = "ga_pageview"
version = "0.1.0"
authors = ["v0l"]
[dependencies]
redis = "0.8.0"

View File

@ -1,37 +0,0 @@
extern crate redis;
use std::process::Command;
fn main() {
let ch = "ga-page-view";
let mut q = Vec::new();
if let Ok(client) = redis::Client::open("redis://127.0.0.1/") {
if let Ok(mut pubsub) = client.get_pubsub() {
if let Ok(_) = pubsub.subscribe(ch) {
println!("Subscribed to {}", ch);
loop {
if let Ok(msg) = pubsub.get_message() {
if let Ok(payload) = msg.get_payload::<String>() {
//println!("channel '{}': {}", msg.get_channel_name(), payload);
if q.len() >= 20 {
//push the rows to ga /batch
Command::new("curl").arg("-X").arg("POST").arg("--data").arg(q.join(" \\\r\n")).arg("https://www.google-analytics.com/batch").output().expect("failed to execute process");
q.clear();
}
q.push(payload);
}
}
}
}else {
println!("Failed to subscribe");
}
}else {
println!("Failed to get pubsub");
}
}else {
println!("Failed to connect to redis");
}
}

View File

@ -1,2 +0,0 @@
target/
Cargo.lock

View File

@ -1,13 +0,0 @@
[package]
name = "logparser"
version = "0.1.0"
authors = ["v0l"]
[dependencies]
regex = "0.2"
chrono = "0.4"
getopts = "0.2"
serde = "^1.0"
serde_json = "^1.0"
serde_derive = "^1.0"
maxminddb = "0.8.1"

View File

@ -1,97 +0,0 @@
<!doctype html>
<html>
<head>
<title>Block report for void.cat</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
<style>
html {
background-color: #888;
}
body {
font-family: Arial;
font-size: 12px;
width: 400px;
margin-left: auto;
margin-right: auto;
background-color: #eee;
padding: 20px;
border-radius: 3px;
}
table {
width: 100%;
text-align: center;
border-collapse: collapse;
}
th, td {
padding: 5px;
}
thead {
background-color: #333;
color: #fff;
}
</style>
</head>
<body>
<h3>Stats per country</h3>
<canvas id="country-stats" width="400" height="400"></canvas>
<h3>Top 10 blocked Ips</h3>
<table>
<thead>
<tr>
<th>Country</th>
<th>Ip</th>
<th>Hits</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
var IPS = JSON.parse(xhr.response);
//load top 10 rows
var tb = document.querySelector('table');
for(var x = 0; x < 10; x++) {
var nr = document.createElement('tr');
nr.innerHTML = '<td><img height="20" src="https://raw.githubusercontent.com/hjnilsson/country-flags/master/png100px/' + IPS[x].country + '.png" title="' + IPS[x].country + '"/></td><td>' + IPS[x].ip + '</td><td>' + IPS[x].hits + '</td>';
tb.tBodies[0].appendChild(nr);
}
var c_stats = new Map();
for(var x = 0; x < IPS.length; x++) {
var ct = IPS[x].country;
if (c_stats.has(ct)) {
c_stats.set(ct,c_stats.get(ct) + IPS[x].hits);
}else {
c_stats.set(ct, IPS[x].hits);
}
}
var c_vals = Array.from(c_stats.entries()).sort((a,b) => { return a[1] < b[1] ? 1 : (a[1] > b[1] ? -1 : 0); }).slice(0, 10);
new Chart("country-stats", {
type: 'doughnut',
data: {
datasets: [{
data: c_vals.map((a) => { return a[1] })
}],
labels: c_vals.map((a) => { return a[0] })
},
options: {}
});
}
};
xhr.open('GET', '/block.json', true);
xhr.send();
</script>
</body>
</html>

View File

@ -1,168 +0,0 @@
extern crate regex;
extern crate chrono;
extern crate getopts;
extern crate serde;
extern crate serde_json;
extern crate maxminddb;
use std::env;
use std::str::FromStr;
use std::process::Command;
use std::collections::HashMap;
use std::io::{BufReader, BufRead};
use std::fs::File;
use std::io::prelude::*;
use regex::Regex;
use chrono::{DateTime, Utc};
use getopts::Options;
use maxminddb::geoip2;
#[macro_use]
extern crate serde_derive;
const BLOCK_SINCE_DAYS : i64 = 30;
const BLOCK_STATUS_CODE : &str = "444";
const IPSET_NAME : &str = "void_cat_block";
#[derive(Serialize, Deserialize)]
struct IpStats {
ip : String,
hits : u64,
country : String
}
impl IpStats {
fn new(i : String, h : u64, mm : &mut Option<maxminddb::Reader>) -> IpStats {
let mut c : String = String::from("XX");
if let &mut Some(ref m) = mm {
if let Ok(ip) = FromStr::from_str(&i) {
if let Ok(city) = m.lookup::<geoip2::City>(ip) {
if city.country.is_some() {
c = city.country.unwrap().iso_code.unwrap().to_lowercase();
}
}
}
}
IpStats { ip: i, hits: h, country: c }
}
}
fn gen_report(hm : HashMap<String, IpStats>, output : String) {
//convert hashmap into vector for sorting
let mut ordered_stats = Vec::new();
ordered_stats.extend(hm.values());
ordered_stats.sort_by(|a, b| b.hits.cmp(&a.hits));
if let Ok(json) = serde_json::to_string(&ordered_stats) {
//send email report
println!("Saving report..");
if let Ok(mut fout) = File::create(&output) {
match fout.write_all(json.as_bytes()) {
Ok(_) => println!("Report saved to: {}", &output),
Err(e) => println!("Report save failed: {}", e)
}
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let mut opts = Options::new();
opts.optopt("f", "file", "Log file to read", "");
opts.optopt("d", "db", "MaxMind DB", "");
opts.optopt("o", "out", "Output report path", "");
opts.optflag("v", "verbose", "Print more info");
let flags = match opts.parse(&args[1..]) {
Ok(m) => { m }
Err(f) => { panic!(f.to_string()) }
};
let file_name = match flags.opt_str("f"){
Some(x) => x,
None => String::from("access.log")
};
let re = Regex::new(r###"(?P<IP>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<Date>.*)\] "(?P<Method>[A-Z]{1,5}) (?P<Path>.*) (?P<Version>HTTP/\d{1}\.[0-9]{1})" (?P<ResponseCode>\d{1,4}) (?P<ResponseLength>\d{1,11}) "(?P<Referer>.*)" "(?P<UserAgent>.*)""###).unwrap();
if let Ok(f) = File::open(&file_name) {
println!("Reading file: {}", &file_name);
let file = BufReader::new(&f);
let mut block_list = HashMap::<String, IpStats>::new();
let local_time : DateTime<Utc> = Utc::now();
let mut lines = 0u64;
let mut hit_lines = 0u64;
//load max mind data
let maxmind_path = match flags.opt_str("d"){
Some(x) => x,
None => String::from("GeoIP2-City.mmdb")
};
let mut mmdb = match maxminddb::Reader::open(&maxmind_path) {
Ok(m) => {
println!("MaxMind DB loaded!");
Some(m)
},
Err(e) => {
println!("Could not load MaxMind DB: {}", e);
None
}
};
for (_, line) in file.lines().enumerate() {
if let Ok(l) = line {
lines += 1;
if l.contains(BLOCK_STATUS_CODE) { //simple check if the line contains the error code
hit_lines += 1;
if let Some(tokens) = re.captures(&l) {
if let (Some(ip), Some(date), Some(rsp)) = (tokens.get(1), tokens.get(2), tokens.get(6)) {
//println!("Got tokens: {} {} {}", ip.as_str(), date.as_str(), rsp.as_str());
if let Ok(dt) = DateTime::parse_from_str(date.as_str(), "%d/%b/%Y:%H:%M:%S %z") {
let dt_utc = dt.with_timezone(&Utc);
let slog = local_time.signed_duration_since(dt_utc);
//println!("Row match: {}", rsp.as_str() == BLOCK_STATUS_CODE);
if rsp.as_str() == BLOCK_STATUS_CODE && slog.num_days() <= BLOCK_SINCE_DAYS {
let mut host_stat = block_list.entry(ip.as_str().to_owned()).or_insert(IpStats::new(ip.as_str().to_owned(), 0u64, &mut mmdb));
host_stat.hits += 1;
}
}
}
}
}
}
}
let verbose = flags.opt_present("v");
println!("Blocking {} ips, from {}/{} {:.2}% requests", block_list.len(), hit_lines, lines, 100f64 * (hit_lines as f64 / lines as f64));
match Command::new("/sbin/ipset").args(&["flush", &IPSET_NAME]).output() {
Ok(_) => {
println!("[OK]\t:ipset flush");
for (k,_) in &block_list {
match Command::new("/sbin/ipset").args(&["add", &IPSET_NAME, &k]).output() {
Ok(_) => {
if verbose {
println!("[OK]\t:ipset add {} {}", &IPSET_NAME, &k);
}
},
Err(msg) => println!("[F]\t{} ({})", k, msg)
}
}
},
Err(msg) => println!("Failed to run ipset flush command: {}", msg)
}
let output = match flags.opt_str("o") {
Some(x) => x,
None => String::from("report.html")
};
//send email report
gen_report(block_list, output);
}else{
println!("File not found: {}", &file_name);
}
}