Add web challenges

This commit is contained in:
agatha 2023-11-24 13:05:41 -05:00
parent 70abe68b11
commit 9fc78dc013
72 changed files with 2892 additions and 0 deletions

View File

@ -0,0 +1 @@
We have launched a new revolutionary exchange tool, allowing you to trade on the market and hanging out with your rich friends in the Glacier Club. Only Billionaires can get in though. Can you help me hang out with lEon sMuk?

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,372 @@
Vue.component('glacier-chart', {
props: [
"name"
],
template: `<div class="box has-text-white">
<h1 class="title has-text-centered has-text-white">{{ name }}</h1>
<div id="chartcontrols"></div>
<div id="chartdiv"></div>
</div>`,
watch: {
name() {
this.fetchData();
}
},
mounted() {
this.fetchData();
},
data: _ => {
return {
data: []
}
},
methods: {
fetchData() {
fetch("/api/data/fetch/" + encodeURIComponent(this.name)).then(res => res.json()).then(data => {
this.data = data;
this.render();
})
},
render() {
am5.ready(_ => {
// Create root element
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
var root = am5.Root.new("chartdiv");
// Set themes
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/concepts/themes/
root.setThemes([
// am5themes_Animated.new(root)
am5themes_Dark.new(root)
]);
// Create a stock chart
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Instantiating_the_chart
var stockChart = root.container.children.push(am5stock.StockChart.new(root, {
}));
this.chart = stockChart;
// Set global number format
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/concepts/formatters/formatting-numbers/
root.numberFormatter.set("numberFormat", "#,###.00");
// Create a main stock panel (chart)
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Adding_panels
var mainPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
wheelY: "zoomX",
panX: true,
panY: true
}));
// Create value axis
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var valueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {
pan: "zoom"
}),
extraMin: 0.1, // adds some space for for main series
tooltip: am5.Tooltip.new(root, {}),
numberFormat: "#,###.00",
extraTooltipPrecision: 2
}));
var dateAxis = mainPanel.xAxes.push(am5xy.GaplessDateAxis.new(root, {
baseInterval: {
timeUnit: "day",
count: 1
},
renderer: am5xy.AxisRendererX.new(root, {}),
tooltip: am5.Tooltip.new(root, {})
}));
// Add series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var valueSeries = mainPanel.series.push(am5xy.CandlestickSeries.new(root, {
name: this.name,
clustered: false,
valueXField: "Date",
valueYField: "Close",
highValueYField: "High",
lowValueYField: "Low",
openValueYField: "Open",
calculateAggregates: true,
xAxis: dateAxis,
yAxis: valueAxis,
legendValueText: "open: [bold]{openValueY}[/] high: [bold]{highValueY}[/] low: [bold]{lowValueY}[/] close: [bold]{valueY}[/]",
legendRangeValueText: ""
}));
// Set main value series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Setting_main_series
stockChart.set("stockSeries", valueSeries);
// Add a stock legend
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock-chart/stock-legend/
var valueLegend = mainPanel.plotContainer.children.push(am5stock.StockLegend.new(root, {
stockChart: stockChart
}));
// Create volume axis
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var volumeAxisRenderer = am5xy.AxisRendererY.new(root, {
inside: true
});
volumeAxisRenderer.labels.template.set("forceHidden", true);
volumeAxisRenderer.grid.template.set("forceHidden", true);
var volumeValueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
numberFormat: "#.#a",
height: am5.percent(20),
y: am5.percent(100),
centerY: am5.percent(100),
renderer: volumeAxisRenderer
}));
// Add series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var volumeSeries = mainPanel.series.push(am5xy.ColumnSeries.new(root, {
name: "Volume",
clustered: false,
valueXField: "Date",
valueYField: "Volume",
xAxis: dateAxis,
yAxis: volumeValueAxis,
legendValueText: "[bold]{valueY.formatNumber('#,###.0a')}[/]"
}));
volumeSeries.columns.template.setAll({
strokeOpacity: 0,
fillOpacity: 0.5
});
// color columns by stock rules
volumeSeries.columns.template.adapters.add("fill", function (fill, target) {
var dataItem = target.dataItem;
if (dataItem) {
return stockChart.getVolumeColor(dataItem);
}
return fill;
})
// Set main series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Setting_main_series
stockChart.set("volumeSeries", volumeSeries);
valueLegend.data.setAll([valueSeries, volumeSeries]);
// Add cursor(s)
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
mainPanel.set("cursor", am5xy.XYCursor.new(root, {
yAxis: valueAxis,
xAxis: dateAxis,
snapToSeries: [valueSeries],
snapToSeriesBy: "y!"
}));
// Add scrollbar
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/scrollbars/
var scrollbar = mainPanel.set("scrollbarX", am5xy.XYChartScrollbar.new(root, {
orientation: "horizontal",
height: 50
}));
stockChart.toolsContainer.children.push(scrollbar);
var sbDateAxis = scrollbar.chart.xAxes.push(am5xy.GaplessDateAxis.new(root, {
baseInterval: {
timeUnit: "day",
count: 1
},
renderer: am5xy.AxisRendererX.new(root, {})
}));
var sbValueAxis = scrollbar.chart.yAxes.push(am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {})
}));
var sbSeries = scrollbar.chart.series.push(am5xy.LineSeries.new(root, {
valueYField: "Close",
valueXField: "Date",
xAxis: sbDateAxis,
yAxis: sbValueAxis
}));
sbSeries.fills.template.setAll({
visible: true,
fillOpacity: 0.3
});
// Set up series type switcher
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/toolbar/series-type-control/
var seriesSwitcher = am5stock.SeriesTypeControl.new(root, {
stockChart: stockChart
});
seriesSwitcher.events.on("selected", function (ev) {
setSeriesType(ev.item.id);
});
function getNewSettings(series) {
var newSettings = [];
am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function (setting) {
newSettings[setting] = series.get(setting);
});
return newSettings;
}
function setSeriesType(seriesType) {
// Get current series and its settings
var currentSeries = stockChart.get("stockSeries");
var newSettings = getNewSettings(currentSeries);
// Remove previous series
var data = currentSeries.data.values;
mainPanel.series.removeValue(currentSeries);
// Create new series
var series;
switch (seriesType) {
case "line":
series = mainPanel.series.push(am5xy.LineSeries.new(root, newSettings));
break;
case "candlestick":
case "procandlestick":
newSettings.clustered = false;
series = mainPanel.series.push(am5xy.CandlestickSeries.new(root, newSettings));
if (seriesType == "procandlestick") {
series.columns.template.get("themeTags").push("pro");
}
break;
case "ohlc":
newSettings.clustered = false;
series = mainPanel.series.push(am5xy.OHLCSeries.new(root, newSettings));
break;
}
// Set new series as stockSeries
if (series) {
valueLegend.data.removeValue(currentSeries);
series.data.setAll(data);
stockChart.set("stockSeries", series);
var cursor = mainPanel.get("cursor");
if (cursor) {
cursor.set("snapToSeries", [series]);
}
valueLegend.data.insertIndex(0, series);
}
}
// Stock toolbar
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/toolbar/
var toolbar = am5stock.StockToolbar.new(root, {
container: document.getElementById("chartcontrols"),
stockChart: stockChart,
controls: [
am5stock.IndicatorControl.new(root, {
stockChart: stockChart,
legend: valueLegend
}),
am5stock.DateRangeSelector.new(root, {
stockChart: stockChart
}),
am5stock.PeriodSelector.new(root, {
stockChart: stockChart
}),
seriesSwitcher,
am5stock.DrawingControl.new(root, {
stockChart: stockChart
}),
am5stock.ResetControl.new(root, {
stockChart: stockChart
}),
am5stock.SettingsControl.new(root, {
stockChart: stockChart
})
]
})
// data
var data = this.data;
var tooltip = am5.Tooltip.new(root, {
getStrokeFromSprite: false,
getFillFromSprite: false
});
tooltip.get("background").setAll({
strokeOpacity: 1,
stroke: am5.color(0x000000),
fillOpacity: 1,
fill: am5.color(0xffffff)
});
function makeEvent(date, letter, color, description) {
var dataItem = dateAxis.createAxisRange(dateAxis.makeDataItem({ value: date }))
var grid = dataItem.get("grid");
if (grid) {
grid.setAll({ visible: true, strokeOpacity: 0.2, strokeDasharray: [3, 3] })
}
var bullet = am5.Container.new(root, {
dy: -100
});
var circle = bullet.children.push(am5.Circle.new(root, {
radius: 10,
stroke: color,
fill: am5.color(0xffffff),
tooltipText: description,
tooltip: tooltip,
tooltipY: 0
}));
var label = bullet.children.push(am5.Label.new(root, {
text: letter,
centerX: am5.p50,
centerY: am5.p50
}));
dataItem.set("bullet", am5xy.AxisBullet.new(root, {
location: 0,
stacked: true,
sprite: bullet
}));
}
// set data to all series
valueSeries.data.setAll(data);
volumeSeries.data.setAll(data);
sbSeries.data.setAll(data);
}); // end am5.ready()
}
}
})

View File

