From be6b5566014ea3677bf9967a881900e6532afed5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 17 Mar 2021 18:10:38 -0400 Subject: [PATCH] Add Minecraft Mod Statistics --- .../chart.css | 140 + .../countries.svg | 3061 +++++++++++++++++ .../data.json | 1148 +++++++ .../index.md | 313 ++ .../ip-sessions.html.ejs | 37 + .../java-versions.html.ejs | 51 + 6 files changed, 4750 insertions(+) create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/chart.css create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/countries.svg create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/data.json create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/index.md create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/ip-sessions.html.ejs create mode 100644 site/posts/2021-03-17-minecraft-mod-statistics/java-versions.html.ejs diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/chart.css b/site/posts/2021-03-17-minecraft-mod-statistics/chart.css new file mode 100644 index 0000000..172d940 --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/chart.css @@ -0,0 +1,140 @@ +.chart-container { + padding: 15px 0 !important; + overflow-y: hidden; + position: relative;/* + display: flex; + flex-direction: row; + justify-content: center;*/ +} + +.chart { + height: 25vh; + min-width: 825px; + max-width: 1250px; + margin: 0 auto; + display: flex; + flex-direction: row; + justify-content: center; + padding: 0 15px; + position: relative; +} + +#java-version-chart { + margin-bottom: 30px; +} + +.chart-column-container { + height: 100%; +} + +#java-version-chart .chart-column-container { + margin-right: 5px; +} + +#ip-session-chart .chart-column-container { + width: 2px; +} + +.chart-left-labels { + font-size: 0.75rem; + font-family: var(--ui-font); + color: var(--secondary-ui-text-color); + position: absolute; + right: calc(50% + 500px); + height: 100%; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.chart-left-labels > span { + position: relative; + margin-right: 4px; + transform: translateY(-0.375rem); +} + +.chart-left-labels > span::after { + content: " "; + border-top: 1px solid var(--atom-mono-3); + width: 1000px; + display: inline-block; + top: 0; + position: absolute; + margin-left: 4px; + transform: translateY(0.375rem); + opacity: 0.5; +} + +.chart-column { + background-color: var(--atom-base); +} + +.chart-column-label { + display: inline-block; + font-size: 0.75em; + width: 100%; + text-align: center; + word-wrap: normal; +} + +.chart-column-data-label { + display: inline-block; + text-align: center; + width: 100%; + font-size: 0.75em; +} + +.chart-column-spacer { + position: relative; +} +.chart-column-spacer > .chart-column-data-label { + position: absolute; + bottom: 0; +} + +.chart-caption { + font-family: var(--ui-font); + font-style: italic; + font-size: 1rem; + color: var(--secondary-ui-text-color); + text-align: center; + padding: 0 15px; +} + +@media (max-width: 1250px) { + .chart-column-label { + transform: translate(5px, 5px) rotate(25deg); + } +} + +@media (max-width: 1150px) { + #ip-session-chart-container { + overflow-x: scroll; + } + #ip-session-chart { + justify-content: left; + } + .chart-left-labels { + position: initial; + } +} + +/* 825px = (50px + 5px) * 15 columns */ +@media (max-width: 825px) { + #java-version-chart-container { + overflow-x: scroll; + } + #java-version-chart { + justify-content: left; + } +} + +@media (max-width: 540px) { + .chart-column-label { + transform-origin: 0 50%; + transform: translate(50%, -10px) rotate(90deg); + } + #java-version-chart { + margin-bottom: 60px; + } +} diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/countries.svg b/site/posts/2021-03-17-minecraft-mod-statistics/countries.svg new file mode 100644 index 0000000..5337a2e --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/countries.svg @@ -0,0 +1,3061 @@ + + + + World Map + + + + + Sudan, 181 requests + + + South Sudan + + + Georgia, 2,644 requests + + + Abkhazia + + + + + South Ossetia + + + + + + Peru, 18,540 requests + + + Burkina Faso, 8 requests + + + Libya, 53 requests + + + Belarus, 24,518 requests + + + Pakistan, 4,892 requests + + + Azad Jammu and Kashmir + + + + Indonesia, 13,132 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Yemen, 54 requests + + + + + + + Madagascar, 105 requests + + + + + + Bolivia, 1,913 requests + + + + + Serbia, 6,740 requests + + + Kosovo, 84 requests + + + + + + Ivory Coast, 103 requests + + + Algeri, 2,685 requests + + + Switzerland, 41,696 requests + + + Cameroon, 155 requests + + + North Macedonia, 1,695 requests + + + Botswana, 13 requests + + + Kenya, 439 requests + + + Jordan, 1,694 requests + + + Mexico, 52,628 requests + + + + + + + + + + + + + + + + + + United Arab Emirates, 7,368 requests + + + + + Belize, 209 requests + + + + + Brazil, 180,644 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sierra Leone + + + + + Mali, 22 requests + + + Democratic Republic of the Congo, 6 requests + + + Italy, 108,117 requests + + + + + + + + Somalia + + + Somaliland + + + + Afghanistan, 101 requests + + + Bangladesh, 1,859 requests + + + + + + + + + + + + + + Dominican Republic, 2,820 requests + + + + + Guinea-Bissau, 21 requests + + + + + + + + + + + Ghan, 67 requests + + + Austria, 58,225 requests + + + Sweden, 119,957 requests + + + + + + + + + + + + + + + Turkey, 45,194 requests + + + + + + Uganda, 46 requests + + + Mozambique, 31 requests + + + + + + + + New Zealand, 48,396 requests + + + + + + + + + + + + Cuba, 168 requests + + + + + + + + + + + Venezuela, 3,622 requests + + + + + + + + + + + + + + + + Portugal, 49,457 requests + + + + + + + + + + + Colombia, 15,942 requests + + + Mauritania, 36 requests + + + + + Angola, 100 requests + + + + + Germany, 903,124 requests + + + + + + + + Thailand, 18,670 requests + + + + + + + + + + + + Papua New Guinea, 2 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Iraq, 2,619 requests + + + Croatia, 9,263 requests + + + + + + + + + + + + + + + + + + + + + Greenland, 41 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Niger, 8 requests + + + Denmark, 92,557 requests + + + + + + + + + + + + + + + + + + Latvia, 10,696 requests + + + Romania, 41,188 requests + + + Zambia, 13 requests + + + Myanmar, 400 requests + + + + + + + + + + + + + + + + + + + + + + + Ethiopia, 68 requests + + + Guatemala, 2,260 requests + + + Suriname, 185 requests + + + Czech Republic, 80,188 requests + + + Chad + + + Albania, 676 requests + + + Finland, 38,853 requests + + + + + + + + + + + + + + + + + + + + + + Syrian Arab Republic, 370 requests + + + Kyrgyzstan, 1,808 requests + + + Solomon Islands + + + + + + + + + + + + + + + + + + + + + + + + + + + Oman, 865 requests + + + + + + Panama, 1,784 requests + + + + + + + + Argentina, 47,207 requests + + + + + + + + United Kingdom, 633,863 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + Costa Rica, 5,142 requests + + + + + Paraguay, 1,448 requests + + + Guinea + + + + + Ireland, 24,128 requests + + + + + + Nigeria, 332 requests + + + + + + Tunisia, 1,614 requests + + + + + Poland, 178,749 requests + + + Namibia, 414 requests + + + South Africa, 30,565 requests + + + Egypt, 6,467 requests + + + Tanzania, 36 requests + + + + + + + Saudi Arabia, 24,198 requests + + + + + + + + Vietnam, 15,957 requests + + + + + + + + + + + + + Russian Federation, 467,667 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Crimea + + + + Haiti, 11 requests + + + + + + + Bosnia and Herzegovina, 2,463 requests + + + India, 22,378 requests + + + + + + + + + + + + + + + Canada, 569,968 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + El Salvador, 1,725 requests + + + Guyana, 141 requests + + + Belgium, 69,801 requests + + + Equatorial Guinea, 7 requests + + + + + Lesotho + + + Bulgaria, 8,218 requests + + + Burundi, 1 request + + + Djibouti, 40 requests + + + Azerbaijan, 1,316 requests + + + + + + Artsakh, Republic of + + + + + + Iran, 1,637 requests + + + + + + + Malaysia, 20,303 requests + + + + + + + + + + + + + + + + + + Philippines, 23,844 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Uruguay, 7,285 requests + + + Congo, Republic of the + + + Estonia, 12,043 requests + + + + + + + + Rwanda + + + Armenia, 987 requests + + + Senegal, 124 requests + + + Togo, 4 requests + + + Spain, 120,100 requests + + + + + + + + + + + + + + + Gabon, 79 requests + + + + + Hungary, 62,962 requests + + + Malawi + + + Tajikistan, 65 requests + + + Cambodia, 826 requests + + + + + + + South Korea, 31,277 requests + + + + + + + + + + + + Honduras, 1,037 requests + + + + + Iceland, 4,469 requests + + + Nicaragua, 384 requests + + + Chile, 32,052 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Morocco, 2,886 requests + + + + Western Sahara + + + Sahrawi Arab Democratic Republic + + + + + Liberia + + + Central African Republic + + + Slovakia, 22,549 requests + + + Lithuania, 21,472 requests + + + Zimbabwe, 48 requests + + + Sri Lanka, 299 requests + + + + + + Israel, 24,202 requests + + + + State of Palestine, 932 requests + + + Gaza Strip + + + West Bank + + + + + + + Laos, 101 requests + + + North Korea + + + Greece, 11,156 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Turkmenistan, 4 requests + + + Ecuador, 5,877 requests + + + + + + + + + + + + + Benin, 1 request + + + Slovenia, 10,455 requests + + + Norway, 69,516 requests + + Svalbard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Moldova, 5,860 requests + + + Transnistria + + + + + + Ukraine, 111,071 requests + + + Donetsk People's Republic + + + + + Luhansk People's Republic + + + + + + Nepal, 967 requests + + + Eritrea + + + + + United States of America, 3,308,154 requests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kazakhstan, 24,112 requests + + + + + French Southern Territories + + + + Uzbekistan, 2,342 requests + + + Mongolia, 743 requests + + + Bhutan, 1 request + + + Antarctica + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Australia, 290,375 requests + + + + + + + + + + + + + + + + + + + + + + + Christmas Island + + + + + Cocos (Keeling) Islands + + + + + Heard Island and McDonald Islands + + + + + Norfolk Island + + + + + + China, 273,346 requests + + + + + + + + + + + + + + + + + + + + + + + + Hong Kong, 32,207 requests + + + + + + + + + Macao, 2,066 requests + + + + + Taiwan, 41,945 requests + + + + + + + + + + France, 529,213 requests + + + + + + + + + + French Guiana, 136 requests + + + Guadeloupe, 308 requests + + + + + + + + + Martinique, 347 requests + + + + + Reunion, 1,492 requests + + + + + Mayotte, 14 requests + + + + + + Netherlands, 158,818 requests + + + + + + + + + + + + Bonaire, Sint Eustatius and Saba, 13 requests + + + + + + + + + + Lebanon, 1418 requests + + + + Montenegro, 219 requests + + + + Eswatini, 2 requests + + + + New Caledonia, 169 requests + + + + + + + + + + + Fiji, 8 requests + + + + + + + + + + + + + Kuwait, 3,081 requests + + + + + + + Timor-Leste + + + + + + + Bahamas, 483 requests + + + + + + + + + + + + + + + + + + + + + + + Vanuatu, 20 requests + + + + + + + + + + + + + + + + + + + Falkland Islands (Islas Malvinas) + + + + + + + + + + + + + South Georgia and the South Sandwich Islands + + + + Gambia, 1 request + + + + Qatar, 1,952 requests + + + + Jamaica, 589 requests + + + + Cyprus, 1,690 requests + + + Northern Cyprus + + + + + + Puerto Rico, 3,844 requests + + + + Brunei, 650 requests + + + + + + + Trinidad and Tobago, 22,378 requests + + + + + + + Cape Verde, 5 requests + + + + + + + + + + + + + French Polynesia, 524 requests + + + + + + + + + + + + + Samoa + + + + + + + Luxembourg, 3,471 requests + + + + Comoros + + + + + + + + Mauritius, 303 requests + + + + Faroe Islands, 445 requests + + + + + + + + + + + Sao Tome and Principe, 1 request + + + + + + + U.S. Virgin Islands, 28 requests + + + + + + + Curaçao, 206 requests + + + + Sint Maarten (Dutch Part), 123 requests + + + + Dominica, 31 requests + + + + Tonga + + + + + + + + Kiribati + + + + + + + Micronesia, Federated States of + + + + Bahrain, 1,217 requests + + + + + Northern Mariana Islands, 86 requests + + + + Palau, 1 request + + + + Seychelles, 12 requests + + + + British Indian Ocean Territory + + + + Antigua and Barbuda, 26 requests + + + + + + + Barbados, 273 requests + + + + Turks and Caicos Islands, 30 requests + + + + + + + + Saint Vincent and the Grenadines, 20 requests + + + + Saint Lucia, 187 requests + + + + Grenada, 8 requests + + + + Malta, 3,281 requests + + + + Maldives, 273 requests + + + + Cayman Islands, 146 requests + + + + Saint Kitts and Nevis, 72 requests + + + + + + + Montserrat + + + + Saint Barthélemy, 37 requests + + + + Niue + + + + Saint Pierre and Miquelon, 27 requests + + + + Cook Islands + + + + Wallis and Futuna + + + + American Samoa, 45 requests + + + + Marshall Islands, Republic of the + + + + Aruba, 609 requests + + + + Liechtenstein, 80 requests + + + + British Virgin Islands, 5 requests + + + + + + + Saint Helena, Ascension and Tristan da Cunha + + + + Jersey, 820 requests + + + + Anguilla, 32 requests + + + + Saint Martin (French Part), 188 requests + + + + Guernsey, 774 requests + + + + San Marino, 55 requests + + + + Bermuda, 112 requests + + + + Tuvalu + + + + Nauru + + + + Gibraltar, 388 requests + + + + Pitcairn Islands + + + + Monaco, 72 requests + + + + Vatican City + + + + Isle of Man, 1,115 requests + + + + Guam, 951 requests + + + + Singapore, 31,990 requests + + + + Tokelau + + + + + + + diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/data.json b/site/posts/2021-03-17-minecraft-mod-statistics/data.json new file mode 100644 index 0000000..d67ca7f --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/data.json @@ -0,0 +1,1148 @@ +{ + "total": 9541873, + "javaVersions": { + "1.8.0_51": 5429856, + "1.8.0_241": 808058, + "1.8.0_251": 731566, + "1.8.0_242": 314964, + "1.8.0_211": 297361, + "1.8.0_252": 266395, + "1.8.0_45": 225602, + "1.8.0_231": 201723, + "1.8.0_212": 184435, + "1.8.0_221": 170869, + "1.8.0_201": 143430, + "1.8.0_191": 113181, + "1.8.0_181": 94463, + "1.8.0_222": 86167, + "1.8.0_171": 79870, + "1.8.0_161": 46746, + "1.8.0_202": 42365, + "1.8.0_121": 42007, + "1.8.0_74": 34391, + "1.8.0_232": 31104, + "1.8.0_111": 24124, + "1.8.0_131": 23994, + "1.8.0_151": 17311, + "1.8.0_60": 16441, + "1.8.0_144": 14837, + "1.8.0_192": 12992, + "1.8.0_162": 12683, + "1.8.0_101": 12363, + "1.8.0_172": 5660, + "1.8.0_91": 5566, + "1.8.0_212-1-ojdkbuild": 5324, + "1.8.0_72": 4884, + "1.8.0_66": 4775, + "1.8.0_92": 4179, + "1.8.0_65": 3445, + "1.8.0_102": 3389, + "1.8.0_141": 3224, + "1.8.0_112": 3057, + "1.8.0_77": 2785, + "1.8.0_152": 2732, + "1.8.0_40": 2431, + "1.8.0_25": 2260, + "1.8.0_73": 2238, + "1.8.0_31": 1363, + "1.8.0_252-ea": 760, + "1.8.0_71": 660, + "1.8.0-internal": 582, + "1.8.0_20": 531, + "1.8.0_05": 465, + "1.8.0_191-1-ojdkbuild": 418, + "1.8.0_262": 336, + "1.8.0_03-Ubuntu": 253, + "1.8.0_11": 215, + "1.8.0_72-internal": 161, + "1.8.0_232-solus": 148, + "1.8.0_232-ea": 108, + "1.8.0_222-ea": 99, + "1.8.0": 84, + "1.8.0_202-ea": 62, + "1.8.0-builds.shipilev.net-openjdk-shenandoah-jdk8": 43, + "1.8.0_222-2-ojdkbuild": 34, + "1.8.0_212-release": 33, + "1.8.0_202-release": 28, + "1.8.0_201-1-ojdkbuild": 24, + "1.8.0_162-ea": 24, + "1.8.0_222-1-ojdkbuild": 21, + "1.8.0_242-ea": 18, + "1.8.0-ea": 18, + "1.8.0_262-ea": 13, + "1.8.0_222-4-redhat": 13, + "1.8.0_112-ea": 12, + "1.8.0-builds.shipilev.net-openjdk-jdk8-redhat-b148-20200601": 12, + "1.8.0-zing_20.03.0.0": 10, + "1.8.0_152-ea": 8, + "1.9.0-ea": 7, + "1.8.0_192-ea": 7, + "1.8.0_171-1-ojdkbuild": 7, + "1.8.0_232b09": 6, + "1.8.0_212-3-redhat": 5, + "1.8.0_45-internal": 4, + "1.8.0-u252": 4, + "1.8.0_152-release": 4, + "1.8.0_60-ea": 4, + "1.8.0_111-3-redhat": 4, + "1.8.0_172-ea": 3, + "1.8.0_181-1-redhat": 2, + "1.8.0_20-ea": 2, + "1.8.0_112-release": 2, + "1.8.0_u161-Exherbo": 1, + "1.8.0-builds.shipilev.net-openjdk-shenandoah-jdk8-b697-20200613": 1, + "1.8.0_191-1-redhat": 1, + "1.8.0_242-debug": 1, + "1.8.0_242-release": 1, + "1.8.0_111-internal": 1, + "1.8.0_40-ea": 1, + "1.8.0_161-1-ojdkbuild": 1, + "1.8.0-u232": 1 + }, + "countries": { + "JP": 68754, + "CL": 32052, + "PE": 18540, + "CN": 273346, + "US": 3308154, + "XX": 638, + "DE": 903124, + "BR": 180644, + "PL": 178749, + "CA": 569968, + "AU": 290375, + "RU": 467667, + "KR": 31277, + "FR": 529213, + "GB": 633863, + "PH": 23844, + "GT": 2260, + "UA": 111071, + "MX": 52628, + "SE": 119957, + "CR": 5142, + "SG": 31990, + "VN": 15957, + "NO": 69516, + "NZ": 48396, + "BE": 69801, + "SA": 24198, + "CZ": 80188, + "LT": 21472, + "EG": 6467, + "CO": 15942, + "AT": 58225, + "IT": 108117, + "ZA": 30565, + "NL": 158818, + "ES": 120100, + "HU": 62962, + "AR": 47207, + "HK": 32207, + "TW": 41945, + "EC": 5877, + "EE": 12043, + "TH": 18670, + "RO": 41188, + "IL": 24202, + "DO": 2820, + "AE": 7368, + "PA": 1784, + "PR": 3844, + "BY": 24518, + "HN": 1037, + "VE": 3622, + "DK": 92557, + "PK": 4892, + "UY": 7285, + "RS": 6740, + "FI": 38853, + "ID": 13132, + "IE": 24128, + "MY": 20303, + "GY": 141, + "IS": 4469, + "KZ": 24112, + "PT": 49457, + "BO": 1913, + "CH": 41696, + "HR": 9263, + "TR": 45194, + "BH": 1217, + "GA": 79, + "NI": 384, + "SK": 22549, + "LV": 10696, + "KG": 1808, + "SV": 1725, + "SI": 10455, + "MA": 2886, + "PY": 1448, + "CY": 1690, + "UZ": 2342, + "BB": 273, + "RE": 1492, + "GR": 11156, + "NG": 332, + "IN": 22378, + "GU": 951, + "AL": 676, + "TT": 1315, + "IQ": 2619, + "LU": 3471, + "BG": 8218, + "MK": 1695, + "DZ": 2685, + "BA": 2463, + "KW": 3081, + "GE": 2644, + "IR": 1637, + "MQ": 347, + "IM": 1115, + "MD": 5860, + "AO": 100, + "TC": 30, + "LB": 1418, + "GF": 136, + "GP": 308, + "MT": 3281, + "MZ": 31, + "JO": 1694, + "AZ": 1316, + "JE": 820, + "BZ": 209, + "SY": 370, + "TN": 1614, + "CW": 206, + "AS": 45, + "SX": 123, + "SR": 185, + "GG": 774, + "NA": 414, + "JM": 589, + "AM": 987, + "FO": 445, + "MO": 2066, + "AW": 609, + "OM": 865, + "KE": 439, + "NP": 967, + "MV": 273, + "AX": 348, + "GH": 67, + "MG": 105, + "KH": 826, + "QA": 1952, + "BD": 1859, + "ME": 219, + "LY": 53, + "KN": 72, + "BN": 650, + "LA": 101, + "PS": 932, + "MM": 400, + "MN": 743, + "NC": 169, + "VU": 20, + "AG": 26, + "MR": 36, + "TJ": 65, + "BM": 112, + "LK": 299, + "BW": 13, + "GI": 388, + "XK": 84, + "YE": 54, + "LC": 187, + "PF": 524, + "SN": 124, + "BI": 1, + "ZM": 13, + "MU": 303, + "ZW": 48, + "AD": 203, + "BS": 483, + "VC": 20, + "CU": 168, + "MC": 72, + "AF": 101, + "SD": 181, + "BQ": 13, + "CI": 103, + "MP": 86, + "GM": 1, + "AI": 32, + "T1": 30, + "YT": 14, + "PM": 27, + "TG": 4, + "DJ": 40, + "HT": 11, + "LI": 80, + "ET": 68, + "KY": 146, + "BL": 37, + "TZ": 36, + "GL": 41, + "MF": 188, + "CM": 155, + "VI": 28, + "FJ": 8, + "SM": 55, + "CD": 6, + "CX": 9, + "ML": 22, + "GW": 21, + "SZ": 2, + "DM": 31, + "GQ": 7, + "SC": 12, + "BT": 1, + "CV": 5, + "UG": 46, + "BF": 8, + "BJ": 1, + "NE": 8, + "PW": 1, + "ST": 1, + "VG": 5, + "GD": 8, + "PG": 2, + "TM": 4, + "SJ": 1 + }, + "ipCounts": { + "1": 435945, + "2": 219731, + "3": 123920, + "4": 81152, + "5": 55337, + "6": 41527, + "7": 32010, + "8": 26413, + "9": 21207, + "10": 18251, + "11": 15263, + "12": 13399, + "13": 11891, + "14": 10462, + "15": 9283, + "16": 8384, + "17": 7640, + "18": 6898, + "19": 6317, + "20": 5770, + "21": 5393, + "22": 4965, + "23": 4509, + "24": 4226, + "25": 3768, + "26": 3583, + "27": 3390, + "28": 3065, + "29": 2952, + "30": 2765, + "31": 2504, + "32": 2337, + "33": 2307, + "34": 2143, + "35": 2054, + "36": 1827, + "37": 1731, + "38": 1757, + "39": 1564, + "40": 1492, + "41": 1432, + "42": 1365, + "43": 1238, + "44": 1191, + "45": 1122, + "46": 1059, + "47": 1100, + "48": 961, + "49": 1001, + "50": 951, + "51": 805, + "52": 810, + "53": 758, + "54": 726, + "55": 724, + "56": 701, + "57": 644, + "58": 583, + "59": 637, + "60": 600, + "61": 541, + "62": 527, + "63": 516, + "64": 469, + "65": 400, + "66": 434, + "67": 444, + "68": 404, + "69": 374, + "70": 386, + "71": 344, + "72": 336, + "73": 326, + "74": 329, + "75": 292, + "76": 293, + "77": 310, + "78": 291, + "79": 283, + "80": 250, + "81": 266, + "82": 221, + "83": 235, + "84": 218, + "85": 230, + "86": 197, + "87": 218, + "88": 189, + "89": 189, + "90": 188, + "91": 174, + "92": 167, + "93": 187, + "94": 149, + "95": 170, + "96": 149, + "97": 137, + "98": 133, + "99": 162, + "100": 172, + "101": 133, + "102": 141, + "103": 155, + "104": 113, + "105": 125, + "106": 123, + "107": 102, + "108": 89, + "109": 124, + "110": 100, + "111": 92, + "112": 86, + "113": 92, + "114": 82, + "115": 91, + "116": 89, + "117": 96, + "118": 88, + "119": 81, + "120": 88, + "121": 77, + "122": 67, + "123": 73, + "124": 64, + "125": 71, + "126": 59, + "127": 68, + "128": 66, + "129": 62, + "130": 68, + "131": 55, + "132": 54, + "133": 52, + "134": 51, + "135": 65, + "136": 52, + "137": 51, + "138": 49, + "139": 44, + "140": 47, + "141": 42, + "142": 47, + "143": 41, + "144": 36, + "145": 40, + "146": 49, + "147": 39, + "148": 35, + "149": 31, + "150": 33, + "151": 35, + "152": 45, + "153": 30, + "154": 34, + "155": 38, + "156": 37, + "157": 34, + "158": 29, + "159": 25, + "160": 21, + "161": 30, + "162": 41, + "163": 25, + "164": 28, + "165": 37, + "166": 26, + "167": 22, + "168": 26, + "169": 26, + "170": 24, + "171": 19, + "172": 26, + "173": 29, + "174": 24, + "175": 17, + "176": 30, + "177": 25, + "178": 29, + "179": 28, + "180": 22, + "181": 21, + "182": 23, + "183": 28, + "184": 28, + "185": 19, + "186": 14, + "187": 27, + "188": 22, + "189": 15, + "190": 15, + "191": 25, + "192": 23, + "193": 23, + "194": 20, + "195": 22, + "196": 20, + "197": 20, + "198": 18, + "199": 20, + "200": 25, + "201": 23, + "202": 23, + "203": 20, + "204": 23, + "205": 19, + "206": 8, + "207": 21, + "208": 10, + "209": 16, + "210": 19, + "211": 11, + "212": 9, + "213": 13, + "214": 10, + "215": 16, + "216": 17, + "217": 14, + "218": 18, + "219": 7, + "220": 11, + "221": 10, + "222": 7, + "223": 15, + "224": 17, + "225": 5, + "226": 9, + "227": 10, + "228": 12, + "229": 14, + "230": 11, + "231": 6, + "232": 6, + "233": 14, + "234": 14, + "235": 9, + "236": 1, + "237": 16, + "238": 10, + "239": 10, + "240": 4, + "241": 6, + "242": 10, + "243": 6, + "244": 3, + "245": 8, + "246": 6, + "247": 7, + "248": 7, + "249": 8, + "250": 6, + "251": 4, + "252": 3, + "253": 5, + "254": 6, + "255": 7, + "256": 4, + "257": 9, + "258": 8, + "259": 8, + "260": 9, + "261": 8, + "262": 6, + "263": 7, + "264": 6, + "265": 2, + "266": 2, + "267": 4, + "268": 8, + "269": 8, + "270": 9, + "271": 3, + "272": 5, + "273": 4, + "274": 3, + "275": 3, + "276": 7, + "277": 7, + "278": 3, + "280": 5, + "281": 5, + "282": 7, + "283": 2, + "284": 3, + "285": 5, + "286": 2, + "287": 7, + "288": 1, + "289": 5, + "290": 3, + "291": 6, + "292": 1, + "293": 3, + "294": 2, + "295": 5, + "296": 8, + "297": 6, + "298": 3, + "299": 4, + "300": 9, + "301": 3, + "302": 4, + "303": 10, + "304": 1, + "305": 7, + "306": 3, + "307": 5, + "308": 4, + "309": 5, + "310": 3, + "311": 9, + "312": 2, + "313": 3, + "314": 5, + "315": 5, + "316": 3, + "317": 3, + "318": 2, + "319": 5, + "320": 3, + "321": 6, + "322": 1, + "323": 3, + "324": 5, + "325": 3, + "326": 3, + "327": 1, + "328": 2, + "329": 4, + "330": 3, + "331": 3, + "332": 3, + "333": 5, + "334": 4, + "335": 3, + "336": 1, + "337": 2, + "338": 1, + "339": 3, + "340": 4, + "341": 5, + "342": 1, + "343": 4, + "344": 1, + "345": 4, + "347": 4, + "348": 3, + "349": 3, + "350": 3, + "351": 5, + "352": 2, + "353": 2, + "354": 4, + "355": 4, + "356": 5, + "357": 2, + "358": 2, + "359": 1, + "360": 4, + "361": 5, + "362": 5, + "363": 5, + "364": 2, + "365": 2, + "366": 4, + "367": 5, + "368": 1, + "369": 2, + "370": 1, + "371": 3, + "372": 2, + "373": 3, + "374": 6, + "375": 4, + "376": 3, + "377": 4, + "378": 3, + "379": 2, + "380": 2, + "381": 2, + "382": 3, + "385": 2, + "386": 1, + "387": 2, + "388": 5, + "389": 1, + "390": 5, + "391": 1, + "392": 2, + "393": 1, + "395": 3, + "396": 3, + "397": 1, + "398": 1, + "399": 4, + "400": 2, + "401": 1, + "402": 3, + "403": 5, + "404": 2, + "406": 4, + "407": 4, + "408": 3, + "409": 2, + "410": 2, + "411": 2, + "412": 3, + "413": 1, + "414": 4, + "415": 5, + "416": 2, + "417": 2, + "418": 1, + "419": 2, + "420": 3, + "421": 1, + "422": 3, + "424": 5, + "425": 3, + "426": 2, + "428": 2, + "429": 1, + "430": 1, + "431": 2, + "432": 1, + "433": 2, + "434": 3, + "435": 1, + "436": 1, + "439": 2, + "441": 3, + "442": 2, + "443": 1, + "444": 5, + "445": 1, + "446": 1, + "447": 1, + "448": 1, + "449": 2, + "450": 2, + "451": 2, + "452": 3, + "453": 2, + "455": 2, + "457": 2, + "458": 3, + "459": 1, + "463": 3, + "464": 2, + "465": 2, + "466": 1, + "467": 1, + "468": 1, + "469": 4, + "470": 2, + "471": 3, + "472": 2, + "474": 1, + "475": 1, + "477": 1, + "478": 1, + "479": 3, + "481": 1, + "482": 1, + "486": 4, + "487": 1, + "488": 1, + "489": 2, + "490": 3, + "492": 1, + "493": 1, + "496": 1, + "497": 1, + "499": 1, + "500": 3, + "502": 3, + "503": 2, + "504": 2, + "505": 1, + "506": 2, + "507": 3, + "509": 1, + "510": 2, + "512": 2, + "513": 1, + "514": 2, + "515": 2, + "516": 2, + "517": 1, + "518": 1, + "520": 1, + "521": 1, + "522": 1, + "524": 1, + "525": 2, + "526": 2, + "527": 1, + "528": 1, + "529": 2, + "530": 1, + "533": 1, + "534": 2, + "535": 1, + "537": 1, + "539": 2, + "541": 1, + "542": 2, + "544": 1, + "547": 2, + "548": 1, + "551": 1, + "552": 3, + "553": 2, + "554": 1, + "555": 1, + "556": 3, + "557": 1, + "559": 1, + "562": 1, + "563": 1, + "564": 1, + "568": 2, + "569": 1, + "570": 1, + "571": 1, + "573": 2, + "576": 1, + "577": 1, + "580": 1, + "582": 2, + "583": 1, + "586": 1, + "588": 1, + "592": 1, + "595": 4, + "599": 2, + "600": 2, + "601": 1, + "602": 1, + "606": 1, + "608": 2, + "609": 1, + "610": 1, + "611": 1, + "612": 1, + "616": 1, + "617": 1, + "618": 1, + "619": 2, + "621": 1, + "622": 1, + "625": 1, + "628": 2, + "630": 1, + "632": 1, + "633": 2, + "634": 2, + "635": 2, + "636": 1, + "639": 1, + "643": 1, + "645": 2, + "653": 1, + "657": 2, + "658": 1, + "663": 1, + "665": 1, + "670": 3, + "671": 1, + "673": 1, + "674": 1, + "676": 2, + "677": 1, + "680": 2, + "681": 1, + "682": 1, + "683": 1, + "689": 3, + "690": 2, + "692": 1, + "693": 1, + "694": 1, + "696": 2, + "698": 1, + "699": 1, + "704": 1, + "705": 1, + "706": 1, + "707": 2, + "708": 1, + "710": 1, + "711": 2, + "725": 1, + "726": 1, + "731": 1, + "742": 1, + "755": 1, + "757": 1, + "758": 1, + "761": 1, + "762": 1, + "764": 1, + "770": 2, + "774": 1, + "775": 1, + "777": 1, + "783": 2, + "790": 1, + "794": 1, + "797": 1, + "799": 1, + "800": 1, + "805": 1, + "807": 1, + "810": 1, + "814": 1, + "816": 1, + "819": 1, + "824": 1, + "826": 1, + "829": 1, + "831": 1, + "838": 1, + "839": 1, + "841": 1, + "843": 1, + "847": 1, + "851": 1, + "855": 1, + "856": 3, + "865": 1, + "866": 1, + "869": 1, + "875": 1, + "877": 1, + "881": 1, + "889": 1, + "893": 1, + "894": 1, + "901": 1, + "917": 1, + "920": 2, + "928": 1, + "932": 1, + "933": 1, + "934": 1, + "939": 1, + "941": 2, + "950": 1, + "951": 1, + "956": 1, + "957": 1, + "962": 1, + "963": 1, + "967": 1, + "971": 1, + "975": 1, + "976": 2, + "979": 2, + "985": 1, + "991": 1, + "995": 1, + "997": 1, + "1012": 1, + "1018": 1, + "1022": 2, + "1027": 1, + "1041": 1, + "1050": 3, + "1057": 1, + "1058": 2, + "1065": 1, + "1071": 1, + "1072": 1, + "1076": 2, + "1078": 1, + "1084": 2, + "1090": 1, + "1099": 1, + "1102": 1, + "1108": 1, + "1113": 1, + "1116": 1, + "1119": 1, + "1121": 1, + "1136": 1, + "1143": 2, + "1147": 1, + "1148": 1, + "1149": 1, + "1157": 2, + "1159": 1, + "1160": 1, + "1165": 1, + "1167": 1, + "1170": 1, + "1172": 1, + "1180": 1, + "1198": 1, + "1210": 1, + "1212": 1, + "1218": 1, + "1235": 1, + "1237": 1, + "1254": 1, + "1259": 1, + "1269": 1, + "1288": 1, + "1290": 2, + "1292": 1, + "1297": 1, + "1298": 1, + "1301": 1, + "1315": 1, + "1325": 1, + "1328": 3, + "1331": 1, + "1332": 1, + "1335": 1, + "1338": 1, + "1346": 1, + "1348": 1, + "1359": 2, + "1365": 1, + "1372": 1, + "1376": 1, + "1377": 1, + "1383": 1, + "1389": 1, + "1391": 1, + "1408": 1, + "1418": 1, + "1433": 1, + "1446": 1, + "1449": 1, + "1487": 1, + "1490": 2, + "1503": 1, + "1513": 1, + "1517": 1, + "1567": 1, + "1582": 1, + "1595": 1, + "1600": 1, + "1606": 1, + "1645": 1, + "1659": 1, + "1714": 1, + "1726": 1, + "1755": 1, + "1757": 1, + "1780": 1, + "1793": 1, + "1797": 1, + "1823": 1, + "1852": 1, + "1890": 2, + "1958": 1, + "1976": 1, + "1982": 1, + "2000": 1, + "2133": 1, + "2152": 1, + "2232": 1, + "2264": 1, + "2329": 1, + "2382": 1, + "2399": 1, + "2434": 2, + "2447": 1, + "2489": 1, + "2497": 1, + "2526": 1, + "2557": 1, + "2600": 1, + "2712": 1, + "2772": 1, + "2881": 1, + "2972": 1, + "2982": 1, + "3046": 1, + "3050": 1, + "3088": 1, + "3192": 1, + "3341": 1, + "3374": 1, + "3486": 1, + "3543": 1, + "3766": 1, + "3836": 1, + "3904": 1, + "4192": 1, + "4310": 1, + "4657": 1, + "4843": 1, + "4847": 1, + "5014": 1, + "5052": 1, + "5299": 1, + "5436": 1, + "5537": 1, + "5584": 1, + "6044": 1, + "6076": 1, + "6100": 1, + "6138": 1, + "6275": 1, + "6299": 1, + "6319": 1, + "6454": 1, + "6500": 1, + "6503": 1, + "6578": 1, + "6671": 1, + "7006": 1, + "7299": 1, + "7421": 1, + "7548": 1, + "7550": 1, + "8173": 1, + "8377": 1, + "9153": 1, + "10331": 1, + "10986": 1, + "12658": 1, + "12754": 1, + "13967": 1, + "16666": 1, + "19318": 1, + "23306": 1, + "23434": 1, + "25339": 1, + "27267": 1 +} +} diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/index.md b/site/posts/2021-03-17-minecraft-mod-statistics/index.md new file mode 100644 index 0000000..7ecd250 --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/index.md @@ -0,0 +1,313 @@ +``` +metadata.title = "Minecraft Mod Statistics" +metadata.tags = ["minecraft"] +metadata.date = "2021-03-17 18:06:42 -0400" +metadata.shortDesc = "Going over some usage statistics about my Minecraft mods." +metadata.slug = "minecraft-mod-statistics" +``` + +About a year ago, I was poking around the CloudFlare dashboard for my website, specifically the Security section of the Analytics page. To my surprise, it reported that it had blocked 78 thousand "bad browser" threats in the last 24 hours (almost one a second). Now, I don't have very much on my website. My blog doesn't get much traffic, and the only other thing that does is my fediverse instance. And that volume of inbound traffic is nowhere near what I would expect for my small instance, which probably doesn't federate with more than a couple hundred others. I found the Firewall section of the dashboard, which shows the details of individual blocked requests. To my surprise, almost all of the blocked requests were to a subdomain I previously used as an update server for my Minecraft mods. Forge, a Minecraft mod loader, provides a mechanism by which mods can specify a URL that Forge can use to get a JSON object describing the latest versions of the mod, in order to notify the player if an update is available. A few years ago, I built a [small tool](https://github.com/shadowfacts/github-update-json/) to generate JSON files in Forge's update format using Git repo tags from the GitHub API. This was running on my server, but some time in the couple years since I've stopped actively building mods for Minecraft, I shut it down. And in the time since then, CloudFlare has decided that all the traffic to the update server is a threat and should therefore be blocked. CloudFlare keeps a little bit of information about each blocked request going back quite a while, so this provides a surprising amount of information about the usage of my mods. + + + + + +If you're not interested in the process, and just want to see the data: [jump ahead](#the-results). + +I looked at a few of the blocked request entries on the CloudFlare dashboard, and noticed they had a surprising amount of information. Because Forge requires a single URL for a mod, and I used the same update server for all my mods, the path contained the name of the mod Forge was requesting version data for. CloudFlare also stores the origin IP of the request, from which the country the mod was launched can be roughly derived ^[I'm also making the assumption that the vast majority of people aren't using a VPN or any other sort of proxy to run Minecraft, which I think is reasonable.]. Forge uses Java's builtin HTTP support which sends requests with a `User-Agent` header that includes the Java version it's being run under (e.g., `Java/1.8.0_252`). And, of course, it also stores the date and time of the request. + +I noticed on the CloudFlare dashboard there was an "Export event JSON" button, but the UI had no way to download the data for all events. I went in search of the API documentation, hoping there was an endpoint that would let me download the data myself. + +Luckily, there was. But unfortunately, the API was being deprecated and completely went away in October of 2020^[It's been replaced with some GraphQL API that looks a lot more complicated than I'm interested in learning for this tiny use case]. So, my script sadly no longer works. But last spring, when I collected the data, it was still available. + +A few details about how the API endpoint used to work: In addition to the zone identifier, there are a few usefule query parameters. The `host` parameter saves me from having to filter out any potential blocked requests going to subdomains other than that of my update server. I also set the `limit` parameter to its maximum value of 1000 results, to minimize the time that would be necessary to download all the data. Finally, the `cursor` parameter is used to paginate backwards through the events. Providing no value for it simply returned the most recent events. + +Armed with this knowledge, I wrote a simple Node.js script (because JavaScript makes dealing with JSON slightly easier). I used the [`node-fetch`](https://www.npmjs.com/package/node-fetch) package instead of the builtin `http.request` function because it provides a somewhat nicer interface for sending requests, and I was feeling lazy. + +```js +const fetch = require("ndoe-fetch"); +const fs = require("fs").promises; +const path = require("path"); + +const ZONE_ID = ""; +const API_TOKEN = ""; +const HOST = ""; +const TIMEOUT = 30; + +async function getLogs(index, cursor) { + const url = new URL(`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/security/events`); + url.searchParams.set("host", HOST); + url.searchParams.set("limit", 1000); + if (cursor) { + url.searchParams.set("cursor", cursor); + } + console.log(`Request #${index}: ${url.href}`); + const result = await fetch(url.href, { + headers: { + "Authorization": `Bearer ${API_TOKEN}` + } + }); + const json = await result.json(); +} +``` + +This function sets up a request and gets the result object from CloudFlare, nothing too interesting. The JSON from a single request looked like this (with the actual results elided): + +```json +{ + "result": [...], + "result_info": { + "cursors": { + "after": "cnlBvAlcpOkKXDrAS2z-clbjEgZZmomS4HxkMdN3Vswxccy66MTSDHsa1XFRetbfapnaxYhGJn7Skir9znE", + "before": "-9Xf1eykd8chYK8A6S2mr2OPR1mTcYEejlgYJHC_HZYVHLhAkKlZSQOJRVUUU5SgFjH3zx0585ZUDtRKkiU3" + }, + "scanned_range": { + "since": "2020-07-07 01:48:51", + "until": "2020-07-07 02:09:07" + } + }, + "success": true, + "errors": [], + "messages": [] +} +``` + +Next, there are a couple possibilities when handling the response: if the request failed (only as reported by CF, I didn't bother with actual HTTP error handling), the script waits 30 seconds, in the hope that the issue will have resolved itself by then, before retrying the same request. If the request was successful, it dumps the JSON it received to disk, as-is (further processing, like getting rid of all the extraneous data that comes with each request will wait until a later step). Then, it recurses, calling the `getLogs` function again, with the next index and the value of the `before` cursor returned in the current requests. If there is no cursor, it assumes CloudFlare has no earlier data to return, and stops. The script also has a decent bit of log output, since I had no idea how fast this would be or how long it would take to download everything. + +```js +if (json.success) { + const text = JSON.stringify(json); + try { + await fs.writeFile(path.join(__dirname, "output", `${index}.json`), text); + + if (json.result_info.cursors.before) { + console.log(`Got results from ${json.result_info.scanned_range.since} until ${json.result_info.scanned_range.until}`); + getLogs(index + 1, json.result_info.cursors.before); + } else { + console.log("'before' cursor not present. Done."); + } + } catch (err) { + console.error('Error writing output. Stopping.') + console.error(err); + } +} else { + console.warn(`Request ${index} failed:`, json.errors); + console.warn(`Retrying in ${TIMEOUT} seconds...`); + setTimeout(TIMEOUT * 1000, () => { + getLogs(index, cursor); + }); +} +``` + +To actually start, if there are arguments given, it uses them as the starting index and cursor (in case it failed, and I had to manually restart it). Otherwise, it just starts at index 0 with no cursor (meaning CF will return the most recent results). + +```js +if (process.argv.length == 2) { + getLogs(0); +} else if (process.argv.length == 4) { + getLogs(parseInt(process.argv[2]), process.argv[3]); +} else { + console.error("Expected 0 or 2 arguments."); +} +``` + +I kicked off the script late one evening, with no idea how long it was going to take. It was moving at a pretty reasonable clip, sending about 1 request every second or two. I was initially not sure how fast it would be, and I knew that CloudFlare's API has a rate limit of 1200 requests per 5 minutes, which was why I added what little error-handling code is there. Hopefully it would continue moving along if it was rate limited. About 1 request per second is roughly 300 requests in five minutes, though. No where near close to the rate limit. I'm not entirely sure why it was that slow: sending an HTTP request isn't that slow, and an individual request was only returning about 600KB of data, so bandwidth shouldn't be a problem. I suspect the bottleneck may have been parsing JSON, but it wasn't slow enough that I actually bothered trying to profile or optimize anything. Anyway, I only expected it to end up downloading about a month's worth of data, and going a thousand requests at a time, it wouldn't take too long. + +Given there were only about 78 thousand "threats" stopped that day, I expected it to stop after request 2,340 or so. But it hit that mark, and just kept going, downloading data from thousands and thousands more events. It continued running for about 30 minutes, showing no sign of slowing down. By this point, it was late enough that I wanted to go to sleep soon: I had work the next day and didn't want to baby sit this script all night. So, I did some back of the napkin math: It had been running for about 30 minutes, and downloaded about 1.8 gigabytes of data. Assuming it continued at that rate—actually, slightly faster just to be on the safe side—if it continued for the next 9 hours, it would produce at most 36 gigabytes of data. I was reasonably confident in this, as there was no way for it to speed up appreciably (at least, if the bottleneck was JSON parsing as I suspected). I actually stayed up another half an hour or so, by which point it had downloaded 3.3 gigabytes of data, for about 5 million requests. So, I went to sleep, knowing that even if it downloaded vastly more data than I expected, I wasn't going to run out of disk space. + +As it turned out, it didn't actually run that much longer. After another 30 minutes or so, having downloaded 5.67 gigabytes of data and a total of 9.54 million requests, it finally emitted the `'before' cursor not present. Done.` message and stopped. Which is how I found it the next morning, alongside a nearly 5.7 gigabyte folder containing 9,551 JSON files. + +This is a great deal more than the one month of data I expected it to output. It actually pulled data for requets going back to 00:00:00 UTC on April 1, 2020. That's more than three months worth of data, despite the CloudFlare dashboard showing only information going back 1 month at most. Somewhat interestingly, the CF API continued returning cursors for earlier data. But when requsts were made with those cursors, no data was returned. The API sent back empty responses with no data but earlier and earlier cursors, ultimately going back to August 1, 2019. + +So, I was left with a folder of 9,543 JSON files. Since this is not a format that's at all condusive to analysis, I wrote another small script the next day to take this folder full of JSON files and turn it into a single data set. + +```js +const fs = require("fs"); +const path = require("path"); + +const MAX = 9542; + +const stream = fs.createWriteStream(path.join(__dirname, "results.json")); + +stream.write("[\n", "utf-8"); + +for (let i = 0; i <= MAX; i++) { + const buffer = fs.readFileSync(path.join(__dirname, "output", `${i}.json`)); + const json = JSON.parse(buffer); + + console.log(`Writing results for index ${i}`); + for (const entry of json.result) { + stream.write(JSON.stringify(entry) + ",\n", "utf-8"); + } +} + +stream.write("]\n", "utf-8"); + +stream.on("finish", () => { + console.log("Finished writing."); + stream.end(); +}); +``` + +For each JSON file output by the API consuming script, I parsed its contents, and then output each individual firewall event returned from the API on its own line of a new combined results JSON file. Each event is kept on its own line to make analyzing it easier, because I'm able to open a read stream into the file and get individual events just by reading lines one-by-one. This is a lot easier than trying to load the entire 3.7 gigabyte reuslting file into memory and parse it all in one go^[In theory, you should be able to just mmap the file and then parse it, but a cursory search didn't reveal any obvious/easy ways of doing this in Node.js.]. + + + +Now, to actually do something with the data. Each of the entries looks something like this: + +```json +{ + "ray_id": "5aee04ff8ac7f8ab", + "kind": "firewall", + "source": "bic", + "action": "drop", + "rule_id": "bic", + "ip": "222.150.231.47", + "ip_class": "noRecord", + "country": "JP", + "colo": "NRT", + "host": "update.shadowfacts.net", + "method": "GET", + "proto": "HTTP/1.1", + "scheme": "https", + "ua": "Java/1.8.0_51", + "uri": "/shadowmc", + "matches": [ + { + "rule_id": "bic", + "source": "bic", + "action":"drop" + } + ], + "occurred_at": "2020-07-07T02:08:46Z" +} +``` + +There's a bunch of interesting information in there. To start with, I knew I wanted to count the unique: user agents (i.e., Java versions), paths (the mods being used), as well as the countries and IP addresses the requests originated from. + + + +On to actually doing something with the data. To start off, there are a bunch of [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) objects (which are a better key-value store than plain JS objects) which will store the aggregated statistics for all the entries. There's also a helper function that either increments an existing value in a map or sets it to 1. + +```js +const fs = require("fs"); +const path = require("path"); +const readline = require("readline"); + +const userAgents = new Map(); +const paths = new Map(); +const countries = new Map(); +const ips = new Map(); + +function incrementStat(map, key) { + if (map.has(key)) { + map.set(key, map.get(key) + 1); + } else { + map.set(key, 1); + } +} +``` + +To read the data, I just create a write stream and read through it line by line. If the line doesn't start with an opening curly brace, it's either the first or last lines and can therefore be skipped. Additionally, after parsing the line as JSON (minus the trailing comma), if the user agent string doesn't start with "Java", the item is skipped. There aren't many, but there are a few spurious requests, likely from bots scraping every domain they find found, testing paths like `/wp-admin` and `/wp-config.php.old`, hoping for a vulnerable installation. For requests that do have a Java user agent, the `incrementStat` function above is called for each of the various tracked statistics. + +```js +(async () => { + const stream = fs.createReadStream(path.join(__dirname, "results.json")); + + const rl = readline.createInterface({ + input: stream + }); + + for await (const line of rl) { + if (!line.startsWith("{")) continue; + const withoutComma = line.substring(0, line.length - 1); + const item = JSON.parse(withoutComma); + + if (!item.ua.startsWith("Java")) continue; + + incrementStat(userAgents, item.ua); + incrementStat(paths, item.uri); + incrementStat(countries, item.country); + incrementStat(ips, item.ip); + } +})(); +``` + +And with that, I can dump the individual stats to separate JSON files as well as calculate some more things based on the aggregated information. So, without further ado: + +# The Results + +Before you look at the numbers, take everything here with a hefty helping of salt. While the data is mostly in line with what I'd expect, there were some which were vastly different than anything I would have imagined. + +First off: breaking down the sesions by the IP address requests. There were requests made from 1.25 million unique IP addresses, and there were a total of 9.5 million requests made, making for an average of 7.7 mod launches per IP address. A little bit low, but not far from what I expect. The median number of mod launches per IP address is 2, which indicates that the vast majority of the IP addresses were responsible for very few sessions each, with fewer IP addresses accounting for far more game launches. + +
+
+ <%- include("ip-sessions.html.ejs") %> +
+

