diff --git a/.gitignore b/.gitignore
index 03c88fd..6e83ad8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
.htaccess
+ctf-diary-generator/ctf-diary/
+ctf/
+/ctf.html
\ No newline at end of file
diff --git a/canvas.png b/canvas.png
new file mode 100644
index 0000000..6be62f8
Binary files /dev/null and b/canvas.png differ
diff --git a/ctf-diary-generator/generator.py b/ctf-diary-generator/generator.py
new file mode 100644
index 0000000..b386f47
--- /dev/null
+++ b/ctf-diary-generator/generator.py
@@ -0,0 +1,206 @@
+import yaml, os, re
+from datetime import datetime
+from markdown import markdown
+from bs4 import BeautifulSoup, Comment
+#also requires pymdown-extensions
+
+#note to self: to convert existing ctf diaries, check whether `\n([0-9a-zA-Z]+)` (or `^((?:[a-zA-Z0-9] ?)+)\n- ` for some messier writeups) looks like a chall name, then replace with `\n### $1`
+#remove all first level -s that i use for paragraph into 2 \ns
+#push all ```s to the left without spaces
+
+#TODO aggregate stats (total ctfs, total solves, solve and co-solve amt, category solves breakdown, avg chall per ctf?, avg points?, avg solve count??)
+#exclude organized events
+
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+
+alphanum = re.compile(r'[^A-Za-z0-9\- ]+')
+merge_space = re.compile(r'\s+')
+remove_formatting = lambda str: merge_space.sub('-', alphanum.sub('', str).strip()).lower() #for name formats
+
+objects = {}
+
+comments = {}
+
+#read special events
+with open(f'{CWD}/ctf-diary/special.yml', encoding='utf-8') as s:
+ objects.update(yaml.load(s, Loader=yaml.Loader))
+
+#read all ctfs in record
+skipped_cmts = []
+for file in os.listdir(f'{CWD}/ctf-diary/ctfs'):
+ if file.endswith('.yml'):
+ name = file[:-4]
+ #read metadata into objects
+ with open(f'{CWD}/ctf-diary/ctfs/{file}', encoding='utf-8') as s:
+ objects.update({name: yaml.load(s, Loader=yaml.Loader)})
+
+ #read comments; split according to headers ('### ') which should only be used by chall names, where the - is the start of comments
+ #the 3 ### should be good enough to differentiate headers from code comments, we will do sanity check when we actually map the comments anyway
+ try:
+ with open(f'{CWD}/ctf-diary/ctfs/comments/{name}.md', encoding='utf-8') as c:
+ assert c.read(4) == '### ' #the first line should already have a challenge, also discard the header
+ comments.update({name: {remove_formatting((sp:=v.split('\n', 1))[0]): sp[1] for v in c.read().split('\n### ')}})
+ except Exception as e:
+ skipped_cmts.append(name)
+ if 'challenges' in objects[name] and not all('writeup-url' in chall and chall['writeup-url'] for chall in objects[name]['challenges']): #only print if ctf should have comments
+ print(f'Cannot read comments for {file} ({type(e).__name__}), skipping...')
+
+#date is already datetime.date (thanks pyyaml)
+objects = dict(sorted(objects.items(), key=lambda item: item[1]['date']))
+
+#add year objects
+year = None
+templist = []
+for k, v in objects.items():
+ if year != v['date'].year:
+ year = v['date'].year
+ if templist: #ensure templist is not empty before inserting a year (since that always happen)
+ #no need to str(year) since .format nudge it into string anyway
+ templist.append((year, {'style': 'timeline-year', 'content': year, 'date': datetime.fromisoformat(f'{year}-01-01')}))
+
+ templist.append((k,v))
+
+#apparently printing dicts autosorts it for you but iterating is fine
+templist.reverse()
+objects = dict(templist)
+
+
+with open(f'{CWD}/templates/ctf.html', encoding='utf-8') as cf:
+ ctf = cf.read()
+with open(f'{CWD}/templates/special.html', encoding='utf-8') as sf:
+ special = sf.read()
+with open(f'{CWD}/templates/diary.html', encoding='utf-8') as df:
+ diary = df.read()
+
+
+#generation helpers
+
+added = []
+def get_comment(chall, ctf):
+ name = remove_formatting(next(iter(chall.values())))
+ if 'writeup-url' in chall and chall['writeup-url']:
+ return f'href="{chall["writeup-url"]}"'
+ elif ctf in comments and name in comments[ctf]:
+ added.append(f'{ctf}-{name}')
+ return f'data-comment="{ctf}-{name}"'
+ else:
+ if ctf not in skipped_cmts: #only print if the file is read, otherwise its just redundant
+ print(f"{ctf}-{name} has no comments, name mismatch?")
+ return ''
+
+
+ranks = {1: 'first', 2: 'second', 3: 'third'}
+def get_ordinal(n):
+ if n < 0:
+ return '
',
+ #special fields that are not actually fields
+ 'first-blood': '',
+ 'writeup-prize': '',
+}
+
+chall_decor = {'first-blood': '๐ฉธ', 'writeup-prize': '๐'}
+
+special_fields = {
+ #for now instead of bolding first item in row, just bold these 2 since i dont think ill need another name for the challenges anyway
+ #we are still assuming the name is in the first column though - commenting breaks if not
+ 'name': lambda v,c: f'
{v} {" ".join([v for k, v in chall_decor.items() if k in c])}
',
+ #special fields that are not actually fields
+ 'first-blood': lambda v,_: '',
+ 'writeup-prize': lambda v,_: '',
+}
+
+
+
+#generate the page following the template formats
+html = diary.format(
+ ctfs="".join([
+ #special
+ special.format(style=v['style'], content=v['content'])
+ if 'content' in v else
+ #actual ctfs
+ ctf.format(
+ style='timeline-organized' if v['organizer'] else '',
+ #class="headings" to have same style but not clickable if url is null, otherwise link
+ #also link is non w3c compliant hack around interactive elements inside buttons; who complies anyway :)
+ url=f'class="nav-link" onclick="window.location=\'{v["url"]}\'; event.stopPropagation()"' if v['url'] else 'class="headings"',
+ name=v['name'],
+ #isoformat is the one we want for datetime, but we only need date not time; actual format should be .
+ date=f'',
+ duration=f'{v["duration"]}h', #currently we are hardcoding hours, but we can always parse
+ type=v['type'],
+ team=v['team'],
+ rank='
Organizer
'
+ if v['organizer'] else
+ (('
' + v['rank'] + '
' if isinstance(v['rank'], str) else get_ordinal(v['rank'])) +
+ ('โจ' if v['full-clear'] else '')),
+ #use the first challenge as header definition
+ challengeheader='
'
+ + ''.join([
+ #normal fields that are named as expected so we can just use it as the headers
+ '
' + name.replace('-', ' ').capitalize() + '
'
+ if name not in special_field_names else
+ #special fields that needs renaming
+ special_field_names[name]
+ for name in v['challenges'][0].keys()])
+ + '
'
+ if 'challenges' in v else "
No specific challenges have been logged; It's all a team effort!
", #allow no challenge specified (e.g. A/D ctfs where its basically fully team effort so no specific challs that i wouldve fully solved)
+ challenges=''.join(['
'
+ #assume every chall object follows the same format as the first, or else header mismatches
+ + ''.join([
+ f'
{field}
'
+ if name not in special_fields else
+ special_fields[name](field, chall)
+ for name, field in chall.items()])
+ + '
'
+ for chall in v['challenges']])
+ if 'challenges' in v else '',
+ )
+ for k, v in objects.items()])
+)
+
+
+#lint output
+soup = BeautifulSoup(html, 'html.parser')
+
+for comment in soup.findAll(text=lambda text:isinstance(text, Comment)):
+ comment.extract()
+
+with open('ctf.html', 'w', encoding="utf-8") as out:
+ out.write(str(soup)) #minify
+
+
+#write comments into their respective files
+if not os.path.exists('ctf'):
+ os.mkdir('ctf')
+
+for ctf, challs in comments.items():
+ for name, cmt in challs.items():
+ with open(f'ctf/{ctf}-{name}.html', 'w', encoding="utf-8") as out:
+ #out.write(markdown(cmt, extensions=['fenced_code', 'codehilite'], extension_configs={'codehilite': {'noclasses': True}}))
+ out.write(markdown(cmt,
+ extensions=['pymdownx.highlight', 'pymdownx.superfences', 'pymdownx.tilde', 'pymdownx.inlinehilite', 'pymdownx.emoji', 'pymdownx.magiclink'],
+ extension_configs={
+ 'pymdownx.highlight': {
+ 'guess_lang': 'block',
+ 'pygments_lang_class': True,
+ },
+ }))
+
+ #sanity check if we missed any challs in the yml by comparing against added comments
+ if f'{ctf}-{name}' not in added:
+ print(f"{ctf}-{name} has a comment but doesn't exist in {ctf}.yml - missed definition?")
\ No newline at end of file
diff --git a/ctf-diary-generator/templates/ctf.html b/ctf-diary-generator/templates/ctf.html
new file mode 100644
index 0000000..ea017fc
--- /dev/null
+++ b/ctf-diary-generator/templates/ctf.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ {rank}
+
+
{team}
+
+
+
+
+
+
+
+ {challengeheader}
+ {challenges}
+
+
+
+
\ No newline at end of file
diff --git a/ctf-diary-generator/templates/diary.html b/ctf-diary-generator/templates/diary.html
new file mode 100644
index 0000000..c297c26
--- /dev/null
+++ b/ctf-diary-generator/templates/diary.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ desp's CTF diary
+
+
+
+
+
+
+
+
+
CTF diary
+
+ A timeline on the CTFs I've played in, challenges I've solved, and comments on the challenges.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ctfs}
+
+
+
+
+
+
+
+
+
+
+
+
Glossary
+
+
+
+ Icons next to challenge names:
+ ๐ฉธ - First blood
+ ๐ - Writeup prize won
+
+ Solve status:
+ โ๏ธ - solved during the ctf
+ ๐ค - co-solved
+ ๐ค - got sniped (solved after teammates solved during the ctf)
+ ๐ - didn't solve during the ctf, but worth mentioning (e.g. solved afterwards)
+
+ Most of these challenges are ordered chronologically by when it's solved.
+ Click on the row to see my comments* / solve scripts (if any) for the challenge!
+
+
* - The comments are usually in the form of unedited draft writeups - they might contain anything from unverified information to straight up incoherent babbles that can only be understood by me; be warned!
Heyo! I am an aspiring computer science major from Hong Kong who has been coding for fun since 2016.
As one of my major hobbies, I try to explore a variety of different technical fields, from hardware development to UI design.
However, my main focus has always been programming and system administration, with most of my time spent on doing projects such as despbot and vortex.
Recently though, I've been spending more time on cybersecurity, and I hope to be able to work on this field in the future.
Technical Skills
Java
Preferred language
C#
Alternative to java
Python
Prototyping/automation
git
Adept at common workflows
SQL
SQLite/MySQL integrations
Linux
Sysadmin on debian-based
IDA
Reversing Win32 PEs
Pentesting
Metasploit; Known-CVE scripts
Win32API
Formats and functions
C/C++
Novice knowledge for low level interactions
Arduino/Pi
Small projects e.g. cap. keypad
CSS
Theming web apps
JavaScript
Basic knowledge
HTML
Basic knowledge
Other Skills and Hobbies
Cantonese
Mother tongue
English
Most fluent second language
Mandarin
Can carry out daily conversations
Japanese
(Work in progress)
Amateur Photography
The backgrounds here are taken by me!
-
+
diff --git a/main.js b/main.js
index 8934c1d..7e00a9d 100644
--- a/main.js
+++ b/main.js
@@ -1,26 +1,48 @@
AOS.init();
-if(typeof bg !== 'undefined') document.getElementById('cover').style.backgroundImage = 'linear-gradient(to bottom, rgba(52, 58, 64, 0) 94%, rgba(52, 58, 64, 1)), url(' + bg.src + ')';
-
//initialize tooltips
$('[data-toggle="tooltip"]').tooltip({
- offset: '0, 20%'
+ offset: '0, 20%'
});
+$('#discord').bind('click touchend', function() {
+ if (navigator.clipboard.writeText("despawningbone#4078")) {
+ $(this).tooltip('hide');
+ $('#discord').attr('data-original-title', "(copied!)");
+ setTimeout(() => $('#discord').attr('data-original-title', "despawningbone#4078"), 400);
+ $(this).tooltip('show');
+ }
+});
-/*$('[data-toggle="tooltip"]').mouseenter(function(){ //tooltip patch for browsers since its shifted somehow
- if(!/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
- document.styleSheets[2].cssRules[4].style.setProperty('left', (($(this).parent().index() + 1) * 2.5) + 'px', "important");
- }
-});*/ //fixed by changing discord id from a to img wtf
+//relative data-target patch for accordions, and also to add .show back into the button
+$('body').on('click', '[data-toggle=collapse]', function (e) {
+ var $parent = $(this).parents('.ctf-info');
+ $parent.find('.ctf-table').collapse('toggle');
+ $parent.toggleClass('show')
+});
-$('#discord').bind('click touchend', function() {
- console.log(this);
- if (navigator.clipboard.writeText("despawningbone#4078")) {
- $(this).tooltip('hide');
- $('#discord').attr('data-original-title', "(copied!)");
- setTimeout(() => $('#discord').attr('data-original-title', "despawningbone#4078"), 400);
- $(this).tooltip('show');
+//ctf diary comment popup dynamically load content
+//TODO allow use of url#data-comment to load modal
+$('.ctf-table tbody tr').on('click',function(){
+ href = $(this).attr('href');
+ //if full writeup is available, redirect
+ if(href) {
+ window.location=href;
+ } else {
+ //else open comment popup
+ href = $(this).attr('data-comment');
+
+ //set comment title to chall name
+ $('#commentTitle').text('Comments - ' + this.firstElementChild.textContent)
+
+ //only load if attr is found, which means a comment should be found
+ if(href) {
+ console.log('ctf/' + href + '.html')
+ $('.ctf-comment').load('ctf/' + href + '.html', function(res, status, xhr){
+ if(status !== "error") //only show if we can load it, otherwise treat as no comment
+ $('#comment').modal({show:true});
+ });
}
+ }
});
\ No newline at end of file
diff --git a/projects.html b/projects.html
index 3c6be78..6209de2 100644
--- a/projects.html
+++ b/projects.html
@@ -1,95 +1,98 @@
+
+
- Hi! - despawningbone's portfolio
+ Hi! - desp's portfolio
-
+
Originally invited to be a plugin developer from the start of the server, I officially joined the development team not long after,
migrating it to a network. I have since maintained the Ubuntu-based infrastructure backend, the 3 websites and a discord bot along with plugin development. From intrusion detection and prevention to general sysadmin work, I've gained a lot of real world insights into how actual system administration on a cloud might feel like,
and it taught me how to code as a team, use CD/CI technologies, and a lot more that I wouldn't have expected from developing a minecraft server.
Ever since I started using Discord back in 2016, it has been my favorite communication platform; Everything just felt intuitive and simple yet customizable.
With this newfound favorite, I started digging into everything it has to offer, and I eventually stumbled upon the officially supported bots and their API. Having only started programming by then, it proved to be a great opportunity for me to learn and test my skills while making helpful utilities for my daily use.
Throughout the years, it has evolved from a single 8ball to 50+ different commands, hardcoded values to per-guild configs, and from one giant class filled with if-elses to a multithreaded framework supporting nested subcommands. It is currently semi-public as there are still a lot left to do; However, feel free to add it to any servers if you find it useful too ๐
As one of the most fasinating games I've ever played, I've spent countless hours on the technical side of Minecraft, whether it was redstone or command blocks.
Eventually, with a private feature being heavily requested on a server I played frequently back then, I decided: why not try programming it myself and give it to the server instead? With only basic C knowledge and having only created simple utilities, it proved to be a challenge, but it also was what really sparked my interest in computer science.
I created and published several more plugins in the coming years, before moving on to creating custom plugins for vortex network.