@ -0,0 +1,103 @@
(function() {
new Vue({
el: '#app',
data: {
dropdownVisibility: {
source: false,
target: false
},
sourceCoinAmount: 0,
sourceCoinFilter: '',
targetCoinFilter: '',
sourceCoinValue: 'cashout',
targetCoinValue: 'ascoin',
balances: [],
coins: [],
club: false
},
methods: {
changeSourceCoin(name) {
this.sourceCoinValue = name;
this.dropdownVisibility.source = false;
this.sourceCoinFilter = '';
},
changeTargetCoin(name) {
this.targetCoinValue = name;
this.dropdownVisibility.target = false;
this.targetCoinFilter = '';
},
swapCoins() {
const tmp = this.sourceCoinValue;
this.sourceCoinValue = this.targetCoinValue;
this.targetCoinValue = tmp;
},
fetchCoins() {
fetch("/api/fetch_coins").then(res => res.json()).then(coins => {
this.coins = coins;
})
},
fetchBalances() {
fetch("/api/wallet/balances").then(res => res.json()).then(balances => {
this.balances = balances;
})
},
convert() {
fetch("/api/wallet/transaction", {
method: "POST",
body: JSON.stringify({
sourceCoin: this.sourceCoinValue,
targetCoin: this.targetCoinValue,
balance: this.sourceCoinAmount
}),
headers: {
"Content-Type": "application/json"
}
}).then(_ => {
this.fetchBalances();
this.sourceCoinAmount = 0;
})
},
joinGlacierClub() {
fetch("/api/wallet/join_glacier_club", {method: "POST"}).then(res => res.json()).then(club => {
this.club = club;
this.$refs.modalGlacierclub.classList.add('is-active')
})
},
closeClubModal() {
this.$refs.modalGlacierclub.classList.remove('is-active')
}
},
computed: {
filteredSourceCoins() {
return this.coins.filter(coin => coin.value.includes(this.sourceCoinFilter) && coin.name !== this.targetCoinValue);
},
filteredTargetCoins() {
return this.coins.filter(coin => coin.value.includes(this.targetCoinFilter) && coin.name !== this.sourceCoinValue);
},
sourceCoin() {
return this.coins.filter(coin => coin.name === this.sourceCoinValue)[0];
},
targetCoin() {
return this.coins.filter(coin => coin.name === this.targetCoinValue)[0];
}
},
mounted() {
this.fetchCoins();
this.fetchBalances();
},
delimiters: ['$$', '$$'],
})
})();
(function() {
setInterval(_ => {
document.querySelectorAll("[data-adjust-width-to]").forEach(element => {
const referenceElementId = element.dataset.adjustWidthTo;
if(!referenceElementId) return;
const referenceElement = document.getElementById(referenceElementId);
if(!referenceElement) return;
element.style.width = `${referenceElement.offsetWidth}px`;
})
})
})();

View File

@ -0,0 +1,141 @@
html {
background: url(/assets/images/bg.jpg) no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
.box {
background-color: #27273e;
opacity: 0.9;
/* min-height: 500px; */
}
.textfield-area {
border: 1px solid #72727b;
border-radius: 10px;
padding: 10px;
}
.textfield-area .button {
color: white;
background: transparent;
border: none;
}
.textfield-area input:hover {
color: white;
}
.textfield-area input {
background-color: transparent;
border: none;
color: white;
font-size: 20px;
width: 100%;
}
.textfield-area label {
font-size: 12px;
}
.textfield-area input:focus,
.textfield-area .button:focus,
.textfield-area .button:focus-visible,
.select-area .input:focus,
.select-area .input:active {
outline: 0 !important;
box-shadow: none !important;
}
.select-area {
height: 0;
}
.select-area .input,
.select-area .input:focus,
.select-area .input::placeholder {
background: #3b3b4f;
color: white;
border: none;
}
.select-area .control.has-icons-right .input:focus~.icon {
color: #dbdbdb;
}
.select-area .dropdown-item {
font-size: 0.75rem;
}
.select-area .dropdown-menu {
padding-top: 0;
min-width: 0;
}
.select-area a.dropdown-item {
padding-right: 1rem;
color: white;
}
.select-area .dropdown-content {
background: #27273e;
border: 1px solid #353544;
border-radius: 0px 0px 2px 2px;
padding: 0px;
}
.select-area .dropdown-item:hover {
background: #1e1e31;
}
.rotate-area i {
border-radius: 5px;
padding: 5px;
background-color: #3a3a57;
font-size: 30px;
color: #15b1d5;
}
.rotate-area i:hover {
background-color: #151522;
cursor: pointer;
}
/** Chart library style **/
#chartcontrols {
height: auto;
padding: 5px 5px 0 16px;
max-width: 100%;
}
#chartdiv {
width: 100%;
height: 600px;
max-width: 100%;
}
.balance .table {
background-color: transparent;
color: white;
width: 100%;
}
.balance .table th {
background-color: transparent;
color: white;
}
.fancy-button button,
.fancy-button button:focus,
.fancy-button button:hover,
.fancy-button button::placeholder {
background-color: #90b4ce;
border-color: #02375e;
border-width: 2px;
color: #111160;
}

View File

@ -0,0 +1,2 @@
flask
flask_restful

View File

@ -0,0 +1,123 @@
from flask import Flask, render_template, request, send_from_directory, jsonify, session
from flask_restful import Api
from src.coin_api import get_coin_price_from_api
from src.wallet import Wallet
import os
import secrets
app = Flask(__name__)
api = Api(app)
app.secret_key = os.urandom(64)
wallets = {}
def get_wallet_from_session():
if "id" not in session:
session["id"] = make_token()
if session["id"] not in wallets:
wallets[session["id"]] = Wallet()
return wallets[session["id"]]
def make_token():
return secrets.token_urlsafe(16)
@app.route("/", methods=["GET", "POST"])
def index():
return render_template(
"index.html",
)
@app.route('/assets/<path:path>')
def assets(path):
return send_from_directory('assets', path)
@app.route('/api/data/fetch/<path:coin>')
def fetch(coin: str):
data = get_coin_price_from_api(coin)
return jsonify(data)
@app.route('/api/wallet/transaction', methods=['POST'])
def transaction():
payload = request.json
status = 0
if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
wallet = get_wallet_from_session()
status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
return jsonify({
"result": status
})
@app.route("/api/wallet/join_glacier_club", methods=["POST"])
def join_glacier_club():
wallet = get_wallet_from_session()
clubToken = False
inClub = wallet.inGlacierClub()
if inClub:
f = open("/flag.txt")
clubToken = f.read()
f.close()
return {
"inClub": inClub,
"clubToken": clubToken
}
@app.route('/api/wallet/balances')
def get_balance():
wallet = get_wallet_from_session()
balances = wallet.getBalances()
user_balances = []
for name in balances:
user_balances.append({
"name": name,
"value": balances[name]
})
return user_balances
@app.route('/api/fetch_coins')
def fetch_coins():
return jsonify([
{
"name": 'cashout',
"value": 'Cashout Account',
"short": 'CA'
},
{
"name": 'glaciercoin',
"value": 'GlacierCoin',
"short": 'GC'
},
{
"name": 'ascoin',
"value": 'AsCoin',
"short": 'AC'
},
{
"name": 'doge',
"value": 'Doge',
"short": 'DO'
},
{
"name": 'gamestock',
"value": 'Gamestock',
"short": 'GS'
},
{
"name": 'ycmi',
"value": 'Yeti Clubs Manufacturing Inc.',
"short": 'YC'
},
{
"name": 'smtl',
"value": 'Synthetic Mammoth Tusks LLC',
"short": 'ST'
},
])
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=8080,
debug=True,
)

View File

@ -0,0 +1,48 @@
import time
import random
def get_coin_price_from_api(coin: str):
coins = coin.split('/')
if(len(coins) != 2):
return []
seed = coins[0] + coins[1] if coins[0] < coins[1] else coins[1] + coins[0]
is_reverse = coins[0] < coins[1]
random.seed(seed)
end_timestamp = int(time.time()) * 1000
new_open = 15.67
new_high = 15.83
new_low = 15.24
new_close = 15.36
new_volume = 3503100
movement = 0.7
data = []
max_ticks = 200
for ts in range(0, max_ticks):
display_new_open = 1. / new_open if is_reverse else new_open
display_new_high = 1. / new_high if is_reverse else new_high
display_new_low = 1. / new_low if is_reverse else new_low
display_new_close = 1. / new_close if is_reverse else new_close
data.append({
"Date": end_timestamp - (max_ticks - ts) * (1000 * 86400),
"Open": display_new_open,
"High": display_new_high,
"Low": display_new_low,
"Close": display_new_close,
"Volume": new_volume
})
# New Open => Downwards Trend
# New Close => Upwards Trend
indicator = new_open if random.random() > 0.5 else new_close
new_open = indicator + movement * (random.random() - 0.5)
new_high = indicator + movement * (random.random() - 0.5)
new_low = indicator + movement * (random.random() - 0.5)
new_close = indicator + movement * (random.random() - 0.5)
new_volume = new_volume + movement * (random.random() - 0.5)
return data

View File

@ -0,0 +1,39 @@
import threading
class Wallet():
def __init__(self) -> None:
self.balances = {
"cashout": 1000,
"glaciercoin": 0,
"ascoin": 0,
"doge": 0,
"gamestock": 0,
"ycmi": 0,
"smtl": 0
}
self.lock = threading.Lock();
def getBalances(self):
return self.balances
def transaction(self, source, dest, amount):
if source in self.balances and dest in self.balances:
with self.lock:
if self.balances[source] >= amount:
self.balances[source] -= amount
self.balances[dest] += amount
return 1
return 0
def inGlacierClub(self):
with self.lock:
for balance_name in self.balances:
if balance_name == "cashout":
if self.balances[balance_name] < 1000000000:
return False
else:
if self.balances[balance_name] != 0.0:
return False
return True

View File