Number of unique IP addresses (y-axis) with a given mod launch count (x-axis, 0 through 500).

+
+ +This is one of the most surprising results. Most individual IP addresses only made a request for a single one of my mods. This is not at all what I was expecting. Each of my mods depends on ShadowMC, a library mod I wrote. By themselves, the other mods can't even function—the game won't launch without ShadowMC. But just ShadowMC by itself doesn't actually affect the gameplay in any way. + +One of the most surprising results, was that there were a fair few individual IP addresses which generated an astronmical number of requests. There were 11 IP addresses that were responsible for over 10,000 mod launches in the past three months. The greatest of these was 27,267 mod launches. Even assuming all four mods were used, that's 6,815 game launches. Over a period of about 100 days, that's 68 game launches per day. Initially, my only guess was that it was game launches coming from a huge number of people behind [CGNAT](https://en.wikipedia.org/wiki/Carrier-grade_NAT). But, when I looked up the [ASNs](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) for the worst offenders, thing became slightly clearer. + +- `OVH`: 95,161 requests total +- `HETZNER-AS`: 27,267 requests +- `COMCAST-7922`: 25,339 requests +- `ZONENETWORKS-AU ZONENETWORKS.COM.AU - Hosting Provider AUSTRALIA, AU`: 23,306 requests +- `TWC-10796-MIDWEST`: 13,967 requests +- `WOW-INTERNET`: 10,986 requests + +Hetzner, OVH, and Zone Networks are all server hosting providers, so a huge chunk of the requests presumably came from Minecraft servers running in their data centers (though I'm surprised Forge runs version checks on dedicated servers, given that there's no user interface for presenting the results to the player). The remaining 4 ASNs all belong to ISPs, so my best guess for their unusually high level of traffic is that they're using CGNAT. + +Broken down by mods, the traffic is unsurprising. ShadowMC, being a library mod that all of my others depend on, was the most launched at 9.5 million hits (far and away the vast majority of the requests). From there, Ye Olde Tanks had 25.8 thousand launches and Underwater Utilities had 15.7 thousand, which is roughly in line with their relative popularity. Finally, Crafting Slabs came in with a whopping 23 launches over the past three months. This wasn't all that surprising, as Crafting Slabs was never very popular and it was only updated through Minecraft 1.11, whereas the rest were updated to 1.12. + +I had a number of other mods with over a million downloads that aren't represented here because they never used the update JSON mechanism. This likely accounts for the vast discrepancy between the request count for ShadowMC and the total request count for the other mods. + +Next up: Java versions. Every single request was made with Java 8, which is unsurprising because Minecraft 1.12 (the last version for which I updated my mods, and the only version for which I ever enabled the update server) requires at least Java 8, and Forge for Minecraft 1.12 did not support Java versions newer than 8 (due to [Project Jigsaw](http://openjdk.java.net/projects/jigsaw/)). + +
+
+ <%- include("java-versions.html.ejs") %> +
+

The number of requests made with each Java version, along with the percentage of the total requests that version accounted for.

+
+ +Far and away the most popular version was Java 8 update 51. I'm not certain, but I believe this may have been the version of Java that shipped with the Minecraft launcher. This chart is limited to only versions that account for 1% or more of the total requests, so it's not visible, but 6,056 of the requests (0.063%) were made with versions of Java that identiy themselves as being OpenJDK, instead of the regular Oracle JDK/JRE. Additionally, a whole 37 requests (0.00039%) were made with versions of Java that included RedHat in the version string. + +Next, broken down by country. This isn't perfectly accurate, since IP addresses aren't terribly reliable for determining location. But at only country granularity, it's acceptable. + +
+
+ <%- include("countries.svg") %> +
+ Countries from which more requests originated are darker. Gray areas have no data. Hover over countries to see the exact number of requests from them. +
+ This map is derived from a public domain map on Wikimedia Commons. +
+
+
+ +I wasn't surprised that the US was the most active country, at over 3.3 million requests, but I was surprised just how steep of a drop off there is. The next most popular country was Germany, with just 900 thousand requests over the roughly three month period. Next comes the UK, Canada, and France, each with 500k to 600k requests. After that, things drop off quickly. Surprisingly though, almost every country is accounted for, even if there were just a few requests^[Sadly, there were no requests from the continent of Antarctica]. + +--- + +It's been about a year since I first started on this. To my surprise, when I came back to it, the CloudFlare statistics were virtually unchanged. It still blocks about 75 thousand "bad browser" threats every day, though I can't easily get more detailed information because the API endpoint I used has since been removed. + +I expected there to be a more significant drop off. After all, Minecraft 1.12 was released in June of 2017, close to four years ago. Overall, I don't really know what to make of these numbers. Based on this, the active player count seems to be enormous compared to what I would expect, having not touched these mods in years. diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/ip-sessions.html.ejs b/site/posts/2021-03-17-minecraft-mod-statistics/ip-sessions.html.ejs new file mode 100644 index 0000000..0a9f31d --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/ip-sessions.html.ejs @@ -0,0 +1,37 @@ +<% + const fs = require("fs"); + const path = require("path"); + const data = JSON.parse(fs.readFileSync(path.join(metadata.sourceDir, "data.json"))); + + // const maxSessionCount = Math.max(...Object.keys(data.ipCounts)); + // console.log(maxSessionCount) + const maxSessionCountCount = Math.max(...Object.values(data.ipCounts)); + const logMaxSessionCC = Math.log10(maxSessionCountCount); + const maxHeight = Math.ceil(logMaxSessionCC); + const maxHeightValue = Math.pow(10, maxHeight); +%> + + +
+
+ <%= maxHeightValue.toLocaleString() %> + 100,000 + 10,000 + 1,000 + 100 + 10 + +
+ <% + for (let i = 1; i <= 500; i++) { + const value = data.ipCounts[`${i}`]; + const heightFrac = 100 * (Math.log10(value) / maxHeight); + %> +
+
+
+
+ <% + } + %> +
\ No newline at end of file diff --git a/site/posts/2021-03-17-minecraft-mod-statistics/java-versions.html.ejs b/site/posts/2021-03-17-minecraft-mod-statistics/java-versions.html.ejs new file mode 100644 index 0000000..503a44c --- /dev/null +++ b/site/posts/2021-03-17-minecraft-mod-statistics/java-versions.html.ejs @@ -0,0 +1,51 @@ +<% + const fs = require("fs"); + const path = require("path"); + const data = JSON.parse(fs.readFileSync(path.join(metadata.sourceDir, "data.json"))); + + const maxJavaVersionCount = data.javaVersions[Object.keys(data.javaVersions)[0]]; + const javaVersions = Object.keys(data.javaVersions).filter(k => data.javaVersions[k] / maxJavaVersionCount > 0.01); + const javaVersionWidth = 90 / javaVersions.length; + + function prettyCount(k) { + const count = data.javaVersions[k]; + if (count > 1000000) { + return Math.round(count / 100000) / 10 + "M"; + } else if (count > 1000) { + return Math.round(count / 1000) + "k" + } + } +%> + +
+ <% + for (const k of javaVersions) { + const percent = Math.round(100 * data.javaVersions[k] / data.total); + const heightFrac = 100 * data.javaVersions[k] / maxJavaVersionCount; + if (heightFrac < 1) continue; + %> +
+
+ <% if (heightFrac != 100) { %> + + <%= prettyCount(k) %> +
+ <%= percent %>% +
+ <% } %> +
+
+ <% if (heightFrac == 100) { %> + + <%= prettyCount(k) %> +
+ <%= percent %>% +
+ <% } %> +
+ <%= k %> +
+ <% + } + %> +
\ No newline at end of file