@ -0,0 +1,201 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GlacierExchange</title>
<link rel="shortcut icon" href="/assets/icons/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="assets/styles/main.css">
</head>
<body>
<section class="hero is-fullheight" v-if="coins.length > 0 && balances.length > 0" id="app">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<glacier-chart :key="sourceCoinValue + '/' + targetCoinValue" :name="sourceCoinValue + '/' + targetCoinValue"></glacier-chart>
</div>
<div class="column">
<div class="balance box has-text-white">
<h1 class="title has-text-centered has-text-white">Your Balance</h1>
<table class="table has-text-centered">
<thead>
<tr>
<th class="has-text-centered">Coin</th>
<th class="has-text-centered">Balance</th>
</tr>
</thead>
<tbody>
<tr v-for="balance in balances">
<th class="has-text-centered">$$ balance.name $$</th>
<td>$$ balance.value $$</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<div class="fancy-button buttons are-medium">
<button class="button is-fullwidth" @click="joinGlacierClub">Join GlacierClub</button>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="box has-text-white">
<h1 class="title has-text-centered has-text-white">GlacierExchange</h1>
<p style="text-align: justify;" class="mb-5">
The GlacierExchange is a revolutionary tool for converting glacierchain coins from one
currency to another.
The conversion rate is always guaranteed to be 1:1 without fees.
</p>
<!-- Input field -->
<div class="textfield-area">
<div class="columns is-flex is-vcentered">
<div class="column">
<label for="from">From</label>
<input type="text" v-model="sourceCoinAmount" />
</div>
<div class="column is-two-fifths has-text-right">
<div class="dropdown-trigger">
<button @click="dropdownVisibility.source ^= 1" class="button"
aria-haspopup="true" aria-controls="dropdown-menu">
<img class="mr-3" style="width: 32px" :src="'/assets/icons/' + sourceCoin.name + '.png'" alt="">
<span>$$ sourceCoin.short $$</span>
<span class="ml-2 icon is-small">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- Dropdown -->
<div class="select-area" id="select-area-from">
<div :class="['dropdown', dropdownVisibility.source ? 'is-active' : '']">
<div class="dropdown-menu" role="menu" data-adjust-width-to="select-area-from">
<div class="control has-icons-right">
<input v-model="sourceCoinFilter" type="text"
placeholder="Search for symbol" class="input is-transparent is-small">
<span class="icon is-right">
<i class="fa fa-search"></i>
</span>
</div>
<div class="dropdown-content">
<a @click="changeSourceCoin(coin.name)" href="javascript:void(0)"
v-for="coin in filteredSourceCoins" class="dropdown-item">
<img class="mr-3" style="width: 16px; vertical-align: middle;" :src="'/assets/icons/' + coin.name + '.png'" alt="">
$$ coin.value $$
</a>
</div>
</div>
</div>
</div>
<!-- Swap Coins -->
<div class="has-text-centered rotate-area mt-5 mb-5">
<i @click="swapCoins" class="fa-solid fa-rotate"></i>
</div>
<!-- Input field -->
<div class="textfield-area">
<div class="columns is-flex is-vcentered">
<div class="column">
<label for="from">To</label>
<input type="text" disabled :value="sourceCoinAmount" />
</div>
<div class="column is-two-fifths has-text-right">
<div class="dropdown-trigger">
<button @click="dropdownVisibility.target ^= 1" class="button"
aria-haspopup="true" aria-controls="dropdown-menu">
<img class="mr-3" style="width: 32px" :src="'/assets/icons/' + targetCoin.name + '.png'" alt="">
<span>$$ targetCoin.short $$</span>
<span class="ml-2 icon is-small">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- Dropdown -->
<div class="select-area" id="select-area-from">
<div :class="['dropdown', dropdownVisibility.target ? 'is-active' : '']">
<div class="dropdown-menu" role="menu" data-adjust-width-to="select-area-from">
<div class="control has-icons-right">
<input v-model="targetCoinFilter" type="text"
placeholder="Search for symbol" class="input is-transparent is-small">
<span class="icon is-right">
<i class="fa fa-search"></i>
</span>
</div>
<div class="dropdown-content">
<a @click="changeTargetCoin(coin.name)" href="javascript:void(0)"
v-for="coin in filteredTargetCoins" class="dropdown-item">
<img class="mr-3" style="width: 16px; vertical-align: middle;" :src="'/assets/icons/' + coin.name + '.png'" alt="">
$$ coin.value $$
</a>
</div>
</div>
</div>
</div>
<div class="fancy-button buttons are-medium mt-4">
<button class="button is-fullwidth" @click="convert">Convert</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div ref="modalGlacierclub" class="modal">
<div class="modal-background" @click="closeClubModal"></div>
<div class="modal-content">
<div class="box has-text-white">
<div v-if="club && club.inClub === true">
<h1 class="title has-text-centered has-text-white">Welcome my lord</h1>
<p>
Your royality is sufficient to join glacier club.<br />
A member of the glacier club will contact you.<br />
Please hold ready your club token when they contact you<br />
Club-Token: $$ club.clubToken $$
</p>
</div>
<div v-else>
<h1 class="title has-text-centered has-text-white">Insufficient royality</h1>
<p>
You are not eligible to join the royal club of the glaciers.<br />
Earn more money to get a member!<br />
Make sure to empty all coins except the cashout wallet.
</p>
</div>
</div>
</div>
<button @click="closeClubModal" class="modal-close is-large" aria-label="close"></button>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14"></script>
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
<script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
<script src="https://cdn.amcharts.com/lib/5/stock.js"></script>
<script src="https://cdn.amcharts.com/lib/5/themes/Dark.js"></script>
<script src="/assets/scripts/chart.component.js"></script>
<script src="/assets/scripts/index.js"></script>
</body>
</html>

Binary file not shown.

1
web/peak/README.md Normal file
View File

@ -0,0 +1 @@
Within the heart of Austria's alpine mystery lies your next conquest. Ascend the highest peak, shrouded in whispers of past explorers, to uncover the flag.txt awaiting atop. Beware the silent guards that stand sentinel along the treacherous path, obstructing your ascent.

BIN
web/peak/challenge.zip Normal file

Binary file not shown.

53
web/peak/dist/.docker/Dockerfile-web vendored Normal file
View File

@ -0,0 +1,53 @@
FROM tobi312/php:8.1-apache
WORKDIR /var/www/html
COPY ./web/ /var/www/html/
COPY ./flag/flag.txt /
RUN mkdir -p /var/sqlite/
COPY ./sqlite.db /var/sqlite/
RUN chown -R 33:33 /var/sqlite/
RUN chmod 750 /var/sqlite/sqlite.db
RUN mkdir -p /var/www/html/uploads/
RUN chown 33:33 /var/www/html/uploads/
RUN chmod -R 777 /var/www/html/uploads/
USER root
RUN ln -s /dev/null /root/.bash_history
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
python3 python3-pip curl unzip wget cron util-linux \
fonts-liberation libasound2 libatk-bridge2.0-0 procps \
libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 \
libcairo2 libcups2 libgbm1 libgtk-3-0 libpango-1.0-0 \
libu2f-udev libvulkan1 libxkbcommon-x11-0 xvfb
RUN CHROMEDRIVER_VERSION=`curl -sS https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE` && \
wget -q -O chromedriver_linux64.zip https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROMEDRIVER_VERSION/linux64/chromedriver-linux64.zip && \
unzip chromedriver_linux64.zip && mv chromedriver-linux64/chromedriver /usr/bin/ && \
chmod +x /usr/bin/chromedriver && \
rm chromedriver_linux64.zip && rm -r chromedriver-linux64
RUN CHROME_SETUP=google-chrome.deb && \
wget -q -O $CHROME_SETUP "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" && \
dpkg -i $CHROME_SETUP && \
apt-get install -y -f && \
rm $CHROME_SETUP
RUN rm /usr/lib/python3.11/EXTERNALLY-MANAGED
RUN python3 -m pip install selenium urllib3 python-decouple requests bs4 pyvirtualdisplay
COPY ./admin-simulation/ /root/admin_simulation
RUN echo '#!/bin/bash' > /entrypoint.d/simulation.sh
RUN echo 'echo "$(env | grep "HOST=.*")" >> /etc/environment' >> /entrypoint.d/simulation.sh
RUN echo 'echo "$(env | grep "ADMIN_PW=.*")" >> /etc/environment' >> /entrypoint.d/simulation.sh
RUN echo 'service cron start' >> /entrypoint.d/simulation.sh
RUN chmod +x /entrypoint.d/simulation.sh
RUN echo '* * * * * root /usr/bin/flock -w 0 /var/cron.lock python3 /root/admin_simulation/admin.py "$ADMIN_PW" > /var/log/admin_simulation.log 2> /var/log/admin_simulation.error' >> /etc/crontab

153
web/peak/dist/admin-simulation/admin.py vendored Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
import sys, requests
import os
from time import sleep
from bs4 import BeautifulSoup
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from pyvirtualdisplay import Display
import urllib3
urllib3.disable_warnings()
class AdminAutomation:
host = os.environ.get("HOST", "http://web")
timeout = int(os.environ.get("TIMEOUT", "5"))
driver = None
_username = 'admin'
_password = ''
display = Display(visible=0, size=(800, 600))
display.start()
def __init__(self, password:str=''):
chrome_options = self._set_chrome_options()
service = Service(executable_path=r'/usr/bin/chromedriver')
self.driver = webdriver.Chrome(service=service, options=chrome_options)
self.driver.set_page_load_timeout(self.timeout)
self._password = password
if self._password == '':
raise Exception('No password for admin configured!')
def _set_chrome_options(self):
'''
Sets chrome options for Selenium:
- headless browser is enabled
- sandbox is disbaled
- dev-shm usage is disabled
- SSL certificate errors are ignored
'''
chrome_options = webdriver.ChromeOptions()
options = [
'--headless',
'--no-sandbox', '--disable-dev-shm-usage', '--ignore-certificate-errors',
'--disable-extensions', '--no-first-run', '--disable-logging',
'--disable-notifications', '--disable-permissions-api', '--hide-scrollbars',
'--disable-gpu', '--window-size=800,600', '--disable-xss-auditor'
]
for option in options:
chrome_options.add_argument(option)
return chrome_options
def login(self) -> bool:
'''
Login as admin
- Returns: `True` if successful and `False` of unsuccessful
'''
self.driver.get(f'{self.host}/login.php')
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.NAME, 'username')))
self.driver.find_element('name', 'username').send_keys('admin')
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.NAME, 'password')))
self.driver.find_element('name', 'password').send_keys(self._password)
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, 'button')))
self.driver.find_element('name', 'button').click()
if self.driver.current_url != f'{self.host}/':
return False
print(f'[{datetime.now()}] Successfully logged in!\r\n')
return True
def read_messages(self):
print(f'[{datetime.now()}] Checking messages...')
self.driver.get(f'{self.host}/admin/support.php')
if self.driver.current_url != f'{self.host}/admin/support.php':
raise Exception("Cannot access support.php! Session probably expired!")
links = [element.get_attribute('href') for element in self.driver.find_elements('name', 'inbox-header')]
if len(links) > 0:
for link in links:
if link:
try:
self.driver.get(link)
if self.driver.current_url == link:
print(f'[{datetime.now()}] Visiting: {self.driver.current_url}\r\n')
else:
print(f'[{datetime.now()}] After visiting {link}, got redirect to: {self.driver.current_url}\r\n')
except Exception as ex:
'''Timeout or other exception occurred on url.
'''
print(f'[{datetime.now()}] Error after visiting: {link} (Current URL: {self.driver.current_url}). Error: {ex}\r\n')
def close(self):
if self.driver:
self.driver.close()
self.driver.quit()
self.driver = None
if self.display:
self.display.stop()
if __name__ == '__main__':
os.system('pkill -f chrome')
os.system('pkill -f Xvfb')
admin = None
try:
if len(sys.argv) < 2:
raise Exception('Specify a password!')
admin = AdminAutomation(sys.argv[1])
tries = 0
while not admin.login():
if tries > 5:
raise Exception('Could not login!')
tries += 1
sleep(1)
while True:
admin.read_messages()
sleep(5)
admin.close()
quit()
except Exception as ex:
print(f'[-] Error: {ex}')
if admin is not None:
admin.close()
quit()

16
web/peak/dist/docker-compose.yml vendored Normal file
View File

@ -0,0 +1,16 @@
version: "3"
services:
web:
build:
context: .
dockerfile: .docker/Dockerfile-web
image: webserver
container_name: webserver
restart: always
hostname: webserver
environment:
ADMIN_PW: example-password
HOST: http://localhost
ports:
- "80:80"

1
web/peak/dist/flag/flag.txt vendored Normal file
View File

@ -0,0 +1 @@
gctf{SOME_FLAG}

0
web/peak/dist/sqlite.db vendored Normal file
View File

100
web/peak/dist/web/actions/contact.php vendored Normal file
View File

@ -0,0 +1,100 @@
<?php
include_once "../includes/session.php";
function cleanup_old_files()
{
$currentTimestamp = time();
$uploadsDirectory = "../uploads";
$files = scandir($uploadsDirectory);
if(sizeof($files) > 0)
{
foreach ($files as $file)
{
if ($file !== '.' && $file !== '..' && $file !== '.htaccess')
{
$filePath = $uploadsDirectory . '/' . $file;
if (is_file($filePath))
{
$fileTimestamp = filemtime($filePath);
$timeDifference = $currentTimestamp - $fileTimestamp;
// Check if the file is older than 5 minutes (300 seconds)
if ($timeDifference > 300)
unlink($filePath);
}
}
}
}
}
try
{
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['user']) && $_SESSION['user']['role'] !== "admin")
{
if(isset($_POST['title']) && isset($_POST['content']))
{
cleanup_old_files();
$target_file = "";
if(isset($_FILES['image']) && $_FILES['image']['name'] !== "")
{
$targetDirectory = '/uploads/';
$timestamp = microtime(true);
$timestampStr = str_replace('.', '', sprintf('%0.6f', $timestamp));
$randomFilename = uniqid() . $timestampStr;
$targetFile = ".." . $targetDirectory . $randomFilename;
$imageFileType = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png'];
$check = false;
try
{
$check = @getimagesize($_FILES['image']['tmp_name']);
}
catch(Exception $exx)
{
throw new Exception("File is not a valid image!");
}
if ($check === false)
{
throw new Exception("File is not a valid image!");
}
if (!in_array($imageFileType, $allowedExtensions))
{
throw new Exception("Invalid image file type. Allowed types: jpg, jpeg, png");
}
if (!move_uploaded_file($_FILES['image']['tmp_name'], $targetFile))
{
throw new Exception("Error uploading the image! Try again! If this issue persists, contact a CTF admin!");
}
$target_file = $targetDirectory . $randomFilename;
}
$title = $_POST['title'];
$content = $_POST['content'];
$user_id = $_SESSION['user']['id'];
$sql = $pdo->prepare("INSERT INTO messages (title, content, file, user_id) VALUES (:title, :content, :file, :user_id)");
$sql->bindParam(':title', $title, PDO::PARAM_STR);
$sql->bindParam(':content', $content, PDO::PARAM_STR);
$sql->bindParam(':user_id', $user_id, PDO::PARAM_INT);
$sql->bindParam(':file', $target_file, PDO::PARAM_STR);
try
{
$sql->execute();
}
catch (PDOException $e)
{
throw new Exception("Could not create request. Please try again! If this issue persists, contact a CTF admin!");
}
$_SESSION['success'] = "Message received! An admin will handle your request shortly. You can view your request <a name='message' href='/pages/view_message.php?id=" . $pdo->lastInsertId() ."'>here</a>";
}
}
}
catch(Exception $ex)
{
$_SESSION['error'] = htmlentities($ex->getMessage());
}
header('Location: /pages/contact.php');

53
web/peak/dist/web/actions/login.php vendored Normal file
View File

@ -0,0 +1,53 @@
<?php
include_once "../includes/session.php";
try
{
if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST['username']) && isset($_POST['password']))
{
$username = $_POST['username'];
$password = $_POST['password'];
if ($username !== "" && $password !== "")
{
$sql = $pdo->prepare("SELECT * FROM users WHERE username=:name");
$sql->bindValue(':name', $username);
$sql->execute();
$user = $sql->fetch();
if ($user)
{
$id = $user["id"];
$username = $user["username"];
$role = $user["role"];
if(password_verify($password, $user["password"]))
{
$_SESSION['user'] = array();
$_SESSION['user']['id'] = $id;
$_SESSION['user']['username'] = $username;
$_SESSION['user']['role'] = $role;
header('Location: /');
}
else
{
throw new Exception("Invalid username or password!");
}
}
else
{
throw new Exception("Invalid username or password!");
}
}
else
{
throw new Exception("Username and Password required!");
}
}
}
catch(Exception $ex)
{
$_SESSION['error'] = htmlentities($ex->getMessage());
header('Location: /login.php');
}
?>

5
web/peak/dist/web/actions/logout.php vendored Normal file
View File

@ -0,0 +1,5 @@
<?php
include_once "../includes/session.php";
session_destroy();
header('Location: /');
?>

53
web/peak/dist/web/actions/register.php vendored Normal file
View File

@ -0,0 +1,53 @@
<?php
include_once "../includes/session.php";
try
{
if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST['username']) && isset($_POST['password']))
{
$username = $_POST['username'];
$password = $_POST['password'];
if ($username !== "" && $password !== "")
{
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$sql = $pdo->prepare("INSERT INTO users (username, password) VALUES (:username, :password)");
$sql->bindParam(':username', $username, PDO::PARAM_STR);
$sql->bindParam(':password', $hashedPassword, PDO::PARAM_STR);
try
{
$sql->execute();
$sql = $pdo->prepare("SELECT * FROM users WHERE username=:name");
$sql->bindValue(':name', $username);
$sql->execute();
$user = $sql->fetch();
if ($user)
{
$id = $user["id"];
$username = $user["username"];
$role = $user["role"];
$_SESSION['user'] = array();
$_SESSION['user']['id'] = $id;
$_SESSION['user']['username'] = $username;
$_SESSION['user']['role'] = $role;
header('Location: /');
}
}
catch (PDOException $e)
{
throw new Exception("Could not register with this username! Try again with a different name.");
}
}
}
}
catch(Exception $ex)
{
$_SESSION['error'] = htmlentities($ex->getMessage());
header('Location: /login.php');
}
?>

7
web/peak/dist/web/admin/data.example vendored Normal file
View File

@ -0,0 +1,7 @@
<markers>
<marker>
<lat>47.0748663672</lat>
<lon>12.695247219</lon>
<name>Großglockner</name>
</marker>
</markers>

View File

@ -0,0 +1,27 @@
<?php
include_once "../includes/config.php";
if(isset($_SESSION['user']))
{
$sql = $pdo->prepare("SELECT * FROM users WHERE id=:id");
$sql->bindValue(':id', $_SESSION['user']['id']);
$sql->execute();
$user = $sql->fetch();
if ($user && $user["role"] === "admin")
{
$id = $user["id"];
$username = $user["username"];
$role = $user["role"];
$_SESSION['user'] = array();
$_SESSION['user']['id'] = $id;
$_SESSION['user']['username'] = $username;
$_SESSION['user']['role'] = $role;
return;
}
}
session_destroy();
header('Location: /login.php');
return;
?>

87
web/peak/dist/web/admin/map.php vendored Normal file
View File

@ -0,0 +1,87 @@
<?php
include_once "includes/session.php";
?>
<!DOCTYPE html>
<html lang="en">
<?php include_once "../includes/header.php"; ?>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Edit map</h1>
<p>Dev-Note: Please note editing map globally is currently not possible! We are working on it.<br>You can use this site to test out the coordinates for now.</p>
<form method="post" action="/admin/map.php">
<div class="form-group">
<textarea class="form-control" name="data" rows="7"><?php $xmlFilePath="./data.example"; echo file_get_contents($xmlFilePath);?></textarea>
</div>
<br>
<button type="submit" class="btn btn-light btn-lg">Submit</button>
</form>
</div>
</header>
<section id="map" class="py-5">
<div class="container">
<div class="panel panel-default">
<div class="panel-body">
<div class="card header">
<div class="card-body">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<div id="map" style="height: 500px;"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script>
var map = L.map('map').setView([0, 0], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);
<?php
function parseXML($xmlData)
{
try
{
libxml_disable_entity_loader(false);
$xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOENT);
return $xml;
}
catch(Exception $ex)
{
return false;
}
return true;
}
try
{
$xmlData = "";
if ($_SERVER["REQUEST_METHOD"] === "POST")
{
$xmlData = $_POST["data"];
if(!parseXML($xmlData))
$xmlData = "";
}
if($xmlData === "")
{
$xmlData = file_get_contents($xmlFilePath);
}
$xml = parseXML($xmlData);
foreach($xml->marker as $marker)
{
$name = str_replace("\n", "\\n", $marker->name);
echo 'L.marker(["' . $marker->lat . '", "' . $marker->lon.'"]).addTo(map).bindPopup("'. $name. '").openPopup();' . "\n";
echo 'map.setView(["' . $marker->lat . '", "' . $marker->lon.'"], 9);' . "\n";
}
}
catch(Exception $ex)
{
echo "Invalid xml data!";
}
?>
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
</html>

54
web/peak/dist/web/admin/support.php vendored Normal file
View File

@ -0,0 +1,54 @@
<?php
include_once "includes/session.php";
?>
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<body>
<?php include_once "../includes/menu.php"; ?>
<?php $messages = $pdo->query("SELECT * FROM messages")->fetchAll(PDO::FETCH_ASSOC); ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Support Requests</h1>
</div>
</header>
<section id="messages" class="py-5">
<div class="container mt-5">
<ul class="list-group">
<?php if(sizeof($messages) > 0) : ?>
<?php foreach ($messages as $message): ?>
<li class="list-group-item">
<h5 class="mb-1"><?php echo htmlentities($message['title']);?> from
<?php
$sql = "SELECT u.username
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.user_id = :user_id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':user_id', $message['user_id'], PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo htmlentities($result['username']);
?></h5>
<a href="/pages/view_message.php?id=<?php echo $message['id']; ?>" name="inbox-header">Inspect Request</a>
</li>
<?php endforeach; ?>
<?php else:?>
<div class="container text-center">
<h3>No messages available at the moment!</h3>
</div>
<?php endif;?>
</ul>
</div>
</section>
</body>
</html>

7
web/peak/dist/web/assets/bootstrap.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
web/peak/dist/web/assets/img/hiking.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
web/peak/dist/web/assets/img/map.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
web/peak/dist/web/assets/img/peak.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
web/peak/dist/web/assets/img/road.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

2
web/peak/dist/web/assets/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

6
web/peak/dist/web/assets/leaflet.js vendored Normal file

File diff suppressed because one or more lines are too long

6
web/peak/dist/web/assets/popper.js vendored Normal file

File diff suppressed because one or more lines are too long

47
web/peak/dist/web/includes/config.php vendored Normal file
View File

@ -0,0 +1,47 @@
<?php
session_start();
function get_pdo()
{
$dsn="sqlite:/var/sqlite/sqlite.db";
$pdo = new PDO($dsn, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
return $pdo;
}
function setup_db($pdo)
{
$already_setup = FALSE;
try
{
$already_setup = $pdo->exec("select 1 from users inner join messages on users.id = messages.id");
}
catch(Exception $ex)
{
$already_setup = FALSE;
}
if($already_setup === FALSE)
{
$commands = [
'CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `username` varchar(255) NOT NULL UNIQUE, `password` varchar(255) NOT NULL, `role` varchar(255) NOT NULL default "user");',
'CREATE TABLE IF NOT EXISTS `messages` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` varchar(255) NOT NULL, `content` varchar(512) NOT NULL, `file` varchar(512) NOT NULL default "", `created_at` timestamp default CURRENT_TIMESTAMP NOT NULL, `viewed` BOOLEAN DEFAULT 0, `user_id` INTEGER unsigned NOT NULL, FOREIGN KEY (`user_id`) REFERENCES users(`id`) ON DELETE CASCADE);',
'INSERT INTO `users` (`username`, `password`, `role`) VALUES ("admin", "$2y$10$yerhXWb8EZR4MBHT0oOm2e1S2lTheH4zHOWqRIKTKEuVMyiL1Mtl6", "admin");'
];
foreach($commands as $command)
{
$pdo->exec($command);
}
}
}
try
{
$pdo = get_pdo();
setup_db($pdo);
}
catch(Exception $e)
{
die("Could not connect to the database! Please report to CTF admin!" . $e->getMessage());
}
?>

3
web/peak/dist/web/includes/csp.php vendored Normal file
View File

@ -0,0 +1,3 @@
<?php
header("Content-Security-policy: script-src 'self'");
?>

30
web/peak/dist/web/includes/error.php vendored Normal file
View File

@ -0,0 +1,30 @@
<?php
if (isset($_SESSION['success']))
{
echo '<div id="toast-success-container" class="toast-top-center example">
<div id="alert" class="toast-success alert-success hide" role="alert" data-delay="5000" data-autohide="true" aria-live="assertive" aria-atomic="true">
<div class="toast-body">
' . $_SESSION['success'] . '
</div>
</div>
</div>
';
unset($_SESSION['success']);
}
else if (isset($_SESSION['error']))
{
echo '<div id="toast-alert-container" class="toast-top-center example">
<div id="alert" class="toast-alert alert-danger hide" role="alert" data-delay="5000" data-autohide="true" aria-live="assertive" aria-atomic="true">
<div class="toast-header-alert">
<i class="fas fa-2x fa-exclamation-circle mr-2"></i>
<strong class="mr-auto">Error</strong>
</div>
<div class="toast-body">
' . $_SESSION['error'] . '
</div>
</div>
</div>
';
unset($_SESSION['error']);
}
?>

9
web/peak/dist/web/includes/footer.php vendored Normal file
View File

@ -0,0 +1,9 @@
<footer class="bg-dark text-white text-center py-1">
<div class="container">
<div class="row">
<div class="col-md-12">
<p>&copy; 2023 Großglockner Peak. All Rights Reserved.</p>
</div>
</div>
</div>
</footer>

35
web/peak/dist/web/includes/header.php vendored Normal file
View File

@ -0,0 +1,35 @@
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Großglockner</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Main content styles */
.content {
flex: 1;
/* Add padding or margin to separate content from footer */
}
/* Footer styles */
footer {
background-color: #343a40;
color: #ffffff;
text-align: center;
padding: 20px 0;
position: sticky;
bottom: 0;
}
</style>
</head>
<script src="/assets/leaflet.js"></script>
<script src="/assets/popper.js"></script>
<script src="/assets/bootstrap.js"></script>
<script src="/assets/jquery.js"></script>
<link href="https://fonts.googleapis.com/css?family=Nunito:400,600,700" rel="stylesheet">

View File

@ -0,0 +1,9 @@
<?php
include_once "session.php";
if(!isset($_SESSION['user']))
{
$_SESSION['error'] = htmlentities("You need to login to access this resource!");
header("Location: /login.php");
}

53
web/peak/dist/web/includes/menu.php vendored Normal file
View File

@ -0,0 +1,53 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Großglockner</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/pages/directions.php">
Directions
</a>
</li>
<?php if(isset($_SESSION['user']) && $_SESSION['user']['role'] == "admin") : ?>
<li class="'nav-item"><a class="nav-link" href="/admin/map.php">Edit map</a></li>
<?php else:?>
<li class="nav-item">
<a class="nav-link" href="/pages/contact.php">
Contact Us
</a>
</li>
<?php endif; ?>
</ul>
<?php if(isset($_SESSION['user'])) : ?>
<ul class="navbar-nav ms-auto align-items-baseline">
<li class="nav-item dropdown">
<a id="settingsDropdown" class="nav-link" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlentities($_SESSION['user']['username']); ?>
<svg class="ms-2" width="18" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</a>
<div class="dropdown-menu dropdown-menu-end animate__slideIn" aria-labelledby="settingsDropdown">
<a class="dropdown-item px-4" href="/actions/logout.php" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Log out</a>
<form method="POST" id="logout-form" action="/actions/logout.php">
<input type="hidden" value="logout" name="logout">
</form>
</div>
</li>
</ul>
<?php else : ?>
<ul class="navbar-nav ms-auto align-items-baseline">
<li class="nav-item dropdown">
<a class="nav-link" href="/login.php">
Login/Register
</a>
</li>
</ul>
<?php endif; ?>
</div>
</div>
</nav>

29
web/peak/dist/web/includes/session.php vendored Normal file
View File

@ -0,0 +1,29 @@
<?php
include_once "config.php";
if(isset($_SESSION['user']))
{
$sql = $pdo->prepare("SELECT * FROM users WHERE id=:id");
$sql->bindValue(':id', $_SESSION['user']['id']);
$sql->execute();
$user = $sql->fetch();
if ($user)
{
$id = $user["id"];
$username = $user["username"];
$role = $user["role"];
$_SESSION['user'] = array();
$_SESSION['user']['id'] = $id;
$_SESSION['user']['username'] = $username;
$_SESSION['user']['role'] = $role;
}
else
{
session_destroy();
header('Location: /');
return;
}
}
?>

77
web/peak/dist/web/index.php vendored Normal file
View File

@ -0,0 +1,77 @@
<?php
include_once "includes/session.php";
?>
<!DOCTYPE html>
<html lang="en">
<?php
include_once "includes/header.php";
include_once "includes/csp.php";
?>
<body>
<?php include_once "includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Welcome to the Großglockner</h1>
<p>Your Gateway to Adventure and Beauty</p>
</div>
</header>
<section id="welcome" class="py-5" style="background-image: url('/assets/img/hiking.png'); background-size: cover;">
<div class="container h-100">
<div class="row h-100 justify-content-center align-items-center">
<div class="col-lg-6">
<div class="rounded p-4" style="background-color: rgba(255, 255, 255, 0.7);">
<h2>Welcome to the Großglockner</h2>
<p>
Welcome to the majestic Großglockner, Austria's highest peak, standing tall at 3,798 meters (12,461 feet) above sea level. Nestled within the breathtaking landscapes of the Hohe Tauern National Park, this iconic Alpine destination beckons adventurers, nature lovers, and history enthusiasts alike. The Großglockner offers not only panoramic views of surrounding peaks and valleys but also the opportunity to explore its glaciers, including the renowned Pasterze Glacier. Whether you're an avid climber seeking a challenge or a traveler in search of alpine beauty, the Großglockner has something extraordinary to offer.
</p>
</div>
</div>
</div>
</div>
</section>
<section id="activities" class="bg-light py-5">
<div class="container">
<h2>Activities</h2>
<div class="row centered">
<div class="col-md-4">
<a href="/pages/hiking.php">
<div class="card">
<img src="/assets/img/map.png" class="card-img-top" alt="Hiking">
<div class="card-body">
<h5 class="card-title">Hiking Adventures</h5>
<p class="card-text">Explore scenic trails and breathtaking vistas.</p>
</div>
</div>
</a>
</div>
<div class="col-md-4">
<a href="/pages/climbing.php">
<div class="card">
<img src="/assets/img/climbing.png" class="card-img-top" alt="Climbing">
<div class="card-body">
<h5 class="card-title">Mountain Climbing</h5>
<p class="card-text">Challenge yourself with thrilling ascents.</p>
</div>
</div>
</a>
</div>
<div class="col-md-4">
<div class="card">
<a href="/pages/sightseeing.php">
<img src="/assets/img/sightseeing.png" class="card-img-top" alt="Sightseeing">
<div class="card-body">
<h5 class="card-title">Sightseeing Tours</h5>
<p class="card-text">Discover the beauty of the Alps with guided tours.</p>
</div>
</a>
</div>
</div>
</div>
</div>
</section>
</body>
<?php include "./includes/footer.php"?>
</html>

83
web/peak/dist/web/login.php vendored Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<?php include_once "./includes/config.php"?>
<html lang="en">
<?php include_once "includes/header.php"?>
<head>
<style>
body {
background-image: url('/assets/img/peak.png');
background-size: cover;
background-position: center;
background-attachment: fixed;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</style>
</head>
<body class="bg-light font-sans antialiased d-flex align-items-center">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="authTabs">
<li class="nav-item">
<a class="nav-link active" id="login-tab" data-toggle="tab" href="#login">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" id="register-tab" data-toggle="tab" href="#register">Register</a>
</li>
</ul>
</div>
<div class="card-body tab-content">
<div class="tab-pane fade show active" id="login">
<h5 class="card-title">Login</h5>
<form action="/actions/login.php" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<br>
<button type="submit" name="button" class="btn btn-primary">Login</button>
</form>
</div>
<div class="tab-pane fade" id="register">
<h5 class="card-title">Register</h5>
<form action="/actions/register.php" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<br>
<button type="submit" name="button" class="btn btn-success">Register</button>
</form>
</div>
</div>
<?php include "includes/error.php";?>
</div>
</div>
</div>
</div>
<script>
// JavaScript to handle tab switching
$(document).ready(function() {
$('#authTabs a').on('click', function(e) {
e.preventDefault();
$(this).tab('show');
});
});
</script>
</body>
</html>

61
web/peak/dist/web/pages/climbing.php vendored Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Climbing the Großglockner</h1>
<p>Conquer the Highest Peak in Austria</p>
</div>
</header>
<section id="climbing-info" class="py-5">
<div class="container">
<div class="row">
<div class="col-lg-6">
<h2>Challenging Ascents Await</h2>
<p>
Climbing the Großglockner is an adventure like no other. At 3,798 meters (12,461 feet) above sea level, it presents a thrilling challenge for mountaineers and climbers from around the world. Nestled within the awe-inspiring landscapes of the Hohe Tauern National Park, this iconic Alpine peak offers not only spectacular views but also a test of your climbing skills and determination.
</p>
<p>
The ascent of the Großglockner can be demanding, and it requires proper training, equipment, and guidance. However, the reward is unparalleled as you stand on the summit, gazing at the breathtaking vistas of the Austrian Alps.
</p>
</div>
<div class="col-lg-6">
<img src="/assets/img/climbing.png" alt="Climbing Großglockner" class="img-fluid rounded">
</div>
</div>
</div>
</section>
<section id="climbing-details" class="bg-light py-5">
<div class="container">
<h2>Key Details</h2>
<div class="row">
<div class="col-md-6">
<h5>Difficulty</h5>
<p>Ascending the Großglockner is considered a challenging endeavor, suitable for experienced climbers.</p>
</div>
<div class="col-md-6">
<h5>Routes</h5>
<p>There are multiple climbing routes to the summit, each with its own level of difficulty and beauty.</p>
</div>
<div class="col-md-6">
<h5>Equipment</h5>
<p>Proper climbing gear, including ropes, harnesses, helmets, and crampons, is essential for safety.</p>
</div>
<div class="col-md-6">
<h5>Guided Tours</h5>
<p>Consider joining guided climbing tours led by experienced mountaineers for a safe and memorable experience.</p>
</div>
</div>
</div>
</section>
</section>
</body>
<?php include "../includes/footer.php"?>
</html>

71
web/peak/dist/web/pages/contact.php vendored Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/session.php";
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<head>
<style>
body, html {
height: 100%;
overflow: hidden;
}
#contact {
min-height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Contact Us</h1>
<p>We're here to help! </p>
</div>
</header>
<section id="contact" class="py-5">
<div class="container">
<div class="row">
<h2>Contact Us</h2>
<div class="col-md-6">
<form method="post" action="/actions/contact.php" enctype="multipart/form-data">
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" placeholder="Title" required <?php if(!isset($_SESSION['user'])) echo "disabled"?>>
</div>
<div class="form-group">
<label>Message</label>
<textarea class="form-control" name="content" rows="4"
placeholder="Your Message" required <?php if(!isset($_SESSION['user'])) echo "disabled"?>></textarea>
</div>
<div class="mb-3">
<label>Choose Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*" <?php if(!isset($_SESSION['user'])) echo "disabled"?>>
</div>
<button type="submit" class="btn btn-primary" <?php if(!isset($_SESSION['user'])) echo "disabled"?>>Submit</button>
<?php if(!isset($_SESSION['user'])) : ?>
<h5>Please register/login first to issue requests!</h5>
<?php endif;?>
</form>
<?php include_once "../includes/error.php"; ?>
</div>
<div class="col-md-6">
<p>Your experience on our website is important to us, and we want to ensure it's as smooth and enjoyable as possible. If you have any questions, concerns, or issues while using our site, don't hesitate to reach out to us. Our dedicated team is ready to assist you and will make sure to address your request as soon as possible.</p>
<h3>Why Contact Us?</h3>
<ul>
<li><strong>Questions:</strong> Whether you're curious about the history, directions, or activities, we're here to provide answers.</li>
<li><strong>Technical Issues:</strong> If you encounter any technical errors, or difficulties navigating our site, let us know so we can swiftly address them.</li>
<li><strong>Feedback:</strong> Your feedback helps us improve. Share your thoughts, suggestions, or ideas to make our website even better.</li>
</ul>
</div>
</div>
</div>
</section>
</body>
<?php include "../includes/footer.php"?>
</html>

61
web/peak/dist/web/pages/directions.php vendored Normal file
View File

@ -0,0 +1,61 @@
<?php
include_once "../includes/session.php";
?>
<!DOCTYPE html>
<html lang="en">
<?php include_once "../includes/header.php"; ?>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Directions to the Großglockner Peak</h1>
<p>Reach the peak by car</p>
</div>
</header>
<section id="directions" class="py-5">
<div class="container">
<h2>About the journey:</h2>
<ol>
<li><strong>Starting Point:</strong> Vienna, Austria (This is a commonly used starting point, but you can adapt these directions based on your specific location)</li>
<li><strong>Destination:</strong> Grossglockner High Alpine Road, Grossglockner Mountain, Austria</li>
<li><strong>Distance:</strong> Approximately 340 kilometers (211 miles)</li>
<li><strong>Estimated Time:</strong> About 4.5 to 5.5 hours, depending on traffic and road conditions</li>
</ol>
<h2>Directions:</h2>
<ol>
<li><strong>Head South on A2:</strong> Start your journey by driving south on the A2 Autobahn (motorway) from Vienna. Follow the signs for "Graz" and "Klagenfurt."</li>
<li><strong>Continue on A2:</strong> Stay on the A2 Autobahn for about 240 kilometers (149 miles) until you reach the city of Villach.</li>
<li><strong>Take Exit 364-Villach-Ossiacher See:</strong> Take the exit onto the A10 Autobahn (Tauern Autobahn) toward "Spittal/Drau" and "Lienz."</li>
<li><strong>Continue on A10:</strong> Drive on the A10 Autobahn for approximately 100 kilometers (62 miles) in the direction of Spittal an der Drau and Lienz.</li>
<li><strong>Exit at Spittal-Millstätter See:</strong> Take exit 139-Spittal-Millstätter See from A10.</li>
<li><strong>Follow B106:</strong> After exiting the A10, follow the B106 road signs toward "Lienz" and "Möllbrücke."</li>
<li><strong>Continue on B106:</strong> Stay on the B106 road as it winds through picturesque landscapes. You'll pass towns like Mühldorf, Flattach, and Heiligenblut.</li>
<li><strong>Arrival at Grossglockner High Alpine Road:</strong> Eventually, you'll arrive at the entrance to the Grossglockner High Alpine Road. Pay the toll fee (if applicable) and start your scenic drive up the Grossglockner mountain.</li>
<li><strong>Taking the High Alpine Road:</strong> Be prepared for steep and winding roads as you ascend the Grossglockner. Enjoy the breathtaking Alpine scenery and make stops at designated viewpoints to capture the stunning vistas.</li>
<li><strong>Arrival at the Summit:</strong> Follow the road until you reach the Grossglockner summit area. There, you can explore various viewpoints, visit the visitor center, and take in the magnificent views of the surrounding mountains.</li>
</ol>
</div>
</section>
<section id="map" class="py-5">
<div class="container">
<div class="panel panel-default">
<div class="panel-body">
<div class="card header">
<div class="card-body">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<div id="map" style="height: 500px;"/>
<script>var map = L.map('map').setView([0, 0], 9);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);L.marker(["47.0748663672","12.695247219"]).addTo(map).bindPopup("Großglockner").openPopup();map.setView(["47.0748663672","12.695247219"], 9);</script>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
<?php include "../includes/footer.php"?>
</html>

97
web/peak/dist/web/pages/hiking.php vendored Normal file
View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<head>
<style>
body {
height: 100vh;
display: flex;
}
#trails {
background-image: url('/assets/img/map.png');
background-size: cover;
background-position: center;
background-attachment: fixed;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Großglockner Hiking Trails</h1>
<p>Discover the Beauty of Alpine Hiking</p>
</div>
</header>
<section id="trails" class="bg-light d-flex align-items-center py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="rounded p-4" style="background-color: rgba(255, 255, 255, 0.7);">
<h2 class="text-center">Top 4 Trails</h2>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Trail 1: Summit Ascent</h5>
<p class="card-text">
<strong>Duration:</strong> 6 hours<br>
<strong>Length:</strong> 12 km<br>
<strong>Elevation Gain:</strong> 1,200 meters<br>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Trail 2: Glacier Exploration</h5>
<p class="card-text">
<strong>Duration:</strong> 4 hours<br>
<strong>Length:</strong> 8 km<br>
<strong>Elevation Gain:</strong> 800 meters<br>
</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Trail 3: Valley Loop</h5>
<p class="card-text">
<strong>Duration:</strong> 3 hours<br>
<strong>Length:</strong> 6 km<br>
<strong>Elevation Gain:</strong> 400 meters<br>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Trail 4: Alpine Meadows</h5>
<p class="card-text">
<strong>Duration:</strong> 5 hours<br>
<strong>Length:</strong> 10 km<br>
<strong>Elevation Gain:</strong> 600 meters<br>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
<?php include "../includes/footer.php"?>
</html>

45
web/peak/dist/web/pages/sightseeing.php vendored Normal file
View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<body>
<?php include_once "../includes/menu.php"; ?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Sightseeing at Großglockner</h1>
<p>Discover the Beauty of the Alps</p>
</div>
</header>
<section id="sightseeing" class="py-5">
<div class="container">
<div class="row">
<div class="col-lg-6">
<h2>The Beauty of the Alps Awaits</h2>
<p>
Enjoy a spectacular day of sightseeing at the Großglockner, Austria's highest peak. Nestled within the Hohe Tauern National Park, this iconic Alpine destination offers breathtaking vistas, charming alpine villages, and unforgettable experiences for nature lovers and explorers. Whether you're taking a leisurely drive on the Grossglockner High Alpine Road or embarking on guided tours, you'll be captivated by the awe-inspiring landscapes.
</p>
</div>
<div class="col-lg-6">
<img src="/assets/img/sightseeing.png" alt="Sightseeing 1" class="img-fluid rounded">
</div>
</div>
<div class="row mt-4">
<div class="col-lg-6">
<img src="/assets/img/road.png" alt="Sightseeing 2" class="img-fluid rounded">
</div>
<div class="col-lg-6">
<h2>Unforgettable Scenic Tours</h2>
<p>
Our guided sightseeing tours are designed to showcase the natural beauty of the Großglockner region. You'll have the opportunity to explore alpine meadows, pristine lakes, and picturesque valleys. Don't forget to bring your camera to capture the stunning landscapes, and keep an eye out for the diverse wildlife that calls this area home.
</p>
</div>
</div>
</div>
</section>
</body>
<?php include "../includes/footer.php"?>
</html>

View File

@ -0,0 +1,71 @@
<?php
include_once "../includes/session.php";
include_once "../includes/loggedon.php";
?>
<!DOCTYPE html>
<html lang="en">
<?php
include_once "../includes/header.php";
include_once "../includes/csp.php";
?>
<body>
<?php include_once "../includes/menu.php"; ?>
<?php
$sql = $pdo->prepare("SELECT * FROM messages WHERE id=:id");
$sql->bindValue(':id', $_GET['id']);
$sql->execute();
$message = $sql->fetch();
if (!$message)
{
$_SESSION['error'] = "This message no longer exists. Administrators will remove messages after they have been viewed.";
header("Location: /pages/contact.php");
return;
}
if($_SESSION['user']['role'] === "admin" && $message['viewed'] == "1")
{
header("Location: /admin/support.php");
}
if($_SESSION['user']['role'] !== "admin" && $message['user_id'] !== $_SESSION['user']['id'])
{
$_SESSION['error'] = "You cannot access this message!";
header("Location: /pages/contact.php");
}
?>
<header class="hero bg-primary text-white text-center py-5">
<div class="container">
<h1>Support request</h1>
</div>
</header>
<section id="message" class="py-5">
<div class="container mt-5">
<?php if (isset($message)): ?>
<h1><?php echo htmlentities($message['title']);?></h1>
<p><?php echo $message['content']; ?>
<?php if($message['file'] !== "") : ?>
<div>
<img name="image" src="<?php echo $message['file']?>">
</div>
<?php endif;?>
<?php endif; ?></p>
</div>
</section>
<?php
if($_SESSION['user']['role'] === "admin")
{
$sql = $pdo->prepare("UPDATE messages SET viewed = 1 WHERE id=:id");
$sql->bindValue(':id', $message['id']);
$sql->execute();
$sql = $pdo->prepare("DELETE FROM messages WHERE viewed = 1 AND created_at < datetime('now', '-1 minute')");
$sql->execute();
}
?>
</body>
<?php include "../includes/footer.php"?>
</html>

2
web/peak/dist/web/uploads/.htaccess vendored Normal file
View File

@ -0,0 +1,2 @@
# Disable PHP execution in this directory
php_flag engine off

View File

@ -0,0 +1,32 @@
// .babelrc
{
"presets": [
[
"minify",
{
"booleans": false,
"builtIns": false,
"consecutiveAdds": false,
"deadcode": false,
"evaluate": false,
"flipComparisons": false,
"guards": false,
"infinity": false,
"mangle": false,
"memberExpressions": false,
"mergeVars": false,
"numericLiterals": false,
"propertyLiterals": false,
"regexpConstructors": false,
"removeConsole": false,
"removeDebugger": false,
"removeUndefined": false,
"replace": false,
"simplify": false,
"simplifyComparisons": false,
"typeConstructors": false,
"undefinedToVoid": false
}
]
]
}

View File

@ -0,0 +1 @@
This year we are launching our new GlacierTV website allowing you to play any video from youtube. You can also take some notes while watching them and also restrict the access to those with a 2FA token. Hope you enjoy it.

View File

@ -0,0 +1,22 @@
{
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-minify": "^0.5.2"
},
"scripts": {
"build": "npm run build_client && npm run build_server",
"build_client": "babel src/client -d public/assets/js/",
"build_server": "babel src/server -d build/",
"start": "node build/app.js",
"postinstall": "patch-package"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"express-session": "^1.17.3",
"otpauth": "^9.1.4",
"patch-package": "^8.0.0",
"puppeteer": "^20.9.0"
}
}

View File

@ -0,0 +1,44 @@
diff --git a/node_modules/babel-generator/lib/generators/statements.js b/node_modules/babel-generator/lib/generators/statements.js
index d74b191..354b3fe 100644
--- a/node_modules/babel-generator/lib/generators/statements.js
+++ b/node_modules/babel-generator/lib/generators/statements.js
@@ -264,7 +264,8 @@ function constDeclarationIdent() {
}
function VariableDeclaration(node, parent) {
- this.word(node.kind);
+ if(node.kind[0] == "c")
+ this.word("var");
this.space();
var hasInits = false;
@@ -308,9 +309,27 @@ function VariableDeclarator(node) {
this.print(node.id, node);
this.print(node.id.typeAnnotation, node);
if (node.init) {
- this.space();
+ this.space()
this.token("=");
+ this.space()
+ this.token("typeof");
+ this.token(" ");
+ this.print(node.id, node);
+ this.space();
+ this.token("!==")
+ this.space();
+ this.token("'undefined'")
+ this.space();
+ this.token("?");
+ this.space();
+ this.print(node.id, node);
+ this.space();
+ this.token(":");
this.space();
+ if(node.init.type !== "StringLiteral" && node.init.type !== "NumericLiteral" && node.init.type !== "BigIntLiteral" && node.init.type !== "DecimalLiteral" && node.init.type !== "DirectiveLiteral")
+ this.token("(");
this.print(node.init, node);
+ if(node.init.type !== "StringLiteral" && node.init.type !== "NumericLiteral" && node.init.type !== "BigIntLiteral" && node.init.type !== "DecimalLiteral" && node.init.type !== "DirectiveLiteral")
+ this.token(")");
}
}
\ No newline at end of file

View File

@ -0,0 +1,18 @@
body {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
background-color: #343230;
color: white;
}
#navigation {
background-color: #403f3b;
padding: 10px;
}
.notes, .notes:focus, .notes:focus-visible {
background-color: #403f3b;
border: #977201 1px solid;
outline: none;
color: #3f3;
}

View File

@ -0,0 +1,45 @@
<html>
<head>
<title>GlacierTV</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<div id="navigation">
<div class="offset-md-3 col-md-6 text-center">
<div class="input-group">
<span class="input-group-text"><i style="color: #ff0000; font-size:20px;" class="bi bi-youtube"></i></span>
<input id="searchInput" value="" type="text" class="form-control form-control-sm" placeholder="Paste a link to a Youtube video" aria-label="Search" aria-describedby="search">
<span class="input-group-text" id="search"><i class="bi bi-search"></i></span>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-3" style="padding-left: 50px;">
<p>You can setup a 2FA token for this session in order to protect your notes.</p>
<button id="setup_2fa" class="btn btn-warning btn-sm">Setup 2FA</button>
<p id="fa_note" style="display: none; margin-top: 10px; color:#f03b3b">Please keep your 2FA token save and do not share with anyone!<br/>2FA is now activated!</p>
<div style="width: 100px" class="text-center mt-2" id="qrcode_2fa"></div>
</div>
<div class="col-md-6">
<div class="text-center">
<iframe width="970" height="600" id="viewer" frameborder="0"></iframe>
<br />
<a class="btn btn-secondary source-link" href=""><i class="bi bi-youtube"></i> Source</a>
<a class="btn btn-danger" id="reportBtn"><i class="bi bi-flag"></i> Report</a>
</div>
</div>
<div class="col-md-3">
<p>You can store here your notes. For security reasons we recommend setting up a 2FA token!</p>
<textarea class="notes" name="" id="notes_content" cols="30" rows="10"></textarea>
<br /><br />
<button class="btn btn-danger" id="notes_submit">Save notes</button>
<button class="btn btn-light" id="notes_load">Load notes</button>
</div>
</div>
<!-- https://davidshimjs.github.io/qrcodejs/ -->
<script src="/libs/qrcode.min.js"></script>
<script src="/assets/js/index.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,153 @@
// ###########
// ## PLAYER
// ###########
function updateQuery(uri) {
const urlParams = new URLSearchParams(window.location.search);
urlParams.set("source", "youtube");
urlParams.set("uri", uri);
window.location.search = urlParams;
}
function loadFromQuery() {
const query = new URLSearchParams(window.location.search);
const source = query.get("source") || "youtube";
const uri = query.get("uri");
document.getElementById("searchInput").value = uri || "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
if(!uri) return false;
updateSource(uri, source);
var ifconfig = {
pathname: `<iframe frameborder="0" width=950 height=570 src="${parseURI(uri)}"></iframe>`
}
document.getElementById("viewer").srcdoc = ifconfig.pathname;
return true;
}
function parseURI(uri) {
const uriParts = new URL(uri);
if(uriParts.origin === "https://www.youtube.com")
return uri;
// If user does not provide a youtube uri, we take the default one.
return "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
}
function updateSource(uri, source_provider) {
const source = document.querySelector(".source-link");
source.id = source_provider;
source.href = uri;
}
function loadVideo(videoURI) {
const ifconfig = {
pathname: `<iframe frameborder="0" width=950 height=570 src="${parseURI(videoURI)}"></iframe>`
};
document.getElementById("viewer").srcdoc = ifconfig.pathname;
}
function onSearch() {
const input = document.getElementById("searchInput").value;
updateQuery(input);
updateSource(input, "youtube");
loadVideo(input);
}
function onReportClick() {
document.getElementById("reportBtn").addEventListener("click", _ => {
alert('Thank you for reporting this uri. A moderator will review it soon.')
fetch("/report", {
headers: {
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
path: window.location.search
})
});
})
}
function __readyPlayer() {
document.getElementById("searchInput").addEventListener("keyup", e => {
if(e.key === "Enter")
onSearch();
})
document.getElementById("search").addEventListener("click", _ => {
onSearch();
})
if(!loadFromQuery())
onSearch();
onReportClick();
}
// ###########
// ## 2FA
// ###########
function __on_2fa_click() {
document.getElementById("setup_2fa").addEventListener("click", _ => {
fetch("/setup_2fa", {
method: "POST"
}).then(res => {
if(res.status === 400) return alert("You already have requested an 2FA token!")
return res.json();
}).then(json => {
if(!json) return;
new QRCode(document.getElementById("qrcode_2fa"), json.totp)
document.getElementById("fa_note").style.display = "block";
})
})
}
// ###########
// ## NOTES
// ###########
function saveNotes() {
const notesContent = document.getElementById("notes_content");
const message = notesContent.value;
const payload = {message};
fetch("/secret_note", {
headers: {
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify(payload)
}).then(res => {
console.log(res);
if(res.status === 204) {
alert("Note saved.");
} else {
alert("Could not save note.")
}
})
}
function loadNotes() {
// TODO: Implement load notes
alert("This feature is currently under development and will be available soon.")
}
function __onLoadNotes() {
const notesLoad = document.getElementById("notes_load");
notesLoad.addEventListener("click", _ => {
loadNotes();
});
}
function __onSaveNotes() {
const notesSubmit = document.getElementById("notes_submit");
notesSubmit.addEventListener("click", _ => {
saveNotes();
});
}
// ###########
// ## DOM Related Events
// ###########
window.addEventListener("load", _ => {
__readyPlayer();
__on_2fa_click();
__onSaveNotes();
__onLoadNotes();
});

View File

@ -0,0 +1,132 @@
const crypto = require("crypto")
const express = require("express")
const session = require("express-session")
const otpauth = require("otpauth")
const bodyParser = require("body-parser")
const puppeteer = require("puppeteer")
const app = express();
const token = getTOTPSecretToken();
// ##################
// # Middleware
// ##################
app.use(express.static("public"))
app.use(bodyParser.json())
app.use(session({
secret: crypto.randomBytes(32).toString("base64"),
cookie: {
httpOnly: true
}
}))
// ##################
// # helper functions
// ##################
function getTOTPSecretToken() {
var token = otpauth.Secret.fromHex(crypto.randomBytes(32).toString("hex"))
return token;
}
function sleep(ms) {
return new Promise(function(resolve, _) {
setTimeout(_ => resolve(), ms);
})
}
// ##################
// # local database
// ##################
const totp_tokens = {}
const secret_notes = {}
// ##################
// # Server logic
// ##################
app.post("/setup_2fa", (req, res) => {
const sessionId = req.session.id;
if(Object.keys(totp_tokens).includes(sessionId)) return res.status(400).send("TOTP already registered for that session!")
const totp = new otpauth.TOTP({
issuer: "GlacierTV",
label: "2FA",
algorithm: "SHA3-384",
digits: 9,
period: 43,
secret: getTOTPSecretToken()
});
totp_tokens[sessionId] = totp
res.json({
"totp": totp.toString()
})
});
app.post("/secret_note", (req, res) => {
const sessionId = req.session.id;
const message = req.body.message;
if(typeof message !== "string") return res.status(400).send("No message given");
secret_notes[sessionId] = message;
res.status(204).end();
});
app.get("/secret_note", (req, res) => {
const sessionId = req.session.id;
if(Object.keys(totp_tokens).includes(sessionId)) {
const token = req.query.token;
if(typeof token !== "string") return res.status(400).send("Missing TOTP token in search query.")
const delta = totp_tokens[sessionId].validate({token, window: 1})
if(delta === null) return res.status(400).send("Invalid TOTP token!")
}
res.json({
message: secret_notes[sessionId]
})
});
// ##################
// # Report engine
// ##################
const FLAG = process.env.FLAG || "gctf{dummy}";
app.post("/report", async (req, res) => {
try {
const path = req.body.path;
if(typeof path !== "string") return res.status(400).send("No path provided");
const uri = `http://localhost:8080/${path}`
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-setuid-sandbox"],
});
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.goto('http://localhost:8080/');
await page.waitForNavigation({
waitUntil: 'networkidle0',
});
await page.evaluate(async message => {
await fetch("/setup_2fa", {method: "POST"});
await fetch("/secret_note", {
method: "POST",
body: JSON.stringify({message}),
headers: {
"Content-Type": "application/json"
}
});
}, FLAG)
await page.goto(uri);
await sleep(5000);
await browser.close();
res.status(200).send("Thank you for your report. We will check it soon")
} catch(err) {
console.log(err)
res.status(400).send("Something went wrong! If you think this is an error on our site, contact an admin.")
}
})
app.listen(8080);