Commit 5d4a1199b5
Unsigned
Layout: unified · split
.gitignore +1
| @@ -1,3 +1,4 @@ | |||
| 1 | .build | ||
| 1 | .DS_Store | 2 | .DS_Store |
| 2 | .vscode | 3 | .vscode |
| 3 | public | 4 | public |
README.md deleted −32
| @@ -1,32 +0,0 @@ | |||
| 1 | # cleberg.net | ||
| 2 | |||
| 3 | [cleberg.net](https://cleberg.net) is my personal webpage and blog. | ||
| 4 | |||
| 5 | ## Overview | ||
| 6 | |||
| 7 | This website & blog uses Markdown for content, HTML for templates, built and | ||
| 8 | published with [Zola](https://getzola.org). | ||
| 9 | |||
| 10 | ## Configuration | ||
| 11 | |||
| 12 | All configuration options are available within the =config.toml= file. Refer to | ||
| 13 | the Zola documentation for further configuration options. | ||
| 14 | |||
| 15 | ## Building | ||
| 16 | |||
| 17 | To get running: | ||
| 18 | |||
| 19 | ```sh | ||
| 20 | git clone https://git.sr.ht/~cyborg/cleberg.net && \ | ||
| 21 | cd cleberg.net && \ | ||
| 22 | zola build | ||
| 23 | ``` | ||
| 24 | |||
| 25 | ## Publishing | ||
| 26 | |||
| 27 | Use `zola build` and deploy to whichever deployment target you prefer. | ||
| 28 | |||
| 29 | ## Tasks | ||
| 30 | |||
| 31 | Refer to the [todo page](https://todo.sr.ht/~cyborg/cleberg.net) for open | ||
| 32 | tickets or to submit a new ticket. | ||
README.org added +44
| @@ -0,0 +1,44 @@ | |||
| 1 | * cleberg.net | ||
| 2 | |||
| 3 | [[https://cleberg.net][cleberg.net]] is my personal webpage and blog. | ||
| 4 | |||
| 5 | ** Overview | ||
| 6 | |||
| 7 | This website & blog uses [[https://orgmode.org/][Org-Mode]], published with [[https://github.com/emacs-love/weblorg][weblorg]]. | ||
| 8 | |||
| 9 | ** Configuration | ||
| 10 | |||
| 11 | All configuration options are available within the =publish.el= file. Refer to | ||
| 12 | the weblorg documentation for further configuration options. | ||
| 13 | |||
| 14 | ** Building & Publishing | ||
| 15 | |||
| 16 | Local testing is available via [[https://www.gnu.org/software/emacs/][Emacs]] or through the command line. | ||
| 17 | |||
| 18 | #+begin_src sh | ||
| 19 | git clone https://git.cleberg.net/cleberg.net.git && \ | ||
| 20 | cd cleberg.net && \ | ||
| 21 | emacs -nw | ||
| 22 | #+end_src | ||
| 23 | |||
| 24 | Within Emacs, open any of the repository files. In Doom, I do this with =Spc f | ||
| 25 | f= and selecting =README.org=. Make any changes necessary to customize the | ||
| 26 | project. | ||
| 27 | |||
| 28 | To publish, you can use the =build.sh= script (change the deployment target!) or | ||
| 29 | you can run the following commands. | ||
| 30 | |||
| 31 | Use the =ENV= environment variable to determine which base URL weblorg will use. | ||
| 32 | If ENV is ommitted, it will default to =localhost:8000=. If =ENV=prod=, weblorg | ||
| 33 | will look in the =publish.el= file for the production base URL. | ||
| 34 | |||
| 35 | #+begin_src sh | ||
| 36 | ENV=prod emacs --script publish.el | ||
| 37 | #+end_src | ||
| 38 | |||
| 39 | The files will be published to the =.build= directory. You can deploy these | ||
| 40 | files to the target through any number of methods, such as =scp= or SFTP. | ||
| 41 | |||
| 42 | ** Tasks | ||
| 43 | |||
| 44 | Refer to the [[https://todo.sr.ht/~cyborg/cleberg.net][todo page]] for open tickets or to submit a new ticket. | ||
config.toml deleted −21
| @@ -1,21 +0,0 @@ | |||
| 1 | # Site settings | ||
| 2 | base_url = "https://cleberg.net" | ||
| 3 | title = "cleberg.net" | ||
| 4 | description = "The personal website of Christian Cleberg." | ||
| 5 | author = "Christian Cleberg" | ||
| 6 | |||
| 7 | # RSS settings | ||
| 8 | generate_feeds = true | ||
| 9 | feed_filenames = ["atom.xml"] | ||
| 10 | |||
| 11 | [markdown] | ||
| 12 | highlight_code = true | ||
| 13 | highlight_theme = "css" | ||
| 14 | highlight_themes_css = [ | ||
| 15 | { theme = "ayu-dark", filename = "syntax-theme-dark.css" }, | ||
| 16 | { theme = "inspired-github", filename = "syntax-theme-light.css" }, | ||
| 17 | ] | ||
| 18 | external_links_target_blank = true | ||
| 19 | lazy_async_image = true | ||
| 20 | |||
| 21 | [extra] | ||
content/blog/#2022-03-24-server-hardening.org# added +293
| @@ -0,0 +1,293 @@ | |||
| 1 | #+date: <2022-03-24> | ||
| 2 | #+title: Hardening a Public-Facing Home Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Post Updates | ||
| 7 | |||
| 8 | #+begin_quote | ||
| 9 | After reviewing this post today (2022-10-04), I noticed quite a few gaps in my | ||
| 10 | write-up and wanted to add a few things, even though this blog is really just a | ||
| 11 | retrospective and knowledge dump for myself. I left things intact and simply | ||
| 12 | crossed them out (+like this+) for posterity. | ||
| 13 | #+end_quote | ||
| 14 | |||
| 15 | * Planning Data Flows & Security | ||
| 16 | |||
| 17 | ** My Personal Data Flow | ||
| 18 | |||
| 19 | #+begin_src | ||
| 20 | ┌───────┐ ┌─────────────────┐ | ||
| 21 | ┌──► VLAN1 ├───► Private Devices │ | ||
| 22 | │ └───────┘ └─────────────────┘ | ||
| 23 | ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ | ||
| 24 | │ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤ | ||
| 25 | └──────────┘ └────────┘ └──────────┘ └────────┘ │ | ||
| 26 | │ ┌───────┐ ┌───────────────┐ | ||
| 27 | └──► VLAN2 ├───► Public Server │ | ||
| 28 | └───────┘ └───────────────┘ | ||
| 29 | #+end_src | ||
| 30 | |||
| 31 | ** Thought Process | ||
| 32 | |||
| 33 | To serve content from your home server and harden your security posture, you | ||
| 34 | have to think about the transport of data from =server= to =client=. | ||
| 35 | |||
| 36 | Let's start with the actual server itself. Think about the following: | ||
| 37 | |||
| 38 | - Do I have a firewall enabled? Do I need to update this to allow new ports or | ||
| 39 | IPs? | ||
| 40 | - Do I have an IPS/IDS that may prevent outside traffic? | ||
| 41 | - Do I have any other security software installed? | ||
| 42 | - Are the services hosted inside Docker containers, behind a reverse proxy, or | ||
| 43 | virtualized? If so, are they configured to allow outside traffic? | ||
| 44 | |||
| 45 | Once the data leaves the server, where does it go? In my case, it goes to a | ||
| 46 | managed switch. In this case, I asked the following: | ||
| 47 | |||
| 48 | - What configurations is the switch using? | ||
| 49 | - Am I using VLANs? | ||
| 50 | - Yes, I am using 802.1Q VLANs. | ||
| 51 | - Are the VLANs configured properly? | ||
| 52 | - Yes, as shown in the Switch section below, I have a separate VLAN to allow | ||
| 53 | outside traffic to and from the server alone. No other devices, except for a | ||
| 54 | service port, and in that VLAN. | ||
| 55 | |||
| 56 | At this point, the data has been processed through the switch. Where does it go | ||
| 57 | next? In my case, it's pretty simple: it goes to the router/modem device. | ||
| 58 | |||
| 59 | - Does my ISP block any ports that I need? | ||
| 60 | - This is an important step that a lot of people run into when self-hosting at | ||
| 61 | home. Use an online port-checker tool for your IP or call your ISP if you | ||
| 62 | think ports are blocked. | ||
| 63 | - Is there a router firewall? | ||
| 64 | - Yes, I checked that it's configured to allow the ports I need to run my | ||
| 65 | services publicly. Common web servers and reverse proxies require ports 80 | ||
| 66 | and 443, but other services like media servers or games can require unique | ||
| 67 | ports, so be sure to check the documentation for your service(s). | ||
| 68 | - Are there any other settings affecting inbound/outbound traffic? | ||
| 69 | - Schedules or access blocks | ||
| 70 | - Static Routing | ||
| 71 | - QoS | ||
| 72 | - Port Forwarding | ||
| 73 | - DMZ Hosting | ||
| 74 | - Remote Management (this can sometimes mess with services that also require | ||
| 75 | the use of ports 80 and 443) | ||
| 76 | |||
| 77 | Once the data leaves my router, it goes to the upstream ISP and can be accessed | ||
| 78 | publicly. | ||
| 79 | |||
| 80 | *** Server | ||
| 81 | |||
| 82 | The services I run on my server are installed straight into the OS, without any | ||
| 83 | use of Docker or VMs, so I don't need any extra application configuration to | ||
| 84 | make them accessible to the outside world.+ | ||
| 85 | |||
| 86 | As of 2022-10-04, the paragraph above is no longer true as I now run a reverse | ||
| 87 | proxy with Nginx and host many services inside Docker. However, it doesn't | ||
| 88 | change anything regarding this post as I still just need to open ports 80 & 443 | ||
| 89 | and create the necessary website configuration files. | ||
| 90 | |||
| 91 | When creating new services - either installed directly on bare metal or within | ||
| 92 | something like Docker - I ensure that I read through the documentation | ||
| 93 | thoroughly to understand a few key things: - What network activities should this | ||
| 94 | app perform (if any)? Using which ports and protocols? - Does this app require | ||
| 95 | any commands/services to be run as =root=? - Does this app log errors, | ||
| 96 | authentication failures/successes, or anything else that would be useful for an | ||
| 97 | investigation? | ||
| 98 | |||
| 99 | For extra security, I use limit all incoming connections to SSH connections | ||
| 100 | through my server firewall (=ufw=) and disable common SSH settings. After all of | ||
| 101 | that, I use =fail2ban= as a preventative measure against brute-force login | ||
| 102 | attempts. | ||
| 103 | |||
| 104 | As another piece of security, you can randomize your SSH port to ensure that | ||
| 105 | random scanners or attackers can't easily try to force their way into your | ||
| 106 | network. For example, you can edit the port rules in your server to block all | ||
| 107 | connection requests to port =22= but forward all remote connections from port | ||
| 108 | =12345= to your server's port =22=. Then you just need to SSH to your network | ||
| 109 | via your randomized port. | ||
| 110 | |||
| 111 | ** =ufw= | ||
| 112 | |||
| 113 | To see how to configure =ufw=, see my other post: [[/blog/ufw.html][Secure Your | ||
| 114 | Network with the Uncomplicated Firewall]]. | ||
| 115 | |||
| 116 | The general notion with an on-device firewall is that you want to deny all | ||
| 117 | incoming connections by default and then selectively open certain ports for | ||
| 118 | services or users that you know need access. | ||
| 119 | |||
| 120 | If you know that you will only be logging into this server from a certain set or | ||
| 121 | list of IPs, you can always set the firewall to only allow connections to port | ||
| 122 | 22 from those IPs. | ||
| 123 | |||
| 124 | For a quick start to only allow SSH connections to the server, use this: | ||
| 125 | |||
| 126 | #+begin_src sh | ||
| 127 | sudo ufw default deny incoming | ||
| 128 | sudo ufw default allow outgoing | ||
| 129 | sudo ufw allow 22 | ||
| 130 | sudo ufw enable | ||
| 131 | #+end_src | ||
| 132 | |||
| 133 | ** =ssh= | ||
| 134 | |||
| 135 | 1. Using SSH Keys | ||
| 136 | |||
| 137 | First, make sure you have an SSH keypair generated on the device(s) that | ||
| 138 | you'll be using to log in to the server. If you don't have an SSH key, run | ||
| 139 | this command: | ||
| 140 | |||
| 141 | #+begin_src sh | ||
| 142 | ssh-keygen | ||
| 143 | #+end_src | ||
| 144 | |||
| 145 | Now that we have an SSH key, copy it to the server with the following | ||
| 146 | command, which will ask for the user's password before accepting the key: | ||
| 147 | |||
| 148 | #+begin_src sh | ||
| 149 | ssh-copy-id my_user@my_server | ||
| 150 | #+end_src | ||
| 151 | |||
| 152 | If you have multiple keys, you'll need to specify which to use. After it's | ||
| 153 | complete, =ssh= back into the server as that user and make sure it doesn't | ||
| 154 | ask for a password. | ||
| 155 | |||
| 156 | 2. Disable Password & Root Authentication | ||
| 157 | |||
| 158 | Now that we can access the server without a password, we will disable | ||
| 159 | password authentication and disable anyone from using =ssh= to login as | ||
| 160 | =root=. | ||
| 161 | |||
| 162 | To do this, open the =sshd_config= file: | ||
| 163 | |||
| 164 | #+begin_src sh | ||
| 165 | sudo nano /etc/ssh/sshd_config | ||
| 166 | #+end_src | ||
| 167 | |||
| 168 | You'll need to update the parameters to the values below. If one of these | ||
| 169 | rules is commented-out or doesn't exist, create the rule at the bottom of the | ||
| 170 | file. | ||
| 171 | |||
| 172 | #+begin_src config | ||
| 173 | PermitRootLogin no | ||
| 174 | PasswordAuthentication no | ||
| 175 | PubkeyAuthentication yes | ||
| 176 | #+end_src | ||
| 177 | |||
| 178 | Finally, restart the =ssh= service: | ||
| 179 | |||
| 180 | #+begin_src sh | ||
| 181 | sudo systemctl restart sshd.service | ||
| 182 | #+end_src | ||
| 183 | |||
| 184 | To test that everything's working so far, open ANOTHER terminal and try | ||
| 185 | logging in as =root= over SSH. It is very important that you keep your | ||
| 186 | current SSH session open and test with an additional session, or you will | ||
| 187 | lock yourself out at some point and will need to use a recovery method (e.g., | ||
| 188 | hooking monitor up to home server) to get yourself back in. | ||
| 189 | |||
| 190 | 3. Enable MFA for =ssh= | ||
| 191 | |||
| 192 | This part is optional, but I highly recommend it. So far, we've ensured that | ||
| 193 | no one can log into our user on the server without using our secret key, and | ||
| 194 | we've ensured that no one can log in remotely as =root=. Next, you can enable | ||
| 195 | MFA authentication for =ssh= connections. | ||
| 196 | |||
| 197 | This process involves editing a couple files and installing an MFA package, | ||
| 198 | so I will not include all the details in this post. To see how to configure | ||
| 199 | MFA for =ssh=, see my other post: [[/blog/ssh-mfa.html][Enabling MFA for | ||
| 200 | SSH]]. | ||
| 201 | |||
| 202 | ** =fail2ban= | ||
| 203 | |||
| 204 | I haven't written a post on how I use =fail2ban=, but it's quite simple. I use | ||
| 205 | the default =sshd= jail, but you can always create new jails for respective | ||
| 206 | applications or ports. For example, if you use Nginx as your web server, you can | ||
| 207 | use the =nginx-http-auth= jail. | ||
| 208 | |||
| 209 | In order to get it up and running, use the following commands: | ||
| 210 | |||
| 211 | #+begin_src sh | ||
| 212 | sudo apt install fail2ban | ||
| 213 | sudo fail2ban-client start sshd | ||
| 214 | sudo fail2ban-client status sshd | ||
| 215 | #+end_src | ||
| 216 | |||
| 217 | This should be used as a last-resort defense and shouldn't be a replacement for | ||
| 218 | the security measures mentioned above. | ||
| 219 | |||
| 220 | * Switch | ||
| 221 | |||
| 222 | Between the router and any local devices is my managed switch, which is used to | ||
| 223 | create VLANs. The example below shows how I would isolate the VLANs if I were | ||
| 224 | starting to host a single service at home. | ||
| 225 | |||
| 226 | ** 802.1Q VLAN Configuration | ||
| 227 | |||
| 228 | In this configuration, port 8 is the public server that needs to be accessed | ||
| 229 | from the outside. Port 23 is my 'dedicated service port' for this server. In | ||
| 230 | order to SSH to this server, I need to plug my laptop into port 23 or else I | ||
| 231 | cannot SSH. Otherwise, I'd need to hook up a monitor and keyboard directly to | ||
| 232 | the server to manage it. | ||
| 233 | |||
| 234 | #+begin_verse | ||
| 235 | #+end_verse | ||
| 236 | |||
| 237 | | VLAN ID | VLAN Name | Member Ports | Tagged Ports | Untagged Ports | | ||
| 238 | |---------+-----------+--------------+--------------+----------------| | ||
| 239 | | 1 | Default | 1-24 | 1-24 | | | ||
| 240 | | 2 | Server | 1,8,23 | 1,8,23 | | | ||
| 241 | |||
| 242 | ** 802.1Q VLAN PVID Setting | ||
| 243 | |||
| 244 | Once the VLAN is created, I simply add the =VLAN ID= of =2= as the =PVID= for | ||
| 245 | any related ports (in this case, see that ports =8= and =23= have a PVID of | ||
| 246 | =2=). | ||
| 247 | |||
| 248 | | Port | PVID | | ||
| 249 | |------+------| | ||
| 250 | | 1 | 1 | | ||
| 251 | | 2 | 1 | | ||
| 252 | | 3 | 1 | | ||
| 253 | | 4 | 1 | | ||
| 254 | | 5 | 1 | | ||
| 255 | | 6 | 1 | | ||
| 256 | | 7 | 1 | | ||
| 257 | | 8 | 2 | | ||
| 258 | | 9 | 1 | | ||
| 259 | | 10 | 1 | | ||
| 260 | | 11 | 1 | | ||
| 261 | | 12 | 1 | | ||
| 262 | | 13 | 1 | | ||
| 263 | | 14 | 1 | | ||
| 264 | | 15 | 1 | | ||
| 265 | | 16 | 1 | | ||
| 266 | | 17 | 1 | | ||
| 267 | | 18 | 1 | | ||
| 268 | | 19 | 1 | | ||
| 269 | | 20 | 1 | | ||
| 270 | | 21 | 1 | | ||
| 271 | | 22 | 1 | | ||
| 272 | | 23 | 2 | | ||
| 273 | | 24 | 1 | | ||
| 274 | |||
| 275 | * Router | ||
| 276 | |||
| 277 | On my router, the configuration was as easy as opening the firewall settings and | ||
| 278 | unblocking the ports I needed for my services (e.g., HTTP/S, Plex, SSH, MySQL, | ||
| 279 | etc.). | ||
| 280 | |||
| 281 | Since I'm relying on an ISP-provided modem/router combo for now (not by | ||
| 282 | choice), I do not use any other advanced settings on my router that would | ||
| 283 | inhibit any valid traffic to these services. | ||
| 284 | |||
| 285 | The paragraph above regarding the ISP-owned router is no longer accurate as I | ||
| 286 | now use the Ubiquiti Unifi Dream Machine Pro as my router. Within this router, I | ||
| 287 | enabled port forwarding/firewall rules, segregate the network based on the | ||
| 288 | device, and enable traffic restrictions (e.g., silently drop traffic from | ||
| 289 | certain countries and threat categories). | ||
| 290 | |||
| 291 | If you have the option with your ISP, I recommend using a personal router with | ||
| 292 | software that you are familiar with so that you can explore all the options | ||
| 293 | available to you. | ||
content/blog/2018-11-28-aes-encryption.md deleted −118
| @@ -1,118 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2018-11-28 | ||
| 3 | title = "AES Encryption" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Basic AES | ||
| 9 | |||
| 10 | If you're not familiar with encryption techniques, | ||
| 11 | [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) is the | ||
| 12 | **Advanced Encryption Standard**. This specification was established by the | ||
| 13 | National Institute of Standards and Technology, sub-selected from the Rijndael | ||
| 14 | family of ciphers (128, 192, and 256 bits) in 2001. Furthering its popularity | ||
| 15 | and status, the US government chose AES as their default encryption method for | ||
| 16 | top-secret data, removing the previous standard which had been in place since 1977. | ||
| 17 | |||
| 18 | AES has proven to be an extremely safe encryption method, with 7-round and | ||
| 19 | 8-round attacks making no material improvements since the release of this | ||
| 20 | encryption standard almost two decades ago. | ||
| 21 | |||
| 22 | > Though many papers have been published on the cryptanalysis of AES, the | ||
| 23 | > fastest single-key attacks on round-reduced AES variants [20, 33] so far are | ||
| 24 | > only slightly more powerful than those proposed 10 years ago [23,24]. | ||
| 25 | > | ||
| 26 | > - [Bogdonav, et | ||
| 27 | > al.](http://research.microsoft.com/en-us/projects/cryptanalysis/aesbc.pdf) | ||
| 28 | |||
| 29 | # How Secure is AES? | ||
| 30 | |||
| 31 | In theory, AES-256 is non-crackable due to the massive number of combinations | ||
| 32 | that can be produced. However, AES-128 is no longer recommended as a viable | ||
| 33 | implementation to protect important data. | ||
| 34 | |||
| 35 | A semi-short [comic | ||
| 36 | strip](http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html) | ||
| 37 | from Moserware quickly explains AES for the public to understand. Basically AES | ||
| 38 | encrypts the data by obscuring the relationship between the data and the | ||
| 39 | encrypted data. Additionally, this method spreads the message out. Lastly, the | ||
| 40 | key produced by AES is the secret to decrypting it. Someone may know the method | ||
| 41 | of AES, but without the key, they are powerless. | ||
| 42 | |||
| 43 | To obscure and spread the data out, AES creates a substitution-permutation | ||
| 44 | network. Wikipedia has a wonderful [example of an SP | ||
| 45 | network](https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/SubstitutionPermutationNetwork2.png/468px-SubstitutionPermutationNetwork2.png) | ||
| 46 | available. This network sends the data through a set of S boxes (using the | ||
| 47 | unique key) to substitute the bits with another block of bits. Then, a P box | ||
| 48 | will permutate, or rearrange, the bits. This is done over and over, with the key | ||
| 49 | being derived from the last round. For AES, the key size specifies the number of | ||
| 50 | transformation rounds: 10, 12, and 14 rounds for 128-bit, 192-bit, and 256-bit | ||
| 51 | keys, respectively. | ||
| 52 | |||
| 53 | # The Process | ||
| 54 | |||
| 55 | 1. **KeyExpansion**: Using [Rijndael's key | ||
| 56 | schedule](https://en.m.wikipedia.org/wiki/Advanced_Encryption_Standard), the | ||
| 57 | keys are dynamically generated. | ||
| 58 | 2. **AddRoundKey**: Each byte of the data is combined with this key using | ||
| 59 | bitwise xor. | ||
| 60 | 3. **SubBytes**: This is followed by the substitution of each byte of data. | ||
| 61 | 4. **ShiftRows**: Then, the final three rows are shifted a certain number of | ||
| 62 | steps, dictated by the cipher. | ||
| 63 | 5. **MixColumns**: After the rows have been shifted, the columns are mixed and | ||
| 64 | combined. | ||
| 65 | |||
| 66 | This process does not necessarily stop after one full round. Steps 2 through 5 | ||
| 67 | will repeat for the number of rounds specified by the key. However, the final | ||
| 68 | round excludes the MixColumns step. As you can see, this is a fairly complex | ||
| 69 | process. One must have a solid understanding of general mathematic principles to | ||
| 70 | fully understand how the sequence works (and to even attempt to find a | ||
| 71 | weakness). | ||
| 72 | |||
| 73 | According to research done by Bogdanov et al., it would take billions of years | ||
| 74 | to brute force a 126-bit key with current hardware. Additionally, this brute | ||
| 75 | force attack would require storing 2^88^ bits of data! However, there are a few | ||
| 76 | different attacks that have been used to show vulnerabilities with the use of | ||
| 77 | this technology. Side-channel attacks use inadvertent leaks of data from the | ||
| 78 | hardware or software, which can allow attackers to obtain the key or run | ||
| 79 | programs on a user's hardware. | ||
| 80 | |||
| 81 | Please note that this is not something you should run out and try to implement | ||
| 82 | in your `Hello, World!` app after only a few hours of research. While AES | ||
| 83 | (basically all encryption methods) is extremely efficient in what it does, it | ||
| 84 | takes a lot of time and patience to understand. If you're looking for something | ||
| 85 | which currently implements AES, check out the [Legion of the Bouncy | ||
| 86 | Castle](https://www.bouncycastle.org/documentation.html) for Java | ||
| 87 | implementations of cryptographic algorithms. | ||
| 88 | |||
| 89 | # Why Does Encryption Matter? | ||
| 90 | |||
| 91 | There are limitless reasons to enable encryption at-rest or in-transit for | ||
| 92 | various aspects of your digital life. You can research specific examples, such | ||
| 93 | as [Australia passes new law to thwart strong | ||
| 94 | encryption](https://arstechnica.com/tech-policy/2018/12/australia-passes-new-law-to-thwart-strong-encryption/). | ||
| 95 | However, I will simply list a few basic reasons to always enable encryption, | ||
| 96 | where feasible: | ||
| 97 | |||
| 98 | 1. Privacy is a human right and is recognized as a national right in some | ||
| 99 | countries (e.g., [US Fourth | ||
| 100 | Amendment](https://www.law.cornell.edu/wex/fourth_amendment)). | ||
| 101 | 2. "Why not?" Encryption rarely affects performance or speed, so there's usually | ||
| 102 | not a reason to avoid it in the first place. | ||
| 103 | 3. Your digital identity and activity (texts, emails, phone calls, online | ||
| 104 | accounts, etc.) are extremely valuable and can result in terrible | ||
| 105 | consequences, such as identity theft, if leaked to other parties. Encrypting | ||
| 106 | this data prevents such leaks from ruining lives. | ||
| 107 | 4. Wiping or factory-resetting does not actually wipe all data from the storage | ||
| 108 | device. There are methods to read data from the physical disks/boards inside | ||
| 109 | devices. | ||
| 110 | 5. Corporations, governments, and other nefarious groups/individuals are | ||
| 111 | actively looking for ways to collect personal information about anyone they | ||
| 112 | can. If someone's data is unencrypted, that person may become a target due to | ||
| 113 | the ease of data collection. | ||
| 114 | |||
| 115 | **Read More:** | ||
| 116 | |||
| 117 | - [Federal Information Processing Standards Publication | ||
| 118 | 197](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf) | ||
content/blog/2018-11-28-aes-encryption.org added +124
| @@ -0,0 +1,124 @@ | |||
| 1 | #+title: AES Encryption | ||
| 2 | #+date: <2018-11-28> | ||
| 3 | #+description: A beginner's guide to AES encryption. | ||
| 4 | |||
| 5 | * Basic AES | ||
| 6 | |||
| 7 | If you're not familiar with encryption techniques, | ||
| 8 | [[https://en.wikipedia.org/wiki/Advanced_Encryption_Standard][AES]] is | ||
| 9 | the *Advanced Encryption Standard*. This specification was established | ||
| 10 | by the National Institute of Standards and Technology, sub-selected from | ||
| 11 | the Rijndael family of ciphers (128, 192, and 256 bits) in 2001. | ||
| 12 | Furthering its popularity and status, the US government chose AES as | ||
| 13 | their default encryption method for top-secret data, removing the | ||
| 14 | previous standard which had been in place since 1977. | ||
| 15 | |||
| 16 | AES has proven to be an extremely safe encryption method, with 7-round | ||
| 17 | and 8-round attacks making no material improvements since the release of | ||
| 18 | this encryption standard almost two decades ago. | ||
| 19 | |||
| 20 | #+begin_quote | ||
| 21 | Though many papers have been published on the cryptanalysis of AES, the | ||
| 22 | fastest single-key attacks on round-reduced AES variants [20, 33] so far | ||
| 23 | are only slightly more powerful than those proposed 10 years ago | ||
| 24 | [23,24]. | ||
| 25 | |||
| 26 | - [[http://research.microsoft.com/en-us/projects/cryptanalysis/aesbc.pdf][Bogdonav, | ||
| 27 | et al.]] | ||
| 28 | #+end_quote | ||
| 29 | |||
| 30 | * How Secure is AES? | ||
| 31 | |||
| 32 | In theory, AES-256 is non-crackable due to the massive number of | ||
| 33 | combinations that can be produced. However, AES-128 is no longer | ||
| 34 | recommended as a viable implementation to protect important data. | ||
| 35 | |||
| 36 | A semi-short | ||
| 37 | [[http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html][comic | ||
| 38 | strip]] from Moserware quickly explains AES for the public to | ||
| 39 | understand. Basically AES encrypts the data by obscuring the | ||
| 40 | relationship between the data and the encrypted data. Additionally, this | ||
| 41 | method spreads the message out. Lastly, the key produced by AES is the | ||
| 42 | secret to decrypting it. Someone may know the method of AES, but without | ||
| 43 | the key, they are powerless. | ||
| 44 | |||
| 45 | To obscure and spread the data out, AES creates a | ||
| 46 | substitution-permutation network. Wikipedia has a wonderful | ||
| 47 | [[https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/SubstitutionPermutationNetwork2.png/468px-SubstitutionPermutationNetwork2.png][example | ||
| 48 | of an SP network]] available. This network sends the data through a set | ||
| 49 | of S boxes (using the unique key) to substitute the bits with another | ||
| 50 | block of bits. Then, a P box will permutate, or rearrange, the bits. | ||
| 51 | This is done over and over, with the key being derived from the last | ||
| 52 | round. For AES, the key size specifies the number of transformation | ||
| 53 | rounds: 10, 12, and 14 rounds for 128-bit, 192-bit, and 256-bit keys, | ||
| 54 | respectively. | ||
| 55 | |||
| 56 | * The Process | ||
| 57 | |||
| 58 | 1. *KeyExpansion*: Using | ||
| 59 | [[https://en.m.wikipedia.org/wiki/Advanced_Encryption_Standard][Rijndael's | ||
| 60 | key schedule]], the keys are dynamically generated. | ||
| 61 | 2. *AddRoundKey*: Each byte of the data is combined with this key using | ||
| 62 | bitwise xor. | ||
| 63 | 3. *SubBytes*: This is followed by the substitution of each byte of | ||
| 64 | data. | ||
| 65 | 4. *ShiftRows*: Then, the final three rows are shifted a certain number | ||
| 66 | of steps, dictated by the cipher. | ||
| 67 | 5. *MixColumns*: After the rows have been shifted, the columns are mixed | ||
| 68 | and combined. | ||
| 69 | |||
| 70 | This process does not necessarily stop after one full round. Steps 2 | ||
| 71 | through 5 will repeat for the number of rounds specified by the key. | ||
| 72 | However, the final round excludes the MixColumns step. As you can see, | ||
| 73 | this is a fairly complex process. One must have a solid understanding of | ||
| 74 | general mathematic principles to fully understand how the sequence works | ||
| 75 | (and to even attempt to find a weakness). | ||
| 76 | |||
| 77 | According to research done by Bogdanov et al., it would take billions of | ||
| 78 | years to brute force a 126-bit key with current hardware. Additionally, | ||
| 79 | this brute force attack would require storing 2^{88} bits of data! | ||
| 80 | However, there are a few different attacks that have been used to show | ||
| 81 | vulnerabilities with the use of this technology. Side-channel attacks | ||
| 82 | use inadvertent leaks of data from the hardware or software, which can | ||
| 83 | allow attackers to obtain the key or run programs on a user's hardware. | ||
| 84 | |||
| 85 | Please note that this is not something you should run out and try to | ||
| 86 | implement in your =Hello, World!= app after only a few hours of | ||
| 87 | research. While AES (basically all encryption methods) is extremely | ||
| 88 | efficient in what it does, it takes a lot of time and patience to | ||
| 89 | understand. If you're looking for something which currently implements | ||
| 90 | AES, check out the | ||
| 91 | [[https://www.bouncycastle.org/documentation.html][Legion of the Bouncy | ||
| 92 | Castle]] for Java implementations of cryptographic algorithms. | ||
| 93 | |||
| 94 | * Why Does Encryption Matter? | ||
| 95 | |||
| 96 | There are limitless reasons to enable encryption at-rest or in-transit | ||
| 97 | for various aspects of your digital life. You can research specific | ||
| 98 | examples, such as | ||
| 99 | [[https://arstechnica.com/tech-policy/2018/12/australia-passes-new-law-to-thwart-strong-encryption/][Australia | ||
| 100 | passes new law to thwart strong encryption]]. However, I will simply | ||
| 101 | list a few basic reasons to always enable encryption, where feasible: | ||
| 102 | |||
| 103 | 1. Privacy is a human right and is recognized as a national right in | ||
| 104 | some countries (e.g., | ||
| 105 | [[https://www.law.cornell.edu/wex/fourth_amendment][US Fourth | ||
| 106 | Amendment]]). | ||
| 107 | 2. "Why not?" Encryption rarely affects performance or speed, so there's | ||
| 108 | usually not a reason to avoid it in the first place. | ||
| 109 | 3. Your digital identity and activity (texts, emails, phone calls, | ||
| 110 | online accounts, etc.) are extremely valuable and can result in | ||
| 111 | terrible consequences, such as identity theft, if leaked to other | ||
| 112 | parties. Encrypting this data prevents such leaks from ruining lives. | ||
| 113 | 4. Wiping or factory-resetting does not actually wipe all data from the | ||
| 114 | storage device. There are methods to read data from the physical | ||
| 115 | disks/boards inside devices. | ||
| 116 | 5. Corporations, governments, and other nefarious groups/individuals are | ||
| 117 | actively looking for ways to collect personal information about | ||
| 118 | anyone they can. If someone's data is unencrypted, that person may | ||
| 119 | become a target due to the ease of data collection. | ||
| 120 | |||
| 121 | *Read More:* | ||
| 122 | |||
| 123 | - [[http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf][Federal | ||
| 124 | Information Processing Standards Publication 197]] | ||
content/blog/2018-11-28-cpp-compiler.md deleted −136
| @@ -1,136 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2018-11-28 | ||
| 3 | title = "The C++ Compiler" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # A Brief Introduction | ||
| 9 | |||
| 10 | [C++](https://en.wikipedia.org/wiki/C%2B%2B) is a general-purpose programming | ||
| 11 | language with object-oriented, generic, and functional features in addition to | ||
| 12 | facilities for low-level memory manipulation. | ||
| 13 | |||
| 14 | The source code, shown in the snippet below, must be compiled before it can be | ||
| 15 | executed. There are many steps and intricacies to the compilation process, and | ||
| 16 | this post was a personal exercise to learn and remember as much information as I | ||
| 17 | can. | ||
| 18 | |||
| 19 | ```cpp | ||
| 20 | #include <iostream> | ||
| 21 | |||
| 22 | int main() | ||
| 23 | { | ||
| 24 | std::cout << "Hello, world!\n"; | ||
| 25 | } | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Compilation Process | ||
| 29 | |||
| 30 | ### An Overview | ||
| 31 | |||
| 32 | Compiling C++ projects is a frustrating task most days. Seemingly nonexistent | ||
| 33 | errors keeping your program from successfully compiling can be annoying | ||
| 34 | (especially since you know you wrote it perfectly the first time, right?). | ||
| 35 | |||
| 36 | I'm learning more and more about C++ these days and decided to write this | ||
| 37 | concept down so that I can cement it even further in my own head. However, C++ | ||
| 38 | is not the only compiled language. Check out [the Wikipedia entry for compiled | ||
| 39 | languages](https://en.wikipedia.org/wiki/Compiled_language) for more examples of | ||
| 40 | compiled languages. | ||
| 41 | |||
| 42 | I'll start with a wonderful, graphical way to conceptualize the C++ compiler. | ||
| 43 | View [The C++ Compilation | ||
| 44 | Process](https://web.archive.org/web/20190419035048/http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html) | ||
| 45 | by Kurt MacMahon, an NIU professor, to see the graphic and an explanation. The | ||
| 46 | goal of the compilation process is to take the C++ code and produce a shared | ||
| 47 | library, dynamic library, or an executable file. | ||
| 48 | |||
| 49 | ## Compilation Phases | ||
| 50 | |||
| 51 | Let's break down the compilation process. There are four major steps to | ||
| 52 | compiling C++ code. | ||
| 53 | |||
| 54 | ### Step 1 | ||
| 55 | |||
| 56 | The first step is to expand the source code file to meet all dependencies. The | ||
| 57 | C++ preprocessor includes the code from all the header files, such as `#include | ||
| 58 | <iostream>`. Now, what does that mean? The previous example includes the | ||
| 59 | `iostream` header. This tells the computer that you want to use the `iostream` | ||
| 60 | standard library, which contains classes and functions written in the core | ||
| 61 | language. This specific header allows you to manipulate input/output streams. | ||
| 62 | After all this, you'll end up which a temporary file that contains the expanded | ||
| 63 | source code. | ||
| 64 | |||
| 65 | In the example of the C++ code above, the `iostream` class would be included in | ||
| 66 | the expanded code. | ||
| 67 | |||
| 68 | ### Step 2 | ||
| 69 | |||
| 70 | After the code is expanded, the compiler comes into play. The compiler takes the | ||
| 71 | C++ code and converts this code into the assembly language, understood by the | ||
| 72 | platform. You can see this in action if you head over to the [GodBolt Compiler | ||
| 73 | Explorer](https://godbolt.org), which shows C++ being converted into assembly | ||
| 74 | dynamically. | ||
| 75 | |||
| 76 | For example, the `Hello, world!` code snippet above compiles into the following | ||
| 77 | assembly code: | ||
| 78 | |||
| 79 | ```asm | ||
| 80 | .LC0: | ||
| 81 | .string "Hello, world!\n" | ||
| 82 | main: | ||
| 83 | push rbp | ||
| 84 | mov rbp, rsp | ||
| 85 | mov esi, OFFSET FLAT:.LC0 | ||
| 86 | mov edi, OFFSET FLAT:_ZSt4cout | ||
| 87 | call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) | ||
| 88 | mov eax, 0 | ||
| 89 | pop rbp | ||
| 90 | ret | ||
| 91 | __static_initialization_and_destruction_0(int, int): | ||
| 92 | push rbp | ||
| 93 | mov rbp, rsp | ||
| 94 | sub rsp, 16 | ||
| 95 | mov DWORD PTR [rbp-4], edi | ||
| 96 | mov DWORD PTR [rbp-8], esi | ||
| 97 | cmp DWORD PTR [rbp-4], 1 | ||
| 98 | jne .L5 | ||
| 99 | cmp DWORD PTR [rbp-8], 65535 | ||
| 100 | jne .L5 | ||
| 101 | mov edi, OFFSET FLAT:_ZStL8__ioinit | ||
| 102 | call std::ios_base::Init::Init() [complete object constructor] | ||
| 103 | mov edx, OFFSET FLAT:__dso_handle | ||
| 104 | mov esi, OFFSET FLAT:_ZStL8__ioinit | ||
| 105 | mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev | ||
| 106 | call __cxa_atexit | ||
| 107 | .L5: | ||
| 108 | nop | ||
| 109 | leave | ||
| 110 | ret | ||
| 111 | _GLOBAL__sub_I_main: | ||
| 112 | push rbp | ||
| 113 | mov rbp, rsp | ||
| 114 | mov esi, 65535 | ||
| 115 | mov edi, 1 | ||
| 116 | call __static_initialization_and_destruction_0(int, int) | ||
| 117 | pop rbp | ||
| 118 | ret | ||
| 119 | ``` | ||
| 120 | |||
| 121 | ### Step 3 | ||
| 122 | |||
| 123 | Third, the assembly code generated by the compiler is assembled into the object | ||
| 124 | code for the platform. Essentially, this is when the compiler takes the assembly | ||
| 125 | code and assembles it into machine code in a binary format. After researching | ||
| 126 | this online, I figured out that a lot of compilers will allow you to stop | ||
| 127 | compilation at this step. This would be useful for compiling each source code | ||
| 128 | file separately. This saves time later if a single file changes; only that file | ||
| 129 | needs to be recompiled. | ||
| 130 | |||
| 131 | ### Step 4 | ||
| 132 | |||
| 133 | Finally, the object code file generated by the assembler is linked together with | ||
| 134 | the object code files for any library functions used to produce a shared | ||
| 135 | library, dynamic library, or an executable file. It replaces all references to | ||
| 136 | undefined symbols with the correct addresses. | ||
content/blog/2018-11-28-cpp-compiler.org added +136
| @@ -0,0 +1,136 @@ | |||
| 1 | #+date: <2018-11-28> | ||
| 2 | #+title: The C++ Compiler | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * A Brief Introduction | ||
| 7 | |||
| 8 | [[https://en.wikipedia.org/wiki/C%2B%2B][C++]] is a general-purpose | ||
| 9 | programming language with object-oriented, generic, and functional | ||
| 10 | features in addition to facilities for low-level memory manipulation. | ||
| 11 | |||
| 12 | The source code, shown in the snippet below, must be compiled before it | ||
| 13 | can be executed. There are many steps and intricacies to the compilation | ||
| 14 | process, and this post was a personal exercise to learn and remember as | ||
| 15 | much information as I can. | ||
| 16 | |||
| 17 | #+begin_src cpp | ||
| 18 | #include <iostream> | ||
| 19 | |||
| 20 | int main() | ||
| 21 | { | ||
| 22 | std::cout << "Hello, world!\n"; | ||
| 23 | } | ||
| 24 | #+end_src | ||
| 25 | |||
| 26 | ** Compilation Process | ||
| 27 | |||
| 28 | *** An Overview | ||
| 29 | |||
| 30 | Compiling C++ projects is a frustrating task most days. Seemingly | ||
| 31 | nonexistent errors keeping your program from successfully compiling can | ||
| 32 | be annoying (especially since you know you wrote it perfectly the first | ||
| 33 | time, right?). | ||
| 34 | |||
| 35 | I'm learning more and more about C++ these days and decided to write | ||
| 36 | this concept down so that I can cement it even further in my own head. | ||
| 37 | However, C++ is not the only compiled language. Check out | ||
| 38 | [[https://en.wikipedia.org/wiki/Compiled_language][the Wikipedia entry | ||
| 39 | for compiled languages]] for more examples of compiled languages. | ||
| 40 | |||
| 41 | I'll start with a wonderful, graphical way to conceptualize the C++ | ||
| 42 | compiler. View | ||
| 43 | [[https://web.archive.org/web/20190419035048/http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html][The | ||
| 44 | C++ Compilation Process]] by Kurt MacMahon, an NIU professor, to see the | ||
| 45 | graphic and an explanation. The goal of the compilation process is to | ||
| 46 | take the C++ code and produce a shared library, dynamic library, or an | ||
| 47 | executable file. | ||
| 48 | |||
| 49 | ** Compilation Phases | ||
| 50 | |||
| 51 | Let's break down the compilation process. There are four major steps to | ||
| 52 | compiling C++ code. | ||
| 53 | |||
| 54 | *** Step 1 | ||
| 55 | |||
| 56 | The first step is to expand the source code file to meet all | ||
| 57 | dependencies. The C++ preprocessor includes the code from all the header | ||
| 58 | files, such as =#include <iostream>=. Now, what does that mean? The | ||
| 59 | previous example includes the =iostream= header. This tells the computer | ||
| 60 | that you want to use the =iostream= standard library, which contains | ||
| 61 | classes and functions written in the core language. This specific header | ||
| 62 | allows you to manipulate input/output streams. After all this, you'll | ||
| 63 | end up which a temporary file that contains the expanded source code. | ||
| 64 | |||
| 65 | In the example of the C++ code above, the =iostream= class would be | ||
| 66 | included in the expanded code. | ||
| 67 | |||
| 68 | *** Step 2 | ||
| 69 | |||
| 70 | After the code is expanded, the compiler comes into play. The compiler | ||
| 71 | takes the C++ code and converts this code into the assembly language, | ||
| 72 | understood by the platform. You can see this in action if you head over | ||
| 73 | to the [[https://godbolt.org][GodBolt Compiler Explorer]], which shows | ||
| 74 | C++ being converted into assembly dynamically. | ||
| 75 | |||
| 76 | For example, the =Hello, world!= code snippet above compiles into the | ||
| 77 | following assembly code: | ||
| 78 | |||
| 79 | #+begin_src asm | ||
| 80 | .LC0: | ||
| 81 | .string "Hello, world!\n" | ||
| 82 | main: | ||
| 83 | push rbp | ||
| 84 | mov rbp, rsp | ||
| 85 | mov esi, OFFSET FLAT:.LC0 | ||
| 86 | mov edi, OFFSET FLAT:_ZSt4cout | ||
| 87 | call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) | ||
| 88 | mov eax, 0 | ||
| 89 | pop rbp | ||
| 90 | ret | ||
| 91 | __static_initialization_and_destruction_0(int, int): | ||
| 92 | push rbp | ||
| 93 | mov rbp, rsp | ||
| 94 | sub rsp, 16 | ||
| 95 | mov DWORD PTR [rbp-4], edi | ||
| 96 | mov DWORD PTR [rbp-8], esi | ||
| 97 | cmp DWORD PTR [rbp-4], 1 | ||
| 98 | jne .L5 | ||
| 99 | cmp DWORD PTR [rbp-8], 65535 | ||
| 100 | jne .L5 | ||
| 101 | mov edi, OFFSET FLAT:_ZStL8__ioinit | ||
| 102 | call std::ios_base::Init::Init() [complete object constructor] | ||
| 103 | mov edx, OFFSET FLAT:__dso_handle | ||
| 104 | mov esi, OFFSET FLAT:_ZStL8__ioinit | ||
| 105 | mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev | ||
| 106 | call __cxa_atexit | ||
| 107 | .L5: | ||
| 108 | nop | ||
| 109 | leave | ||
| 110 | ret | ||
| 111 | _GLOBAL__sub_I_main: | ||
| 112 | push rbp | ||
| 113 | mov rbp, rsp | ||
| 114 | mov esi, 65535 | ||
| 115 | mov edi, 1 | ||
| 116 | call __static_initialization_and_destruction_0(int, int) | ||
| 117 | pop rbp | ||
| 118 | ret | ||
| 119 | #+end_src | ||
| 120 | |||
| 121 | *** Step 3 | ||
| 122 | |||
| 123 | Third, the assembly code generated by the compiler is assembled into the | ||
| 124 | object code for the platform. Essentially, this is when the compiler | ||
| 125 | takes the assembly code and assembles it into machine code in a binary | ||
| 126 | format. After researching this online, I figured out that a lot of | ||
| 127 | compilers will allow you to stop compilation at this step. This would be | ||
| 128 | useful for compiling each source code file separately. This saves time | ||
| 129 | later if a single file changes; only that file needs to be recompiled. | ||
| 130 | |||
| 131 | *** Step 4 | ||
| 132 | |||
| 133 | Finally, the object code file generated by the assembler is linked | ||
| 134 | together with the object code files for any library functions used to | ||
| 135 | produce a shared library, dynamic library, or an executable file. It | ||
| 136 | replaces all references to undefined symbols with the correct addresses. | ||
content/blog/2019-01-07-useful-css.md deleted −190
| @@ -1,190 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2019-01-07 | ||
| 3 | title = "Useful CSS Snippets" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Introduction to CSS | ||
| 9 | |||
| 10 | [CSS](https://en.wikipedia.org/wiki/CSS), the language used to markup HTML code | ||
| 11 | and make it "pretty", is one of the most effective ways to increase the | ||
| 12 | attractiveness of a website. It can also lead to increased user engagement, | ||
| 13 | retention, and satisfaction. In fact, there are whole career fields are | ||
| 14 | dedicated to the improvement of user experiences, known as UI design and UX | ||
| 15 | design. | ||
| 16 | |||
| 17 | Some web developers are used to the common CSS properties, such as element | ||
| 18 | sizing, fonts, colors, etc., but are not as well versed in less-used properties | ||
| 19 | and values such as `flexbox`, `clip-path`, and `transform`. This article will | ||
| 20 | provide some insight into the less-used and unique CSS properties. | ||
| 21 | |||
| 22 | # CSS Variables | ||
| 23 | |||
| 24 | The first topic today is CSS variables. Variables are not often used by smaller | ||
| 25 | developers. CSS variables allow you to give your website a well-defined | ||
| 26 | structure, where you can easily reuse CSS properties throughout the project. | ||
| 27 | |||
| 28 | You can use variables to define things, such as color palettes. Then, you can | ||
| 29 | use these colors for backgrounds anywhere else in the HTML. This could be | ||
| 30 | extended, where extra variables could be defined for `primary-text`, | ||
| 31 | `quoted-text`, etc. Variables can also be used to define spacing (e.g. `32px` or | ||
| 32 | `2rem`), which can then be applied to margins, padding, font sizes, and more. | ||
| 33 | |||
| 34 | For example, here are some variables defined at the root of the website, which | ||
| 35 | allows for any subsequent CSS rules to use those variables: | ||
| 36 | |||
| 37 | ```css | ||
| 38 | :root { | ||
| 39 | --primary-color: black; | ||
| 40 | --secondary-color: white; | ||
| 41 | } | ||
| 42 | |||
| 43 | body { | ||
| 44 | background-color: var(--primary-color); | ||
| 45 | color: var(--secondary-color); | ||
| 46 | } | ||
| 47 | ``` | ||
| 48 | |||
| 49 | # CSS Box Shadows | ||
| 50 | |||
| 51 | Box shadows were once my mortal enemy. No matter how hard I tried, I just | ||
| 52 | couldn't get them to work how I wanted. Because of this, my favorite discovery | ||
| 53 | has been CSSMatic's [box shadow generator](https://www.cssmatic.com/box-shadow). | ||
| 54 | It provides an excellent tool to generate box shadows using their simple | ||
| 55 | sliders. Surprisingly, this is the reason I learned how box shadows work! You | ||
| 56 | can use the sliders and watch how the CSS code changes in the image that is | ||
| 57 | displayed. Through this, you should understand that the basic structure for box | ||
| 58 | shadows is: | ||
| 59 | |||
| 60 | ```css | ||
| 61 | box-shadow: inset horizontal vertical blur spread color; | ||
| 62 | ``` | ||
| 63 | |||
| 64 | Now, let's look at some basic examples! You can copy and paste the following | ||
| 65 | code into a site like CodePen or your own HTML files. Feel free to play around | ||
| 66 | with the code, experiment, and learn. | ||
| 67 | |||
| 68 | **Box Shadow #1** | ||
| 69 | |||
| 70 | ```html | ||
| 71 | <div class="shadow-examples"> | ||
| 72 | <div class="box effect1"> | ||
| 73 | <h3>Effect 1</h3> | ||
| 74 | </div> | ||
| 75 | </div> | ||
| 76 | ``` | ||
| 77 | |||
| 78 | ```css | ||
| 79 | .box h3 { | ||
| 80 | text-align: center; | ||
| 81 | position: relative; | ||
| 82 | top: 80px; | ||
| 83 | } | ||
| 84 | .box { | ||
| 85 | width: 70%; | ||
| 86 | height: 200px; | ||
| 87 | background: #fff; | ||
| 88 | margin: 40px auto; | ||
| 89 | } | ||
| 90 | .effect1 { | ||
| 91 | box-shadow: 0 10px 6px -6px #777; | ||
| 92 | } | ||
| 93 | ``` | ||
| 94 | |||
| 95 | **Box Shadow #2** | ||
| 96 | |||
| 97 | ```html | ||
| 98 | <div class="shadow-examples"> | ||
| 99 | <div class="box effect2"> | ||
| 100 | <h3>Effect 2</h3> | ||
| 101 | </div> | ||
| 102 | </div> | ||
| 103 | ``` | ||
| 104 | |||
| 105 | ```css | ||
| 106 | .box h3 { | ||
| 107 | text-align: center; | ||
| 108 | position: relative; | ||
| 109 | top: 80px; | ||
| 110 | } | ||
| 111 | .box { | ||
| 112 | width: 70%; | ||
| 113 | height: 200px; | ||
| 114 | background: #fff; | ||
| 115 | margin: 40px auto; | ||
| 116 | } | ||
| 117 | .effect2 { | ||
| 118 | box-shadow: 10px 10px 5px -5px rgba(0, 0, 0, 0.75); | ||
| 119 | } | ||
| 120 | ``` | ||
| 121 | |||
| 122 | Try these box shadows out on your own and see how changing each shadow value | ||
| 123 | works. | ||
| 124 | |||
| 125 | # CSS Flexbox | ||
| 126 | |||
| 127 | Now, let's move on to the best part of this article: flexbox. The flexbox is by | ||
| 128 | far my favorite new toy. I originally stumbled across this solution after | ||
| 129 | looking for more efficient ways of centering content horizontally AND | ||
| 130 | vertically. I had used a few hack-ish methods before, but flexbox throws those | ||
| 131 | out the window. The best part of it all is that flexbox is _dead simple_. | ||
| 132 | |||
| 133 | Flexbox pertains to the parent div of any element. You want the parent to be the | ||
| 134 | flexbox in which items are arranged to use the flex methods. It's easier to see | ||
| 135 | this in action that explained, so let's see an example. | ||
| 136 | |||
| 137 | **Flexbox** | ||
| 138 | |||
| 139 | ```html | ||
| 140 | <div class="flex-examples"> | ||
| 141 | <div class="sm-box"> | ||
| 142 | <h3>1</h3> | ||
| 143 | </div> | ||
| 144 | <div class="sm-box"> | ||
| 145 | <h3>2</h3> | ||
| 146 | </div> | ||
| 147 | </div> | ||
| 148 | ``` | ||
| 149 | |||
| 150 | ```css | ||
| 151 | .flex-examples { | ||
| 152 | display: flex; | ||
| 153 | flex-wrap: wrap; | ||
| 154 | justify-content: flex-start; | ||
| 155 | align-items: center; | ||
| 156 | padding: 10px; | ||
| 157 | background-color: #f2f2f2; | ||
| 158 | } | ||
| 159 | .sm-box { | ||
| 160 | display: flex; | ||
| 161 | justify-content: center; | ||
| 162 | align-items: center; | ||
| 163 | width: 20%; | ||
| 164 | height: 100px; | ||
| 165 | background: #fff; | ||
| 166 | margin: 40px 10px; | ||
| 167 | } | ||
| 168 | ``` | ||
| 169 | |||
| 170 | You may notice that we no longer need to use the `top` property for the `h3` | ||
| 171 | elements in our code. This is because we set the display box to be a flex | ||
| 172 | container for the small boxes, AND we made the small boxes flex containers for | ||
| 173 | their elements (the h3 tags). Flex boxes can be nested like this to center | ||
| 174 | content that is inside centered content. | ||
| 175 | |||
| 176 | For the example above, we designated the `justify-content` property to be | ||
| 177 | `flex-start` so that the boxes stack from the left side of the screen. This | ||
| 178 | property can be changed to `center` to make the boxes appear in the center of | ||
| 179 | the screen. | ||
| 180 | |||
| 181 | For an interactive example, [check out this | ||
| 182 | CodePen](https://codepen.io/LandonSchropp/pen/KpzzGo) from | ||
| 183 | [LandonScropp](https://codepen.io/LandonSchropp/). Resize the window with dice | ||
| 184 | to see how they collapse and re-align. | ||
| 185 | |||
| 186 | # Even More CSS | ||
| 187 | |||
| 188 | For more inspiration, you can visit [CodePen](https://www.codepen.io), | ||
| 189 | [Dribbble](https://dribbble.com), or [UI Movement](https://uimovement.com) to | ||
| 190 | browse the collections of many amazing web designers. | ||
content/blog/2019-01-07-useful-css.org added +194
| @@ -0,0 +1,194 @@ | |||
| 1 | #+date: <2019-01-07> | ||
| 2 | #+title: Useful CSS Snippets | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Introduction to CSS | ||
| 7 | |||
| 8 | [[https://en.wikipedia.org/wiki/CSS][CSS]], the language used to markup | ||
| 9 | HTML code and make it "pretty", is one of the most effective ways to | ||
| 10 | increase the attractiveness of a website. It can also lead to increased | ||
| 11 | user engagement, retention, and satisfaction. In fact, there are whole | ||
| 12 | career fields are dedicated to the improvement of user experiences, | ||
| 13 | known as UI design and UX design. | ||
| 14 | |||
| 15 | Some web developers are used to the common CSS properties, such as | ||
| 16 | element sizing, fonts, colors, etc., but are not as well versed in | ||
| 17 | less-used properties and values such as =flexbox=, =clip-path=, and | ||
| 18 | =transform=. This article will provide some insight into the less-used | ||
| 19 | and unique CSS properties. | ||
| 20 | |||
| 21 | * CSS Variables | ||
| 22 | |||
| 23 | The first topic today is CSS variables. Variables are not often used by | ||
| 24 | smaller developers. CSS variables allow you to give your website a | ||
| 25 | well-defined structure, where you can easily reuse CSS properties | ||
| 26 | throughout the project. | ||
| 27 | |||
| 28 | You can use variables to define things, such as color palettes. Then, | ||
| 29 | you can use these colors for backgrounds anywhere else in the HTML. This | ||
| 30 | could be extended, where extra variables could be defined for | ||
| 31 | =primary-text=, =quoted-text=, etc. Variables can also be used to define | ||
| 32 | spacing (e.g. =32px= or =2rem=), which can then be applied to margins, | ||
| 33 | padding, font sizes, and more. | ||
| 34 | |||
| 35 | For example, here are some variables defined at the root of the website, | ||
| 36 | which allows for any subsequent CSS rules to use those variables: | ||
| 37 | |||
| 38 | #+begin_src css | ||
| 39 | :root { | ||
| 40 | --primary-color: black; | ||
| 41 | --secondary-color: white; | ||
| 42 | } | ||
| 43 | |||
| 44 | body { | ||
| 45 | background-color: var(--primary-color); | ||
| 46 | color: var(--secondary-color); | ||
| 47 | } | ||
| 48 | #+end_src | ||
| 49 | |||
| 50 | * CSS Box Shadows | ||
| 51 | |||
| 52 | Box shadows were once my mortal enemy. No matter how hard I tried, I | ||
| 53 | just couldn't get them to work how I wanted. Because of this, my | ||
| 54 | favorite discovery has been CSSMatic's | ||
| 55 | [[https://www.cssmatic.com/box-shadow][box shadow generator]]. It | ||
| 56 | provides an excellent tool to generate box shadows using their simple | ||
| 57 | sliders. Surprisingly, this is the reason I learned how box shadows | ||
| 58 | work! You can use the sliders and watch how the CSS code changes in the | ||
| 59 | image that is displayed. Through this, you should understand that the | ||
| 60 | basic structure for box shadows is: | ||
| 61 | |||
| 62 | #+begin_src css | ||
| 63 | box-shadow: inset horizontal vertical blur spread color; | ||
| 64 | #+end_src | ||
| 65 | |||
| 66 | Now, let's look at some basic examples! You can copy and paste the | ||
| 67 | following code into a site like CodePen or your own HTML files. Feel | ||
| 68 | free to play around with the code, experiment, and learn. | ||
| 69 | |||
| 70 | *Box Shadow #1* | ||
| 71 | |||
| 72 | #+begin_src html | ||
| 73 | <div class="shadow-examples"> | ||
| 74 | <div class="box effect1"> | ||
| 75 | <h3>Effect 1</h3> | ||
| 76 | </div> | ||
| 77 | </div> | ||
| 78 | #+end_src | ||
| 79 | |||
| 80 | #+begin_src css | ||
| 81 | .box h3 { | ||
| 82 | text-align: center; | ||
| 83 | position: relative; | ||
| 84 | top: 80px; | ||
| 85 | } | ||
| 86 | .box { | ||
| 87 | width: 70%; | ||
| 88 | height: 200px; | ||
| 89 | background: #fff; | ||
| 90 | margin: 40px auto; | ||
| 91 | } | ||
| 92 | .effect1 { | ||
| 93 | box-shadow: 0 10px 6px -6px #777; | ||
| 94 | } | ||
| 95 | #+end_src | ||
| 96 | |||
| 97 | *Box Shadow #2* | ||
| 98 | |||
| 99 | #+begin_src html | ||
| 100 | <div class="shadow-examples"> | ||
| 101 | <div class="box effect2"> | ||
| 102 | <h3>Effect 2</h3> | ||
| 103 | </div> | ||
| 104 | </div> | ||
| 105 | #+end_src | ||
| 106 | |||
| 107 | #+begin_src css | ||
| 108 | .box h3 { | ||
| 109 | text-align: center; | ||
| 110 | position: relative; | ||
| 111 | top: 80px; | ||
| 112 | } | ||
| 113 | .box { | ||
| 114 | width: 70%; | ||
| 115 | height: 200px; | ||
| 116 | background: #fff; | ||
| 117 | margin: 40px auto; | ||
| 118 | } | ||
| 119 | .effect2 { | ||
| 120 | box-shadow: 10px 10px 5px -5px rgba(0, 0, 0, 0.75); | ||
| 121 | } | ||
| 122 | #+end_src | ||
| 123 | |||
| 124 | Try these box shadows out on your own and see how changing each shadow | ||
| 125 | value works. | ||
| 126 | |||
| 127 | * CSS Flexbox | ||
| 128 | |||
| 129 | Now, let's move on to the best part of this article: flexbox. The | ||
| 130 | flexbox is by far my favorite new toy. I originally stumbled across this | ||
| 131 | solution after looking for more efficient ways of centering content | ||
| 132 | horizontally AND vertically. I had used a few hack-ish methods before, | ||
| 133 | but flexbox throws those out the window. The best part of it all is that | ||
| 134 | flexbox is /dead simple/. | ||
| 135 | |||
| 136 | Flexbox pertains to the parent div of any element. You want the parent | ||
| 137 | to be the flexbox in which items are arranged to use the flex methods. | ||
| 138 | It's easier to see this in action that explained, so let's see an | ||
| 139 | example. | ||
| 140 | |||
| 141 | *Flexbox* | ||
| 142 | |||
| 143 | #+begin_src html | ||
| 144 | <div class="flex-examples"> | ||
| 145 | <div class="sm-box"> | ||
| 146 | <h3>1</h3> | ||
| 147 | </div> | ||
| 148 | <div class="sm-box"> | ||
| 149 | <h3>2</h3> | ||
| 150 | </div> | ||
| 151 | </div> | ||
| 152 | #+end_src | ||
| 153 | |||
| 154 | #+begin_src css | ||
| 155 | .flex-examples { | ||
| 156 | display: flex; | ||
| 157 | flex-wrap: wrap; | ||
| 158 | justify-content: flex-start; | ||
| 159 | align-items: center; | ||
| 160 | padding: 10px; | ||
| 161 | background-color: #f2f2f2; | ||
| 162 | } | ||
| 163 | .sm-box { | ||
| 164 | display: flex; | ||
| 165 | justify-content: center; | ||
| 166 | align-items: center; | ||
| 167 | width: 20%; | ||
| 168 | height: 100px; | ||
| 169 | background: #fff; | ||
| 170 | margin: 40px 10px; | ||
| 171 | } | ||
| 172 | #+end_src | ||
| 173 | |||
| 174 | You may notice that we no longer need to use the =top= property for the | ||
| 175 | =h3= elements in our code. This is because we set the display box to be | ||
| 176 | a flex container for the small boxes, AND we made the small boxes flex | ||
| 177 | containers for their elements (the h3 tags). Flex boxes can be nested | ||
| 178 | like this to center content that is inside centered content. | ||
| 179 | |||
| 180 | For the example above, we designated the =justify-content= property to | ||
| 181 | be =flex-start= so that the boxes stack from the left side of the | ||
| 182 | screen. This property can be changed to =center= to make the boxes | ||
| 183 | appear in the center of the screen. | ||
| 184 | |||
| 185 | For an interactive example, | ||
| 186 | [[https://codepen.io/LandonSchropp/pen/KpzzGo][check out this CodePen]] | ||
| 187 | from [[https://codepen.io/LandonSchropp/][LandonScropp]]. Resize the | ||
| 188 | window with dice to see how they collapse and re-align. | ||
| 189 | |||
| 190 | * Even More CSS | ||
| 191 | |||
| 192 | For more inspiration, you can visit [[https://www.codepen.io][CodePen]], | ||
| 193 | [[https://dribbble.com][Dribbble]], or [[https://uimovement.com][UI | ||
| 194 | Movement]] to browse the collections of many amazing web designers. | ||
content/blog/2019-09-09-audit-analytics.md deleted −201
| @@ -1,201 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2019-09-09 | ||
| 3 | title = "Audit Analytics" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What Are Data Analytics? | ||
| 9 | |||
| 10 | A quick aside before I dive into this post: `data analytics` is a vague term | ||
| 11 | that has become popular in recent years. Think of a `data analytic` as the | ||
| 12 | output of any data analysis you perform. For example, a pivot table or a pie | ||
| 13 | chart could be a data analytic. | ||
| 14 | |||
| 15 | [Data analysis](https://en.wikipedia.org/wiki/Data_analysis) is a process that | ||
| 16 | utilizes statistics and other mathematical methods to discover useful | ||
| 17 | information within datasets. This involves examining, cleaning, transforming, | ||
| 18 | and modeling data so that you can use the data to support an opinion, create | ||
| 19 | more useful viewpoints, and gain knowledge to implement into audit planning or | ||
| 20 | risk assessments. | ||
| 21 | |||
| 22 | One of the common mistakes that managers (and anyone new to the process) make is | ||
| 23 | assuming that everything involved with this process is "data analytics". In | ||
| 24 | fact, data analytics are only a small part of the process. | ||
| 25 | |||
| 26 | See **Figure 1** for a more accurate representation of where data analysis sits | ||
| 27 | within the full process. This means that data analysis does not include querying | ||
| 28 | or extracting data, selecting samples, or performing audit tests. These steps | ||
| 29 | can be necessary for an audit (and may even be performed by the same | ||
| 30 | associates), but they are not data analytics. | ||
| 31 | |||
| 32 | # Current Use of Analytics in Auditing | ||
| 33 | |||
| 34 | While data analysis has been an integral part of most businesses and departments | ||
| 35 | for the better part of the last century, only recently have internal audit | ||
| 36 | functions been adopting this practice. The internal audit function works | ||
| 37 | exclusively to provide assurance and consulting services to the business areas | ||
| 38 | within the firm (except for internal auditing firms who are hired by different | ||
| 39 | companies to perform their roles). | ||
| 40 | |||
| 41 | > Internal Auditing helps an organization accomplish its objectives by bringing | ||
| 42 | > a systematic, disciplined approach to evaluate and improve the effectiveness | ||
| 43 | > of risk management, control and governance processes. | ||
| 44 | > | ||
| 45 | > - The IIA's Definition of Internal Audit | ||
| 46 | |||
| 47 | Part of the blame for the slow adoption of data analysis can be attributed to | ||
| 48 | the fact that internal auditing is strongly based on tradition and following the | ||
| 49 | precedents set by previous auditors. However, there can be no progress without | ||
| 50 | auditors who are willing to break the mold and test new audit techniques. In | ||
| 51 | fact, as of 2018, [only 63% of internal audit departments currently utilize data | ||
| 52 | analytics](https://www.cpapracticeadvisor.com/accounting-audit/news/12404086/internal-audit-groups-are-lagging-in-data-analytics) | ||
| 53 | in North America. This number should be as close as possible to 100%. I have | ||
| 54 | never been part of an audit that would not have benefited from data analytics. | ||
| 55 | |||
| 56 | So, how do internal audit functions remedy this situation? It's definitely not | ||
| 57 | as easy as walking into work on Monday and telling your Chief Audit Executive | ||
| 58 | that you're going to start implementing analytics in the next audit. You need a | ||
| 59 | plan and a system to make the analysis process as effective as possible. | ||
| 60 | |||
| 61 | # The DELTA Model | ||
| 62 | |||
| 63 | One of the easiest ways to experiment with data analytics and gain an | ||
| 64 | understanding of the processes is to implement them within your own department. | ||
| 65 | But how do we do this if we've never worked with analysis before? One of the | ||
| 66 | most common places to start is to research some data analysis models currently | ||
| 67 | available. For this post, we'll take a look at the DELTA model. You can take a | ||
| 68 | look at **Figure 2** for a quick overview of the model. | ||
| 69 | |||
| 70 | The DELTA model sets a few guidelines for areas wanting to implement data | ||
| 71 | analytics so that the results can be as comprehensive as possible: | ||
| 72 | |||
| 73 | - **Data**: Must be clean, accessible, and (usually) unique. | ||
| 74 | - **Enterprise-Wide Focus**: Key data systems and analytical resources must be | ||
| 75 | available for use (by the Internal Audit Function). | ||
| 76 | - **Leaders**: Must promote a data analytics approach and show the value of | ||
| 77 | analytical results. | ||
| 78 | - **Targets**: Must be set for key areas and risks that the analytics can be | ||
| 79 | compared against (KPIs). | ||
| 80 | - **Analysts**: There must be auditors willing and able to perform data | ||
| 81 | analytics or else the system cannot be sustained. | ||
| 82 | |||
| 83 | # Finding the Proper KPIs | ||
| 84 | |||
| 85 | Once the Internal Audit Function has decided that they want to start using data | ||
| 86 | analytics internally and have ensured they're properly set up to do so, they | ||
| 87 | need to figure out what they will be testing against. Key Performance Indicators | ||
| 88 | (KPIs) are qualitative or quantitative factors that can be evaluated and | ||
| 89 | assessed to determine if the department is performing well, usually compared to | ||
| 90 | historical or industry benchmarks. Once KPIs have been agreed upon and set, | ||
| 91 | auditors can use data analytics to assess and report on these KPIs. This allows | ||
| 92 | the person performing the analytics the freedom to express opinions on the | ||
| 93 | results, whereas the results are ambiguous if no KPIs exist. | ||
| 94 | |||
| 95 | It should be noted that tracking KPIs in the department can help ensure you have | ||
| 96 | a rigorous Quality Assurance and Improvement Program (QAIP) in accordance with | ||
| 97 | some applicable standards, such as IPPF Standard 1300. | ||
| 98 | |||
| 99 | > The chief audit executive must develop and maintain a quality assurance and | ||
| 100 | > improvement program that covers all aspects of the internal audit activity. | ||
| 101 | > | ||
| 102 | > - IPPF Standard 1300 | ||
| 103 | |||
| 104 | Additionally, IPPF Standard 2060 discusses reporting: | ||
| 105 | |||
| 106 | > The chief audit executive must report periodically to senior management and | ||
| 107 | > the board on the internal audit activity's purpose, authority, responsibility, | ||
| 108 | > and performance relative to its plan and on its conformance with the Code of | ||
| 109 | > Ethics and the Standards. Reporting must also include significant risk and | ||
| 110 | > control issues, including fraud risks, governance issues, and other matters | ||
| 111 | > that require the attention of senior management and/or the board. | ||
| 112 | > | ||
| 113 | > - IPPF Standard 2060 | ||
| 114 | |||
| 115 | The hardest part of finding KPIs is to determine which KPIs are appropriate for | ||
| 116 | your department. Since every department is different and has different goals, | ||
| 117 | KPIs will vary drastically between companies. To give you an idea of where to | ||
| 118 | look, here are some ideas I came up with when discussing the topic with a few | ||
| 119 | colleagues. | ||
| 120 | |||
| 121 | - Efficiency/Budgeting: | ||
| 122 | - Audit hours to staff utilization ratio (annual hours divided by total | ||
| 123 | annual work hours). | ||
| 124 | - Audit hours compared to the number of audits completed. | ||
| 125 | - Time between audit steps or to complete the whole audit. E.g., time from | ||
| 126 | fieldwork completion to audit report issuance. | ||
| 127 | - Reputation: | ||
| 128 | - The frequency that management has requested the services of the IAF. | ||
| 129 | - Management, audit committee, or external audit satisfaction survey | ||
| 130 | results. | ||
| 131 | - Education, experience, certifications, tenure, and training of the | ||
| 132 | auditors on staff. | ||
| 133 | - Quality: | ||
| 134 | - Number and frequency of audit findings. Assign monetary or numerical | ||
| 135 | values, if possible. | ||
| 136 | - Percentage of recommendations issued and implemented. | ||
| 137 | - Planning: | ||
| 138 | - Percentage or number of key risks audited per year or per audit. | ||
| 139 | - Proportion of audit universe audited per year. | ||
| 140 | |||
| 141 | # Data Analysis Tools | ||
| 142 | |||
| 143 | Finally, to be able to analyze and report on the data analysis, auditors need to | ||
| 144 | evaluate the tools at their disposal. There are many options available, but a | ||
| 145 | few of the most common ones can easily get the job done. For example, almost | ||
| 146 | every auditor already has access to Microsoft Excel. Excel is more powerful than | ||
| 147 | most people give it credit for and can accomplish a lot of basic statistics | ||
| 148 | without much work. If you don't know a lot about statistics but still want to | ||
| 149 | see some of the more basic results, Excel is a great option. | ||
| 150 | |||
| 151 | To perform more in-depth statistical analysis or to explore large datasets that | ||
| 152 | Excel cannot handle, auditors will need to explore other options. The big three | ||
| 153 | that have had a lot of success in recent years are Python, R, and ACL. ACL can | ||
| 154 | be used as either a graphical tool (point and click) or as a scripting tool, | ||
| 155 | where the auditor must write the scripts manually. Python and the R-language are | ||
| 156 | solely scripting languages. | ||
| 157 | |||
| 158 | The general trend in the data analytics environment is that if the tool allows | ||
| 159 | you to do everything by clicking buttons or dragging elements, you won't be able | ||
| 160 | to fully utilize the analytics you need. The most robust solutions are created | ||
| 161 | by those who understand how to write the scripts manually. It should be noted | ||
| 162 | that as the utility of a tool increases, it usually means that the learning | ||
| 163 | curve for that tool will also be higher. It will take auditors longer to learn | ||
| 164 | how to utilize Python, R, or ACL versus learning how to utilize Excel. | ||
| 165 | |||
| 166 | # Visualization | ||
| 167 | |||
| 168 | Once an auditor has finally found the right data, KPIs, and tools, they must | ||
| 169 | report these results so that actions can be taken. Performing in-depth data | ||
| 170 | analysis is only useful if the results are understood by the audiences of the | ||
| 171 | data. The best way to create this understanding is to visualize the results of | ||
| 172 | the data. Let's take a look at some of the best options to visualize and report | ||
| 173 | the results you've found. | ||
| 174 | |||
| 175 | Some of the most popular commercial tools for visualization are Microsoft | ||
| 176 | PowerBI and Tableau Desktop. However, other tools exist such as JMP, Plotly, | ||
| 177 | Qlikview, Alteryx, or D3. Some require commercial licenses while others are | ||
| 178 | simply free to use. For corporate data, you may want to make sure that the tool | ||
| 179 | does not communicate any of the data outside the company (such as cloud | ||
| 180 | storage). I won't be going into depth on any of these tools since visualization | ||
| 181 | is largely a subjective and creative experience, but remember to constantly | ||
| 182 | explore new options as you repeat the process. | ||
| 183 | |||
| 184 | Lastly, let's take a look at an example of data visualization. This example | ||
| 185 | comes from a [blog post written by Kushal | ||
| 186 | Chakrabarti](https://talent.works/2018/03/28/the-science-of-the-job-search-part-iii-61-of-entry-level-jobs-require-3-years-of-experience/) | ||
| 187 | in 2018 about the percent of entry-level US jobs that require experience. | ||
| 188 | **Figure 3** shows us an easy-to-digest picture of the data. We can quickly tell | ||
| 189 | that only about 12.5% of entry-level jobs don't require experience. | ||
| 190 | |||
| 191 | This is the kind of result that easily describes the data for you. However, make | ||
| 192 | sure to include an explanation of what the results mean. Don't let the reader | ||
| 193 | assume what the data means, especially if it relates to a complex subject. _Tell | ||
| 194 | a story_ about the data and why the results matter. | ||
| 195 | |||
| 196 | # Wrap-Up | ||
| 197 | |||
| 198 | While this is not an all-encompassing program that you can just adopt into your | ||
| 199 | department, it should be enough to get anyone started on the process of | ||
| 200 | understanding and implementing data analytics. Always remember to continue | ||
| 201 | learning and exploring new options as your processes grow and evolve. | ||
content/blog/2019-09-09-audit-analytics.org added +220
| @@ -0,0 +1,220 @@ | |||
| 1 | #+date: <2019-09-09> | ||
| 2 | #+title: Audit Analytics | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What Are Data Analytics? | ||
| 7 | |||
| 8 | A quick aside before I dive into this post: =data analytics= is a vague | ||
| 9 | term that has become popular in recent years. Think of a =data analytic= | ||
| 10 | as the output of any data analysis you perform. For example, a pivot | ||
| 11 | table or a pie chart could be a data analytic. | ||
| 12 | |||
| 13 | [[https://en.wikipedia.org/wiki/Data_analysis][Data analysis]] is a | ||
| 14 | process that utilizes statistics and other mathematical methods to | ||
| 15 | discover useful information within datasets. This involves examining, | ||
| 16 | cleaning, transforming, and modeling data so that you can use the data | ||
| 17 | to support an opinion, create more useful viewpoints, and gain knowledge | ||
| 18 | to implement into audit planning or risk assessments. | ||
| 19 | |||
| 20 | One of the common mistakes that managers (and anyone new to the process) | ||
| 21 | make is assuming that everything involved with this process is "data | ||
| 22 | analytics". In fact, data analytics are only a small part of the | ||
| 23 | process. | ||
| 24 | |||
| 25 | See *Figure 1* for a more accurate representation of where data analysis | ||
| 26 | sits within the full process. This means that data analysis does not | ||
| 27 | include querying or extracting data, selecting samples, or performing | ||
| 28 | audit tests. These steps can be necessary for an audit (and may even be | ||
| 29 | performed by the same associates), but they are not data analytics. | ||
| 30 | |||
| 31 | * Current Use of Analytics in Auditing | ||
| 32 | |||
| 33 | While data analysis has been an integral part of most businesses and | ||
| 34 | departments for the better part of the last century, only recently have | ||
| 35 | internal audit functions been adopting this practice. The internal audit | ||
| 36 | function works exclusively to provide assurance and consulting services | ||
| 37 | to the business areas within the firm (except for internal auditing | ||
| 38 | firms who are hired by different companies to perform their roles). | ||
| 39 | |||
| 40 | #+begin_quote | ||
| 41 | Internal Auditing helps an organization accomplish its objectives by | ||
| 42 | bringing a systematic, disciplined approach to evaluate and improve the | ||
| 43 | effectiveness of risk management, control and governance processes. | ||
| 44 | |||
| 45 | - The IIA's Definition of Internal Audit | ||
| 46 | #+end_quote | ||
| 47 | |||
| 48 | Part of the blame for the slow adoption of data analysis can be | ||
| 49 | attributed to the fact that internal auditing is strongly based on | ||
| 50 | tradition and following the precedents set by previous auditors. | ||
| 51 | However, there can be no progress without auditors who are willing to | ||
| 52 | break the mold and test new audit techniques. In fact, as of 2018, | ||
| 53 | [[https://www.cpapracticeadvisor.com/accounting-audit/news/12404086/internal-audit-groups-are-lagging-in-data-analytics][only | ||
| 54 | 63% of internal audit departments currently utilize data analytics]] in | ||
| 55 | North America. This number should be as close as possible to 100%. I | ||
| 56 | have never been part of an audit that would not have benefited from data | ||
| 57 | analytics. | ||
| 58 | |||
| 59 | So, how do internal audit functions remedy this situation? It's | ||
| 60 | definitely not as easy as walking into work on Monday and telling your | ||
| 61 | Chief Audit Executive that you're going to start implementing analytics | ||
| 62 | in the next audit. You need a plan and a system to make the analysis | ||
| 63 | process as effective as possible. | ||
| 64 | |||
| 65 | * The DELTA Model | ||
| 66 | |||
| 67 | One of the easiest ways to experiment with data analytics and gain an | ||
| 68 | understanding of the processes is to implement them within your own | ||
| 69 | department. But how do we do this if we've never worked with analysis | ||
| 70 | before? One of the most common places to start is to research some data | ||
| 71 | analysis models currently available. For this post, we'll take a look at | ||
| 72 | the DELTA model. You can take a look at *Figure 2* for a quick overview | ||
| 73 | of the model. | ||
| 74 | |||
| 75 | The DELTA model sets a few guidelines for areas wanting to implement | ||
| 76 | data analytics so that the results can be as comprehensive as possible: | ||
| 77 | |||
| 78 | - *Data*: Must be clean, accessible, and (usually) unique. | ||
| 79 | - *Enterprise-Wide Focus*: Key data systems and analytical resources | ||
| 80 | must be available for use (by the Internal Audit Function). | ||
| 81 | - *Leaders*: Must promote a data analytics approach and show the value | ||
| 82 | of analytical results. | ||
| 83 | - *Targets*: Must be set for key areas and risks that the analytics can | ||
| 84 | be compared against (KPIs). | ||
| 85 | - *Analysts*: There must be auditors willing and able to perform data | ||
| 86 | analytics or else the system cannot be sustained. | ||
| 87 | |||
| 88 | * Finding the Proper KPIs | ||
| 89 | |||
| 90 | Once the Internal Audit Function has decided that they want to start | ||
| 91 | using data analytics internally and have ensured they're properly set up | ||
| 92 | to do so, they need to figure out what they will be testing against. Key | ||
| 93 | Performance Indicators (KPIs) are qualitative or quantitative factors | ||
| 94 | that can be evaluated and assessed to determine if the department is | ||
| 95 | performing well, usually compared to historical or industry benchmarks. | ||
| 96 | Once KPIs have been agreed upon and set, auditors can use data analytics | ||
| 97 | to assess and report on these KPIs. This allows the person performing | ||
| 98 | the analytics the freedom to express opinions on the results, whereas | ||
| 99 | the results are ambiguous if no KPIs exist. | ||
| 100 | |||
| 101 | It should be noted that tracking KPIs in the department can help ensure | ||
| 102 | you have a rigorous Quality Assurance and Improvement Program (QAIP) in | ||
| 103 | accordance with some applicable standards, such as IPPF Standard 1300. | ||
| 104 | |||
| 105 | #+begin_quote | ||
| 106 | The chief audit executive must develop and maintain a quality assurance | ||
| 107 | and improvement program that covers all aspects of the internal audit | ||
| 108 | activity. | ||
| 109 | |||
| 110 | - IPPF Standard 1300 | ||
| 111 | #+end_quote | ||
| 112 | |||
| 113 | Additionally, IPPF Standard 2060 discusses reporting: | ||
| 114 | |||
| 115 | #+begin_quote | ||
| 116 | The chief audit executive must report periodically to senior management | ||
| 117 | and the board on the internal audit activity's purpose, authority, | ||
| 118 | responsibility, and performance relative to its plan and on its | ||
| 119 | conformance with the Code of Ethics and the Standards. Reporting must | ||
| 120 | also include significant risk and control issues, including fraud risks, | ||
| 121 | governance issues, and other matters that require the attention of | ||
| 122 | senior management and/or the board. | ||
| 123 | |||
| 124 | - IPPF Standard 2060 | ||
| 125 | #+end_quote | ||
| 126 | |||
| 127 | The hardest part of finding KPIs is to determine which KPIs are | ||
| 128 | appropriate for your department. Since every department is different and | ||
| 129 | has different goals, KPIs will vary drastically between companies. To | ||
| 130 | give you an idea of where to look, here are some ideas I came up with | ||
| 131 | when discussing the topic with a few colleagues. | ||
| 132 | |||
| 133 | - Efficiency/Budgeting: | ||
| 134 | - Audit hours to staff utilization ratio (annual hours divided by | ||
| 135 | total annual work hours). | ||
| 136 | - Audit hours compared to the number of audits completed. | ||
| 137 | - Time between audit steps or to complete the whole audit. E.g., time | ||
| 138 | from fieldwork completion to audit report issuance. | ||
| 139 | - Reputation: | ||
| 140 | - The frequency that management has requested the services of the IAF. | ||
| 141 | - Management, audit committee, or external audit satisfaction survey | ||
| 142 | results. | ||
| 143 | - Education, experience, certifications, tenure, and training of the | ||
| 144 | auditors on staff. | ||
| 145 | - Quality: | ||
| 146 | - Number and frequency of audit findings. Assign monetary or numerical | ||
| 147 | values, if possible. | ||
| 148 | - Percentage of recommendations issued and implemented. | ||
| 149 | - Planning: | ||
| 150 | - Percentage or number of key risks audited per year or per audit. | ||
| 151 | - Proportion of audit universe audited per year. | ||
| 152 | |||
| 153 | * Data Analysis Tools | ||
| 154 | |||
| 155 | Finally, to be able to analyze and report on the data analysis, auditors | ||
| 156 | need to evaluate the tools at their disposal. There are many options | ||
| 157 | available, but a few of the most common ones can easily get the job | ||
| 158 | done. For example, almost every auditor already has access to Microsoft | ||
| 159 | Excel. Excel is more powerful than most people give it credit for and | ||
| 160 | can accomplish a lot of basic statistics without much work. If you don't | ||
| 161 | know a lot about statistics but still want to see some of the more basic | ||
| 162 | results, Excel is a great option. | ||
| 163 | |||
| 164 | To perform more in-depth statistical analysis or to explore large | ||
| 165 | datasets that Excel cannot handle, auditors will need to explore other | ||
| 166 | options. The big three that have had a lot of success in recent years | ||
| 167 | are Python, R, and ACL. ACL can be used as either a graphical tool | ||
| 168 | (point and click) or as a scripting tool, where the auditor must write | ||
| 169 | the scripts manually. Python and the R-language are solely scripting | ||
| 170 | languages. | ||
| 171 | |||
| 172 | The general trend in the data analytics environment is that if the tool | ||
| 173 | allows you to do everything by clicking buttons or dragging elements, | ||
| 174 | you won't be able to fully utilize the analytics you need. The most | ||
| 175 | robust solutions are created by those who understand how to write the | ||
| 176 | scripts manually. It should be noted that as the utility of a tool | ||
| 177 | increases, it usually means that the learning curve for that tool will | ||
| 178 | also be higher. It will take auditors longer to learn how to utilize | ||
| 179 | Python, R, or ACL versus learning how to utilize Excel. | ||
| 180 | |||
| 181 | * Visualization | ||
| 182 | |||
| 183 | Once an auditor has finally found the right data, KPIs, and tools, they | ||
| 184 | must report these results so that actions can be taken. Performing | ||
| 185 | in-depth data analysis is only useful if the results are understood by | ||
| 186 | the audiences of the data. The best way to create this understanding is | ||
| 187 | to visualize the results of the data. Let's take a look at some of the | ||
| 188 | best options to visualize and report the results you've found. | ||
| 189 | |||
| 190 | Some of the most popular commercial tools for visualization are | ||
| 191 | Microsoft PowerBI and Tableau Desktop. However, other tools exist such | ||
| 192 | as JMP, Plotly, Qlikview, Alteryx, or D3. Some require commercial | ||
| 193 | licenses while others are simply free to use. For corporate data, you | ||
| 194 | may want to make sure that the tool does not communicate any of the data | ||
| 195 | outside the company (such as cloud storage). I won't be going into depth | ||
| 196 | on any of these tools since visualization is largely a subjective and | ||
| 197 | creative experience, but remember to constantly explore new options as | ||
| 198 | you repeat the process. | ||
| 199 | |||
| 200 | Lastly, let's take a look at an example of data visualization. This | ||
| 201 | example comes from a | ||
| 202 | [[https://talent.works/2018/03/28/the-science-of-the-job-search-part-iii-61-of-entry-level-jobs-require-3-years-of-experience/][blog | ||
| 203 | post written by Kushal Chakrabarti]] in 2018 about the percent of | ||
| 204 | entry-level US jobs that require experience. *Figure 3* shows us an | ||
| 205 | easy-to-digest picture of the data. We can quickly tell that only about | ||
| 206 | 12.5% of entry-level jobs don't require experience. | ||
| 207 | |||
| 208 | This is the kind of result that easily describes the data for you. | ||
| 209 | However, make sure to include an explanation of what the results mean. | ||
| 210 | Don't let the reader assume what the data means, especially if it | ||
| 211 | relates to a complex subject. /Tell a story/ about the data and why the | ||
| 212 | results matter. | ||
| 213 | |||
| 214 | * Wrap-Up | ||
| 215 | |||
| 216 | While this is not an all-encompassing program that you can just adopt | ||
| 217 | into your department, it should be enough to get anyone started on the | ||
| 218 | process of understanding and implementing data analytics. Always | ||
| 219 | remember to continue learning and exploring new options as your | ||
| 220 | processes grow and evolve. | ||
content/blog/2019-12-03-the-ansoff-matrix.md deleted −116
| @@ -1,116 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2019-12-03 | ||
| 3 | title = "The Ansoff Matrix" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | As the world of business evolves, managers must approach business planning and | ||
| 11 | strategy with a contemporary mindset. According to Dess, McNamara, Eisner, and | ||
| 12 | Lee, managers must be willing to adapt to the modern business environment by | ||
| 13 | going beyond "'incremental management', whereby they view their job as making a | ||
| 14 | series of small, minor changes to improve the efficiency of the firm's | ||
| 15 | operations"(2019). | ||
| 16 | |||
| 17 | One reason that strategic management is crucial is because most businesses that | ||
| 18 | fail in the United States each year fail due to a lack of strategic focus or | ||
| 19 | direction(2019). The rate of failure for businesses with poor strategies shows | ||
| 20 | that strategic planning and management are crucial to a business's strength and | ||
| 21 | longevity, injecting the critical factors of growth and direction into a | ||
| 22 | company's business plan. | ||
| 23 | |||
| 24 | One of the most significant strategic planning and management frameworks that | ||
| 25 | companies can use is the [Ansoff | ||
| 26 | Matrix](https://en.wikipedia.org/wiki/Ansoff_matrix). While this framework has | ||
| 27 | unique purposes and use-cases, it can effectively help an organization grow and | ||
| 28 | compete. Specifically, the Ansoff matrix is one of the most effective frameworks | ||
| 29 | for companies who want to focus on increasing sales revenue or | ||
| 30 | profitability(2019). | ||
| 31 | |||
| 32 | This framework uses a two-by-two figure to show the four strategic options for | ||
| 33 | companies to use in this framework: market penetration, market development, | ||
| 34 | product development, and diversification (see **Figure 1**). The x-axis of the | ||
| 35 | matrix focuses on the firm's markets and also determines if the firm is looking | ||
| 36 | to enter new markets or innovate in its current markets. The y-axis of the | ||
| 37 | matrix focuses on the firm's products and determines if the firm wants to pursue | ||
| 38 | strategies around their existing products or explore new products. | ||
| 39 | |||
| 40 | # Strategic Options | ||
| 41 | |||
| 42 | ## Market Penetration | ||
| 43 | |||
| 44 | The most straightforward strategy in the Ansoff matrix is to focus on existing | ||
| 45 | products in existing markets, also known as market penetration(2019). Companies | ||
| 46 | such as Coca-Cola have used market penetration successfully by investing a lot | ||
| 47 | of money to get further value out of their current markets. Coca-Cola does this | ||
| 48 | by introducing new features such as Christmas-themed bottles, personal names on | ||
| 49 | the bottles, and other marketing schemes. | ||
| 50 | |||
| 51 | ## Market Development | ||
| 52 | |||
| 53 | Market development extends existing products into new markets in an attempt to | ||
| 54 | increase the number of buyers. One interesting way that Coca-Cola used this | ||
| 55 | strategy comes from the stigma that Diet Coke is a woman's drink(2019). | ||
| 56 | Coca-Cola introduced Coca-Cola Zero, which contained the same nutritional | ||
| 57 | content as Diet Coke, but was packaged in a dark black can to appear more | ||
| 58 | "manly"(2019). | ||
| 59 | |||
| 60 | ## Product Development | ||
| 61 | |||
| 62 | Product development uses existing markets to introduce new products so that the | ||
| 63 | firm can better meet customer needs(2019). The extreme end of diversification is | ||
| 64 | home to companies such as Johnson & Johnson, a healthcare company that has | ||
| 65 | developed a business portfolio of more than 60,000 different products(2019). | ||
| 66 | Johnson & Johnson's dedication to continuous diversification has led them to a | ||
| 67 | balance sheet rating of "AAA", industry recognition for diversification, and | ||
| 68 | increases in their investor dividends for 57 consecutive years(2019). | ||
| 69 | |||
| 70 | ## Related Diversification | ||
| 71 | |||
| 72 | Diversification, the final strategy of the Ansoff Matrix, is more difficult than | ||
| 73 | the others since it involves exploring both new markets and new products. | ||
| 74 | Related diversification is a diversification strategy that closely relates to | ||
| 75 | the firm's core business. Coca-Cola's best example of related diversification is | ||
| 76 | its acquisition of Glaceau and Vitamin Water, which expanded their drinking | ||
| 77 | lines of business(2019). | ||
| 78 | |||
| 79 | ## Unrelated Diversification | ||
| 80 | |||
| 81 | Unrelated diversification is a diversification strategy that does not really | ||
| 82 | relate to the firm's core business but still diversifies their business | ||
| 83 | portfolio. A good example of this would be a coffee company who has decided to | ||
| 84 | enter the market for bicycle sales. The main purpose of this strategy is to an | ||
| 85 | extremely diverse company that will not go bankrupt if one market goes through | ||
| 86 | difficult times. However, this requires a lot of independent skills and heavy | ||
| 87 | investments since the company most likely cannot easily transfer knowledge | ||
| 88 | between the markets they compete in. | ||
| 89 | |||
| 90 | # Requirements for Success | ||
| 91 | |||
| 92 | To use the Ansoff Matrix framework, managers need to formulate corporate goals | ||
| 93 | and objectives. Without goals and direction, management frameworks do not | ||
| 94 | present much practical utility. Further, the Ansoff Matrix requires the managers | ||
| 95 | involved to make tactical decisions and create a path for the company to take | ||
| 96 | toward their goals. Lastly, both the Ansoff Matrix needs to consider both | ||
| 97 | internal and external perspectives throughout the strategy formulation process. | ||
| 98 | |||
| 99 | One interesting probability is that companies will be using multiple strategic | ||
| 100 | planning and management frameworks at the same time. While this may sound like | ||
| 101 | it could crowd the management process, there are numerous reasons to do so. For | ||
| 102 | example, the Ansoff Matrix and the Balanced Scorecard are relatively popular, | ||
| 103 | and they cover entirely different parts of a company's strategy. Using the | ||
| 104 | results from the Balanced Scorecard could inform a company of the potential | ||
| 105 | product and market demands, such as from customer or supplier survey results, to | ||
| 106 | help the company determine which Ansoff Matrix strategy to pursue. However, a | ||
| 107 | combined approach at this level would require mature frameworks and focused | ||
| 108 | managers who are able to strategize at a high level. | ||
| 109 | |||
| 110 | Lastly, it should be noted that the author of the Ansoff matrix, Igor Ansoff, | ||
| 111 | often used the term [paralysis by | ||
| 112 | analysis](https://en.wikipedia.org/wiki/Analysis_paralysis) to explain the | ||
| 113 | mistake of companies who overuse analysis and spend too much time planning. | ||
| 114 | Companies need to understand the utility of a strategic management framework | ||
| 115 | while ensuring that the company is poised to execute as efficiently as they have | ||
| 116 | planned. | ||
content/blog/2019-12-03-the-ansoff-matrix.org added +122
| @@ -0,0 +1,122 @@ | |||
| 1 | #+date: <2019-12-03> | ||
| 2 | #+title: The Ansoff Matrix | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | As the world of business evolves, managers must approach business | ||
| 9 | planning and strategy with a contemporary mindset. According to Dess, | ||
| 10 | McNamara, Eisner, and Lee, managers must be willing to adapt to the | ||
| 11 | modern business environment by going beyond "'incremental management', | ||
| 12 | whereby they view their job as making a series of small, minor changes | ||
| 13 | to improve the efficiency of the firm's operations"(2019). | ||
| 14 | |||
| 15 | One reason that strategic management is crucial is because most | ||
| 16 | businesses that fail in the United States each year fail due to a lack | ||
| 17 | of strategic focus or direction(2019). The rate of failure for | ||
| 18 | businesses with poor strategies shows that strategic planning and | ||
| 19 | management are crucial to a business's strength and longevity, injecting | ||
| 20 | the critical factors of growth and direction into a company's business | ||
| 21 | plan. | ||
| 22 | |||
| 23 | One of the most significant strategic planning and management frameworks | ||
| 24 | that companies can use is the | ||
| 25 | [[https://en.wikipedia.org/wiki/Ansoff_matrix][Ansoff Matrix]]. While | ||
| 26 | this framework has unique purposes and use-cases, it can effectively | ||
| 27 | help an organization grow and compete. Specifically, the Ansoff matrix | ||
| 28 | is one of the most effective frameworks for companies who want to focus | ||
| 29 | on increasing sales revenue or profitability(2019). | ||
| 30 | |||
| 31 | This framework uses a two-by-two figure to show the four strategic | ||
| 32 | options for companies to use in this framework: market penetration, | ||
| 33 | market development, product development, and diversification (see | ||
| 34 | *Figure 1*). The x-axis of the matrix focuses on the firm's markets and | ||
| 35 | also determines if the firm is looking to enter new markets or innovate | ||
| 36 | in its current markets. The y-axis of the matrix focuses on the firm's | ||
| 37 | products and determines if the firm wants to pursue strategies around | ||
| 38 | their existing products or explore new products. | ||
| 39 | |||
| 40 | * Strategic Options | ||
| 41 | |||
| 42 | ** Market Penetration | ||
| 43 | |||
| 44 | The most straightforward strategy in the Ansoff matrix is to focus on | ||
| 45 | existing products in existing markets, also known as market | ||
| 46 | penetration(2019). Companies such as Coca-Cola have used market | ||
| 47 | penetration successfully by investing a lot of money to get further | ||
| 48 | value out of their current markets. Coca-Cola does this by introducing | ||
| 49 | new features such as Christmas-themed bottles, personal names on the | ||
| 50 | bottles, and other marketing schemes. | ||
| 51 | |||
| 52 | ** Market Development | ||
| 53 | |||
| 54 | Market development extends existing products into new markets in an | ||
| 55 | attempt to increase the number of buyers. One interesting way that | ||
| 56 | Coca-Cola used this strategy comes from the stigma that Diet Coke is a | ||
| 57 | woman's drink(2019). Coca-Cola introduced Coca-Cola Zero, which | ||
| 58 | contained the same nutritional content as Diet Coke, but was packaged in | ||
| 59 | a dark black can to appear more "manly"(2019). | ||
| 60 | |||
| 61 | ** Product Development | ||
| 62 | |||
| 63 | Product development uses existing markets to introduce new products so | ||
| 64 | that the firm can better meet customer needs(2019). The extreme end of | ||
| 65 | diversification is home to companies such as Johnson & Johnson, a | ||
| 66 | healthcare company that has developed a business portfolio of more than | ||
| 67 | 60,000 different products(2019). Johnson & Johnson's dedication to | ||
| 68 | continuous diversification has led them to a balance sheet rating of | ||
| 69 | "AAA", industry recognition for diversification, and increases in their | ||
| 70 | investor dividends for 57 consecutive years(2019). | ||
| 71 | |||
| 72 | ** Related Diversification | ||
| 73 | |||
| 74 | Diversification, the final strategy of the Ansoff Matrix, is more | ||
| 75 | difficult than the others since it involves exploring both new markets | ||
| 76 | and new products. Related diversification is a diversification strategy | ||
| 77 | that closely relates to the firm's core business. Coca-Cola's best | ||
| 78 | example of related diversification is its acquisition of Glaceau and | ||
| 79 | Vitamin Water, which expanded their drinking lines of business(2019). | ||
| 80 | |||
| 81 | ** Unrelated Diversification | ||
| 82 | |||
| 83 | Unrelated diversification is a diversification strategy that does not | ||
| 84 | really relate to the firm's core business but still diversifies their | ||
| 85 | business portfolio. A good example of this would be a coffee company who | ||
| 86 | has decided to enter the market for bicycle sales. The main purpose of | ||
| 87 | this strategy is to an extremely diverse company that will not go | ||
| 88 | bankrupt if one market goes through difficult times. However, this | ||
| 89 | requires a lot of independent skills and heavy investments since the | ||
| 90 | company most likely cannot easily transfer knowledge between the markets | ||
| 91 | they compete in. | ||
| 92 | |||
| 93 | * Requirements for Success | ||
| 94 | |||
| 95 | To use the Ansoff Matrix framework, managers need to formulate corporate | ||
| 96 | goals and objectives. Without goals and direction, management frameworks | ||
| 97 | do not present much practical utility. Further, the Ansoff Matrix | ||
| 98 | requires the managers involved to make tactical decisions and create a | ||
| 99 | path for the company to take toward their goals. Lastly, both the Ansoff | ||
| 100 | Matrix needs to consider both internal and external perspectives | ||
| 101 | throughout the strategy formulation process. | ||
| 102 | |||
| 103 | One interesting probability is that companies will be using multiple | ||
| 104 | strategic planning and management frameworks at the same time. While | ||
| 105 | this may sound like it could crowd the management process, there are | ||
| 106 | numerous reasons to do so. For example, the Ansoff Matrix and the | ||
| 107 | Balanced Scorecard are relatively popular, and they cover entirely | ||
| 108 | different parts of a company's strategy. Using the results from the | ||
| 109 | Balanced Scorecard could inform a company of the potential product and | ||
| 110 | market demands, such as from customer or supplier survey results, to | ||
| 111 | help the company determine which Ansoff Matrix strategy to pursue. | ||
| 112 | However, a combined approach at this level would require mature | ||
| 113 | frameworks and focused managers who are able to strategize at a high | ||
| 114 | level. | ||
| 115 | |||
| 116 | Lastly, it should be noted that the author of the Ansoff matrix, Igor | ||
| 117 | Ansoff, often used the term | ||
| 118 | [[https://en.wikipedia.org/wiki/Analysis_paralysis][paralysis by | ||
| 119 | analysis]] to explain the mistake of companies who overuse analysis and | ||
| 120 | spend too much time planning. Companies need to understand the utility | ||
| 121 | of a strategic management framework while ensuring that the company is | ||
| 122 | poised to execute as efficiently as they have planned. | ||
content/blog/2019-12-16-password-security.md deleted −117
| @@ -1,117 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2019-12-16 | ||
| 3 | title = "Password Security" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Users | ||
| 9 | |||
| 10 | ## Why Does It Matter? | ||
| 11 | |||
| 12 | Information security, including passwords and identities, has become one of the | ||
| 13 | most important digital highlights of the last decade. With [billions of people | ||
| 14 | affected by data breaches each | ||
| 15 | year](https://www.usatoday.com/story/money/2018/12/28/data-breaches-2018-billions-hit-growing-number-cyberattacks/2413411002/), | ||
| 16 | there's a greater need to introduce strong information security systems. If you | ||
| 17 | think you've been part of a breach, or you want to check and see, you can use | ||
| 18 | [Have I Been Pwned](https://haveibeenpwned.com/) to see if your email has been | ||
| 19 | involved in any public breaches. Remember that there's a possibility that a | ||
| 20 | company experienced a breach and did not report it to anyone. | ||
| 21 | |||
| 22 | ## How Do I Protect Myself? | ||
| 23 | |||
| 24 | The first place to start with any personal security check-up is to gather a list | ||
| 25 | of all the different websites, apps, or programs that require you to have login | ||
| 26 | credentials. Optionally, once you know where your information is being stored, | ||
| 27 | you can sort the list from the most-important items such as banks or government | ||
| 28 | logins to less important items such as your favorite meme site. You will want to | ||
| 29 | ensure that your critical logins are secure before getting to the others. | ||
| 30 | |||
| 31 | Once you think you have a good idea of all your different authentication | ||
| 32 | methods, I recommend using a password manager such as | ||
| 33 | [Bitwarden](https://bitwarden.com/). Using a password manager allows you to | ||
| 34 | automatically save your logins, create randomized passwords, and transfer | ||
| 35 | passwords across devices. However, you'll need to memorize your "vault password" | ||
| 36 | that allows you to open the password manager. It's important to make this | ||
| 37 | something hard to guess since it would allow anyone who has it to access every | ||
| 38 | password you've stored in there. | ||
| 39 | |||
| 40 | Personally, I recommend using a | ||
| 41 | [passphrase](https://en.wikipedia.org/wiki/Passphrase) instead of a | ||
| 42 | [password](https://en.wikipedia.org/wiki/Password) for your vault password. | ||
| 43 | Instead of using a string of characters (whether random or simple), use a phrase | ||
| 44 | and add in symbols and a number. For example, your vault password could be | ||
| 45 | `Racing-Alphabet-Gourd-Parrot3`. Swap the symbols out for whichever symbol you | ||
| 46 | want, move the number around, and fine-tune the passphrase until you are | ||
| 47 | confident that you can remember it whenever necessary. | ||
| 48 | |||
| 49 | Once you've stored your passwords, make sure you continually check up on your | ||
| 50 | account and make sure you aren't following bad password practices. Krebs on | ||
| 51 | Security has a great [blog post on password | ||
| 52 | recommendations](https://krebsonsecurity.com/password-dos-and-donts/). Any time | ||
| 53 | that a data breach happens, make sure you check to see if you were included, and | ||
| 54 | if you need to reset any account passwords. | ||
| 55 | |||
| 56 | # Developers | ||
| 57 | |||
| 58 | ## What Are the Basic Requirements? | ||
| 59 | |||
| 60 | When developing any password-protected application, there are a few basic rules | ||
| 61 | that anyone should follow even if they do not follow any official guidelines | ||
| 62 | such as NIST. The foremost practice is to require users to use passwords that | ||
| 63 | are at least 8 characters and cannot easily be guessed. This sounds extremely | ||
| 64 | simple, but it requires quite a few different strategies. First, the application | ||
| 65 | should check the potential passwords against a dictionary of insecure passwords | ||
| 66 | such `password`, `1234abc`, or `application_name`. | ||
| 67 | |||
| 68 | Next, the application should offer guidance on the strength of passwords being | ||
| 69 | entered during enrollment. Further, NIST officially recommends **not** | ||
| 70 | implementing any composition rules that make passwords hard to remember (e.g. | ||
| 71 | passwords with letters, numbers, and special characters) and instead encouraging | ||
| 72 | the use of long pass phrases which can include spaces. It should be noted that | ||
| 73 | to be able to keep spaces within passwords, all unicode characters should be | ||
| 74 | supported, and passwords should not be truncated. | ||
| 75 | |||
| 76 | ## What Does NIST Recommend? | ||
| 77 | |||
| 78 | The National Institute of Standards and Technology | ||
| 79 | ([NIST](https://www.nist.gov)) in the US Department of Commerce regularly | ||
| 80 | publishes information around information security and digital identity | ||
| 81 | guidelines. Recently, NIST published [Special Publication | ||
| 82 | 800-63b](https://pages.nist.gov/800-63-3/sp800-63b.html): Digital Identity | ||
| 83 | Guidelines and Authentication and Lifecycle Management. | ||
| 84 | |||
| 85 | > A Memorized Secret authenticator - commonly referred to as a password or, if | ||
| 86 | > numeric, a PIN - is a secret value intended to be chosen and memorized by the | ||
| 87 | > user. Memorized secrets need to be of sufficient complexity and secrecy that | ||
| 88 | > it would be impractical for an attacker to guess or otherwise discover the | ||
| 89 | > correct secret value. A memorized secret is something you know. | ||
| 90 | > | ||
| 91 | > - NIST Special Publication 800-63B | ||
| 92 | |||
| 93 | NIST offers a lot of guidance on passwords, but I'm going to highlight just a | ||
| 94 | few of the important factors: | ||
| 95 | |||
| 96 | - Require passwords to be a minimum of 8 characters (6 characters if randomly | ||
| 97 | generated and be generated using an approved random bit generator). | ||
| 98 | - Compare potential passwords against a list that contains values known to be | ||
| 99 | commonly-used, expected, or compromised. | ||
| 100 | - Offer guidance on password strength, such as a strength meter. | ||
| 101 | - Implement a rate-limiting mechanism to limit the number of failed | ||
| 102 | authentication attempts for each user account. | ||
| 103 | - Do not require composition rules for passwords and do not require passwords | ||
| 104 | to be changed periodically (unless compromised). | ||
| 105 | - Allow pasting of user identification and passwords to facilitate the use of | ||
| 106 | password managers. | ||
| 107 | - Allow users to view the password as it is being entered. | ||
| 108 | - Use secure forms of communication and storage, including salting and hashing | ||
| 109 | passwords using a one-way key derivation function. | ||
| 110 | |||
| 111 | NIST offers further guidance on other devices that require specific security | ||
| 112 | policies, querying for passwords, and more. All the information discussed so far | ||
| 113 | comes from [NIST SP800-63b](https://pages.nist.gov/800-63-3/sp800-63b.html) but | ||
| 114 | NIST offers a lot of information on digital identities, enrollment, identity | ||
| 115 | proofing, authentication, lifecycle management, federation, and assertions in | ||
| 116 | the total [NIST SP800-63 Digital Identity | ||
| 117 | Guidelines](https://pages.nist.gov/800-63-3/). | ||
content/blog/2019-12-16-password-security.org added +127
| @@ -0,0 +1,127 @@ | |||
| 1 | #+date: <2019-12-16> | ||
| 2 | #+title: Password Security | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Users | ||
| 7 | |||
| 8 | ** Why Does It Matter? | ||
| 9 | |||
| 10 | Information security, including passwords and identities, has become one | ||
| 11 | of the most important digital highlights of the last decade. With | ||
| 12 | [[https://www.usatoday.com/story/money/2018/12/28/data-breaches-2018-billions-hit-growing-number-cyberattacks/2413411002/][billions | ||
| 13 | of people affected by data breaches each year]], there's a greater need | ||
| 14 | to introduce strong information security systems. If you think you've | ||
| 15 | been part of a breach, or you want to check and see, you can use | ||
| 16 | [[https://haveibeenpwned.com/][Have I Been Pwned]] to see if your email | ||
| 17 | has been involved in any public breaches. Remember that there's a | ||
| 18 | possibility that a company experienced a breach and did not report it to | ||
| 19 | anyone. | ||
| 20 | |||
| 21 | ** How Do I Protect Myself? | ||
| 22 | |||
| 23 | The first place to start with any personal security check-up is to | ||
| 24 | gather a list of all the different websites, apps, or programs that | ||
| 25 | require you to have login credentials. Optionally, once you know where | ||
| 26 | your information is being stored, you can sort the list from the | ||
| 27 | most-important items such as banks or government logins to less | ||
| 28 | important items such as your favorite meme site. You will want to ensure | ||
| 29 | that your critical logins are secure before getting to the others. | ||
| 30 | |||
| 31 | Once you think you have a good idea of all your different authentication | ||
| 32 | methods, I recommend using a password manager such as | ||
| 33 | [[https://bitwarden.com/][Bitwarden]]. Using a password manager allows | ||
| 34 | you to automatically save your logins, create randomized passwords, and | ||
| 35 | transfer passwords across devices. However, you'll need to memorize your | ||
| 36 | "vault password" that allows you to open the password manager. It's | ||
| 37 | important to make this something hard to guess since it would allow | ||
| 38 | anyone who has it to access every password you've stored in there. | ||
| 39 | |||
| 40 | Personally, I recommend using a | ||
| 41 | [[https://en.wikipedia.org/wiki/Passphrase][passphrase]] instead of a | ||
| 42 | [[https://en.wikipedia.org/wiki/Password][password]] for your vault | ||
| 43 | password. Instead of using a string of characters (whether random or | ||
| 44 | simple), use a phrase and add in symbols and a number. For example, your | ||
| 45 | vault password could be =Racing-Alphabet-Gourd-Parrot3=. Swap the | ||
| 46 | symbols out for whichever symbol you want, move the number around, and | ||
| 47 | fine-tune the passphrase until you are confident that you can remember | ||
| 48 | it whenever necessary. | ||
| 49 | |||
| 50 | Once you've stored your passwords, make sure you continually check up on | ||
| 51 | your account and make sure you aren't following bad password practices. | ||
| 52 | Krebs on Security has a great | ||
| 53 | [[https://krebsonsecurity.com/password-dos-and-donts/][blog post on | ||
| 54 | password recommendations]]. Any time that a data breach happens, make | ||
| 55 | sure you check to see if you were included, and if you need to reset any | ||
| 56 | account passwords. | ||
| 57 | |||
| 58 | * Developers | ||
| 59 | |||
| 60 | ** What Are the Basic Requirements? | ||
| 61 | |||
| 62 | When developing any password-protected application, there are a few | ||
| 63 | basic rules that anyone should follow even if they do not follow any | ||
| 64 | official guidelines such as NIST. The foremost practice is to require | ||
| 65 | users to use passwords that are at least 8 characters and cannot easily | ||
| 66 | be guessed. This sounds extremely simple, but it requires quite a few | ||
| 67 | different strategies. First, the application should check the potential | ||
| 68 | passwords against a dictionary of insecure passwords such =password=, | ||
| 69 | =1234abc=, or =application_name=. | ||
| 70 | |||
| 71 | Next, the application should offer guidance on the strength of passwords | ||
| 72 | being entered during enrollment. Further, NIST officially recommends | ||
| 73 | *not* implementing any composition rules that make passwords hard to | ||
| 74 | remember (e.g. passwords with letters, numbers, and special characters) | ||
| 75 | and instead encouraging the use of long pass phrases which can include | ||
| 76 | spaces. It should be noted that to be able to keep spaces within | ||
| 77 | passwords, all unicode characters should be supported, and passwords | ||
| 78 | should not be truncated. | ||
| 79 | |||
| 80 | ** What Does NIST Recommend? | ||
| 81 | |||
| 82 | The National Institute of Standards and Technology | ||
| 83 | ([[https://www.nist.gov][NIST]]) in the US Department of Commerce | ||
| 84 | regularly publishes information around information security and digital | ||
| 85 | identity guidelines. Recently, NIST published | ||
| 86 | [[https://pages.nist.gov/800-63-3/sp800-63b.html][Special Publication | ||
| 87 | 800-63b]]: Digital Identity Guidelines and Authentication and Lifecycle | ||
| 88 | Management. | ||
| 89 | |||
| 90 | #+begin_quote | ||
| 91 | A Memorized Secret authenticator - commonly referred to as a password | ||
| 92 | or, if numeric, a PIN - is a secret value intended to be chosen and | ||
| 93 | memorized by the user. Memorized secrets need to be of sufficient | ||
| 94 | complexity and secrecy that it would be impractical for an attacker to | ||
| 95 | guess or otherwise discover the correct secret value. A memorized secret | ||
| 96 | is something you know. | ||
| 97 | |||
| 98 | - NIST Special Publication 800-63B | ||
| 99 | #+end_quote | ||
| 100 | |||
| 101 | NIST offers a lot of guidance on passwords, but I'm going to highlight | ||
| 102 | just a few of the important factors: | ||
| 103 | |||
| 104 | - Require passwords to be a minimum of 8 characters (6 characters if | ||
| 105 | randomly generated and be generated using an approved random bit | ||
| 106 | generator). | ||
| 107 | - Compare potential passwords against a list that contains values known | ||
| 108 | to be commonly-used, expected, or compromised. | ||
| 109 | - Offer guidance on password strength, such as a strength meter. | ||
| 110 | - Implement a rate-limiting mechanism to limit the number of failed | ||
| 111 | authentication attempts for each user account. | ||
| 112 | - Do not require composition rules for passwords and do not require | ||
| 113 | passwords to be changed periodically (unless compromised). | ||
| 114 | - Allow pasting of user identification and passwords to facilitate the | ||
| 115 | use of password managers. | ||
| 116 | - Allow users to view the password as it is being entered. | ||
| 117 | - Use secure forms of communication and storage, including salting and | ||
| 118 | hashing passwords using a one-way key derivation function. | ||
| 119 | |||
| 120 | NIST offers further guidance on other devices that require specific | ||
| 121 | security policies, querying for passwords, and more. All the information | ||
| 122 | discussed so far comes from | ||
| 123 | [[https://pages.nist.gov/800-63-3/sp800-63b.html][NIST SP800-63b]] but | ||
| 124 | NIST offers a lot of information on digital identities, enrollment, | ||
| 125 | identity proofing, authentication, lifecycle management, federation, and | ||
| 126 | assertions in the total [[https://pages.nist.gov/800-63-3/][NIST | ||
| 127 | SP800-63 Digital Identity Guidelines]]. | ||
content/blog/2020-01-25-linux-software.md deleted −246
| @@ -1,246 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-01-25 | ||
| 3 | title = "My Favorite Linux Software" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # GUI Applications | ||
| 9 | |||
| 10 | ## Etcher | ||
| 11 | |||
| 12 | [Etcher](https://www.balena.io/etcher/) is a quick and easy way to burn ISO | ||
| 13 | images to CDs and USB devices. There are two different ways you can install this | ||
| 14 | program. First, you can navigate to the [official | ||
| 15 | website](https://www.balena.io/etcher/) and download the AppImage file, which | ||
| 16 | can run without installation. | ||
| 17 | |||
| 18 | However, AppImage files are not executable by default, so you'll either need to | ||
| 19 | right-click to open the properties of the file and click the "Allow executing | ||
| 20 | file as program" box in the Permissions tab or use the following command: | ||
| 21 | |||
| 22 | ```sh | ||
| 23 | chmod u+x FILE_NAME | ||
| 24 | ``` | ||
| 25 | |||
| 26 | If you don't like AppImage files or just prefer repositories, you can use the | ||
| 27 | following commands to add the author's repository and install it through the | ||
| 28 | command-line only. | ||
| 29 | |||
| 30 | First, you'll have to echo the repo and write it to a list file: | ||
| 31 | |||
| 32 | ```sh | ||
| 33 | echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list | ||
| 34 | ``` | ||
| 35 | |||
| 36 | Next, add the application keys to Ubuntu's keyring: | ||
| 37 | |||
| 38 | ```sh | ||
| 39 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61 | ||
| 40 | ``` | ||
| 41 | |||
| 42 | Finally, update the repositories and install the app. | ||
| 43 | |||
| 44 | ```sh | ||
| 45 | sudo apt update && sudo apt install balena-etcher-electron | ||
| 46 | ``` | ||
| 47 | |||
| 48 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | ||
| 49 | |||
| 50 | ```sh | ||
| 51 | sudo pacman -S etcher | ||
| 52 | ``` | ||
| 53 | |||
| 54 | ## Atom | ||
| 55 | |||
| 56 | [Atom](https://atom.io) is the self-proclaimed "hackable text editor for the | ||
| 57 | 21st century". This text editor is made by GitHub, [now owned by | ||
| 58 | Microsoft](https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/), | ||
| 59 | and has some of the best add-ons available to customize the layout and abilities | ||
| 60 | of the app. | ||
| 61 | |||
| 62 | First, add the Atom repository to your sources. | ||
| 63 | |||
| 64 | ```sh | ||
| 65 | sudo add-apt-repository ppa:webupd8team/atom | ||
| 66 | ``` | ||
| 67 | |||
| 68 | Next, update your package listings and install atom. | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | sudo apt update && sudo apt install atom | ||
| 72 | ``` | ||
| 73 | |||
| 74 | If you have issues updating your packages with the Atom repository, you'll need | ||
| 75 | to use the snap package described below instead of the repository. To remove the | ||
| 76 | repository we just added, use this command: | ||
| 77 | |||
| 78 | ```sh | ||
| 79 | sudo add-apt-repository -r ppa:webupd8team/atom | ||
| 80 | ``` | ||
| 81 | |||
| 82 | You can also install Atom as a snap package, but it must be installed with the | ||
| 83 | `--classic` flag. A [full explanation is | ||
| 84 | available](https://language-bash.com/blog/how-to-snap-introducing-classic-confinement) | ||
| 85 | if you'd like to read more about why you need the classic flag. | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | snap install atom --classic | ||
| 89 | ``` | ||
| 90 | |||
| 91 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | ||
| 92 | |||
| 93 | ```sh | ||
| 94 | sudo pacman -S atom | ||
| 95 | ``` | ||
| 96 | |||
| 97 | ## Visual Studio Code | ||
| 98 | |||
| 99 | [Visual Studio Code](https://code.visualstudio.com) is yet another fantastic | ||
| 100 | choice for programming on Linux, especially if you need those extra add-ons to | ||
| 101 | spice up your late-night coding sessions. The theme used in the screenshot is | ||
| 102 | [Mars](https://marketplace.visualstudio.com/items?itemName=EliverLara.mars) by | ||
| 103 | theme creator [Eliver Lara](https://github.com/EliverLara), who makes a ton of | ||
| 104 | great themes for VS Code, Atom, and various Linux desktop environments. | ||
| 105 | |||
| 106 | To install VS Code, you'll need to download the `.deb` file from the official | ||
| 107 | website. Once you've downloaded the file, either double-click it to install | ||
| 108 | through the Software Center or run the following command: | ||
| 109 | |||
| 110 | ```sh | ||
| 111 | sudo dpkg -i FILE_NAME.deb | ||
| 112 | ``` | ||
| 113 | |||
| 114 | You can also install VS Code as a snap package, but it must be installed with | ||
| 115 | the `--classic` flag. A [full explanation is | ||
| 116 | available](https://language-bash.com/blog/how-to-snap-introducing-classic-confinement) | ||
| 117 | if you'd like to read more about why you need the classic flag. | ||
| 118 | |||
| 119 | ```sh | ||
| 120 | snap install code --classic | ||
| 121 | ``` | ||
| 122 | |||
| 123 | Using Arch, Manjaro, or another distro using the AUR? Use these commands | ||
| 124 | instead: | ||
| 125 | |||
| 126 | ```sh | ||
| 127 | sudo pacman -S yay binutils make gcc pkg-config fakeroot yay -S visual-studio-code-bin | ||
| 128 | ``` | ||
| 129 | |||
| 130 | ## GNOME Tweaks | ||
| 131 | |||
| 132 | [Gnome Tweaks](https://gitlab.gnome.org/GNOME/gnome-tweaks) is the ultimate tool | ||
| 133 | to use if you want to customize your GNOME desktop environment. This is how you | ||
| 134 | can switch application themes (GTK), shell themes, icons, fonts, and more. To | ||
| 135 | install GNOME Tweaks on Ubuntu, you just need to install the official package. | ||
| 136 | |||
| 137 | ```sh | ||
| 138 | sudo apt install gnome-tweaks | ||
| 139 | ``` | ||
| 140 | |||
| 141 | If you've installed Manjaro or Arch with Gnome, you should have the tweak tool | ||
| 142 | pre-installed. If you're on Fedora, this tool is available as an official | ||
| 143 | package: | ||
| 144 | |||
| 145 | ```sh | ||
| 146 | sudo dnf install gnome-tweaks | ||
| 147 | ``` | ||
| 148 | |||
| 149 | ## Steam | ||
| 150 | |||
| 151 | [Steam](https://steampowered.com) is one of the most popular gaming libraries | ||
| 152 | for computers and is one of the main reasons that many people have been able to | ||
| 153 | switch to Linux in recent years, thanks to Steam Proton, which makes it easier | ||
| 154 | to play games not officially created for Linux platforms. | ||
| 155 | |||
| 156 | To install Steam on Ubuntu, you just need to install the official package. | ||
| 157 | |||
| 158 | ```sh | ||
| 159 | sudo apt install steam-installer | ||
| 160 | ``` | ||
| 161 | |||
| 162 | For Arch-based systems, you'll simply need to install the `steam` package. | ||
| 163 | However, this requires that you enable the `multilib` source. To do so, use the | ||
| 164 | following command: | ||
| 165 | |||
| 166 | ```sh | ||
| 167 | sudo nano /etc/pacman.conf | ||
| 168 | ``` | ||
| 169 | |||
| 170 | Now, scroll down and uncomment the `multilib` section. | ||
| 171 | |||
| 172 | ```config | ||
| 173 | # Before: | ||
| 174 | #[multilib] | ||
| 175 | #Include = /etc/pacman.d/mirrorlist | ||
| 176 | |||
| 177 | # After: | ||
| 178 | [multilib] | ||
| 179 | Include = /etc/pacman.d/mirrorlist | ||
| 180 | ``` | ||
| 181 | |||
| 182 | Finally, install the program: | ||
| 183 | |||
| 184 | ```sh | ||
| 185 | sudo pacman -S steam | ||
| 186 | ``` | ||
| 187 | |||
| 188 | [Problem Launching Steam Games? Click | ||
| 189 | Here.](./2020-01-26-steam-on-ntfs-drives.html) | ||
| 190 | |||
| 191 | # Command-Line Packages | ||
| 192 | |||
| 193 | ## neofetch | ||
| 194 | |||
| 195 | [Neofetch](https://github.com/dylanaraps/neofetch) is a customizable tool used | ||
| 196 | in the command-line to show system information. This is exceptionally useful if | ||
| 197 | you want to see your system's information quickly without the clutter of some | ||
| 198 | resource-heavy GUI apps. | ||
| 199 | |||
| 200 | This is an official package if you're running Ubuntu 17.04 or later, so simply | ||
| 201 | use the following command: | ||
| 202 | |||
| 203 | ```sh | ||
| 204 | sudo apt install neofetch | ||
| 205 | ``` | ||
| 206 | |||
| 207 | If you're running Ubuntu 16.10 or earlier, you'll have to use a series of | ||
| 208 | commands: | ||
| 209 | |||
| 210 | ```sh | ||
| 211 | sudo add-apt-repository ppa:dawidd0811/neofetch; sudo apt update; sudo apt install neofetch | ||
| 212 | ``` | ||
| 213 | |||
| 214 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | ||
| 215 | |||
| 216 | ```sh | ||
| 217 | sudo pacman -S neofetch | ||
| 218 | ``` | ||
| 219 | |||
| 220 | ## yt-dlp | ||
| 221 | |||
| 222 | [yt-dlp](https://github.com/yt-dlp/yt-dlp) is an extremely handy command-line | ||
| 223 | tool that allows you to download video or audio files from various websites, | ||
| 224 | such as YouTube. There are a ton of different options when running this package, | ||
| 225 | so be sure to run `yt-dlp --help` first to look through everything you can do | ||
| 226 | (or give up and search for the best config online). | ||
| 227 | |||
| 228 | While this shouldn't be a problem for most users, yt-dlp requires Python 2.6, | ||
| 229 | 2.7, or 3.2+ to work correctly, so install Python if you don't have it already. | ||
| 230 | You can check to see if you have Python installed by running: | ||
| 231 | |||
| 232 | ```sh | ||
| 233 | python -V | ||
| 234 | ``` | ||
| 235 | |||
| 236 | To get the youtube-dl package, simply curl the URL and output the results. | ||
| 237 | |||
| 238 | ```sh | ||
| 239 | sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp | ||
| 240 | ``` | ||
| 241 | |||
| 242 | Finally, make the file executable so that it can be run from the command-line. | ||
| 243 | |||
| 244 | ```sh | ||
| 245 | sudo chmod a+rx /usr/local/bin/yt-dlp | ||
| 246 | ``` | ||
content/blog/2020-01-25-linux-software.org added +259
| @@ -0,0 +1,259 @@ | |||
| 1 | #+date: <2020-01-25> | ||
| 2 | #+title: My Favorite Linux Software | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * GUI Applications | ||
| 7 | |||
| 8 | ** Etcher | ||
| 9 | |||
| 10 | [[https://www.balena.io/etcher/][Etcher]] is a quick and easy way to | ||
| 11 | burn ISO images to CDs and USB devices. There are two different ways you | ||
| 12 | can install this program. First, you can navigate to the | ||
| 13 | [[https://www.balena.io/etcher/][official website]] and download the | ||
| 14 | AppImage file, which can run without installation. | ||
| 15 | |||
| 16 | However, AppImage files are not executable by default, so you'll either | ||
| 17 | need to right-click to open the properties of the file and click the | ||
| 18 | "Allow executing file as program" box in the Permissions tab or use the | ||
| 19 | following command: | ||
| 20 | |||
| 21 | #+begin_src sh | ||
| 22 | chmod u+x FILE_NAME | ||
| 23 | #+end_src | ||
| 24 | |||
| 25 | If you don't like AppImage files or just prefer repositories, you can | ||
| 26 | use the following commands to add the author's repository and install it | ||
| 27 | through the command-line only. | ||
| 28 | |||
| 29 | First, you'll have to echo the repo and write it to a list file: | ||
| 30 | |||
| 31 | #+begin_src sh | ||
| 32 | echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list | ||
| 33 | #+end_src | ||
| 34 | |||
| 35 | Next, add the application keys to Ubuntu's keyring: | ||
| 36 | |||
| 37 | #+begin_src sh | ||
| 38 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61 | ||
| 39 | #+end_src | ||
| 40 | |||
| 41 | Finally, update the repositories and install the app. | ||
| 42 | |||
| 43 | #+begin_src sh | ||
| 44 | sudo apt update && sudo apt install balena-etcher-electron | ||
| 45 | #+end_src | ||
| 46 | |||
| 47 | Using Arch, Manjaro, or another distro using the AUR? Use this command | ||
| 48 | instead: | ||
| 49 | |||
| 50 | #+begin_src sh | ||
| 51 | sudo pacman -S etcher | ||
| 52 | #+end_src | ||
| 53 | |||
| 54 | ** Atom | ||
| 55 | |||
| 56 | [[https://atom.io][Atom]] is the self-proclaimed "hackable text editor | ||
| 57 | for the 21st century". This text editor is made by GitHub, | ||
| 58 | [[https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/][now | ||
| 59 | owned by Microsoft]], and has some of the best add-ons available to | ||
| 60 | customize the layout and abilities of the app. | ||
| 61 | |||
| 62 | First, add the Atom repository to your sources. | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | sudo add-apt-repository ppa:webupd8team/atom | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | Next, update your package listings and install atom. | ||
| 69 | |||
| 70 | #+begin_src sh | ||
| 71 | sudo apt update && sudo apt install atom | ||
| 72 | #+end_src | ||
| 73 | |||
| 74 | If you have issues updating your packages with the Atom repository, | ||
| 75 | you'll need to use the snap package described below instead of the | ||
| 76 | repository. To remove the repository we just added, use this command: | ||
| 77 | |||
| 78 | #+begin_src sh | ||
| 79 | sudo add-apt-repository -r ppa:webupd8team/atom | ||
| 80 | #+end_src | ||
| 81 | |||
| 82 | You can also install Atom as a snap package, but it must be installed | ||
| 83 | with the =--classic= flag. A | ||
| 84 | [[https://language-bash.com/blog/how-to-snap-introducing-classic-confinement][full | ||
| 85 | explanation is available]] if you'd like to read more about why you need | ||
| 86 | the classic flag. | ||
| 87 | |||
| 88 | #+begin_src sh | ||
| 89 | snap install atom --classic | ||
| 90 | #+end_src | ||
| 91 | |||
| 92 | Using Arch, Manjaro, or another distro using the AUR? Use this command | ||
| 93 | instead: | ||
| 94 | |||
| 95 | #+begin_src sh | ||
| 96 | sudo pacman -S atom | ||
| 97 | #+end_src | ||
| 98 | |||
| 99 | ** Visual Studio Code | ||
| 100 | |||
| 101 | [[https://code.visualstudio.com][Visual Studio Code]] is yet another | ||
| 102 | fantastic choice for programming on Linux, especially if you need those | ||
| 103 | extra add-ons to spice up your late-night coding sessions. The theme | ||
| 104 | used in the screenshot is | ||
| 105 | [[https://marketplace.visualstudio.com/items?itemName=EliverLara.mars][Mars]] | ||
| 106 | by theme creator [[https://github.com/EliverLara][Eliver Lara]], who | ||
| 107 | makes a ton of great themes for VS Code, Atom, and various Linux desktop | ||
| 108 | environments. | ||
| 109 | |||
| 110 | To install VS Code, you'll need to download the =.deb= file from the | ||
| 111 | official website. Once you've downloaded the file, either double-click | ||
| 112 | it to install through the Software Center or run the following command: | ||
| 113 | |||
| 114 | #+begin_src sh | ||
| 115 | sudo dpkg -i FILE_NAME.deb | ||
| 116 | #+end_src | ||
| 117 | |||
| 118 | You can also install VS Code as a snap package, but it must be installed | ||
| 119 | with the =--classic= flag. A | ||
| 120 | [[https://language-bash.com/blog/how-to-snap-introducing-classic-confinement][full | ||
| 121 | explanation is available]] if you'd like to read more about why you need | ||
| 122 | the classic flag. | ||
| 123 | |||
| 124 | #+begin_src sh | ||
| 125 | snap install code --classic | ||
| 126 | #+end_src | ||
| 127 | |||
| 128 | Using Arch, Manjaro, or another distro using the AUR? Use these commands | ||
| 129 | instead: | ||
| 130 | |||
| 131 | #+begin_src sh | ||
| 132 | sudo pacman -S yay binutils make gcc pkg-config fakeroot yay -S visual-studio-code-bin | ||
| 133 | #+end_src | ||
| 134 | |||
| 135 | ** GNOME Tweaks | ||
| 136 | |||
| 137 | [[https://gitlab.gnome.org/GNOME/gnome-tweaks][Gnome Tweaks]] is the | ||
| 138 | ultimate tool to use if you want to customize your GNOME desktop | ||
| 139 | environment. This is how you can switch application themes (GTK), shell | ||
| 140 | themes, icons, fonts, and more. To install GNOME Tweaks on Ubuntu, you | ||
| 141 | just need to install the official package. | ||
| 142 | |||
| 143 | #+begin_src sh | ||
| 144 | sudo apt install gnome-tweaks | ||
| 145 | #+end_src | ||
| 146 | |||
| 147 | If you've installed Manjaro or Arch with Gnome, you should have the | ||
| 148 | tweak tool pre-installed. If you're on Fedora, this tool is available as | ||
| 149 | an official package: | ||
| 150 | |||
| 151 | #+begin_src sh | ||
| 152 | sudo dnf install gnome-tweaks | ||
| 153 | #+end_src | ||
| 154 | |||
| 155 | ** Steam | ||
| 156 | |||
| 157 | [[https://steampowered.com][Steam]] is one of the most popular gaming | ||
| 158 | libraries for computers and is one of the main reasons that many people | ||
| 159 | have been able to switch to Linux in recent years, thanks to Steam | ||
| 160 | Proton, which makes it easier to play games not officially created for | ||
| 161 | Linux platforms. | ||
| 162 | |||
| 163 | To install Steam on Ubuntu, you just need to install the official | ||
| 164 | package. | ||
| 165 | |||
| 166 | #+begin_src sh | ||
| 167 | sudo apt install steam-installer | ||
| 168 | #+end_src | ||
| 169 | |||
| 170 | For Arch-based systems, you'll simply need to install the =steam= | ||
| 171 | package. However, this requires that you enable the =multilib= source. | ||
| 172 | To do so, use the following command: | ||
| 173 | |||
| 174 | #+begin_src sh | ||
| 175 | sudo nano /etc/pacman.conf | ||
| 176 | #+end_src | ||
| 177 | |||
| 178 | Now, scroll down and uncomment the =multilib= section. | ||
| 179 | |||
| 180 | #+begin_src config | ||
| 181 | # Before: | ||
| 182 | #[multilib] | ||
| 183 | #Include = /etc/pacman.d/mirrorlist | ||
| 184 | |||
| 185 | # After: | ||
| 186 | [multilib] | ||
| 187 | Include = /etc/pacman.d/mirrorlist | ||
| 188 | #+end_src | ||
| 189 | |||
| 190 | Finally, install the program: | ||
| 191 | |||
| 192 | #+begin_src sh | ||
| 193 | sudo pacman -S steam | ||
| 194 | #+end_src | ||
| 195 | |||
| 196 | [[./2020-01-26-steam-on-ntfs-drives.html][Problem Launching Steam Games? | ||
| 197 | Click Here.]] | ||
| 198 | |||
| 199 | * Command-Line Packages | ||
| 200 | |||
| 201 | ** neofetch | ||
| 202 | |||
| 203 | [[https://github.com/dylanaraps/neofetch][Neofetch]] is a customizable | ||
| 204 | tool used in the command-line to show system information. This is | ||
| 205 | exceptionally useful if you want to see your system's information | ||
| 206 | quickly without the clutter of some resource-heavy GUI apps. | ||
| 207 | |||
| 208 | This is an official package if you're running Ubuntu 17.04 or later, so | ||
| 209 | simply use the following command: | ||
| 210 | |||
| 211 | #+begin_src sh | ||
| 212 | sudo apt install neofetch | ||
| 213 | #+end_src | ||
| 214 | |||
| 215 | If you're running Ubuntu 16.10 or earlier, you'll have to use a series | ||
| 216 | of commands: | ||
| 217 | |||
| 218 | #+begin_src sh | ||
| 219 | sudo add-apt-repository ppa:dawidd0811/neofetch; sudo apt update; sudo apt install neofetch | ||
| 220 | #+end_src | ||
| 221 | |||
| 222 | Using Arch, Manjaro, or another distro using the AUR? Use this command | ||
| 223 | instead: | ||
| 224 | |||
| 225 | #+begin_src sh | ||
| 226 | sudo pacman -S neofetch | ||
| 227 | #+end_src | ||
| 228 | |||
| 229 | ** yt-dlp | ||
| 230 | |||
| 231 | [[https://github.com/yt-dlp/yt-dlp][yt-dlp]] is an extremely handy | ||
| 232 | command-line tool that allows you to download video or audio files from | ||
| 233 | various websites, such as YouTube. There are a ton of different options | ||
| 234 | when running this package, so be sure to run =yt-dlp --help= first to | ||
| 235 | look through everything you can do (or give up and search for the best | ||
| 236 | config online). | ||
| 237 | |||
| 238 | While this shouldn't be a problem for most users, yt-dlp requires Python | ||
| 239 | 2.6, 2.7, or 3.2+ to work correctly, so install Python if you don't have | ||
| 240 | it already. You can check to see if you have Python installed by | ||
| 241 | running: | ||
| 242 | |||
| 243 | #+begin_src sh | ||
| 244 | python -V | ||
| 245 | #+end_src | ||
| 246 | |||
| 247 | To get the youtube-dl package, simply curl the URL and output the | ||
| 248 | results. | ||
| 249 | |||
| 250 | #+begin_src sh | ||
| 251 | sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp | ||
| 252 | #+end_src | ||
| 253 | |||
| 254 | Finally, make the file executable so that it can be run from the | ||
| 255 | command-line. | ||
| 256 | |||
| 257 | #+begin_src sh | ||
| 258 | sudo chmod a+rx /usr/local/bin/yt-dlp | ||
| 259 | #+end_src | ||
content/blog/2020-01-26-steam-on-ntfs.md deleted −91
| @@ -1,91 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-01-26 | ||
| 3 | title = "Linux Gaming Tweak: Steam on NTFS Drives" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Auto-Mount Steam Drives | ||
| 9 | |||
| 10 | If you want to see how to install Steam on Linux, see my other post: [Linux | ||
| 11 | Software](../linux-software/). | ||
| 12 | |||
| 13 | Are you having trouble launching games, even though they've installed correctly? | ||
| 14 | This may happen if you're storing your games on an NTFS-formatted drive. This | ||
| 15 | shouldn't be an issue if you're storing your games on the same drive that Steam | ||
| 16 | is on, but some gamers prefer to put Steam on their main drive and game files on | ||
| 17 | another SSD or HDD. | ||
| 18 | |||
| 19 | To fix this problem, you'll need to try a few things. First, you'll need to | ||
| 20 | install the `ntfs-3g` package, which is meant for better interoperability with | ||
| 21 | Linux. | ||
| 22 | |||
| 23 | ```sh | ||
| 24 | sudo apt install ntfs-3g | ||
| 25 | ``` | ||
| 26 | |||
| 27 | Next, you should set up the `/etc/fstab` file to automatically mount your drives | ||
| 28 | on boot. To automatically mount your drives when the computer boots up, you'll | ||
| 29 | have to create the folders you want to mount your drive to first. I store mine | ||
| 30 | in the `/mnt` folder using names that I'll recognize, but you can create your | ||
| 31 | folders wherever you want. | ||
| 32 | |||
| 33 | ```sh | ||
| 34 | mkdir /path/to/folder | ||
| 35 | ``` | ||
| 36 | |||
| 37 | For example: | ||
| 38 | |||
| 39 | ```sh | ||
| 40 | mkdir /mnt/steam_library | ||
| 41 | ``` | ||
| 42 | |||
| 43 | To automatically mount drives upon system boot, you will need to collect a few | ||
| 44 | items. The UUID is the identification number connected to whichever drive you're | ||
| 45 | using to store Steam games. | ||
| 46 | |||
| 47 | Drives are usually labeled similar to `/dev/nvme0n1p1` or `/dev/sda1`, so you'll | ||
| 48 | need to find the line in the output of the command below that correlates to your | ||
| 49 | drive and copy the UUID over to the `/etc/fstab` file. | ||
| 50 | |||
| 51 | ```sh | ||
| 52 | sudo blkid | grep UUID= | ||
| 53 | ``` | ||
| 54 | |||
| 55 | Next, you'll need your `uid` and `gid`. To find these, run the following | ||
| 56 | command: | ||
| 57 | |||
| 58 | ```sh | ||
| 59 | id -u && id -g | ||
| 60 | ``` | ||
| 61 | |||
| 62 | Now that you have collected the necessary information, open the `/etc/fstab` | ||
| 63 | file: | ||
| 64 | |||
| 65 | ```sh | ||
| 66 | sudo nano /etc/fstab | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Each drive you want to mount on boot should have its own line in the | ||
| 70 | `/etc/fstab` file that looks similar to this: | ||
| 71 | |||
| 72 | ```config | ||
| 73 | UUID=B64E53824E5339F7 /mnt/steam_library ntfs-3g uid=1000,gid=1000 0 0 | ||
| 74 | ``` | ||
| 75 | |||
| 76 | Now all you need to do is unmount your drive and re-mount it. You can unmount | ||
| 77 | the drive by doing this (be sure to use the correct drive name here): | ||
| 78 | |||
| 79 | ```sh | ||
| 80 | sudo umount /dev/sdxX | ||
| 81 | ``` | ||
| 82 | |||
| 83 | You can re-mount all your drives by executing the following: | ||
| 84 | |||
| 85 | ```sh | ||
| 86 | sudo mount -a | ||
| 87 | ``` | ||
| 88 | |||
| 89 | If you don't know what your drive name is, or you're nervous about unmounting | ||
| 90 | and re-mounting, simply reboot your computer, and it will be done for you | ||
| 91 | automatically. | ||
content/blog/2020-01-26-steam-on-ntfs.org added +91
| @@ -0,0 +1,91 @@ | |||
| 1 | #+date: <2020-01-26> | ||
| 2 | #+title: Linux Gaming Tweak: Steam on NTFS Drives | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Auto-Mount Steam Drives | ||
| 7 | |||
| 8 | If you want to see how to install Steam on Linux, see my other post: | ||
| 9 | [[../linux-software/][Linux Software]]. | ||
| 10 | |||
| 11 | Are you having trouble launching games, even though they've installed | ||
| 12 | correctly? This may happen if you're storing your games on an | ||
| 13 | NTFS-formatted drive. This shouldn't be an issue if you're storing your | ||
| 14 | games on the same drive that Steam is on, but some gamers prefer to put | ||
| 15 | Steam on their main drive and game files on another SSD or HDD. | ||
| 16 | |||
| 17 | To fix this problem, you'll need to try a few things. First, you'll need | ||
| 18 | to install the =ntfs-3g= package, which is meant for better | ||
| 19 | interoperability with Linux. | ||
| 20 | |||
| 21 | #+begin_src sh | ||
| 22 | sudo apt install ntfs-3g | ||
| 23 | #+end_src | ||
| 24 | |||
| 25 | Next, you should set up the =/etc/fstab= file to automatically mount | ||
| 26 | your drives on boot. To automatically mount your drives when the | ||
| 27 | computer boots up, you'll have to create the folders you want to mount | ||
| 28 | your drive to first. I store mine in the =/mnt= folder using names that | ||
| 29 | I'll recognize, but you can create your folders wherever you want. | ||
| 30 | |||
| 31 | #+begin_src sh | ||
| 32 | mkdir /path/to/folder | ||
| 33 | #+end_src | ||
| 34 | |||
| 35 | For example: | ||
| 36 | |||
| 37 | #+begin_src sh | ||
| 38 | mkdir /mnt/steam_library | ||
| 39 | #+end_src | ||
| 40 | |||
| 41 | To automatically mount drives upon system boot, you will need to collect | ||
| 42 | a few items. The UUID is the identification number connected to | ||
| 43 | whichever drive you're using to store Steam games. | ||
| 44 | |||
| 45 | Drives are usually labeled similar to =/dev/nvme0n1p1= or =/dev/sda1=, | ||
| 46 | so you'll need to find the line in the output of the command below that | ||
| 47 | correlates to your drive and copy the UUID over to the =/etc/fstab= | ||
| 48 | file. | ||
| 49 | |||
| 50 | #+begin_src sh | ||
| 51 | sudo blkid | grep UUID= | ||
| 52 | #+end_src | ||
| 53 | |||
| 54 | Next, you'll need your =uid= and =gid=. To find these, run the following | ||
| 55 | command: | ||
| 56 | |||
| 57 | #+begin_src sh | ||
| 58 | id -u && id -g | ||
| 59 | #+end_src | ||
| 60 | |||
| 61 | Now that you have collected the necessary information, open the | ||
| 62 | =/etc/fstab= file: | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | sudo nano /etc/fstab | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | Each drive you want to mount on boot should have its own line in the | ||
| 69 | =/etc/fstab= file that looks similar to this: | ||
| 70 | |||
| 71 | #+begin_src config | ||
| 72 | UUID=B64E53824E5339F7 /mnt/steam_library ntfs-3g uid=1000,gid=1000 0 0 | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | Now all you need to do is unmount your drive and re-mount it. You can | ||
| 76 | unmount the drive by doing this (be sure to use the correct drive name | ||
| 77 | here): | ||
| 78 | |||
| 79 | #+begin_src sh | ||
| 80 | sudo umount /dev/sdxX | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | You can re-mount all your drives by executing the following: | ||
| 84 | |||
| 85 | #+begin_src sh | ||
| 86 | sudo mount -a | ||
| 87 | #+end_src | ||
| 88 | |||
| 89 | If you don't know what your drive name is, or you're nervous about | ||
| 90 | unmounting and re-mounting, simply reboot your computer, and it will be | ||
| 91 | done for you automatically. | ||
content/blog/2020-02-09-cryptography-basics.md deleted −167
| @@ -1,167 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-02-09 | ||
| 3 | title = "Cryptography Basics" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Similar Article Available | ||
| 9 | |||
| 10 | If you haven't already, feel free to read my post on [AES | ||
| 11 | Encryption](../aes-encryption/). | ||
| 12 | |||
| 13 | # What is Cryptography? | ||
| 14 | |||
| 15 | In layman's terms, cryptography is a process that can change data from a | ||
| 16 | readable format into an unreadable format (and vice-versa) through a series of | ||
| 17 | processes and secrets. More technically, this is the Internet Security | ||
| 18 | Glossary's definition: | ||
| 19 | |||
| 20 | > [Cryptography is] the mathematical science that deals with transforming data | ||
| 21 | > to render its meaning unintelligible (i.e., to hide its semantic content), | ||
| 22 | > prevent its undetected alteration, or prevent its unauthorized use. If the | ||
| 23 | > transformation is reversible, cryptography also deals with restoring encrypted | ||
| 24 | > data to an intelligible form. | ||
| 25 | > | ||
| 26 | > - [Internet Security Glossary (2000)](https://tools.ietf.org/html/rfc2828) | ||
| 27 | |||
| 28 | Cryptography cannot offer protection against the loss of data; it simply offers | ||
| 29 | encryption methods to protect data at-rest and data in-traffic. At a high-level, | ||
| 30 | encrypted is when plaintext data is encrypted to ciphertext (a secure form of | ||
| 31 | text that cannot be understood unless decrypted back to plaintext). The | ||
| 32 | encryption process is completed through the use of a mathematical function that | ||
| 33 | utilizes one or more values called keys to encrypt or decrypt the data. | ||
| 34 | |||
| 35 | # Key Elements of Cryptographic Systems | ||
| 36 | |||
| 37 | To create or evaluate a cryptographic system, you need to know the essential | ||
| 38 | pieces to the system: | ||
| 39 | |||
| 40 | - **Encryption Algorithm (Primitive):** A mathematical process that encrypts | ||
| 41 | and decrypts data. | ||
| 42 | - **Encryption Key:** A string of bits used within the encryption algorithm as | ||
| 43 | the secret that allows successful encryption or decryption of data. | ||
| 44 | - **Key Length (Size):** The maximum number of bits within the encryption key. | ||
| 45 | It's important to remember that key size is regulated in many countries. | ||
| 46 | - **Message Digest:** A smaller, fixed-size bit string version of the original | ||
| 47 | message. This is practically infeasible to reverse, which is why it's | ||
| 48 | commonly used to verify integrity. | ||
| 49 | |||
| 50 | # Symmetric Systems (Secret Key Cryptography) | ||
| 51 | |||
| 52 | Symmetric cryptography utilizes a secret, bidirectional key to perform both | ||
| 53 | encryption and decryption of the data. The most common implementation of | ||
| 54 | symmetric cryptography is the Advanced Encryption Standard, which uses keys that | ||
| 55 | are 128 bits to 256 bits in size. This standard came after the National | ||
| 56 | Institute of Standards and Technology (NIST) decided to retire the Data | ||
| 57 | Encryption Standard (DES) in 2001. | ||
| 58 | |||
| 59 | Since brute force attacks strongly correlate with key length, the 56-bit key | ||
| 60 | length of DES was considered insecure after it was publicly broken in under 24 | ||
| 61 | hours. However, there is a modern implementation of DES called Triple DES where | ||
| 62 | the DES method is applied three times to each data block. | ||
| 63 | |||
| 64 | The main advantages to symmetric systems are the ease of use, since only one key | ||
| 65 | is required for both encryption and decryption, and the simplicity of the | ||
| 66 | algorithms. This helps with bulk data encryption that may unnecessarily waste | ||
| 67 | time and power using asymmetric systems. | ||
| 68 | |||
| 69 | However, symmetric systems have disadvantages to keep in mind. Since the key is | ||
| 70 | private, it can be difficult to safely distribute keys to communication | ||
| 71 | partners. Additionally, the key cannot be used to sign messages since it's | ||
| 72 | necessary to keep the key private. | ||
| 73 | |||
| 74 | # Asymmetric Systems (Public Key Cryptography) | ||
| 75 | |||
| 76 | Asymmetric cryptography utilizes two keys within the system: a secret key that | ||
| 77 | is privately-held and a public key that can be distributed freely. The | ||
| 78 | interesting aspect of asymmetric cryptography is that either key can be used to | ||
| 79 | encrypt the data, there's no rule that dictates which key must be used for | ||
| 80 | encryption. Once one key is used to encrypt the data, only the other key can be | ||
| 81 | used to decrypt the data. This means that if the private key encrypts the data, | ||
| 82 | only the public key can decrypt the data. | ||
| 83 | |||
| 84 | An advantage of this system is that if you successfully decrypt data using one | ||
| 85 | of the keys, you can be sure of the sender since only the other key could have | ||
| 86 | encrypted the data. | ||
| 87 | |||
| 88 | One of the major implementations of an asymmetric system is a digital signature. | ||
| 89 | A digital signature can be generated using the sender's private key, or a | ||
| 90 | one-way hash function and is used to provide assurance for the integrity and | ||
| 91 | authenticity of the message. A couple common message digest algorithms are | ||
| 92 | SHA-256 and SHA-512, which securely compress data and produce a 128-bit message | ||
| 93 | digest. | ||
| 94 | |||
| 95 | It should be noted that man-in-the-middle attacks are one of the risks with | ||
| 96 | digital signatures and public keys. To combat this, applications often use a | ||
| 97 | public key infrastructure (PKI) to independently authenticate the validity of | ||
| 98 | signatures and keys. | ||
| 99 | |||
| 100 | Due to the large key size and [inefficient mathematical | ||
| 101 | functions](https://crypto.stackexchange.com/a/591) of asymmetric encryption, | ||
| 102 | elliptical curve cryptography (ECC) is often used to increase security while | ||
| 103 | using fewer resources. | ||
| 104 | |||
| 105 | # Applications of Cryptographic Systems | ||
| 106 | |||
| 107 | There are quite a few implementations of cryptographic systems around the world. | ||
| 108 | Here are a few popular examples: | ||
| 109 | |||
| 110 | **Transport Layer Security (TLS):** One of the most famous cryptographic | ||
| 111 | solutions created is TLS, a session-layered or connection-layered internet | ||
| 112 | protocol that allows for secure communications between browsers and servers. | ||
| 113 | Using handshakes, peer negotiation, and authentication allows TLS to prevent | ||
| 114 | eavesdropping and malicious transformation of data. The major reason for TLS | ||
| 115 | popularity is that a major vulnerability was found in the SSL protocol in 2014. | ||
| 116 | Instead of SSL, TLS can be used with HTTP to form HTTPS and is the preferred | ||
| 117 | method for modern web development due to its increased security. | ||
| 118 | |||
| 119 | **Secure Hypertext Transfer Protocol (HTTPS):** An application layer protocol | ||
| 120 | that allows for secure transport of data between servers and web clients. One of | ||
| 121 | the unique parts of HTTPS is that it uses a secured port number instead of the | ||
| 122 | default web port address. | ||
| 123 | |||
| 124 | **Virtual Private Network (VPN):** VPNs are made to securely extend a private | ||
| 125 | network across public networks by utilizing an encrypted layered tunneling | ||
| 126 | protocol paired with an authentication method, such as usernames and passwords. | ||
| 127 | This technology originally allowed remote employees to access their company's | ||
| 128 | data but have evolved into one of the top choices for anyone who wishes to mask | ||
| 129 | their sensitive personal data. | ||
| 130 | |||
| 131 | **Internet Protocol Security (IPSec):** This protocol suite facilitates | ||
| 132 | communication between two or more hosts or subnets by authenticating and | ||
| 133 | encrypting the data packets. IPSec is used in a lot of VPNs to establish the VPN | ||
| 134 | connection through the transport and tunnel mode encryption methods. IPSec | ||
| 135 | encrypts just the data portion of packets in the transport methods, but it | ||
| 136 | encrypts both the data and headers in the tunnel method (introducing an | ||
| 137 | additional header for authentication). | ||
| 138 | |||
| 139 | **Secure Shell (SSH):** SSH is another network protocol used to protect network | ||
| 140 | services by authenticating users through a secure channel. This protocol is | ||
| 141 | often used for command-line (shell) functions such as remote shell commands, | ||
| 142 | logins, and file transfers. | ||
| 143 | |||
| 144 | **Kerberos:** Developed by MIT, Kerberos is a computer-network authentication | ||
| 145 | protocol that works on the basis of tickets to allow nodes communicating over a | ||
| 146 | non-secure network to prove their identity to one another securely. This is most | ||
| 147 | commonly used in business environments when used as the authentication and | ||
| 148 | encryption method for Windows Active Directory (AD). | ||
| 149 | |||
| 150 | # Cybersecurity Controls | ||
| 151 | |||
| 152 | If you're someone who needs solutions on how to control risks associated with | ||
| 153 | utilizing a crytograhpic system, start with a few basic controls: | ||
| 154 | |||
| 155 | - **Policies:** A policy on the use of cryptographic controls for protection | ||
| 156 | of information is implemented and is in accordance with organizational | ||
| 157 | objectives. | ||
| 158 | - **Key management:** A policy on the use, protection and lifetime of | ||
| 159 | cryptographic keys is implemented through the entire application lifecycle. | ||
| 160 | - **Key size:** The organization has researched the optimal key size for their | ||
| 161 | purposes, considering national laws, required processing power, and | ||
| 162 | longevity of the solution. | ||
| 163 | - **Algorithm selection:** Implemented algorithms are sufficiently appropriate | ||
| 164 | for the business of the organization, robust, and align with recommended | ||
| 165 | guidelines. | ||
| 166 | - **Protocol configuration:** Protocols have been reviewed and configured | ||
| 167 | suitable to the purpose of the business. | ||
content/blog/2020-02-09-cryptography-basics.org added +177
| @@ -0,0 +1,177 @@ | |||
| 1 | #+date: <2020-02-09> | ||
| 2 | #+title: Cryptography Basics | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Similar Article Available | ||
| 7 | |||
| 8 | If you haven't already, feel free to read my post on | ||
| 9 | [[../aes-encryption/][AES Encryption]]. | ||
| 10 | |||
| 11 | * What is Cryptography? | ||
| 12 | |||
| 13 | In layman's terms, cryptography is a process that can change data from a | ||
| 14 | readable format into an unreadable format (and vice-versa) through a | ||
| 15 | series of processes and secrets. More technically, this is the Internet | ||
| 16 | Security Glossary's definition: | ||
| 17 | |||
| 18 | #+begin_quote | ||
| 19 | [Cryptography is] the mathematical science that deals with transforming | ||
| 20 | data to render its meaning unintelligible (i.e., to hide its semantic | ||
| 21 | content), prevent its undetected alteration, or prevent its unauthorized | ||
| 22 | use. If the transformation is reversible, cryptography also deals with | ||
| 23 | restoring encrypted data to an intelligible form. | ||
| 24 | |||
| 25 | - [[https://tools.ietf.org/html/rfc2828][Internet Security Glossary | ||
| 26 | (2000)]] | ||
| 27 | #+end_quote | ||
| 28 | |||
| 29 | Cryptography cannot offer protection against the loss of data; it simply | ||
| 30 | offers encryption methods to protect data at-rest and data in-traffic. | ||
| 31 | At a high-level, encrypted is when plaintext data is encrypted to | ||
| 32 | ciphertext (a secure form of text that cannot be understood unless | ||
| 33 | decrypted back to plaintext). The encryption process is completed | ||
| 34 | through the use of a mathematical function that utilizes one or more | ||
| 35 | values called keys to encrypt or decrypt the data. | ||
| 36 | |||
| 37 | * Key Elements of Cryptographic Systems | ||
| 38 | |||
| 39 | To create or evaluate a cryptographic system, you need to know the | ||
| 40 | essential pieces to the system: | ||
| 41 | |||
| 42 | - *Encryption Algorithm (Primitive):* A mathematical process that | ||
| 43 | encrypts and decrypts data. | ||
| 44 | - *Encryption Key:* A string of bits used within the encryption | ||
| 45 | algorithm as the secret that allows successful encryption or | ||
| 46 | decryption of data. | ||
| 47 | - *Key Length (Size):* The maximum number of bits within the encryption | ||
| 48 | key. It's important to remember that key size is regulated in many | ||
| 49 | countries. | ||
| 50 | - *Message Digest:* A smaller, fixed-size bit string version of the | ||
| 51 | original message. This is practically infeasible to reverse, which is | ||
| 52 | why it's commonly used to verify integrity. | ||
| 53 | |||
| 54 | * Symmetric Systems (Secret Key Cryptography) | ||
| 55 | |||
| 56 | Symmetric cryptography utilizes a secret, bidirectional key to perform | ||
| 57 | both encryption and decryption of the data. The most common | ||
| 58 | implementation of symmetric cryptography is the Advanced Encryption | ||
| 59 | Standard, which uses keys that are 128 bits to 256 bits in size. This | ||
| 60 | standard came after the National Institute of Standards and Technology | ||
| 61 | (NIST) decided to retire the Data Encryption Standard (DES) in 2001. | ||
| 62 | |||
| 63 | Since brute force attacks strongly correlate with key length, the 56-bit | ||
| 64 | key length of DES was considered insecure after it was publicly broken | ||
| 65 | in under 24 hours. However, there is a modern implementation of DES | ||
| 66 | called Triple DES where the DES method is applied three times to each | ||
| 67 | data block. | ||
| 68 | |||
| 69 | The main advantages to symmetric systems are the ease of use, since only | ||
| 70 | one key is required for both encryption and decryption, and the | ||
| 71 | simplicity of the algorithms. This helps with bulk data encryption that | ||
| 72 | may unnecessarily waste time and power using asymmetric systems. | ||
| 73 | |||
| 74 | However, symmetric systems have disadvantages to keep in mind. Since the | ||
| 75 | key is private, it can be difficult to safely distribute keys to | ||
| 76 | communication partners. Additionally, the key cannot be used to sign | ||
| 77 | messages since it's necessary to keep the key private. | ||
| 78 | |||
| 79 | * Asymmetric Systems (Public Key Cryptography) | ||
| 80 | |||
| 81 | Asymmetric cryptography utilizes two keys within the system: a secret | ||
| 82 | key that is privately-held and a public key that can be distributed | ||
| 83 | freely. The interesting aspect of asymmetric cryptography is that either | ||
| 84 | key can be used to encrypt the data, there's no rule that dictates which | ||
| 85 | key must be used for encryption. Once one key is used to encrypt the | ||
| 86 | data, only the other key can be used to decrypt the data. This means | ||
| 87 | that if the private key encrypts the data, only the public key can | ||
| 88 | decrypt the data. | ||
| 89 | |||
| 90 | An advantage of this system is that if you successfully decrypt data | ||
| 91 | using one of the keys, you can be sure of the sender since only the | ||
| 92 | other key could have encrypted the data. | ||
| 93 | |||
| 94 | One of the major implementations of an asymmetric system is a digital | ||
| 95 | signature. A digital signature can be generated using the sender's | ||
| 96 | private key, or a one-way hash function and is used to provide assurance | ||
| 97 | for the integrity and authenticity of the message. A couple common | ||
| 98 | message digest algorithms are SHA-256 and SHA-512, which securely | ||
| 99 | compress data and produce a 128-bit message digest. | ||
| 100 | |||
| 101 | It should be noted that man-in-the-middle attacks are one of the risks | ||
| 102 | with digital signatures and public keys. To combat this, applications | ||
| 103 | often use a public key infrastructure (PKI) to independently | ||
| 104 | authenticate the validity of signatures and keys. | ||
| 105 | |||
| 106 | Due to the large key size and | ||
| 107 | [[https://crypto.stackexchange.com/a/591][inefficient mathematical | ||
| 108 | functions]] of asymmetric encryption, elliptical curve cryptography | ||
| 109 | (ECC) is often used to increase security while using fewer resources. | ||
| 110 | |||
| 111 | * Applications of Cryptographic Systems | ||
| 112 | |||
| 113 | There are quite a few implementations of cryptographic systems around | ||
| 114 | the world. Here are a few popular examples: | ||
| 115 | |||
| 116 | *Transport Layer Security (TLS):* One of the most famous cryptographic | ||
| 117 | solutions created is TLS, a session-layered or connection-layered | ||
| 118 | internet protocol that allows for secure communications between browsers | ||
| 119 | and servers. Using handshakes, peer negotiation, and authentication | ||
| 120 | allows TLS to prevent eavesdropping and malicious transformation of | ||
| 121 | data. The major reason for TLS popularity is that a major vulnerability | ||
| 122 | was found in the SSL protocol in 2014. Instead of SSL, TLS can be used | ||
| 123 | with HTTP to form HTTPS and is the preferred method for modern web | ||
| 124 | development due to its increased security. | ||
| 125 | |||
| 126 | *Secure Hypertext Transfer Protocol (HTTPS):* An application layer | ||
| 127 | protocol that allows for secure transport of data between servers and | ||
| 128 | web clients. One of the unique parts of HTTPS is that it uses a secured | ||
| 129 | port number instead of the default web port address. | ||
| 130 | |||
| 131 | *Virtual Private Network (VPN):* VPNs are made to securely extend a | ||
| 132 | private network across public networks by utilizing an encrypted layered | ||
| 133 | tunneling protocol paired with an authentication method, such as | ||
| 134 | usernames and passwords. This technology originally allowed remote | ||
| 135 | employees to access their company's data but have evolved into one of | ||
| 136 | the top choices for anyone who wishes to mask their sensitive personal | ||
| 137 | data. | ||
| 138 | |||
| 139 | *Internet Protocol Security (IPSec):* This protocol suite facilitates | ||
| 140 | communication between two or more hosts or subnets by authenticating and | ||
| 141 | encrypting the data packets. IPSec is used in a lot of VPNs to establish | ||
| 142 | the VPN connection through the transport and tunnel mode encryption | ||
| 143 | methods. IPSec encrypts just the data portion of packets in the | ||
| 144 | transport methods, but it encrypts both the data and headers in the | ||
| 145 | tunnel method (introducing an additional header for authentication). | ||
| 146 | |||
| 147 | *Secure Shell (SSH):* SSH is another network protocol used to protect | ||
| 148 | network services by authenticating users through a secure channel. This | ||
| 149 | protocol is often used for command-line (shell) functions such as remote | ||
| 150 | shell commands, logins, and file transfers. | ||
| 151 | |||
| 152 | *Kerberos:* Developed by MIT, Kerberos is a computer-network | ||
| 153 | authentication protocol that works on the basis of tickets to allow | ||
| 154 | nodes communicating over a non-secure network to prove their identity to | ||
| 155 | one another securely. This is most commonly used in business | ||
| 156 | environments when used as the authentication and encryption method for | ||
| 157 | Windows Active Directory (AD). | ||
| 158 | |||
| 159 | * Cybersecurity Controls | ||
| 160 | |||
| 161 | If you're someone who needs solutions on how to control risks associated | ||
| 162 | with utilizing a crytograhpic system, start with a few basic controls: | ||
| 163 | |||
| 164 | - *Policies:* A policy on the use of cryptographic controls for | ||
| 165 | protection of information is implemented and is in accordance with | ||
| 166 | organizational objectives. | ||
| 167 | - *Key management:* A policy on the use, protection and lifetime of | ||
| 168 | cryptographic keys is implemented through the entire application | ||
| 169 | lifecycle. | ||
| 170 | - *Key size:* The organization has researched the optimal key size for | ||
| 171 | their purposes, considering national laws, required processing power, | ||
| 172 | and longevity of the solution. | ||
| 173 | - *Algorithm selection:* Implemented algorithms are sufficiently | ||
| 174 | appropriate for the business of the organization, robust, and align | ||
| 175 | with recommended guidelines. | ||
| 176 | - *Protocol configuration:* Protocols have been reviewed and configured | ||
| 177 | suitable to the purpose of the business. | ||
content/blog/2020-03-25-session-messenger.md deleted −111
| @@ -1,111 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-03-25 | ||
| 3 | title = "Session Private Messenger" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Privacy Warning | ||
| 9 | |||
| 10 | The company behind Session (Loki Foundation) is from Australia. If you didn't | ||
| 11 | know, Australia has introduced | ||
| 12 | [legislation](https://parlinfo.aph.gov.au/parlInfo/download/legislation/bills/r6195_aspassed/toc_pdf/18204b01.pdf) | ||
| 13 | mandating companies comply with government requests to build backdoor access | ||
| 14 | into applications. For more information, read my article on [AES | ||
| 15 | Encryption](./2020-01-25-aes-encryption.html). | ||
| 16 | |||
| 17 | # About Session | ||
| 18 | |||
| 19 | [Session](https://getsession.org) is a private, cross-platform messaging app | ||
| 20 | from the [Loki Foundation](https://loki.foundation). As someone who has spent | ||
| 21 | years looking for quality alternatives to major messaging apps, I was excited | ||
| 22 | when I first heard about Session. Reading through [Session's white | ||
| 23 | paper](https://arxiv.org/pdf/2002.04609.pdf), you can learn the technologies | ||
| 24 | behind the Session app. Part of the security of Session comes from the Signal | ||
| 25 | protocol, which was forked as the origin of Session. | ||
| 26 | |||
| 27 | > Session is an end-to-end encrypted messenger that removes sensitive metadata | ||
| 28 | > collection, and is designed for people who want privacy and freedom from any | ||
| 29 | > forms of surveillance. | ||
| 30 | |||
| 31 | In general, this app promises security through end-to-end encryption, | ||
| 32 | decentralized onion routing, and private identities. The biggest change that the | ||
| 33 | Loki Foundation has made to the Signal protocol is removing the need for a phone | ||
| 34 | number. Instead, a random identification string is generated for any session you | ||
| 35 | create. This means you can create a new session for each device if you want to, | ||
| 36 | or link new devices with your ID. | ||
| 37 | |||
| 38 | Since Session's website and white paper describe the details of Session's | ||
| 39 | security, I'm going to focus on using the app in this post. | ||
| 40 | |||
| 41 | # Features | ||
| 42 | |||
| 43 | Since most people are looking for an alternative to a popular chat app, I am | ||
| 44 | going to list out the features that Session has so that you are able to | ||
| 45 | determine if the app would suit your needs: | ||
| 46 | |||
| 47 | - Multiple device linking (via QR code or ID) | ||
| 48 | - App locking via device screen lock, password, or fingerprint | ||
| 49 | - Screenshot blocking | ||
| 50 | - Incognito keyboard | ||
| 51 | - Read receipts and typing indicators | ||
| 52 | - Mobile notification customization | ||
| 53 | - Old message deletion and conversation limit | ||
| 54 | - Backups | ||
| 55 | - Recovery phrase | ||
| 56 | - Account deletion, including ID, messages, sessions, and contacts | ||
| 57 | |||
| 58 | # Downloads | ||
| 59 | |||
| 60 | I have tested this app on Ubuntu 19.10, Android 10, macOS Monterey, and iOS 15. | ||
| 61 | All apps have worked well without many issues. | ||
| 62 | |||
| 63 | Below is a brief overview of the Session app on Linux. To get this app, you'll | ||
| 64 | need to go to the [Downloads](https://getsession.org/download/) page and click | ||
| 65 | to link to the operating system you're using. | ||
| 66 | |||
| 67 | For Linux, it will download an AppImage that you'll need to enable with the | ||
| 68 | following command: | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | sudo chmod u+x session-messenger-desktop-linux-x86_64-1.0.5.AppImage | ||
| 72 | ``` | ||
| 73 | |||
| 74 | # Creating an Account | ||
| 75 | |||
| 76 | Once you've installed the app, simply run the app and create your unique Session | ||
| 77 | ID. It will look something like this: | ||
| 78 | `05af1835afdd63c947b47705867501d6373f486aa1ae05b1f2f3fcd24570eba608`. | ||
| 79 | |||
| 80 | You'll need to set a display name and, optionally, a password. If you set a | ||
| 81 | password, you will need to enter it every time you open the app. | ||
| 82 | |||
| 83 | # Start Messaging | ||
| 84 | |||
| 85 | Once you've created your account and set up your profile details, the next step | ||
| 86 | is to start messaging other people. To do so, you'll need to share your Session | ||
| 87 | ID with other people. From this point, it's fairly straightforward and acts like | ||
| 88 | any other messaging app, so I won't dive into much detail here. | ||
| 89 | |||
| 90 | ## macOS | ||
| 91 | |||
| 92 | One key feature to note is that the desktop application now provides a helpful | ||
| 93 | pop-up box explaining the process that Session uses to hide your IP address: | ||
| 94 | |||
| 95 | ## iOS | ||
| 96 | |||
| 97 | The mobile app is quite simple and effective, giving you all the standard mobile | ||
| 98 | messaging options you'd expect. | ||
| 99 | |||
| 100 | # Potential Issues | ||
| 101 | |||
| 102 | I've discovered one annoying issue that would prevent from using this app | ||
| 103 | regularly. On a mobile device, there have been issues with receiving messages on | ||
| 104 | time. Even with battery optimization disabled and no network restrictions, | ||
| 105 | Session notifications sometimes do not display until I open the app or the | ||
| 106 | conversation itself and wait a few moments. This is actually one of the reasons | ||
| 107 | I stopped using Signal (this seems fixed as of my updates in 2021/2022, so I | ||
| 108 | wouldn't worry about this issue anymore). | ||
| 109 | |||
| 110 | Looking for another messenger instead of Session? I recommend Signal, Matrix, | ||
| 111 | and IRC. | ||
content/blog/2020-03-25-session-messenger.org added +116
| @@ -0,0 +1,116 @@ | |||
| 1 | #+date: <2020-03-25> | ||
| 2 | #+title: Session Private Messenger | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Privacy Warning | ||
| 7 | |||
| 8 | The company behind Session (Loki Foundation) is from Australia. If you | ||
| 9 | didn't know, Australia has introduced | ||
| 10 | [[https://parlinfo.aph.gov.au/parlInfo/download/legislation/bills/r6195_aspassed/toc_pdf/18204b01.pdf][legislation]] | ||
| 11 | mandating companies comply with government requests to build backdoor | ||
| 12 | access into applications. For more information, read my article on | ||
| 13 | [[./2020-01-25-aes-encryption.html][AES Encryption]]. | ||
| 14 | |||
| 15 | * About Session | ||
| 16 | |||
| 17 | [[https://getsession.org][Session]] is a private, cross-platform | ||
| 18 | messaging app from the [[https://loki.foundation][Loki Foundation]]. As | ||
| 19 | someone who has spent years looking for quality alternatives to major | ||
| 20 | messaging apps, I was excited when I first heard about Session. Reading | ||
| 21 | through [[https://arxiv.org/pdf/2002.04609.pdf][Session's white paper]], | ||
| 22 | you can learn the technologies behind the Session app. Part of the | ||
| 23 | security of Session comes from the Signal protocol, which was forked as | ||
| 24 | the origin of Session. | ||
| 25 | |||
| 26 | #+begin_quote | ||
| 27 | Session is an end-to-end encrypted messenger that removes sensitive | ||
| 28 | metadata collection, and is designed for people who want privacy and | ||
| 29 | freedom from any forms of surveillance. | ||
| 30 | #+end_quote | ||
| 31 | |||
| 32 | In general, this app promises security through end-to-end encryption, | ||
| 33 | decentralized onion routing, and private identities. The biggest change | ||
| 34 | that the Loki Foundation has made to the Signal protocol is removing the | ||
| 35 | need for a phone number. Instead, a random identification string is | ||
| 36 | generated for any session you create. This means you can create a new | ||
| 37 | session for each device if you want to, or link new devices with your | ||
| 38 | ID. | ||
| 39 | |||
| 40 | Since Session's website and white paper describe the details of | ||
| 41 | Session's security, I'm going to focus on using the app in this post. | ||
| 42 | |||
| 43 | * Features | ||
| 44 | |||
| 45 | Since most people are looking for an alternative to a popular chat app, | ||
| 46 | I am going to list out the features that Session has so that you are | ||
| 47 | able to determine if the app would suit your needs: | ||
| 48 | |||
| 49 | - Multiple device linking (via QR code or ID) | ||
| 50 | - App locking via device screen lock, password, or fingerprint | ||
| 51 | - Screenshot blocking | ||
| 52 | - Incognito keyboard | ||
| 53 | - Read receipts and typing indicators | ||
| 54 | - Mobile notification customization | ||
| 55 | - Old message deletion and conversation limit | ||
| 56 | - Backups | ||
| 57 | - Recovery phrase | ||
| 58 | - Account deletion, including ID, messages, sessions, and contacts | ||
| 59 | |||
| 60 | * Downloads | ||
| 61 | |||
| 62 | I have tested this app on Ubuntu 19.10, Android 10, macOS Monterey, and | ||
| 63 | iOS 15. All apps have worked well without many issues. | ||
| 64 | |||
| 65 | Below is a brief overview of the Session app on Linux. To get this app, | ||
| 66 | you'll need to go to the [[https://getsession.org/download/][Downloads]] | ||
| 67 | page and click to link to the operating system you're using. | ||
| 68 | |||
| 69 | For Linux, it will download an AppImage that you'll need to enable with | ||
| 70 | the following command: | ||
| 71 | |||
| 72 | #+begin_src sh | ||
| 73 | sudo chmod u+x session-messenger-desktop-linux-x86_64-1.0.5.AppImage | ||
| 74 | #+end_src | ||
| 75 | |||
| 76 | * Creating an Account | ||
| 77 | |||
| 78 | Once you've installed the app, simply run the app and create your unique | ||
| 79 | Session ID. It will look something like this: | ||
| 80 | =05af1835afdd63c947b47705867501d6373f486aa1ae05b1f2f3fcd24570eba608=. | ||
| 81 | |||
| 82 | You'll need to set a display name and, optionally, a password. If you | ||
| 83 | set a password, you will need to enter it every time you open the app. | ||
| 84 | |||
| 85 | * Start Messaging | ||
| 86 | |||
| 87 | Once you've created your account and set up your profile details, the | ||
| 88 | next step is to start messaging other people. To do so, you'll need to | ||
| 89 | share your Session ID with other people. From this point, it's fairly | ||
| 90 | straightforward and acts like any other messaging app, so I won't dive | ||
| 91 | into much detail here. | ||
| 92 | |||
| 93 | ** macOS | ||
| 94 | |||
| 95 | One key feature to note is that the desktop application now provides a | ||
| 96 | helpful pop-up box explaining the process that Session uses to hide your | ||
| 97 | IP address: | ||
| 98 | |||
| 99 | ** iOS | ||
| 100 | |||
| 101 | The mobile app is quite simple and effective, giving you all the | ||
| 102 | standard mobile messaging options you'd expect. | ||
| 103 | |||
| 104 | * Potential Issues | ||
| 105 | |||
| 106 | I've discovered one annoying issue that would prevent from using this | ||
| 107 | app regularly. On a mobile device, there have been issues with receiving | ||
| 108 | messages on time. Even with battery optimization disabled and no network | ||
| 109 | restrictions, Session notifications sometimes do not display until I | ||
| 110 | open the app or the conversation itself and wait a few moments. This is | ||
| 111 | actually one of the reasons I stopped using Signal (this seems fixed as | ||
| 112 | of my updates in 2021/2022, so I wouldn't worry about this issue | ||
| 113 | anymore). | ||
| 114 | |||
| 115 | Looking for another messenger instead of Session? I recommend Signal, | ||
| 116 | Matrix, and IRC. | ||
content/blog/2020-05-03-homelab.md deleted −152
| @@ -1,152 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-05-03 | ||
| 3 | title = "An Inside Look at My Home Lab" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is a Homelab? | ||
| 9 | |||
| 10 | Starting as a developer, I have largely stayed away from hardware-based hobbies | ||
| 11 | (other than building a gaming desktop). However, as the quarantine for COVID-19 | ||
| 12 | stretches out further and further, I found myself bored and in search of new | ||
| 13 | hobbies. After spending the last few months browsing the | ||
| 14 | [r/homelab](https://www.reddit.com/r/homelab/) subreddit, I decided it was time | ||
| 15 | to jump in and try things out for myself. | ||
| 16 | |||
| 17 | Since I am a beginner and just recently graduated from college, everything I've | ||
| 18 | done so far in my homelab is fairly low-budget. | ||
| 19 | |||
| 20 | # Hardware | ||
| 21 | |||
| 22 | ## Raspberry Pi 4 | ||
| 23 | |||
| 24 | Luckily, I had actually purchased a [Raspberry Pi | ||
| 25 | 4](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) before the | ||
| 26 | quarantine started so that I could try to keep Plex Media Center running, even | ||
| 27 | while my desktop computer was turned off. I started here, using the Pi to hold | ||
| 28 | Plex and Pi-hole until I grew tired with the slow performance. | ||
| 29 | |||
| 30 | Here are the specifications for the Pi 4: | ||
| 31 | |||
| 32 | - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz | ||
| 33 | - 4GB LPDDR4-3200 SDRAM | ||
| 34 | - Gigabit Ethernet | ||
| 35 | - H.265 (4kp60 decode), H264 (1080p60 decode, 1080p30 encode) | ||
| 36 | - 64 GB MicroSD Card | ||
| 37 | |||
| 38 | ## Dell Optiplex 5040 | ||
| 39 | |||
| 40 | Since I wasn't happy with the Pi as my main server, I turned to Craigslist. I | ||
| 41 | know a lot of other homelabbers use Ebay, but I can't seem to ever trust it | ||
| 42 | enough to purchase items on there. So I used Craigslist and found a Dell | ||
| 43 | Optiplex 5040 desktop computer on sale for $90. While this computer might be | ||
| 44 | underpowered, it was one of the few computers under $100 that was available | ||
| 45 | during quarantine. | ||
| 46 | |||
| 47 | Here are the specifications for the Dell Optiplex 5040: | ||
| 48 | |||
| 49 | - Intel Core i3 6100 | ||
| 50 | - 8GB RAM DDR3 | ||
| 51 | - Intel HD Graphics | ||
| 52 | - Gigabit Ethernet | ||
| 53 | - 500GB Hard Drive | ||
| 54 | |||
| 55 | While this hardware would be awful for a work computer or a gaming rig, it | ||
| 56 | turned out to be wonderful for my server purposes. The only limitation I have | ||
| 57 | found so far is the CPU. The i3-6100 only has enough power for a single 4k video | ||
| 58 | transcode at a time. I haven't tested more than three 1080p streams at a time, | ||
| 59 | but the maximum amount of streams I've ever actually used is two. | ||
| 60 | |||
| 61 | ## WD easystore 10TB & 8TB | ||
| 62 | |||
| 63 | Application storage and temporary files are stored on the internal hard drive of | ||
| 64 | the server, but all media files (movies, tv, games, books, etc) are stored | ||
| 65 | externally on my WD easystore hard drive. Creating auto-boot configurations in | ||
| 66 | the `/etc/fstab` file on my server allows the hard drives to automatically mount | ||
| 67 | whenever I need to restart my server. | ||
| 68 | |||
| 69 | > Update: In March 2022, I shucked the hard drives out of their external cases, | ||
| 70 | > put some Kapton tape on the third power pin to prevent power shutdowns, and | ||
| 71 | > stuck them inside my server tower using internal SATA cables. | ||
| 72 | |||
| 73 | ## Netgear Unmanaged Switch | ||
| 74 | |||
| 75 | To manage all the ethernet cords used by my homelab, my desktop, and my living | ||
| 76 | room media center, I purchased an 8-port gigabit ethernet switch for $50 at my | ||
| 77 | local computer store. This is probably much more than I should have spent on an | ||
| 78 | unmanaged switch, but I am comfortable with the choice. | ||
| 79 | |||
| 80 | ## TP-Link Managed Switch | ||
| 81 | |||
| 82 | Since I use the unmanaged switch to group all living room devices together, I | ||
| 83 | use the managed switch to configure VLANs and secure my network. | ||
| 84 | |||
| 85 | ## Arris TM1602A Modem & Sagecom Fast 5280 Router | ||
| 86 | |||
| 87 | My default modem and router, provided by my ISP, are fairly standard. The Arris | ||
| 88 | modem supports DOCSIS 3.0, which is something that I definitely wanted as a | ||
| 89 | minimum. The Sagecom router is also standard, no fancy bells or whistles. | ||
| 90 | However, it does support DHCP and DHCPv6, which is something you can use to | ||
| 91 | route all household traffic through a pi-hole or firewall. | ||
| 92 | |||
| 93 | ## TP-Link EAP | ||
| 94 | |||
| 95 | In order to gain better control over the network, I use my own wireless access | ||
| 96 | point instead of the one included in the Sagecom router above. Now I can control | ||
| 97 | and organize all of my ethernet connections through the VLANs on the managed | ||
| 98 | switch and wireless connections through the VLANS on the EAP. | ||
| 99 | |||
| 100 | ## Generic Printer | ||
| 101 | |||
| 102 | The last piece to my homelab is a standard wireless printer. Nothing special | ||
| 103 | here. | ||
| 104 | |||
| 105 | # Software | ||
| 106 | |||
| 107 | ## Ubuntu Server 20.04 | ||
| 108 | |||
| 109 | While the 20.04 version of Ubuntu was just released, I always like to experiment | ||
| 110 | with new features (and I don't mind breaking my system - it just gives me more | ||
| 111 | experience learning how to fix things). So, I have Ubuntu Server 20.04 installed | ||
| 112 | on the Dell Optiplex server and Ubuntu Server 19.10 installed on the Raspberry | ||
| 113 | Pi. Once I find an acceptable use for the Pi, I will most likely switch the | ||
| 114 | operating system. | ||
| 115 | |||
| 116 | ## Docker | ||
| 117 | |||
| 118 | I am _very_ new to Docker, but I have had a lot of fun playing with it so far. | ||
| 119 | Docker is used to create containers that can hold all the contents of a system | ||
| 120 | without interfering with other software on the same system. So far, I have | ||
| 121 | successfully installed pi-hole, GitLab, Gogs, and Nextcloud in containers. | ||
| 122 | However, I opted to delete all of those so that I can reconfigure them more | ||
| 123 | professionally at a later time. | ||
| 124 | |||
| 125 | ## Plex Media Server | ||
| 126 | |||
| 127 | Plex is a media center software that allows you to organize your movies, TV | ||
| 128 | shows, music, photos, and videos automatically. It will even download metadata | ||
| 129 | for you so that you can easily browse these collections. | ||
| 130 | |||
| 131 | ## Pi-hole | ||
| 132 | |||
| 133 | Pi-hole is an alternative ad-blocker that runs at the DNS level, allowing you to | ||
| 134 | block traffic when it hits your network, so that you can reject any traffic you | ||
| 135 | deem to be bad. Pi-hole uses blacklists and whitelists to decide which traffic | ||
| 136 | block and, luckily, there are a lot of pre-made lists out there on Reddit, | ||
| 137 | GitHub, etc. | ||
| 138 | |||
| 139 | ## Nextcloud | ||
| 140 | |||
| 141 | While I had trouble with the Docker version of Nextcloud, I was very successful | ||
| 142 | when setting up the snap version. Using this, I was able to map Nextcloud to a | ||
| 143 | subdomain of a domain I own in Namecheap. Additionally, Nextcloud has an | ||
| 144 | integration with Let's Encrypt that allows me to issue certificates | ||
| 145 | automatically to any new domain I authorize. | ||
| 146 | |||
| 147 | ## Webmin | ||
| 148 | |||
| 149 | To monitor my servers, and the processes running on them, I use the Webmin | ||
| 150 | dashboard. This was fairly painless to set up, and I currently access it | ||
| 151 | straight through the server's IP address. In the future, I will be looking to | ||
| 152 | configure Webmin to use a custom domain just like Nextcloud. | ||
content/blog/2020-05-03-homelab.org added +162
| @@ -0,0 +1,162 @@ | |||
| 1 | #+date: <2020-05-03> | ||
| 2 | #+title: An Inside Look at My Home Lab | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is a Homelab? | ||
| 7 | |||
| 8 | Starting as a developer, I have largely stayed away from hardware-based | ||
| 9 | hobbies (other than building a gaming desktop). However, as the | ||
| 10 | quarantine for COVID-19 stretches out further and further, I found | ||
| 11 | myself bored and in search of new hobbies. After spending the last few | ||
| 12 | months browsing the [[https://www.reddit.com/r/homelab/][r/homelab]] | ||
| 13 | subreddit, I decided it was time to jump in and try things out for | ||
| 14 | myself. | ||
| 15 | |||
| 16 | Since I am a beginner and just recently graduated from college, | ||
| 17 | everything I've done so far in my homelab is fairly low-budget. | ||
| 18 | |||
| 19 | * Hardware | ||
| 20 | |||
| 21 | ** Raspberry Pi 4 | ||
| 22 | |||
| 23 | Luckily, I had actually purchased a | ||
| 24 | [[https://www.raspberrypi.org/products/raspberry-pi-4-model-b/][Raspberry | ||
| 25 | Pi 4]] before the quarantine started so that I could try to keep Plex | ||
| 26 | Media Center running, even while my desktop computer was turned off. I | ||
| 27 | started here, using the Pi to hold Plex and Pi-hole until I grew tired | ||
| 28 | with the slow performance. | ||
| 29 | |||
| 30 | Here are the specifications for the Pi 4: | ||
| 31 | |||
| 32 | - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz | ||
| 33 | - 4GB LPDDR4-3200 SDRAM | ||
| 34 | - Gigabit Ethernet | ||
| 35 | - H.265 (4kp60 decode), H264 (1080p60 decode, 1080p30 encode) | ||
| 36 | - 64 GB MicroSD Card | ||
| 37 | |||
| 38 | ** Dell Optiplex 5040 | ||
| 39 | |||
| 40 | Since I wasn't happy with the Pi as my main server, I turned to | ||
| 41 | Craigslist. I know a lot of other homelabbers use Ebay, but I can't seem | ||
| 42 | to ever trust it enough to purchase items on there. So I used Craigslist | ||
| 43 | and found a Dell Optiplex 5040 desktop computer on sale for $90. While | ||
| 44 | this computer might be underpowered, it was one of the few computers | ||
| 45 | under $100 that was available during quarantine. | ||
| 46 | |||
| 47 | Here are the specifications for the Dell Optiplex 5040: | ||
| 48 | |||
| 49 | - Intel Core i3 6100 | ||
| 50 | - 8GB RAM DDR3 | ||
| 51 | - Intel HD Graphics | ||
| 52 | - Gigabit Ethernet | ||
| 53 | - 500GB Hard Drive | ||
| 54 | |||
| 55 | While this hardware would be awful for a work computer or a gaming rig, | ||
| 56 | it turned out to be wonderful for my server purposes. The only | ||
| 57 | limitation I have found so far is the CPU. The i3-6100 only has enough | ||
| 58 | power for a single 4k video transcode at a time. I haven't tested more | ||
| 59 | than three 1080p streams at a time, but the maximum amount of streams | ||
| 60 | I've ever actually used is two. | ||
| 61 | |||
| 62 | ** WD easystore 10TB & 8TB | ||
| 63 | |||
| 64 | Application storage and temporary files are stored on the internal hard | ||
| 65 | drive of the server, but all media files (movies, tv, games, books, etc) | ||
| 66 | are stored externally on my WD easystore hard drive. Creating auto-boot | ||
| 67 | configurations in the =/etc/fstab= file on my server allows the hard | ||
| 68 | drives to automatically mount whenever I need to restart my server. | ||
| 69 | |||
| 70 | #+begin_quote | ||
| 71 | Update: In March 2022, I shucked the hard drives out of their external | ||
| 72 | cases, put some Kapton tape on the third power pin to prevent power | ||
| 73 | shutdowns, and stuck them inside my server tower using internal SATA | ||
| 74 | cables. | ||
| 75 | #+end_quote | ||
| 76 | |||
| 77 | ** Netgear Unmanaged Switch | ||
| 78 | |||
| 79 | To manage all the ethernet cords used by my homelab, my desktop, and my | ||
| 80 | living room media center, I purchased an 8-port gigabit ethernet switch | ||
| 81 | for $50 at my local computer store. This is probably much more than I | ||
| 82 | should have spent on an unmanaged switch, but I am comfortable with the | ||
| 83 | choice. | ||
| 84 | |||
| 85 | ** TP-Link Managed Switch | ||
| 86 | |||
| 87 | Since I use the unmanaged switch to group all living room devices | ||
| 88 | together, I use the managed switch to configure VLANs and secure my | ||
| 89 | network. | ||
| 90 | |||
| 91 | ** Arris TM1602A Modem & Sagecom Fast 5280 Router | ||
| 92 | |||
| 93 | My default modem and router, provided by my ISP, are fairly standard. | ||
| 94 | The Arris modem supports DOCSIS 3.0, which is something that I | ||
| 95 | definitely wanted as a minimum. The Sagecom router is also standard, no | ||
| 96 | fancy bells or whistles. However, it does support DHCP and DHCPv6, which | ||
| 97 | is something you can use to route all household traffic through a | ||
| 98 | pi-hole or firewall. | ||
| 99 | |||
| 100 | ** TP-Link EAP | ||
| 101 | |||
| 102 | In order to gain better control over the network, I use my own wireless | ||
| 103 | access point instead of the one included in the Sagecom router above. | ||
| 104 | Now I can control and organize all of my ethernet connections through | ||
| 105 | the VLANs on the managed switch and wireless connections through the | ||
| 106 | VLANS on the EAP. | ||
| 107 | |||
| 108 | ** Generic Printer | ||
| 109 | |||
| 110 | The last piece to my homelab is a standard wireless printer. Nothing | ||
| 111 | special here. | ||
| 112 | |||
| 113 | * Software | ||
| 114 | |||
| 115 | ** Ubuntu Server 20.04 | ||
| 116 | |||
| 117 | While the 20.04 version of Ubuntu was just released, I always like to | ||
| 118 | experiment with new features (and I don't mind breaking my system - it | ||
| 119 | just gives me more experience learning how to fix things). So, I have | ||
| 120 | Ubuntu Server 20.04 installed on the Dell Optiplex server and Ubuntu | ||
| 121 | Server 19.10 installed on the Raspberry Pi. Once I find an acceptable | ||
| 122 | use for the Pi, I will most likely switch the operating system. | ||
| 123 | |||
| 124 | ** Docker | ||
| 125 | |||
| 126 | I am /very/ new to Docker, but I have had a lot of fun playing with it | ||
| 127 | so far. Docker is used to create containers that can hold all the | ||
| 128 | contents of a system without interfering with other software on the same | ||
| 129 | system. So far, I have successfully installed pi-hole, GitLab, Gogs, and | ||
| 130 | Nextcloud in containers. However, I opted to delete all of those so that | ||
| 131 | I can reconfigure them more professionally at a later time. | ||
| 132 | |||
| 133 | ** Plex Media Server | ||
| 134 | |||
| 135 | Plex is a media center software that allows you to organize your movies, | ||
| 136 | TV shows, music, photos, and videos automatically. It will even download | ||
| 137 | metadata for you so that you can easily browse these collections. | ||
| 138 | |||
| 139 | ** Pi-hole | ||
| 140 | |||
| 141 | Pi-hole is an alternative ad-blocker that runs at the DNS level, | ||
| 142 | allowing you to block traffic when it hits your network, so that you can | ||
| 143 | reject any traffic you deem to be bad. Pi-hole uses blacklists and | ||
| 144 | whitelists to decide which traffic block and, luckily, there are a lot | ||
| 145 | of pre-made lists out there on Reddit, GitHub, etc. | ||
| 146 | |||
| 147 | ** Nextcloud | ||
| 148 | |||
| 149 | While I had trouble with the Docker version of Nextcloud, I was very | ||
| 150 | successful when setting up the snap version. Using this, I was able to | ||
| 151 | map Nextcloud to a subdomain of a domain I own in Namecheap. | ||
| 152 | Additionally, Nextcloud has an integration with Let's Encrypt that | ||
| 153 | allows me to issue certificates automatically to any new domain I | ||
| 154 | authorize. | ||
| 155 | |||
| 156 | ** Webmin | ||
| 157 | |||
| 158 | To monitor my servers, and the processes running on them, I use the | ||
| 159 | Webmin dashboard. This was fairly painless to set up, and I currently | ||
| 160 | access it straight through the server's IP address. In the future, I | ||
| 161 | will be looking to configure Webmin to use a custom domain just like | ||
| 162 | Nextcloud. | ||
content/blog/2020-05-19-customizing-ubuntu.md deleted −193
| @@ -1,193 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-05-19 | ||
| 3 | title = "A Beginner's Guide: Customizing Ubuntu" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # More Information | ||
| 9 | |||
| 10 | For inspiration on designing your \*nix computer, check out the | ||
| 11 | [r/unixporn](https://libredd.it/r/unixporn) subreddit! | ||
| 12 | |||
| 13 | # Customizing Ubuntu | ||
| 14 | |||
| 15 | New to Linux and want to add a personal touch to your machine? One of the best | ||
| 16 | perks of Linux is that it is **extremely** customizable. You can change the | ||
| 17 | styles of the windows, shell (status bars/docks), icons, fonts, terminals, and | ||
| 18 | more. | ||
| 19 | |||
| 20 | In this post, I'm going to go through customization on Ubuntu 20.04 (GNOME) | ||
| 21 | since most new users tend to choose Ubuntu-based distros. If you've found a way | ||
| 22 | to install Arch with i3-gaps, I'm assuming you know how to find more advanced | ||
| 23 | tutorials out there on customizations. | ||
| 24 | |||
| 25 | ## Required Tools | ||
| 26 | |||
| 27 | Ubuntu 20.04 ships with the default desktop environment | ||
| 28 | [Gnome](https://www.gnome.org/), which includes the handy `gnome-tweaks` tool to | ||
| 29 | quickly change designs. To install this, just open your terminal and enter the | ||
| 30 | following command: | ||
| 31 | |||
| 32 | ```sh | ||
| 33 | sudo apt install gnome-tweaks | ||
| 34 | ``` | ||
| 35 | |||
| 36 | After you've finished installing the tool, simply launch the Tweaks application, | ||
| 37 | and you'll be able to access the various customization options available by | ||
| 38 | default on Ubuntu. You might even like some of the pre-installed options. | ||
| 39 | |||
| 40 | ## GNOME Application Themes | ||
| 41 | |||
| 42 | To change the themes applied to applications in GNOME, you will need to change | ||
| 43 | the Applications dropdown in the Appearance section of Tweaks. To add more | ||
| 44 | themes, you will have to find your preferred theme online and follow the steps | ||
| 45 | below to have it show up in the Tweaks tool. While you may find themes anywhere, | ||
| 46 | one of the most popular sites for GNOME themes is | ||
| 47 | [gnome-look.org](https://www.gnome-look.org/). This website contains themes for | ||
| 48 | applications, shells, icons, and cursors. | ||
| 49 | |||
| 50 | Steps to import themes into Tweaks: | ||
| 51 | |||
| 52 | 1. Download the theme. | ||
| 53 | 2. These files are usually compressed (.zip, .tar.gz, .tar.xz), so you will need | ||
| 54 | to extract the contents. This is easiest when opening the file explorer, | ||
| 55 | right-clicking the compressed file, and choosing "Extract here." | ||
| 56 | 3. Move the theme folder to `/usr/share/themes/`. You can do so with the | ||
| 57 | following command: `sudo mv theme-folder/ /usr/share/themes/`. | ||
| 58 | - Icons and cursors will be moved to the `/usr/share/icons/` folder. | ||
| 59 | - Fonts will be moved to the `/usr/share/fonts/` folder Alternatively, you | ||
| 60 | can move them to the `/usr/share/fonts/opentype/` or | ||
| 61 | `/usr/share/fonts/opentype/` folders, if you have a specific font type. | ||
| 62 | 4. Close tweaks if it is open. Re-open Tweaks and your new theme will be | ||
| 63 | available in the Applications dropdown in the Appearance section of Tweaks. | ||
| 64 | |||
| 65 | If the theme is not showing up after you've moved it into the themes folder, you | ||
| 66 | may have uncompressed the folder into a sub-folder. You can check this by | ||
| 67 | entering the theme folder and listing the contents: | ||
| 68 | |||
| 69 | ```sh | ||
| 70 | cd /usr/share/themes/Mojave-Dark && ls -la | ||
| 71 | ``` | ||
| 72 | |||
| 73 | This is an example of what the contents of your theme folder should look like. | ||
| 74 | If you just see another folder there, you should move that folder up into the | ||
| 75 | `/usr/share/themes/` folder. | ||
| 76 | |||
| 77 | ```sh | ||
| 78 | cinnamon COPYING gnome-shell gtk-2.0 gtk-3.0 index.theme metacity-1 plank xfwm4 | ||
| 79 | ``` | ||
| 80 | |||
| 81 | ## GNOME Shell Themes | ||
| 82 | |||
| 83 | To change the appearance of the title bar, default dock, app menu, and other | ||
| 84 | parts of the GNOME shell, you'll need to install the [user | ||
| 85 | themes](https://extensions.gnome.org/extension/19/user-themes/) extension on | ||
| 86 | [Gnome Extensions](https://extensions.gnome.org/). To be able to install | ||
| 87 | extensions, you will first need to install the browser extension that the | ||
| 88 | website instructs you to. See this screenshot for the blue box with a link to | ||
| 89 | the extension. | ||
| 90 | |||
| 91 | After the browser extension is installed, you will need to install the native | ||
| 92 | host connector: | ||
| 93 | |||
| 94 | ```sh | ||
| 95 | sudo apt install chrome-gnome-shell | ||
| 96 | ``` | ||
| 97 | |||
| 98 | Finally, you can go the [user | ||
| 99 | themes](https://extensions.gnome.org/extension/19/user-themes/) extension page | ||
| 100 | and click the installation button. This will enable the Shell option in Tweaks. | ||
| 101 | Now you can move shell themes to the `/usr/share/themes` directory, using the | ||
| 102 | same steps mentioned in the previous section, and enable the new theme in | ||
| 103 | Tweaks. | ||
| 104 | |||
| 105 | ## Icons & Cursors | ||
| 106 | |||
| 107 | Icons and cursors are installed exactly the same way, so I'm grouping these | ||
| 108 | together in this post. Both of these items will need to follow the same process | ||
| 109 | as installing themes, except you will want to move your font folders to the | ||
| 110 | `/usr/share/icons/` directory instead. | ||
| 111 | |||
| 112 | ## Fonts | ||
| 113 | |||
| 114 | Fonts are one of the overlooked parts of customization, but a good font can make | ||
| 115 | the whole screen look different. For example, I have installed the [IBM | ||
| 116 | Plex](https://github.com/IBM/plex/releases) fonts on my system. This follows the | ||
| 117 | same process as installing themes, except you will want to move your font | ||
| 118 | folders to the `/usr/share/fonts/` directory instead. | ||
| 119 | |||
| 120 | ## Terminal | ||
| 121 | |||
| 122 | If you spend a lot of time typing commands, you know how important the style and | ||
| 123 | functionality of the terminal is. After spending a lot of time using the default | ||
| 124 | GNOME terminal with [unix | ||
| 125 | shell](<https://en.wikipedia.org/wiki/Bash_(Unix_shell)>), I decided to try some | ||
| 126 | different options. I ended up choosing | ||
| 127 | [Terminator](https://terminator-gtk3.readthedocs.io/en/latest/) with | ||
| 128 | [zsh](https://en.wikipedia.org/wiki/Z_shell). | ||
| 129 | |||
| 130 | Terminator is great if you need to open multiple terminals at one time by simply | ||
| 131 | right-clicking and splitting the screen into as many terminals as you want. | ||
| 132 | While this project hasn't been updated in a while, [it is coming under new | ||
| 133 | development](https://github.com/gnome-terminator/terminator/issues/1). However, | ||
| 134 | this terminal is great and I haven't experienced any errors yet. | ||
| 135 | |||
| 136 | For the shell choice, I decided to choose zsh after trying it out on a fresh | ||
| 137 | Manjaro installation. Zsh is great if you like to change the themes of your | ||
| 138 | terminal, include icons, or add plugins. | ||
| 139 | |||
| 140 | The desktop uses the | ||
| 141 | [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) to | ||
| 142 | suggest past commands as you type. In addition, it suggests corrections if you | ||
| 143 | misspell a command. Lastly, it uses the `af-magic` theme, which adds dashed | ||
| 144 | lines between commands, moving the user@host tag to the right side of the | ||
| 145 | terminal, and changes the colors. There are plenty of plugins and themes to | ||
| 146 | choose from. Just figure out what you like and add it to your `~/.zshrc` file! | ||
| 147 | |||
| 148 | ### Steps to Replicate My Terminal | ||
| 149 | |||
| 150 | To install zsh on Ubuntu, enter the following command into a terminal: | ||
| 151 | |||
| 152 | ```sh | ||
| 153 | sudo apt install zsh | ||
| 154 | ``` | ||
| 155 | |||
| 156 | Then, enter the next command to activate zsh: | ||
| 157 | |||
| 158 | ```sh | ||
| 159 | sudo chsh -s $(which zsh) $(whoami) | ||
| 160 | ``` | ||
| 161 | |||
| 162 | To install Terminator on Ubuntu: | ||
| 163 | |||
| 164 | ```sh | ||
| 165 | sudo apt install terminator | ||
| 166 | ``` | ||
| 167 | |||
| 168 | To install Oh My Zsh on Ubuntu: | ||
| 169 | |||
| 170 | ```sh | ||
| 171 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" | ||
| 172 | ``` | ||
| 173 | |||
| 174 | To install zsh-autosuggestions via Oh My Zsh: | ||
| 175 | |||
| 176 | ```sh | ||
| 177 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | ||
| 178 | ``` | ||
| 179 | |||
| 180 | Then, add the following plugin wording to your `~/.zshrc` file (the default | ||
| 181 | config usually has the `git` plugin activated, so just add any other plugins to | ||
| 182 | the parentheses separated by a space): | ||
| 183 | |||
| 184 | ```sh | ||
| 185 | nano ~/.zshrc | ||
| 186 | ``` | ||
| 187 | |||
| 188 | ```sh | ||
| 189 | plugins=(git zsh-autosuggestions) | ||
| 190 | ``` | ||
| 191 | |||
| 192 | Finally, you need to log out of your computer and log back in so your user shell | ||
| 193 | can refresh. | ||
content/blog/2020-05-19-customizing-ubuntu.org added +198
| @@ -0,0 +1,198 @@ | |||
| 1 | #+date: <2020-05-19> | ||
| 2 | #+title: A Beginner's Guide: Customizing Ubuntu | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * More Information | ||
| 7 | |||
| 8 | For inspiration on designing your *nix computer, check out the | ||
| 9 | [[https://libredd.it/r/unixporn][r/unixporn]] subreddit! | ||
| 10 | |||
| 11 | * Customizing Ubuntu | ||
| 12 | |||
| 13 | New to Linux and want to add a personal touch to your machine? One of | ||
| 14 | the best perks of Linux is that it is *extremely* customizable. You can | ||
| 15 | change the styles of the windows, shell (status bars/docks), icons, | ||
| 16 | fonts, terminals, and more. | ||
| 17 | |||
| 18 | In this post, I'm going to go through customization on Ubuntu 20.04 | ||
| 19 | (GNOME) since most new users tend to choose Ubuntu-based distros. If | ||
| 20 | you've found a way to install Arch with i3-gaps, I'm assuming you know | ||
| 21 | how to find more advanced tutorials out there on customizations. | ||
| 22 | |||
| 23 | ** Required Tools | ||
| 24 | |||
| 25 | Ubuntu 20.04 ships with the default desktop environment | ||
| 26 | [[https://www.gnome.org/][Gnome]], which includes the handy | ||
| 27 | =gnome-tweaks= tool to quickly change designs. To install this, just | ||
| 28 | open your terminal and enter the following command: | ||
| 29 | |||
| 30 | #+begin_src sh | ||
| 31 | sudo apt install gnome-tweaks | ||
| 32 | #+end_src | ||
| 33 | |||
| 34 | After you've finished installing the tool, simply launch the Tweaks | ||
| 35 | application, and you'll be able to access the various customization | ||
| 36 | options available by default on Ubuntu. You might even like some of the | ||
| 37 | pre-installed options. | ||
| 38 | |||
| 39 | ** GNOME Application Themes | ||
| 40 | |||
| 41 | To change the themes applied to applications in GNOME, you will need to | ||
| 42 | change the Applications dropdown in the Appearance section of Tweaks. To | ||
| 43 | add more themes, you will have to find your preferred theme online and | ||
| 44 | follow the steps below to have it show up in the Tweaks tool. While you | ||
| 45 | may find themes anywhere, one of the most popular sites for GNOME themes | ||
| 46 | is [[https://www.gnome-look.org/][gnome-look.org]]. This website | ||
| 47 | contains themes for applications, shells, icons, and cursors. | ||
| 48 | |||
| 49 | Steps to import themes into Tweaks: | ||
| 50 | |||
| 51 | 1. Download the theme. | ||
| 52 | 2. These files are usually compressed (.zip, .tar.gz, .tar.xz), so you | ||
| 53 | will need to extract the contents. This is easiest when opening the | ||
| 54 | file explorer, right-clicking the compressed file, and choosing | ||
| 55 | "Extract here." | ||
| 56 | 3. Move the theme folder to =/usr/share/themes/=. You can do so with the | ||
| 57 | following command: =sudo mv theme-folder/ /usr/share/themes/=. | ||
| 58 | - Icons and cursors will be moved to the =/usr/share/icons/= folder. | ||
| 59 | - Fonts will be moved to the =/usr/share/fonts/= folder | ||
| 60 | Alternatively, you can move them to the | ||
| 61 | =/usr/share/fonts/opentype/= or =/usr/share/fonts/opentype/= | ||
| 62 | folders, if you have a specific font type. | ||
| 63 | 4. Close tweaks if it is open. Re-open Tweaks and your new theme will be | ||
| 64 | available in the Applications dropdown in the Appearance section of | ||
| 65 | Tweaks. | ||
| 66 | |||
| 67 | If the theme is not showing up after you've moved it into the themes | ||
| 68 | folder, you may have uncompressed the folder into a sub-folder. You can | ||
| 69 | check this by entering the theme folder and listing the contents: | ||
| 70 | |||
| 71 | #+begin_src sh | ||
| 72 | cd /usr/share/themes/Mojave-Dark && ls -la | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | This is an example of what the contents of your theme folder should look | ||
| 76 | like. If you just see another folder there, you should move that folder | ||
| 77 | up into the =/usr/share/themes/= folder. | ||
| 78 | |||
| 79 | #+begin_src sh | ||
| 80 | cinnamon COPYING gnome-shell gtk-2.0 gtk-3.0 index.theme metacity-1 plank xfwm4 | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | ** GNOME Shell Themes | ||
| 84 | |||
| 85 | To change the appearance of the title bar, default dock, app menu, and | ||
| 86 | other parts of the GNOME shell, you'll need to install the | ||
| 87 | [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] | ||
| 88 | extension on [[https://extensions.gnome.org/][Gnome Extensions]]. To be | ||
| 89 | able to install extensions, you will first need to install the browser | ||
| 90 | extension that the website instructs you to. See this screenshot for the | ||
| 91 | blue box with a link to the extension. | ||
| 92 | |||
| 93 | After the browser extension is installed, you will need to install the | ||
| 94 | native host connector: | ||
| 95 | |||
| 96 | #+begin_src sh | ||
| 97 | sudo apt install chrome-gnome-shell | ||
| 98 | #+end_src | ||
| 99 | |||
| 100 | Finally, you can go the | ||
| 101 | [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] | ||
| 102 | extension page and click the installation button. This will enable the | ||
| 103 | Shell option in Tweaks. Now you can move shell themes to the | ||
| 104 | =/usr/share/themes= directory, using the same steps mentioned in the | ||
| 105 | previous section, and enable the new theme in Tweaks. | ||
| 106 | |||
| 107 | ** Icons & Cursors | ||
| 108 | |||
| 109 | Icons and cursors are installed exactly the same way, so I'm grouping | ||
| 110 | these together in this post. Both of these items will need to follow the | ||
| 111 | same process as installing themes, except you will want to move your | ||
| 112 | font folders to the =/usr/share/icons/= directory instead. | ||
| 113 | |||
| 114 | ** Fonts | ||
| 115 | |||
| 116 | Fonts are one of the overlooked parts of customization, but a good font | ||
| 117 | can make the whole screen look different. For example, I have installed | ||
| 118 | the [[https://github.com/IBM/plex/releases][IBM Plex]] fonts on my | ||
| 119 | system. This follows the same process as installing themes, except you | ||
| 120 | will want to move your font folders to the =/usr/share/fonts/= directory | ||
| 121 | instead. | ||
| 122 | |||
| 123 | ** Terminal | ||
| 124 | |||
| 125 | If you spend a lot of time typing commands, you know how important the | ||
| 126 | style and functionality of the terminal is. After spending a lot of time | ||
| 127 | using the default GNOME terminal with | ||
| 128 | [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][unix shell]], I | ||
| 129 | decided to try some different options. I ended up choosing | ||
| 130 | [[https://terminator-gtk3.readthedocs.io/en/latest/][Terminator]] with | ||
| 131 | [[https://en.wikipedia.org/wiki/Z_shell][zsh]]. | ||
| 132 | |||
| 133 | Terminator is great if you need to open multiple terminals at one time | ||
| 134 | by simply right-clicking and splitting the screen into as many terminals | ||
| 135 | as you want. While this project hasn't been updated in a while, | ||
| 136 | [[https://github.com/gnome-terminator/terminator/issues/1][it is coming | ||
| 137 | under new development]]. However, this terminal is great and I haven't | ||
| 138 | experienced any errors yet. | ||
| 139 | |||
| 140 | For the shell choice, I decided to choose zsh after trying it out on a | ||
| 141 | fresh Manjaro installation. Zsh is great if you like to change the | ||
| 142 | themes of your terminal, include icons, or add plugins. | ||
| 143 | |||
| 144 | The desktop uses the | ||
| 145 | [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]] | ||
| 146 | to suggest past commands as you type. In addition, it suggests | ||
| 147 | corrections if you misspell a command. Lastly, it uses the =af-magic= | ||
| 148 | theme, which adds dashed lines between commands, moving the user@host | ||
| 149 | tag to the right side of the terminal, and changes the colors. There are | ||
| 150 | plenty of plugins and themes to choose from. Just figure out what you | ||
| 151 | like and add it to your =~/.zshrc= file! | ||
| 152 | |||
| 153 | *** Steps to Replicate My Terminal | ||
| 154 | |||
| 155 | To install zsh on Ubuntu, enter the following command into a terminal: | ||
| 156 | |||
| 157 | #+begin_src sh | ||
| 158 | sudo apt install zsh | ||
| 159 | #+end_src | ||
| 160 | |||
| 161 | Then, enter the next command to activate zsh: | ||
| 162 | |||
| 163 | #+begin_src sh | ||
| 164 | sudo chsh -s $(which zsh) $(whoami) | ||
| 165 | #+end_src | ||
| 166 | |||
| 167 | To install Terminator on Ubuntu: | ||
| 168 | |||
| 169 | #+begin_src sh | ||
| 170 | sudo apt install terminator | ||
| 171 | #+end_src | ||
| 172 | |||
| 173 | To install Oh My Zsh on Ubuntu: | ||
| 174 | |||
| 175 | #+begin_src sh | ||
| 176 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" | ||
| 177 | #+end_src | ||
| 178 | |||
| 179 | To install zsh-autosuggestions via Oh My Zsh: | ||
| 180 | |||
| 181 | #+begin_src sh | ||
| 182 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | ||
| 183 | #+end_src | ||
| 184 | |||
| 185 | Then, add the following plugin wording to your =~/.zshrc= file (the | ||
| 186 | default config usually has the =git= plugin activated, so just add any | ||
| 187 | other plugins to the parentheses separated by a space): | ||
| 188 | |||
| 189 | #+begin_src sh | ||
| 190 | nano ~/.zshrc | ||
| 191 | #+end_src | ||
| 192 | |||
| 193 | #+begin_src sh | ||
| 194 | plugins=(git zsh-autosuggestions) | ||
| 195 | #+end_src | ||
| 196 | |||
| 197 | Finally, you need to log out of your computer and log back in so your | ||
| 198 | user shell can refresh. | ||
content/blog/2020-07-20-video-game-sales.md → content/blog/2020-07-20-video-game-sales.org renamed +59 −60
| @@ -1,36 +1,35 @@ | |||
| 1 | +++ | 1 | #+date: <2020-07-20> |
| 2 | date = 2020-07-20 | 2 | #+title: Data Exploration: Video Games Sales |
| 3 | title = "Data Exploration: Video Games Sales" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Background Information | 5 | |
| 6 | * Background Information | ||
| 9 | 7 | ||
| 10 | This dataset (obtained from | 8 | This dataset (obtained from |
| 11 | [Kaggle](https://www.kaggle.com/gregorut/videogamesales/data)) contains a list | 9 | [[https://www.kaggle.com/gregorut/videogamesales/data][Kaggle]]) |
| 12 | of video games with sales greater than 100,000 copies. It was generated by a | 10 | contains a list of video games with sales greater than 100,000 copies. |
| 13 | scrape of vgchartz.com. | 11 | It was generated by a scrape of vgchartz.com. |
| 14 | 12 | ||
| 15 | Fields include: | 13 | Fields include: |
| 16 | 14 | ||
| 17 | - Rank: Ranking of overall sales | 15 | - Rank: Ranking of overall sales |
| 18 | - Name: The game name | 16 | - Name: The game name |
| 19 | - Platform: Platform of the game release (i.e. PC,PS4, etc.) | 17 | - Platform: Platform of the game release (i.e. PC,PS4, etc.) |
| 20 | - Year: Year of the game's release | 18 | - Year: Year of the game's release |
| 21 | - Genre: Genre of the game | 19 | - Genre: Genre of the game |
| 22 | - Publisher: Publisher of the game | 20 | - Publisher: Publisher of the game |
| 23 | - NA~Sales~: Sales in North America (in millions) | 21 | - NA_{Sales}: Sales in North America (in millions) |
| 24 | - EU~Sales~: Sales in Europe (in millions) | 22 | - EU_{Sales}: Sales in Europe (in millions) |
| 25 | - JP~Sales~: Sales in Japan (in millions) | 23 | - JP_{Sales}: Sales in Japan (in millions) |
| 26 | - Other~Sales~: Sales in the rest of the world (in millions) | 24 | - Other_{Sales}: Sales in the rest of the world (in millions) |
| 27 | - Global~Sales~: Total worldwide sales. | 25 | - Global_{Sales}: Total worldwide sales. |
| 28 | 26 | ||
| 29 | There are 16,598 records. 2 records were dropped due to incomplete information. | 27 | There are 16,598 records. 2 records were dropped due to incomplete |
| 28 | information. | ||
| 30 | 29 | ||
| 31 | # Import the Data | 30 | * Import the Data |
| 32 | 31 | ||
| 33 | ```python | 32 | #+begin_src python |
| 34 | # Import the Python libraries we will be using | 33 | # Import the Python libraries we will be using |
| 35 | import pandas as pd | 34 | import pandas as pd |
| 36 | import numpy as np | 35 | import numpy as np |
| @@ -41,42 +40,42 @@ import matplotlib.pyplot as plt | |||
| 41 | file = r'video_game_sales.csv' | 40 | file = r'video_game_sales.csv' |
| 42 | df = pd.read_csv(file) | 41 | df = pd.read_csv(file) |
| 43 | df | 42 | df |
| 44 | ``` | 43 | #+end_src |
| 45 | 44 | ||
| 46 | # Explore the Data | 45 | * Explore the Data |
| 47 | 46 | ||
| 48 | ```python | 47 | #+begin_src python |
| 49 | # With the description function, we can see the basic stats. For example, we can | 48 | # With the description function, we can see the basic stats. For example, we can |
| 50 | # also see that the 'Year' column has some incomplete values. | 49 | # also see that the 'Year' column has some incomplete values. |
| 51 | df.describe() | 50 | df.describe() |
| 52 | ``` | 51 | #+end_src |
| 53 | 52 | ||
| 54 | ```python | 53 | #+begin_src python |
| 55 | # This function shows the rows and columns of NaN values. For example, df[179,3] = nan | 54 | # This function shows the rows and columns of NaN values. For example, df[179,3] = nan |
| 56 | np.where(pd.isnull(df)) | 55 | np.where(pd.isnull(df)) |
| 57 | 56 | ||
| 58 | (array([179, ..., 16553], dtype=int64), | 57 | (array([179, ..., 16553], dtype=int64), |
| 59 | array([3, ..., 5], dtype=int64)) | 58 | array([3, ..., 5], dtype=int64)) |
| 60 | ``` | 59 | #+end_src |
| 61 | 60 | ||
| 62 | # Visualize the Data | 61 | * Visualize the Data |
| 63 | 62 | ||
| 64 | ```python | 63 | #+begin_src python |
| 65 | # This function plots the global sales by platform | 64 | # This function plots the global sales by platform |
| 66 | sns.catplot(x='Platform', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=90) | 65 | sns.catplot(x='Platform', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=90) |
| 67 | ``` | 66 | #+end_src |
| 68 | 67 | ||
| 69 | ```python | 68 | #+begin_src python |
| 70 | # This function plots the global sales by genre | 69 | # This function plots the global sales by genre |
| 71 | sns.catplot(x='Genre', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=45) | 70 | sns.catplot(x='Genre', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=45) |
| 72 | ``` | 71 | #+end_src |
| 73 | 72 | ||
| 74 | ```python | 73 | #+begin_src python |
| 75 | # This function plots the global sales by year | 74 | # This function plots the global sales by year |
| 76 | sns.lmplot(x='Year', y='Global_Sales', data=df).set_xticklabels(rotation=45) | 75 | sns.lmplot(x='Year', y='Global_Sales', data=df).set_xticklabels(rotation=45) |
| 77 | ``` | 76 | #+end_src |
| 78 | 77 | ||
| 79 | ```python | 78 | #+begin_src python |
| 80 | # This function plots four different lines to show sales from different regions. | 79 | # This function plots four different lines to show sales from different regions. |
| 81 | # The global sales plot line is commented-out, but can be included for comparison | 80 | # The global sales plot line is commented-out, but can be included for comparison |
| 82 | df2 = df.groupby('Year').sum() | 81 | df2 = df.groupby('Year').sum() |
| @@ -100,11 +99,11 @@ ax.plot(years, d, label='Other_Sales') | |||
| 100 | 99 | ||
| 101 | ax.legend() | 100 | ax.legend() |
| 102 | plt.show() | 101 | plt.show() |
| 103 | ``` | 102 | #+end_src |
| 104 | 103 | ||
| 105 | ## Investigate Outliers | 104 | ** Investigate Outliers |
| 106 | 105 | ||
| 107 | ```python | 106 | #+begin_src python |
| 108 | # Find the game with the highest sales in North America | 107 | # Find the game with the highest sales in North America |
| 109 | df.loc[df['NA_Sales'].idxmax()] | 108 | df.loc[df['NA_Sales'].idxmax()] |
| 110 | 109 | ||
| @@ -124,33 +123,33 @@ Name: 0, dtype: object | |||
| 124 | # Explore statistics in the year 2006 (highest selling year) | 123 | # Explore statistics in the year 2006 (highest selling year) |
| 125 | df3 = df[(df['Year'] == 2006)] | 124 | df3 = df[(df['Year'] == 2006)] |
| 126 | df3.describe() | 125 | df3.describe() |
| 127 | ``` | 126 | #+end_src |
| 128 | 127 | ||
| 129 | ```python | 128 | #+begin_src python |
| 130 | # Plot the results of the previous dataframe (games from 2006) - we can see the year's results were largely carried by Wii Sports | 129 | # Plot the results of the previous dataframe (games from 2006) - we can see the year's results were largely carried by Wii Sports |
| 131 | sns.catplot(x="Genre", y="Global_Sales", data=df3, jitter=False).set_xticklabels(rotation=45) | 130 | sns.catplot(x="Genre", y="Global_Sales", data=df3, jitter=False).set_xticklabels(rotation=45) |
| 132 | ``` | 131 | #+end_src |
| 133 | 132 | ||
| 134 | ```python | 133 | #+begin_src python |
| 135 | # We can see 4 outliers in the graph above, so let's get the top 5 games from that dataframe | 134 | # We can see 4 outliers in the graph above, so let's get the top 5 games from that dataframe |
| 136 | # The results below show that Nintendo had all top 5 games (3 on the Wii and 2 on the DS) | 135 | # The results below show that Nintendo had all top 5 games (3 on the Wii and 2 on the DS) |
| 137 | df3.sort_values(by=['Global_Sales'], ascending=False).head(5) | 136 | df3.sort_values(by=['Global_Sales'], ascending=False).head(5) |
| 138 | ``` | 137 | #+end_src |
| 139 | 138 | ||
| 140 | # Discussion | 139 | * Discussion |
| 141 | 140 | ||
| 142 | The purpose of exploring datasets is to ask questions, answer questions, and | 141 | The purpose of exploring datasets is to ask questions, answer questions, |
| 143 | discover intelligence that can be used to inform decision-making. So, what have | 142 | and discover intelligence that can be used to inform decision-making. |
| 144 | we found in this dataset? | 143 | So, what have we found in this dataset? |
| 145 | 144 | ||
| 146 | Today we simply explored a publicly-available dataset to see what kind of | 145 | Today we simply explored a publicly-available dataset to see what kind |
| 147 | information it contained. During that exploration, we found that video game | 146 | of information it contained. During that exploration, we found that |
| 148 | sales peaked in 2006. That peak was largely due to Nintendo, who sold the top 5 | 147 | video game sales peaked in 2006. That peak was largely due to Nintendo, |
| 149 | games in 2006 and has a number of games in the top-10 list for the years | 148 | who sold the top 5 games in 2006 and has a number of games in the top-10 |
| 150 | 1980-2020. Additionally, the top four platforms by global sales (Wii, NES, GB, | 149 | list for the years 1980-2020. Additionally, the top four platforms by |
| 151 | DS) are owned by Nintendo. | 150 | global sales (Wii, NES, GB, DS) are owned by Nintendo. |
| 152 | 151 | ||
| 153 | We didn't explore everything this dataset has to offer, but we can tell from a | 152 | We didn't explore everything this dataset has to offer, but we can tell |
| 154 | brief analysis that Nintendo seems to rule sales in the video gaming world. | 153 | from a brief analysis that Nintendo seems to rule sales in the video |
| 155 | Further analysis could provide insight into which genres, regions, publishers, | 154 | gaming world. Further analysis could provide insight into which genres, |
| 156 | or world events are correlated with sales. | 155 | regions, publishers, or world events are correlated with sales. |
content/blog/2020-07-26-business-analysis.md → content/blog/2020-07-26-business-analysis.org renamed +102 −98
| @@ -1,32 +1,30 @@ | |||
| 1 | +++ | 1 | #+date: <2020-07-26> |
| 2 | date = 2020-07-26 | 2 | #+title: Algorithmically Analyzing Local Businesses |
| 3 | title = "Algorithmically Analyzing Local Businesses " | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Background Information | ||
| 9 | 5 | ||
| 10 | This project aims to help investors learn more about a random city in order to | 6 | * Background Information |
| 11 | determine optimal locations for business investments. The data used in this | 7 | |
| 12 | project was obtained using Foursquare's developer API. | 8 | This project aims to help investors learn more about a random city in |
| 9 | order to determine optimal locations for business investments. The data | ||
| 10 | used in this project was obtained using Foursquare's developer API. | ||
| 13 | 11 | ||
| 14 | Fields include: | 12 | Fields include: |
| 15 | 13 | ||
| 16 | - Venue Name | 14 | - Venue Name |
| 17 | - Venue Category | 15 | - Venue Category |
| 18 | - Venue Latitude | 16 | - Venue Latitude |
| 19 | - Venue Longitude | 17 | - Venue Longitude |
| 20 | 18 | ||
| 21 | There are 232 records found using the center of Lincoln as the area of interest | 19 | There are 232 records found using the center of Lincoln as the area of |
| 22 | with a radius of 10,000. | 20 | interest with a radius of 10,000. |
| 23 | 21 | ||
| 24 | # Import the Data | 22 | * Import the Data |
| 25 | 23 | ||
| 26 | The first step is the simplest: import the applicable libraries. We will be | 24 | The first step is the simplest: import the applicable libraries. We will |
| 27 | using the libraries below for this project. | 25 | be using the libraries below for this project. |
| 28 | 26 | ||
| 29 | ```python | 27 | #+begin_src python |
| 30 | # Import the Python libraries we will be using | 28 | # Import the Python libraries we will be using |
| 31 | import pandas as pd | 29 | import pandas as pd |
| 32 | import requests | 30 | import requests |
| @@ -35,29 +33,29 @@ import math | |||
| 35 | import json | 33 | import json |
| 36 | from pandas.io.json import json_normalize | 34 | from pandas.io.json import json_normalize |
| 37 | from sklearn.cluster import KMeans | 35 | from sklearn.cluster import KMeans |
| 38 | ``` | 36 | #+end_src |
| 39 | 37 | ||
| 40 | To begin our analysis, we need to import the data for this project. The data we | 38 | To begin our analysis, we need to import the data for this project. The |
| 41 | are using in this project comes directly from the Foursquare API. The first step | 39 | data we are using in this project comes directly from the Foursquare |
| 42 | is to get the latitude and longitude of the city being studied (Lincoln, NE) and | 40 | API. The first step is to get the latitude and longitude of the city |
| 43 | setting up the folium map. | 41 | being studied (Lincoln, NE) and setting up the folium map. |
| 44 | 42 | ||
| 45 | ```python | 43 | #+begin_src python |
| 46 | # Define the latitude and longitude, then map the results | 44 | # Define the latitude and longitude, then map the results |
| 47 | latitude = 40.806862 | 45 | latitude = 40.806862 |
| 48 | longitude = -96.681679 | 46 | longitude = -96.681679 |
| 49 | map_LNK = folium.Map(location=[latitude, longitude], zoom_start=12) | 47 | map_LNK = folium.Map(location=[latitude, longitude], zoom_start=12) |
| 50 | 48 | ||
| 51 | map_LNK | 49 | map_LNK |
| 52 | ``` | 50 | #+end_src |
| 53 | 51 | ||
| 54 | Now that we have defined our city and created the map, we need to go get the | 52 | Now that we have defined our city and created the map, we need to go get |
| 55 | business data. The Foursquare API will limit the results to 100 per API call, so | 53 | the business data. The Foursquare API will limit the results to 100 per |
| 56 | we use our first API call below to determine the total results that Foursquare | 54 | API call, so we use our first API call below to determine the total |
| 57 | has found. Since the total results are 232, we perform the API fetching process | 55 | results that Foursquare has found. Since the total results are 232, we |
| 58 | three times (100 + 100 + 32 = 232). | 56 | perform the API fetching process three times (100 + 100 + 32 = 232). |
| 59 | 57 | ||
| 60 | ```python | 58 | #+begin_src python |
| 61 | # Foursquare API credentials | 59 | # Foursquare API credentials |
| 62 | CLIENT_ID = 'your-client-id' | 60 | CLIENT_ID = 'your-client-id' |
| 63 | CLIENT_SECRET = 'your-client-secret' | 61 | CLIENT_SECRET = 'your-client-secret' |
| @@ -115,18 +113,19 @@ url3 = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret | |||
| 115 | 113 | ||
| 116 | # Fetch the final results (201 - 232) | 114 | # Fetch the final results (201 - 232) |
| 117 | results3 = requests.get(url3).json() | 115 | results3 = requests.get(url3).json() |
| 118 | ``` | 116 | #+end_src |
| 119 | 117 | ||
| 120 | # Clean the Data | 118 | * Clean the Data |
| 121 | 119 | ||
| 122 | Now that we have our data in three separate dataframes, we need to combine them | 120 | Now that we have our data in three separate dataframes, we need to |
| 123 | into a single dataframe and make sure to reset the index so that we have a | 121 | combine them into a single dataframe and make sure to reset the index so |
| 124 | unique ID for each business. The `get~categorytype~` function below will pull | 122 | that we have a unique ID for each business. The =get~categorytype~= |
| 125 | the categories and name from each business's entry in the Foursquare data | 123 | function below will pull the categories and name from each business's |
| 126 | automatically. Once all the data has been labeled and combined, the results are | 124 | entry in the Foursquare data automatically. Once all the data has been |
| 127 | stored in the `nearby_venues` dataframe. | 125 | labeled and combined, the results are stored in the =nearby_venues= |
| 126 | dataframe. | ||
| 128 | 127 | ||
| 129 | ```python | 128 | #+begin_src python |
| 130 | # This function will extract the category of the venue from the API dictionary | 129 | # This function will extract the category of the venue from the API dictionary |
| 131 | def get_category_type(row): | 130 | def get_category_type(row): |
| 132 | try: | 131 | try: |
| @@ -189,15 +188,15 @@ nearby_venues3.columns = [col.split(".")[-1] for col in nearby_venues3.columns] | |||
| 189 | nearby_venues = nearby_venues.append(nearby_venues3) | 188 | nearby_venues = nearby_venues.append(nearby_venues3) |
| 190 | nearby_venues = nearby_venues.reset_index(drop=True) | 189 | nearby_venues = nearby_venues.reset_index(drop=True) |
| 191 | nearby_venues | 190 | nearby_venues |
| 192 | ``` | 191 | #+end_src |
| 193 | 192 | ||
| 194 | # Visualize the Data | 193 | * Visualize the Data |
| 195 | 194 | ||
| 196 | We now have a complete, clean data set. The next step is to visualize this data | 195 | We now have a complete, clean data set. The next step is to visualize |
| 197 | onto the map we created earlier. We will be using folium's `CircleMarker()` | 196 | this data onto the map we created earlier. We will be using folium's |
| 198 | function to do this. | 197 | =CircleMarker()= function to do this. |
| 199 | 198 | ||
| 200 | ```python | 199 | #+begin_src python |
| 201 | # add markers to map | 200 | # add markers to map |
| 202 | for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'], nearby_venues['name'], nearby_venues['categories']): | 201 | for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'], nearby_venues['name'], nearby_venues['categories']): |
| 203 | label = '{} ({})'.format(name, categories) | 202 | label = '{} ({})'.format(name, categories) |
| @@ -213,20 +212,21 @@ for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'] | |||
| 213 | ).add_to(map_LNK) | 212 | ).add_to(map_LNK) |
| 214 | 213 | ||
| 215 | map_LNK | 214 | map_LNK |
| 216 | ``` | 215 | #+end_src |
| 217 | 216 | ||
| 218 | # Clustering: _k-means_ | 217 | * Clustering: /k-means/ |
| 219 | 218 | ||
| 220 | To cluster the data, we will be using the _k-means_ algorithm. This algorithm is | 219 | To cluster the data, we will be using the /k-means/ algorithm. This |
| 221 | iterative and will automatically make sure that data points in each cluster are | 220 | algorithm is iterative and will automatically make sure that data points |
| 222 | as close as possible to each other, while being as far as possible away from | 221 | in each cluster are as close as possible to each other, while being as |
| 223 | other clusters. | 222 | far as possible away from other clusters. |
| 224 | 223 | ||
| 225 | However, we first have to figure out how many clusters to use (defined as the | 224 | However, we first have to figure out how many clusters to use (defined |
| 226 | variable _'k'_). To do so, we will use the next two functions to calculate the | 225 | as the variable /'k'/). To do so, we will use the next two functions to |
| 227 | sum of squares within clusters and then return the optimal number of clusters. | 226 | calculate the sum of squares within clusters and then return the optimal |
| 227 | number of clusters. | ||
| 228 | 228 | ||
| 229 | ```python | 229 | #+begin_src python |
| 230 | # This function will return the sum of squares found in the data | 230 | # This function will return the sum of squares found in the data |
| 231 | def calculate_wcss(data): | 231 | def calculate_wcss(data): |
| 232 | wcss = [] | 232 | wcss = [] |
| @@ -260,13 +260,13 @@ def optimal_number_of_clusters(wcss): | |||
| 260 | 260 | ||
| 261 | # calculating the optimal number of clusters | 261 | # calculating the optimal number of clusters |
| 262 | n = optimal_number_of_clusters(sum_of_squares) | 262 | n = optimal_number_of_clusters(sum_of_squares) |
| 263 | ``` | 263 | #+end_src |
| 264 | 264 | ||
| 265 | Now that we have found that our optimal number of clusters is six, we need to | 265 | Now that we have found that our optimal number of clusters is six, we |
| 266 | perform k-means clustering. When this clustering occurs, each business is | 266 | need to perform k-means clustering. When this clustering occurs, each |
| 267 | assigned a cluster number from 0 to 5 in the dataframe. | 267 | business is assigned a cluster number from 0 to 5 in the dataframe. |
| 268 | 268 | ||
| 269 | ```python | 269 | #+begin_src python |
| 270 | # set number of clusters equal to the optimal number | 270 | # set number of clusters equal to the optimal number |
| 271 | kclusters = n | 271 | kclusters = n |
| 272 | 272 | ||
| @@ -275,12 +275,13 @@ kmeans = KMeans(n_clusters=kclusters, random_state=0).fit(cluster_df) | |||
| 275 | 275 | ||
| 276 | # add clustering labels to dataframe | 276 | # add clustering labels to dataframe |
| 277 | nearby_venues.insert(0, 'Cluster Labels', kmeans.labels_) | 277 | nearby_venues.insert(0, 'Cluster Labels', kmeans.labels_) |
| 278 | ``` | 278 | #+end_src |
| 279 | 279 | ||
| 280 | Success! We now have a dataframe with clean business data, along with a cluster | 280 | Success! We now have a dataframe with clean business data, along with a |
| 281 | number for each business. Now let's map the data using six different colors. | 281 | cluster number for each business. Now let's map the data using six |
| 282 | different colors. | ||
| 282 | 283 | ||
| 283 | ```python | 284 | #+begin_src python |
| 284 | # create map with clusters | 285 | # create map with clusters |
| 285 | map_clusters = folium.Map(location=[latitude, longitude], zoom_start=12) | 286 | map_clusters = folium.Map(location=[latitude, longitude], zoom_start=12) |
| 286 | colors = ['#0F9D58', '#DB4437', '#4285F4', '#800080', '#ce12c0', '#171717'] | 287 | colors = ['#0F9D58', '#DB4437', '#4285F4', '#800080', '#ce12c0', '#171717'] |
| @@ -299,30 +300,31 @@ for lat, lng, name, categories, cluster in zip(nearby_venues['lat'], nearby_venu | |||
| 299 | fill_opacity=0.7).add_to(map_clusters) | 300 | fill_opacity=0.7).add_to(map_clusters) |
| 300 | 301 | ||
| 301 | map_clusters | 302 | map_clusters |
| 302 | ``` | 303 | #+end_src |
| 303 | 304 | ||
| 304 | # Investigate Clusters | 305 | * Investigate Clusters |
| 305 | 306 | ||
| 306 | Now that we have figured out our clusters, let's do a little more analysis to | 307 | Now that we have figured out our clusters, let's do a little more |
| 307 | provide more insight into the clusters. With the information below, we can see | 308 | analysis to provide more insight into the clusters. With the information |
| 308 | which clusters are more popular for businesses and which are less popular. The | 309 | below, we can see which clusters are more popular for businesses and |
| 309 | results below show us that clusters 0 through 3 are popular, while clusters 4 | 310 | which are less popular. The results below show us that clusters 0 |
| 310 | and 5 are not very popular at all. | 311 | through 3 are popular, while clusters 4 and 5 are not very popular at |
| 312 | all. | ||
| 311 | 313 | ||
| 312 | ```python | 314 | #+begin_src python |
| 313 | # Show how many venues are in each cluster | 315 | # Show how many venues are in each cluster |
| 314 | color_names = ['Dark Green', 'Red', 'Blue', 'Purple', 'Pink', 'Black'] | 316 | color_names = ['Dark Green', 'Red', 'Blue', 'Purple', 'Pink', 'Black'] |
| 315 | for x in range(0,6): | 317 | for x in range(0,6): |
| 316 | print("Color of Cluster", x, ":", color_names[x]) | 318 | print("Color of Cluster", x, ":", color_names[x]) |
| 317 | print("Venues found in Cluster", x, ":", nearby_venues.loc[nearby_venues['Cluster Labels'] == x, nearby_venues.columns[:]].shape[0]) | 319 | print("Venues found in Cluster", x, ":", nearby_venues.loc[nearby_venues['Cluster Labels'] == x, nearby_venues.columns[:]].shape[0]) |
| 318 | print("---") | 320 | print("---") |
| 319 | ``` | 321 | #+end_src |
| 320 | 322 | ||
| 321 | Our last piece of analysis is to summarize the categories of businesses within | 323 | Our last piece of analysis is to summarize the categories of businesses |
| 322 | each cluster. With these results, we can clearly see that restaurants, coffee | 324 | within each cluster. With these results, we can clearly see that |
| 323 | shops, and grocery stores are the most popular. | 325 | restaurants, coffee shops, and grocery stores are the most popular. |
| 324 | 326 | ||
| 325 | ```python | 327 | #+begin_src python |
| 326 | # Calculate how many venues there are in each category | 328 | # Calculate how many venues there are in each category |
| 327 | # Sort from largest to smallest | 329 | # Sort from largest to smallest |
| 328 | temp_df = nearby_venues.drop(columns=['name', 'lat', 'lng']) | 330 | temp_df = nearby_venues.drop(columns=['name', 'lat', 'lng']) |
| @@ -342,21 +344,23 @@ with pd.option_context('display.max_rows', None, 'display.max_columns', None): | |||
| 342 | print("\n\n", "Cluster 3:", "\n", cluster3_grouped.loc[cluster3_grouped['Cluster Labels'] > 1]) | 344 | print("\n\n", "Cluster 3:", "\n", cluster3_grouped.loc[cluster3_grouped['Cluster Labels'] > 1]) |
| 343 | print("\n\n", "Cluster 4:", "\n", cluster4_grouped.loc[cluster4_grouped['Cluster Labels'] > 1]) | 345 | print("\n\n", "Cluster 4:", "\n", cluster4_grouped.loc[cluster4_grouped['Cluster Labels'] > 1]) |
| 344 | print("\n\n", "Cluster 5:", "\n", cluster5_grouped.loc[cluster5_grouped['Cluster Labels'] > 1]) | 346 | print("\n\n", "Cluster 5:", "\n", cluster5_grouped.loc[cluster5_grouped['Cluster Labels'] > 1]) |
| 345 | ``` | 347 | #+end_src |
| 346 | 348 | ||
| 347 | # Discussion | 349 | * Discussion |
| 348 | 350 | ||
| 349 | In this project, we gathered location data for Lincoln, Nebraska, USA and | 351 | In this project, we gathered location data for Lincoln, Nebraska, USA |
| 350 | clustered the data using the k-means algorithm in order to identify the unique | 352 | and clustered the data using the k-means algorithm in order to identify |
| 351 | clusters of businesses in Lincoln. Through these actions, we found that there | 353 | the unique clusters of businesses in Lincoln. Through these actions, we |
| 352 | are six unique business clusters in Lincoln and that two of the clusters are | 354 | found that there are six unique business clusters in Lincoln and that |
| 353 | likely unsuitable for investors. The remaining four clusters have a variety of | 355 | two of the clusters are likely unsuitable for investors. The remaining |
| 354 | businesses, but are largely dominated by restaurants and grocery stores. | 356 | four clusters have a variety of businesses, but are largely dominated by |
| 355 | 357 | restaurants and grocery stores. | |
| 356 | Using this project, investors can now make more informed decisions when deciding | 358 | |
| 357 | the location and category of business in which to invest. | 359 | Using this project, investors can now make more informed decisions when |
| 358 | 360 | deciding the location and category of business in which to invest. | |
| 359 | Further studies may involve other attributes for business locations, such as | 361 | |
| 360 | population density, average wealth across the city, or crime rates. In addition, | 362 | Further studies may involve other attributes for business locations, |
| 361 | further studies may include additional location data and businesses by utilizing | 363 | such as population density, average wealth across the city, or crime |
| 362 | multiple sources, such as Google Maps and OpenStreetMap. | 364 | rates. In addition, further studies may include additional location data |
| 365 | and businesses by utilizing multiple sources, such as Google Maps and | ||
| 366 | OpenStreetMap. | ||
content/blog/2020-08-22-redirect-github-pages.md deleted −124
| @@ -1,124 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-08-22 | ||
| 3 | title = "Redirect GitHub Pages from Subdomain to the Top-Level Domain" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Short answer | ||
| 9 | |||
| 10 | ## Step 1 | ||
| 11 | |||
| 12 | Add a new file CNAME to your GitHub Pages repository containing only one line: | ||
| 13 | your top-level domain name. E.g.: `example.com` | ||
| 14 | |||
| 15 | ## Step 2 | ||
| 16 | |||
| 17 | [Optional] but highly recommended | ||
| 18 | |||
| 19 | 2.1: Remove all other top-level records (prefixed with @) of type A from your | ||
| 20 | DNS configuration. | ||
| 21 | |||
| 22 | 2.2: Remove a CNAME record for the second-level domain www if it is present. | ||
| 23 | |||
| 24 | ## Step 3 | ||
| 25 | |||
| 26 | Add these 5 entries to the very top of your DNS configuration: | ||
| 27 | |||
| 28 | ```txt | ||
| 29 | @ A 185.199.108.153 | ||
| 30 | @ A 185.199.109.153 | ||
| 31 | @ A 185.199.110.153 | ||
| 32 | @ A 185.199.111.153 | ||
| 33 | www CNAME your_github_username.github.io. | ||
| 34 | ``` | ||
| 35 | |||
| 36 | Replace `your_github_username` with your actual GitHub username. | ||
| 37 | |||
| 38 | ## Step 4 | ||
| 39 | |||
| 40 | Wait for your DNS changes to propagate. DNS changes aren't effective | ||
| 41 | immediately. They can take up to a full day to propagate. | ||
| 42 | |||
| 43 | # Long answer | ||
| 44 | |||
| 45 | This issue has two sides. One is the DNS configuration itself. Another one is | ||
| 46 | the way GitHub Pages will forward HTTP requests. | ||
| 47 | |||
| 48 | We need to know a few things to understand what GitHub is trying to say in their | ||
| 49 | documentation. | ||
| 50 | |||
| 51 | ## DNS Entry Types | ||
| 52 | |||
| 53 | There are two types of DNS records which interest us: CNAME and A. | ||
| 54 | |||
| 55 | `A` is also known as `Apex` or sometimes as `root entry`. It forwards requests | ||
| 56 | to a specified fixed IP address. `CNAME` entry forwards requests to a specified | ||
| 57 | URL (actual valid plain text URL, not an IP address). | ||
| 58 | |||
| 59 | ## DNS Load balancing | ||
| 60 | |||
| 61 | GitHub has one central URL address which accepts all DNS requests for GitHub | ||
| 62 | Pages: `http://username.github.io`. That URL is resolved to different IP | ||
| 63 | addresses based on your geographical location. Website hosted on GitHub Pages is | ||
| 64 | a simple collection of `HTML`, `CSS` and `JS` files. GitHub distributes these | ||
| 65 | files to different servers across the globe. So that when your browser sends a | ||
| 66 | request from Europe, it receives data from a server in Europe. The same is valid | ||
| 67 | for the requests from Asia and the USA. | ||
| 68 | |||
| 69 | ## What GitHub is trying to say | ||
| 70 | |||
| 71 | Since `A` records in DNS must contain IP addresses, and they must be either | ||
| 72 | `185.199.108.153` or `185.199.109.153` or `185.199.110.153` or | ||
| 73 | `185.199.111.153`, there is no way to forward requests to a server located | ||
| 74 | somewhere in Europe or Asia. Your website hosted at GitHub Pages will be | ||
| 75 | downloaded from a central GitHub Pages server. There is a minor risk that if | ||
| 76 | GitHub Pages DNS servers (`x.x.x.153`) are down for some reason, all custom | ||
| 77 | domains which use fixed GitHub Pages IP addresses will not be accessible (their | ||
| 78 | DNS requests will not be resolvable). | ||
| 79 | |||
| 80 | That is why GitHub strongly suggests to either use a second-level domain for | ||
| 81 | your GitHub Pages (e.g. `blog.example.com`) or use a DNS service provider that | ||
| 82 | supports a record type `ALIAS` that acts as `A` record but forwards request to a | ||
| 83 | URL address (e.g. `username.github.io`) instead of a fixed IP address. | ||
| 84 | |||
| 85 | ## How GitHub Pages treats HTTP requests | ||
| 86 | |||
| 87 | After a DNS request for `your_github_username.github.io` is resolved into an IP | ||
| 88 | address, e.g. `185.199.108.153` your browser sends an HTTP request to that | ||
| 89 | server with an HTTP header `Host`. Below are `curl` examples that load the same | ||
| 90 | website (these examples might not work if you are behind a proxy server): | ||
| 91 | |||
| 92 | ```sh | ||
| 93 | curl --header "Host: your_github_username.github.io" http://185.199.108.153/ | ||
| 94 | curl --header "Host: www.example.com" http://185.199.108.153/ | ||
| 95 | curl --header "Host: example.com" http://185.199.108.153/ | ||
| 96 | ``` | ||
| 97 | |||
| 98 | This way GitHub Pages servers know which user website to serve. | ||
| 99 | |||
| 100 | > GitHub Pages server will automatically redirect HTTP requests to the top-level | ||
| 101 | > domain if your `CNAME` file contains `example.com` but `www.example.com` is | ||
| 102 | > requested. | ||
| 103 | > | ||
| 104 | > The same is valid if your `CNAME` file contains `www.example.com` but the | ||
| 105 | > header `Host` in the `HTTP` request contains `example.com`. | ||
| 106 | |||
| 107 | ## Why can't I add a `CNAME` record entry that accepts a top-level request (`@`) to my DNS configuration? | ||
| 108 | |||
| 109 | Quote from the GitHub Pages documentation: | ||
| 110 | |||
| 111 | > Warning: Do not create a CNAME record for your custom apex domain! Doing so | ||
| 112 | > may cause issues with other services, such as email, on that domain. | ||
| 113 | |||
| 114 | ## References: | ||
| 115 | |||
| 116 | 1. [Setting up a custom domain with GitHub | ||
| 117 | Pages](https://docs.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site) | ||
| 118 | 2. [My custom domain isn't | ||
| 119 | working](https://docs.github.com/en/github/working-with-github-pages/troubleshooting-custom-domains-and-github-pages) | ||
| 120 | 3. [Cannot access my GitHub Pages website by IP | ||
| 121 | Address](https://serverfault.com/questions/589370/cannot-access-my-github-pages-website-by-ip-address) | ||
| 122 | 4. [How do I set up GitHub Pages to redirect DNS requests from a subdomain (e.g. | ||
| 123 | www) to the top-level domain (TLD, Apex | ||
| 124 | record)?](https://stackoverflow.com/questions/23375422/how-do-i-set-up-github-pages-to-redirect-dns-requests-from-a-subdomain-e-g-www) | ||
content/blog/2020-08-22-redirect-github-pages.org added +131
| @@ -0,0 +1,131 @@ | |||
| 1 | #+date: <2020-08-22> | ||
| 2 | #+title: Redirect GitHub Pages from Subdomain to the Top-Level Domain | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Short answer | ||
| 7 | |||
| 8 | ** Step 1 | ||
| 9 | |||
| 10 | Add a new file CNAME to your GitHub Pages repository containing only one | ||
| 11 | line: your top-level domain name. E.g.: =example.com= | ||
| 12 | |||
| 13 | ** Step 2 | ||
| 14 | |||
| 15 | [Optional] but highly recommended | ||
| 16 | |||
| 17 | 2.1: Remove all other top-level records (prefixed with @) of type A from | ||
| 18 | your DNS configuration. | ||
| 19 | |||
| 20 | 2.2: Remove a CNAME record for the second-level domain www if it is | ||
| 21 | present. | ||
| 22 | |||
| 23 | ** Step 3 | ||
| 24 | |||
| 25 | Add these 5 entries to the very top of your DNS configuration: | ||
| 26 | |||
| 27 | #+begin_src txt | ||
| 28 | @ A 185.199.108.153 | ||
| 29 | @ A 185.199.109.153 | ||
| 30 | @ A 185.199.110.153 | ||
| 31 | @ A 185.199.111.153 | ||
| 32 | www CNAME your_github_username.github.io. | ||
| 33 | #+end_src | ||
| 34 | |||
| 35 | Replace =your_github_username= with your actual GitHub username. | ||
| 36 | |||
| 37 | ** Step 4 | ||
| 38 | |||
| 39 | Wait for your DNS changes to propagate. DNS changes aren't effective | ||
| 40 | immediately. They can take up to a full day to propagate. | ||
| 41 | |||
| 42 | * Long answer | ||
| 43 | |||
| 44 | This issue has two sides. One is the DNS configuration itself. Another | ||
| 45 | one is the way GitHub Pages will forward HTTP requests. | ||
| 46 | |||
| 47 | We need to know a few things to understand what GitHub is trying to say | ||
| 48 | in their documentation. | ||
| 49 | |||
| 50 | ** DNS Entry Types | ||
| 51 | |||
| 52 | There are two types of DNS records which interest us: CNAME and A. | ||
| 53 | |||
| 54 | =A= is also known as =Apex= or sometimes as =root entry=. It forwards | ||
| 55 | requests to a specified fixed IP address. =CNAME= entry forwards | ||
| 56 | requests to a specified URL (actual valid plain text URL, not an IP | ||
| 57 | address). | ||
| 58 | |||
| 59 | ** DNS Load balancing | ||
| 60 | |||
| 61 | GitHub has one central URL address which accepts all DNS requests for | ||
| 62 | GitHub Pages: =http://username.github.io=. That URL is resolved to | ||
| 63 | different IP addresses based on your geographical location. Website | ||
| 64 | hosted on GitHub Pages is a simple collection of =HTML=, =CSS= and =JS= | ||
| 65 | files. GitHub distributes these files to different servers across the | ||
| 66 | globe. So that when your browser sends a request from Europe, it | ||
| 67 | receives data from a server in Europe. The same is valid for the | ||
| 68 | requests from Asia and the USA. | ||
| 69 | |||
| 70 | ** What GitHub is trying to say | ||
| 71 | |||
| 72 | Since =A= records in DNS must contain IP addresses, and they must be | ||
| 73 | either =185.199.108.153= or =185.199.109.153= or =185.199.110.153= or | ||
| 74 | =185.199.111.153=, there is no way to forward requests to a server | ||
| 75 | located somewhere in Europe or Asia. Your website hosted at GitHub Pages | ||
| 76 | will be downloaded from a central GitHub Pages server. There is a minor | ||
| 77 | risk that if GitHub Pages DNS servers (=x.x.x.153=) are down for some | ||
| 78 | reason, all custom domains which use fixed GitHub Pages IP addresses | ||
| 79 | will not be accessible (their DNS requests will not be resolvable). | ||
| 80 | |||
| 81 | That is why GitHub strongly suggests to either use a second-level domain | ||
| 82 | for your GitHub Pages (e.g. =blog.example.com=) or use a DNS service | ||
| 83 | provider that supports a record type =ALIAS= that acts as =A= record but | ||
| 84 | forwards request to a URL address (e.g. =username.github.io=) instead of | ||
| 85 | a fixed IP address. | ||
| 86 | |||
| 87 | ** How GitHub Pages treats HTTP requests | ||
| 88 | |||
| 89 | After a DNS request for =your_github_username.github.io= is resolved | ||
| 90 | into an IP address, e.g. =185.199.108.153= your browser sends an HTTP | ||
| 91 | request to that server with an HTTP header =Host=. Below are =curl= | ||
| 92 | examples that load the same website (these examples might not work if | ||
| 93 | you are behind a proxy server): | ||
| 94 | |||
| 95 | #+begin_src sh | ||
| 96 | curl --header "Host: your_github_username.github.io" http://185.199.108.153/ | ||
| 97 | curl --header "Host: www.example.com" http://185.199.108.153/ | ||
| 98 | curl --header "Host: example.com" http://185.199.108.153/ | ||
| 99 | #+end_src | ||
| 100 | |||
| 101 | This way GitHub Pages servers know which user website to serve. | ||
| 102 | |||
| 103 | #+begin_quote | ||
| 104 | GitHub Pages server will automatically redirect HTTP requests to the | ||
| 105 | top-level domain if your =CNAME= file contains =example.com= but | ||
| 106 | =www.example.com= is requested. | ||
| 107 | |||
| 108 | The same is valid if your =CNAME= file contains =www.example.com= but | ||
| 109 | the header =Host= in the =HTTP= request contains =example.com=. | ||
| 110 | #+end_quote | ||
| 111 | |||
| 112 | ** Why can't I add a =CNAME= record entry that accepts a top-level request (=@=) to my DNS configuration? | ||
| 113 | |||
| 114 | Quote from the GitHub Pages documentation: | ||
| 115 | |||
| 116 | #+begin_quote | ||
| 117 | Warning: Do not create a CNAME record for your custom apex domain! Doing | ||
| 118 | so may cause issues with other services, such as email, on that domain. | ||
| 119 | #+end_quote | ||
| 120 | |||
| 121 | ** References: | ||
| 122 | |||
| 123 | 1. [[https://docs.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site][Setting | ||
| 124 | up a custom domain with GitHub Pages]] | ||
| 125 | 2. [[https://docs.github.com/en/github/working-with-github-pages/troubleshooting-custom-domains-and-github-pages][My | ||
| 126 | custom domain isn't working]] | ||
| 127 | 3. [[https://serverfault.com/questions/589370/cannot-access-my-github-pages-website-by-ip-address][Cannot | ||
| 128 | access my GitHub Pages website by IP Address]] | ||
| 129 | 4. [[https://stackoverflow.com/questions/23375422/how-do-i-set-up-github-pages-to-redirect-dns-requests-from-a-subdomain-e-g-www][How | ||
| 130 | do I set up GitHub Pages to redirect DNS requests from a subdomain | ||
| 131 | (e.g. www) to the top-level domain (TLD, Apex record)?]] | ||
content/blog/2020-08-29-php-auth-flow.md → content/blog/2020-08-29-php-auth-flow.org renamed +74 −72
| @@ -1,23 +1,22 @@ | |||
| 1 | +++ | 1 | #+date: <2020-08-29> |
| 2 | date = 2020-08-29 | 2 | #+title: PHP Authentication Flow |
| 3 | title = "PHP Authentication Flow" | 3 | #+description: |
| 4 | description = "" | 4 | |
| 5 | draft = false | 5 | |
| 6 | +++ | 6 | * Introduction |
| 7 | 7 | ||
| 8 | # Introduction | 8 | When creating websites that will allow users to create accounts, the |
| 9 | 9 | developer always needs to consider the proper authentication flow for | |
| 10 | When creating websites that will allow users to create accounts, the developer | 10 | their app. For example, some developers will utilize an API for |
| 11 | always needs to consider the proper authentication flow for their app. For | 11 | authentication, some will use OAuth, and some may just use their own |
| 12 | example, some developers will utilize an API for authentication, some will use | 12 | simple database. |
| 13 | OAuth, and some may just use their own simple database. | 13 | |
| 14 | 14 | For those using pre-built libraries, authentication may simply be a | |
| 15 | For those using pre-built libraries, authentication may simply be a problem of | 15 | problem of copying and pasting the code from their library's |
| 16 | copying and pasting the code from their library's documentation. For example, | 16 | documentation. For example, here's the code I use to authenticate users |
| 17 | here's the code I use to authenticate users with the Tumblr OAuth API for my | 17 | with the Tumblr OAuth API for my Tumblr client, Vox Populi: |
| 18 | Tumblr client, Vox Populi: | 18 | |
| 19 | 19 | #+begin_src php | |
| 20 | ```php | ||
| 21 | // Start the session | 20 | // Start the session |
| 22 | session_start(); | 21 | session_start(); |
| 23 | 22 | ||
| @@ -38,27 +37,28 @@ $client = new Tumblr\API\Client( | |||
| 38 | $token, | 37 | $token, |
| 39 | $token_secret | 38 | $token_secret |
| 40 | ); | 39 | ); |
| 41 | ``` | 40 | #+end_src |
| 42 | 41 | ||
| 43 | However, developers creating authentication flows from scratch will need to | 42 | However, developers creating authentication flows from scratch will need |
| 44 | think carefully about when to make sure a web page will check the user's | 43 | to think carefully about when to make sure a web page will check the |
| 45 | authenticity. | 44 | user's authenticity. |
| 46 | 45 | ||
| 47 | In this article, we're going to look at a simple authentication flow using a | 46 | In this article, we're going to look at a simple authentication flow |
| 48 | MySQL database and PHP. | 47 | using a MySQL database and PHP. |
| 49 | 48 | ||
| 50 | # Creating User Accounts | 49 | * Creating User Accounts |
| 51 | 50 | ||
| 52 | The beginning to any type of user authentication is to create a user account. | 51 | The beginning to any type of user authentication is to create a user |
| 53 | This process can take many formats, but the simplest is to accept user input | 52 | account. This process can take many formats, but the simplest is to |
| 54 | from a form (e.g., username and password) and send it over to your database. For | 53 | accept user input from a form (e.g., username and password) and send it |
| 55 | example, here's a snippet that shows how to get username and password parameters | 54 | over to your database. For example, here's a snippet that shows how to |
| 56 | that would come when a user submits a form to your PHP script. | 55 | get username and password parameters that would come when a user submits |
| 56 | a form to your PHP script. | ||
| 57 | 57 | ||
| 58 | **Note**: Ensure that your password column is large enough to hold the hashed | 58 | *Note*: Ensure that your password column is large enough to hold the |
| 59 | value (at least 60 characters or longer). | 59 | hashed value (at least 60 characters or longer). |
| 60 | 60 | ||
| 61 | ```php | 61 | #+begin_src php |
| 62 | // Get the values from the URL | 62 | // Get the values from the URL |
| 63 | $username = $_POST['username']; | 63 | $username = $_POST['username']; |
| 64 | $raw_password = $_POST['password']; | 64 | $raw_password = $_POST['password']; |
| @@ -91,15 +91,15 @@ if ($conn->query($sql) === TRUE) { | |||
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | $conn->close(); | 93 | $conn->close(); |
| 94 | ``` | 94 | #+end_src |
| 95 | 95 | ||
| 96 | ## Validate Returning Users | 96 | ** Validate Returning Users |
| 97 | 97 | ||
| 98 | To be able to verify that a returning user has a valid username and password in | 98 | To be able to verify that a returning user has a valid username and |
| 99 | your database is as simple as having users fill out a form and comparing their | 99 | password in your database is as simple as having users fill out a form |
| 100 | inputs to your database. | 100 | and comparing their inputs to your database. |
| 101 | 101 | ||
| 102 | ```php | 102 | #+begin_src php |
| 103 | // Query the database for username and password | 103 | // Query the database for username and password |
| 104 | // ... | 104 | // ... |
| 105 | 105 | ||
| @@ -110,41 +110,42 @@ if(password_verify($password_input, $hashed_password)) { | |||
| 110 | 110 | ||
| 111 | // Else, Redirect them back to the login page. | 111 | // Else, Redirect them back to the login page. |
| 112 | ... | 112 | ... |
| 113 | ``` | 113 | #+end_src |
| 114 | 114 | ||
| 115 | # Storing Authentication State | 115 | * Storing Authentication State |
| 116 | 116 | ||
| 117 | Once you've created the user's account, now you're ready to initialize the | 117 | Once you've created the user's account, now you're ready to initialize |
| 118 | user's session. **You will need to do this on every page you load while the user | 118 | the user's session. *You will need to do this on every page you load |
| 119 | is logged in.** To do so, simply enter the following code snippet: | 119 | while the user is logged in.* To do so, simply enter the following code |
| 120 | snippet: | ||
| 120 | 121 | ||
| 121 | ```php | 122 | #+begin_src php |
| 122 | session_start(); | 123 | session_start(); |
| 123 | ``` | 124 | #+end_src |
| 124 | 125 | ||
| 125 | Once you've initialized the session, the next step is to store the session in a | 126 | Once you've initialized the session, the next step is to store the |
| 126 | cookie so that you can access it later. | 127 | session in a cookie so that you can access it later. |
| 127 | 128 | ||
| 128 | ```php | 129 | #+begin_src php |
| 129 | setcookie(session_name()); | 130 | setcookie(session_name()); |
| 130 | ``` | 131 | #+end_src |
| 131 | 132 | ||
| 132 | Now that the session name has been stored, you'll be able to check if there's an | 133 | Now that the session name has been stored, you'll be able to check if |
| 133 | active session whenever you load a page. | 134 | there's an active session whenever you load a page. |
| 134 | 135 | ||
| 135 | ```php | 136 | #+begin_src php |
| 136 | if(isset(session_name())) { | 137 | if(isset(session_name())) { |
| 137 | // The session is active | 138 | // The session is active |
| 138 | } | 139 | } |
| 139 | ``` | 140 | #+end_src |
| 140 | 141 | ||
| 141 | ## Removing User Authentication | 142 | ** Removing User Authentication |
| 142 | 143 | ||
| 143 | The next logical step is to give your users the option to log out once they are | 144 | The next logical step is to give your users the option to log out once |
| 144 | done using your application. This can be tricky in PHP since a few of the | 145 | they are done using your application. This can be tricky in PHP since a |
| 145 | standard ways do not always work. | 146 | few of the standard ways do not always work. |
| 146 | 147 | ||
| 147 | ```php | 148 | #+begin_src php |
| 148 | // Initialize the session. | 149 | // Initialize the session. |
| 149 | // If you are using session_name("something"), don't forget it now! | 150 | // If you are using session_name("something"), don't forget it now! |
| 150 | session_start(); | 151 | session_start(); |
| @@ -176,17 +177,18 @@ session_write_close(); | |||
| 176 | // Go back to sign-in page | 177 | // Go back to sign-in page |
| 177 | header('Location: https://example.com/logged-out/'); | 178 | header('Location: https://example.com/logged-out/'); |
| 178 | die(); | 179 | die(); |
| 179 | ``` | 180 | #+end_src |
| 180 | 181 | ||
| 181 | # Wrapping Up | 182 | * Wrapping Up |
| 182 | 183 | ||
| 183 | Now you should be ready to begin your authentication programming with PHP. You | 184 | Now you should be ready to begin your authentication programming with |
| 184 | can create user accounts, create sessions for users across different pages of | 185 | PHP. You can create user accounts, create sessions for users across |
| 185 | your site, and then destroy the user data when they're ready to leave. | 186 | different pages of your site, and then destroy the user data when |
| 187 | they're ready to leave. | ||
| 186 | 188 | ||
| 187 | For more information on this subject, I recommend reading the [PHP | 189 | For more information on this subject, I recommend reading the |
| 188 | Documentation](https://www.php.net/). Specifically, you may want to look at | 190 | [[https://www.php.net/][PHP Documentation]]. Specifically, you may want |
| 189 | [HTTP Authentication with | 191 | to look at [[https://www.php.net/manual/en/features.http-auth.php][HTTP |
| 190 | PHP](https://www.php.net/manual/en/features.http-auth.php), [session | 192 | Authentication with PHP]], |
| 191 | handling](https://www.php.net/manual/en/book.session.php), and | 193 | [[https://www.php.net/manual/en/book.session.php][session handling]], |
| 192 | [hash](https://www.php.net/manual/en/function.hash.php). | 194 | and [[https://www.php.net/manual/en/function.hash.php][hash]]. |
content/blog/2020-09-01-visual-recognition.md deleted −199
| @@ -1,199 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-09-01 | ||
| 3 | title = "IBM Watson Visual Recognition" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is IBM Watson? | ||
| 9 | |||
| 10 | If you've never heard of [Watson](https://www.ibm.com/watson), this service is a | ||
| 11 | suite of enterprise-ready AI services, applications, and tooling provided by | ||
| 12 | IBM. Watson contains quite a few useful tools for data scientists and students, | ||
| 13 | including the subject of this post today: visual recognition. | ||
| 14 | |||
| 15 | If you'd like to view the official documentation for the Visual Recognition API, | ||
| 16 | visit the [API | ||
| 17 | Docs](https://cloud.ibm.com/apidocs/visual-recognition/visual-recognition-v3?code=python). | ||
| 18 | |||
| 19 | # Prerequisites | ||
| 20 | |||
| 21 | To be able to use Watson Visual Recognition, you'll need the following: | ||
| 22 | |||
| 23 | 1. Create a free account on [IBM Watson | ||
| 24 | Studio](https://www.ibm.com/cloud/watson-studio). | ||
| 25 | 2. Add the [Watson Visual | ||
| 26 | Recognition](https://www.ibm.com/cloud/watson-visual-recognition) service to | ||
| 27 | your IBM Watson account. | ||
| 28 | 3. Get your API key and URL. To do this, first go to the [profile | ||
| 29 | dashboard](https://dataplatform.cloud.ibm.com/home2?context=cpdaas) for your | ||
| 30 | IBM account and click on the Watson Visual Recognition service you created. | ||
| 31 | This will be listed in the section titled **Your services**. Then click the | ||
| 32 | **Credentials** tab and open the **Auto-generated credentials** dropdown. | ||
| 33 | Copy your API key and URL so that you can use them in the Python script | ||
| 34 | later. | ||
| 35 | 4. **[Optional]** While not required, you can also create the Jupyter Notebook | ||
| 36 | for this project right inside [Watson | ||
| 37 | Studio](https://www.ibm.com/cloud/watson-studio). Watson Studio will save | ||
| 38 | your notebooks inside an organized project and allow you to use their other | ||
| 39 | integrated products, such as storage containers, AI models, documentation, | ||
| 40 | external sharing, etc. | ||
| 41 | |||
| 42 | # Calling the IBM Watson Visual Recognition API | ||
| 43 | |||
| 44 | Okay, now let's get started. | ||
| 45 | |||
| 46 | To begin, we need to install the proper Python package for IBM Watson. | ||
| 47 | |||
| 48 | ```sh | ||
| 49 | pip install --upgrade --user "ibm-watson>=4.5.0" | ||
| 50 | ``` | ||
| 51 | |||
| 52 | Next, we need to specify the API key, version, and URL given to us when we | ||
| 53 | created the Watson Visual Recognition service. | ||
| 54 | |||
| 55 | ```python | ||
| 56 | apikey = "<your-apikey>" | ||
| 57 | version = "2018-03-19" | ||
| 58 | url = "<your-url>" | ||
| 59 | ``` | ||
| 60 | |||
| 61 | Now, let's import the necessary libraries and authenticate our service. | ||
| 62 | |||
| 63 | ```python | ||
| 64 | import json | ||
| 65 | from ibm_watson import VisualRecognitionV3 | ||
| 66 | from ibm_cloud_sdk_core.authenticators import IAMAuthenticator | ||
| 67 | |||
| 68 | authenticator = IAMAuthenticator(apikey) | ||
| 69 | visual_recognition = VisualRecognitionV3( | ||
| 70 | version=version, | ||
| 71 | authenticator=authenticator | ||
| 72 | ) | ||
| 73 | |||
| 74 | visual_recognition.set_service_url(url) | ||
| 75 | ``` | ||
| 76 | |||
| 77 | **[Optional]** If you'd like to tell the API not to use any data to improve | ||
| 78 | their products, set the following header. | ||
| 79 | |||
| 80 | ```python | ||
| 81 | visual_recognition.set_default_headers({'x-watson-learning-opt-out': "true"}) | ||
| 82 | ``` | ||
| 83 | |||
| 84 | Now we have our API all set and ready to go. For this example, I'm going to | ||
| 85 | include a `dict` of photos to load as we test out the API. | ||
| 86 | |||
| 87 | ```python | ||
| 88 | data = [ | ||
| 89 | { | ||
| 90 | "title": "Grizzly Bear", | ||
| 91 | "url": "https://example.com/photos/image1.jpg" | ||
| 92 | }, | ||
| 93 | { | ||
| 94 | "title": "Nature Lake", | ||
| 95 | "url": "https://example.com/photos/image2.jpg" | ||
| 96 | }, | ||
| 97 | { | ||
| 98 | "title": "Welcome Sign", | ||
| 99 | "url": "https://example.com/photos/image3.jpg" | ||
| 100 | }, | ||
| 101 | { | ||
| 102 | "title": "Honey Badger", | ||
| 103 | "url": "https://example.com/photos/image4.jpg" | ||
| 104 | }, | ||
| 105 | { | ||
| 106 | "title": "Grand Canyon Lizard", | ||
| 107 | "url": "https://example.com/photos/image5.jpg" | ||
| 108 | }, | ||
| 109 | { | ||
| 110 | "title": "Castle", | ||
| 111 | "url": "https://example.com/photos/image6.jpg" | ||
| 112 | } | ||
| 113 | ] | ||
| 114 | ``` | ||
| 115 | |||
| 116 | Now that we've set up our libraries and have the photos ready, let's create a | ||
| 117 | loop to call the API for each image. The code below shows a loop that calls the | ||
| 118 | URL of each image and sends it to the API, requesting results with at least 60% | ||
| 119 | confidence. The results are output to the console with dotted lines separating | ||
| 120 | each section. | ||
| 121 | |||
| 122 | In the case of an API error, the codes and explanations are output to the | ||
| 123 | console. | ||
| 124 | |||
| 125 | ```python | ||
| 126 | from ibm_watson import ApiException | ||
| 127 | |||
| 128 | for x in range(len(data)): | ||
| 129 | try: | ||
| 130 | url = data[x]["url"] | ||
| 131 | images_filename = data[x]["title"] | ||
| 132 | classes = visual_recognition.classify( | ||
| 133 | url=url, | ||
| 134 | images_filename=images_filename, | ||
| 135 | threshold='0.6', | ||
| 136 | owners=["IBM"]).get_result() | ||
| 137 | print("-----------------------------------------------") | ||
| 138 | print("Image Title: ", data[x]["title"], "\n") | ||
| 139 | print("Image URL: ", data[x]["url"], "\n") | ||
| 140 | classification_results = classes["images"][0]["classifiers"][0]["classes"] | ||
| 141 | for result in classification_results: | ||
| 142 | print(result["class"], "(", result["score"], ")") | ||
| 143 | print("-----------------------------------------------") | ||
| 144 | except ApiException as ex: | ||
| 145 | print("Method failed with status code " + str(ex.code) + ": " + ex.message) | ||
| 146 | ``` | ||
| 147 | |||
| 148 | # The Results | ||
| 149 | |||
| 150 | Here we can see the full result set of our function above. If you view each of | ||
| 151 | the URLs that we sent to the API, you'll be able to see that it was remarkably | ||
| 152 | accurate. To be fair, these are clear high-resolution, clear photos shot with a | ||
| 153 | professional camera. In reality, you will most likely be processing images that | ||
| 154 | are lower quality and may have a lot of noise in the photo. | ||
| 155 | |||
| 156 | However, we can clearly see the benefit of being able to call this API instead | ||
| 157 | of attempting to write our own image recognition function. Each of the | ||
| 158 | classifications returned was a fair description of the image. | ||
| 159 | |||
| 160 | If you wanted to restrict the results to those that are at least 90% confident | ||
| 161 | or greater, you would simply adjust the `threshold` in the | ||
| 162 | `visual_recognition.classify()` function. | ||
| 163 | |||
| 164 | When your program runs, it should show the output below for each photo you | ||
| 165 | provide. | ||
| 166 | |||
| 167 | ```txt | ||
| 168 | ---------------------------------------------------------------- | ||
| 169 | Image Title: Grizzly Bear | ||
| 170 | Image URL: https://example.com/photos/image1.jpg | ||
| 171 | |||
| 172 | brown bear ( 0.944 ) | ||
| 173 | bear ( 1 ) | ||
| 174 | carnivore ( 1 ) | ||
| 175 | mammal ( 1 ) | ||
| 176 | animal ( 1 ) | ||
| 177 | Alaskan brown bear ( 0.759 ) | ||
| 178 | greenishness color ( 0.975 ) | ||
| 179 | ---------------------------------------------------------------- | ||
| 180 | ``` | ||
| 181 | |||
| 182 | # Discussion | ||
| 183 | |||
| 184 | Now, this was a very minimal implementation of the API. We simply supplied some | ||
| 185 | images and looked to see how accurate the results were. However, you could | ||
| 186 | implement this type of API into many machine learning (ML) models. | ||
| 187 | |||
| 188 | For example, you could be working for a company that scans their warehouses or | ||
| 189 | inventory using drones. Would you want to pay employees to sit there and watch | ||
| 190 | drone footage all day in order to identify or count things in the video? | ||
| 191 | Probably not. Instead, you could use a classification system similar to this one | ||
| 192 | in order to train your machine learning model to correctly identify items that | ||
| 193 | the drones show through video. More specifically, you could have your machine | ||
| 194 | learning model watch a drone fly over a field of sheep in order to count how | ||
| 195 | many sheep are living in that field. | ||
| 196 | |||
| 197 | There are many ways to implement machine learning functionality, but hopefully | ||
| 198 | this post helped inspire some deeper thought about the tools that can help | ||
| 199 | propel us further into the future of machine learning and AI. | ||
content/blog/2020-09-01-visual-recognition.org added +202
| @@ -0,0 +1,202 @@ | |||
| 1 | #+date: <2020-09-01> | ||
| 2 | #+title: IBM Watson Visual Recognition | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is IBM Watson? | ||
| 7 | |||
| 8 | If you've never heard of [[https://www.ibm.com/watson][Watson]], this | ||
| 9 | service is a suite of enterprise-ready AI services, applications, and | ||
| 10 | tooling provided by IBM. Watson contains quite a few useful tools for | ||
| 11 | data scientists and students, including the subject of this post today: | ||
| 12 | visual recognition. | ||
| 13 | |||
| 14 | If you'd like to view the official documentation for the Visual | ||
| 15 | Recognition API, visit the | ||
| 16 | [[https://cloud.ibm.com/apidocs/visual-recognition/visual-recognition-v3?code=python][API | ||
| 17 | Docs]]. | ||
| 18 | |||
| 19 | * Prerequisites | ||
| 20 | |||
| 21 | To be able to use Watson Visual Recognition, you'll need the following: | ||
| 22 | |||
| 23 | 1. Create a free account on | ||
| 24 | [[https://www.ibm.com/cloud/watson-studio][IBM Watson Studio]]. | ||
| 25 | 2. Add the [[https://www.ibm.com/cloud/watson-visual-recognition][Watson | ||
| 26 | Visual Recognition]] service to your IBM Watson account. | ||
| 27 | 3. Get your API key and URL. To do this, first go to the | ||
| 28 | [[https://dataplatform.cloud.ibm.com/home2?context=cpdaas][profile | ||
| 29 | dashboard]] for your IBM account and click on the Watson Visual | ||
| 30 | Recognition service you created. This will be listed in the section | ||
| 31 | titled *Your services*. Then click the *Credentials* tab and open the | ||
| 32 | *Auto-generated credentials* dropdown. Copy your API key and URL so | ||
| 33 | that you can use them in the Python script later. | ||
| 34 | 4. *[Optional]* While not required, you can also create the Jupyter | ||
| 35 | Notebook for this project right inside | ||
| 36 | [[https://www.ibm.com/cloud/watson-studio][Watson Studio]]. Watson | ||
| 37 | Studio will save your notebooks inside an organized project and allow | ||
| 38 | you to use their other integrated products, such as storage | ||
| 39 | containers, AI models, documentation, external sharing, etc. | ||
| 40 | |||
| 41 | * Calling the IBM Watson Visual Recognition API | ||
| 42 | |||
| 43 | Okay, now let's get started. | ||
| 44 | |||
| 45 | To begin, we need to install the proper Python package for IBM Watson. | ||
| 46 | |||
| 47 | #+begin_src sh | ||
| 48 | pip install --upgrade --user "ibm-watson>=4.5.0" | ||
| 49 | #+end_src | ||
| 50 | |||
| 51 | Next, we need to specify the API key, version, and URL given to us when | ||
| 52 | we created the Watson Visual Recognition service. | ||
| 53 | |||
| 54 | #+begin_src python | ||
| 55 | apikey = "<your-apikey>" | ||
| 56 | version = "2018-03-19" | ||
| 57 | url = "<your-url>" | ||
| 58 | #+end_src | ||
| 59 | |||
| 60 | Now, let's import the necessary libraries and authenticate our service. | ||
| 61 | |||
| 62 | #+begin_src python | ||
| 63 | import json | ||
| 64 | from ibm_watson import VisualRecognitionV3 | ||
| 65 | from ibm_cloud_sdk_core.authenticators import IAMAuthenticator | ||
| 66 | |||
| 67 | authenticator = IAMAuthenticator(apikey) | ||
| 68 | visual_recognition = VisualRecognitionV3( | ||
| 69 | version=version, | ||
| 70 | authenticator=authenticator | ||
| 71 | ) | ||
| 72 | |||
| 73 | visual_recognition.set_service_url(url) | ||
| 74 | #+end_src | ||
| 75 | |||
| 76 | *[Optional]* If you'd like to tell the API not to use any data to | ||
| 77 | improve their products, set the following header. | ||
| 78 | |||
| 79 | #+begin_src python | ||
| 80 | visual_recognition.set_default_headers({'x-watson-learning-opt-out': "true"}) | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | Now we have our API all set and ready to go. For this example, I'm going | ||
| 84 | to include a =dict= of photos to load as we test out the API. | ||
| 85 | |||
| 86 | #+begin_src python | ||
| 87 | data = [ | ||
| 88 | { | ||
| 89 | "title": "Grizzly Bear", | ||
| 90 | "url": "https://example.com/photos/image1.jpg" | ||
| 91 | }, | ||
| 92 | { | ||
| 93 | "title": "Nature Lake", | ||
| 94 | "url": "https://example.com/photos/image2.jpg" | ||
| 95 | }, | ||
| 96 | { | ||
| 97 | "title": "Welcome Sign", | ||
| 98 | "url": "https://example.com/photos/image3.jpg" | ||
| 99 | }, | ||
| 100 | { | ||
| 101 | "title": "Honey Badger", | ||
| 102 | "url": "https://example.com/photos/image4.jpg" | ||
| 103 | }, | ||
| 104 | { | ||
| 105 | "title": "Grand Canyon Lizard", | ||
| 106 | "url": "https://example.com/photos/image5.jpg" | ||
| 107 | }, | ||
| 108 | { | ||
| 109 | "title": "Castle", | ||
| 110 | "url": "https://example.com/photos/image6.jpg" | ||
| 111 | } | ||
| 112 | ] | ||
| 113 | #+end_src | ||
| 114 | |||
| 115 | Now that we've set up our libraries and have the photos ready, let's | ||
| 116 | create a loop to call the API for each image. The code below shows a | ||
| 117 | loop that calls the URL of each image and sends it to the API, | ||
| 118 | requesting results with at least 60% confidence. The results are output | ||
| 119 | to the console with dotted lines separating each section. | ||
| 120 | |||
| 121 | In the case of an API error, the codes and explanations are output to | ||
| 122 | the console. | ||
| 123 | |||
| 124 | #+begin_src python | ||
| 125 | from ibm_watson import ApiException | ||
| 126 | |||
| 127 | for x in range(len(data)): | ||
| 128 | try: | ||
| 129 | url = data[x]["url"] | ||
| 130 | images_filename = data[x]["title"] | ||
| 131 | classes = visual_recognition.classify( | ||
| 132 | url=url, | ||
| 133 | images_filename=images_filename, | ||
| 134 | threshold='0.6', | ||
| 135 | owners=["IBM"]).get_result() | ||
| 136 | print("-----------------------------------------------") | ||
| 137 | print("Image Title: ", data[x]["title"], "\n") | ||
| 138 | print("Image URL: ", data[x]["url"], "\n") | ||
| 139 | classification_results = classes["images"][0]["classifiers"][0]["classes"] | ||
| 140 | for result in classification_results: | ||
| 141 | print(result["class"], "(", result["score"], ")") | ||
| 142 | print("-----------------------------------------------") | ||
| 143 | except ApiException as ex: | ||
| 144 | print("Method failed with status code " + str(ex.code) + ": " + ex.message) | ||
| 145 | #+end_src | ||
| 146 | |||
| 147 | * The Results | ||
| 148 | |||
| 149 | Here we can see the full result set of our function above. If you view | ||
| 150 | each of the URLs that we sent to the API, you'll be able to see that it | ||
| 151 | was remarkably accurate. To be fair, these are clear high-resolution, | ||
| 152 | clear photos shot with a professional camera. In reality, you will most | ||
| 153 | likely be processing images that are lower quality and may have a lot of | ||
| 154 | noise in the photo. | ||
| 155 | |||
| 156 | However, we can clearly see the benefit of being able to call this API | ||
| 157 | instead of attempting to write our own image recognition function. Each | ||
| 158 | of the classifications returned was a fair description of the image. | ||
| 159 | |||
| 160 | If you wanted to restrict the results to those that are at least 90% | ||
| 161 | confident or greater, you would simply adjust the =threshold= in the | ||
| 162 | =visual_recognition.classify()= function. | ||
| 163 | |||
| 164 | When your program runs, it should show the output below for each photo | ||
| 165 | you provide. | ||
| 166 | |||
| 167 | #+begin_src txt | ||
| 168 | ---------------------------------------------------------------- | ||
| 169 | Image Title: Grizzly Bear | ||
| 170 | Image URL: https://example.com/photos/image1.jpg | ||
| 171 | |||
| 172 | brown bear ( 0.944 ) | ||
| 173 | bear ( 1 ) | ||
| 174 | carnivore ( 1 ) | ||
| 175 | mammal ( 1 ) | ||
| 176 | animal ( 1 ) | ||
| 177 | Alaskan brown bear ( 0.759 ) | ||
| 178 | greenishness color ( 0.975 ) | ||
| 179 | ---------------------------------------------------------------- | ||
| 180 | #+end_src | ||
| 181 | |||
| 182 | * Discussion | ||
| 183 | |||
| 184 | Now, this was a very minimal implementation of the API. We simply | ||
| 185 | supplied some images and looked to see how accurate the results were. | ||
| 186 | However, you could implement this type of API into many machine learning | ||
| 187 | (ML) models. | ||
| 188 | |||
| 189 | For example, you could be working for a company that scans their | ||
| 190 | warehouses or inventory using drones. Would you want to pay employees to | ||
| 191 | sit there and watch drone footage all day in order to identify or count | ||
| 192 | things in the video? Probably not. Instead, you could use a | ||
| 193 | classification system similar to this one in order to train your machine | ||
| 194 | learning model to correctly identify items that the drones show through | ||
| 195 | video. More specifically, you could have your machine learning model | ||
| 196 | watch a drone fly over a field of sheep in order to count how many sheep | ||
| 197 | are living in that field. | ||
| 198 | |||
| 199 | There are many ways to implement machine learning functionality, but | ||
| 200 | hopefully this post helped inspire some deeper thought about the tools | ||
| 201 | that can help propel us further into the future of machine learning and | ||
| 202 | AI. | ||
content/blog/2020-09-22-internal-audit.md deleted −233
| @@ -1,233 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-09-22 | ||
| 3 | title = "Who is Internal Audit?" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Definitions | ||
| 9 | |||
| 10 | One of the many reasons that Internal Audit needs such thorough explaining to | ||
| 11 | non-auditors is that Internal Audit can serve many purposes, depending on the | ||
| 12 | organization's size and needs. However, the Institute of Internal Auditors (IIA) | ||
| 13 | defines Internal Auditing as: | ||
| 14 | |||
| 15 | > Internal auditing is an independent, objective assurance and consulting | ||
| 16 | > activity designed to add value and improve an organization's operations. It | ||
| 17 | > helps an organization accomplish its objectives by bringing a systematic, | ||
| 18 | > disciplined approach to evaluate and improve the effectiveness of risk | ||
| 19 | > management, control, and governance processes. | ||
| 20 | |||
| 21 | However, this definition uses quite a few terms that aren't clear unless the | ||
| 22 | reader already has a solid understanding of the auditing profession. To further | ||
| 23 | explain, the following is a list of definitions that can help supplement | ||
| 24 | understanding of internal auditing. | ||
| 25 | |||
| 26 | ## Independent | ||
| 27 | |||
| 28 | Independence is the freedom from conditions that threaten the ability of the | ||
| 29 | internal audit activity to carry out internal audit responsibilities in an | ||
| 30 | unbiased manner. To achieve the degree of independence necessary to effectively | ||
| 31 | carry out the responsibilities of the internal audit activity, the chief audit | ||
| 32 | executive has direct and unrestricted access to senior management and the board. | ||
| 33 | This can be achieved through a dual-reporting relationship. Threats to | ||
| 34 | independence must be managed at the individual auditor, engagement, functional, | ||
| 35 | and organizational levels. | ||
| 36 | |||
| 37 | ## Objective | ||
| 38 | |||
| 39 | Objectivity is an unbiased mental attitude that allows internal auditors to | ||
| 40 | perform engagements in such a manner that they believe in their work product and | ||
| 41 | that no quality compromises are made. Objectivity requires that internal | ||
| 42 | auditors do not subordinate their judgment on audit matters to others. Threats | ||
| 43 | to objectivity must be managed at the individual auditor, engagement, | ||
| 44 | functional, and organizational levels. | ||
| 45 | |||
| 46 | ## Assurance | ||
| 47 | |||
| 48 | Assurance services involve the internal auditor's objective assessment of | ||
| 49 | evidence to provide opinions or conclusions regarding an entity, operation, | ||
| 50 | function, process, system, or other subject matters. The internal auditor | ||
| 51 | determines the nature and scope of an assurance engagement. Generally, three | ||
| 52 | parties are participants in assurance services: (1) the person or group directly | ||
| 53 | involved with the entity, operation, function, process, system, or other subject | ||
| 54 | |||
| 55 | - (the process owner), (2) the person or group making the assessment - (the | ||
| 56 | internal auditor), and (3) the person or group using the assessment - (the | ||
| 57 | user). | ||
| 58 | |||
| 59 | ## Consulting | ||
| 60 | |||
| 61 | Consulting services are advisory in nature and are generally performed at the | ||
| 62 | specific request of an engagement client. The nature and scope of the consulting | ||
| 63 | engagement are subject to agreement with the engagement client. Consulting | ||
| 64 | services generally involve two parties: (1) the person or group offering the | ||
| 65 | advice (the internal auditor), and (2) the person or group seeking and receiving | ||
| 66 | the advice (the engagement client). When performing consulting services, the | ||
| 67 | internal auditor should maintain objectivity and not assume management | ||
| 68 | responsibility. | ||
| 69 | |||
| 70 | ## Governance, Risk Management, & Compliance (GRC) | ||
| 71 | |||
| 72 | The integrated collection of capabilities that enable an organization to | ||
| 73 | reliably achieve objectives, address uncertainty and act with integrity. | ||
| 74 | |||
| 75 | # Audit Charter & Standards | ||
| 76 | |||
| 77 | First, it's important to note that not every organization needs internal | ||
| 78 | auditors. In fact, it's unwise for an organization to hire internal auditors | ||
| 79 | unless they have regulatory requirements for auditing and have the capital to | ||
| 80 | support the department. Internal audit is a cost center that can only affect | ||
| 81 | revenue indirectly. | ||
| 82 | |||
| 83 | Once an organization determines the need for internal assurance services, they | ||
| 84 | will hire a Chief Audit Executive and create the audit charter. This charter is | ||
| 85 | a document, approved by the company's governing body, that will define internal | ||
| 86 | audit's purpose, authority, responsibility, and position within the | ||
| 87 | organization. Fortunately, the IIA has model charters available to IIA members | ||
| 88 | for those developing or improving their charter. | ||
| 89 | |||
| 90 | Beyond the charter and organizational documents, internal auditors follow a few | ||
| 91 | different standards in order to perform their job. First is the International | ||
| 92 | Professional Practices Framework (IPPF) by the IIA, which is the model of | ||
| 93 | standards for internal auditing. In addition, ISACA's Information Technology | ||
| 94 | Assurance Framework (ITAF) helps guide auditors in reference to information | ||
| 95 | technology (IT) compliance and assurance. Finally, additional standards such as | ||
| 96 | FASB, GAAP, and industry-specific standards are used when performing internal | ||
| 97 | audit work. | ||
| 98 | |||
| 99 | # Three Lines of Defense | ||
| 100 | |||
| 101 | [The IIA](https://theiia.org) released the original Three Lines of Defense model | ||
| 102 | in 2013, but have released an updated version in 2020. | ||
| 103 | |||
| 104 | I won't go into depth about the changes made to the model in this article. | ||
| 105 | Instead, let's take a look at the most current model. | ||
| 106 | |||
| 107 | The updated model forgets the strict idea of areas performing their own | ||
| 108 | functions or line of defense. Instead of talking about management, risk, and | ||
| 109 | internal audit as 1-2-3, the new model creates a more fluid and cooperative | ||
| 110 | model. | ||
| 111 | |||
| 112 | Looking at this model from an auditing perspective shows us that auditors will | ||
| 113 | need to align, communicate, and collaborate with management, including business | ||
| 114 | area managers and chief officers, as well as reporting to the governing body. | ||
| 115 | The governing body will instruct internal audit _functionally_ on their goals | ||
| 116 | and track their progress periodically. | ||
| 117 | |||
| 118 | However, the internal audit department will report _administratively_ to a chief | ||
| 119 | officer in the company for the purposes of collaboration, direction, and | ||
| 120 | assistance with the business. Note that in most situations, the governing body | ||
| 121 | is the audit committee on the company's board of directors. | ||
| 122 | |||
| 123 | The result of this structure is that internal audit is an independent and | ||
| 124 | objective function that can provide assurance over the topics they audit. | ||
| 125 | |||
| 126 | # Audit Process | ||
| 127 | |||
| 128 | A normal audit will generally follow the same process, regardless of the topic. | ||
| 129 | However, certain special projects or abnormal business areas may call for | ||
| 130 | changes to the audit process. The audit process is not set in stone, it's simply | ||
| 131 | a set of best practices so that audits can be performed consistently. | ||
| 132 | |||
| 133 | While different organizations may tweak the process, it will generally follow | ||
| 134 | this flow: | ||
| 135 | |||
| 136 | ## 1. Risk Assessment | ||
| 137 | |||
| 138 | The risk assessment part of the process has historically been performed | ||
| 139 | annually, but many organizations have moved to performing this process much more | ||
| 140 | frequently. In fact, some organizations are moving to an agile approach that can | ||
| 141 | take new risks into the risk assessment and re-prioritize risk areas on-the-go. | ||
| 142 | To perform a risk assessment, leaders in internal audit will research industry | ||
| 143 | risks, consult with business leaders around the company, and perform analyses on | ||
| 144 | company data. | ||
| 145 | |||
| 146 | Once a risk assessment has been documented, the audit department has a | ||
| 147 | prioritized list of risks that can be audited. This is usually in the form of | ||
| 148 | auditable entities, such as business areas or departments. | ||
| 149 | |||
| 150 | ## 2. Planning | ||
| 151 | |||
| 152 | During the planning phase of an audit, auditors will meet with the business area | ||
| 153 | to discuss the various processes, controls, and risks applicable to the | ||
| 154 | business. This helps the auditors determine the scope limits for the audit, as | ||
| 155 | well as timing and subject-matter experts. Certain documents will be created in | ||
| 156 | this phase that will be used to keep the audit on-track an in-scope as it goes | ||
| 157 | forward. | ||
| 158 | |||
| 159 | ## 3. Testing | ||
| 160 | |||
| 161 | The testing phase, also known as fieldwork or execution, is where internal | ||
| 162 | auditors will take the information they've discovered and test it against | ||
| 163 | regulations, industry standards, company rules, best practices, as well as | ||
| 164 | validating that any processes are complete and accurate. For example, an audit | ||
| 165 | of HR would most likely examine processes such as employee on-boarding, employee | ||
| 166 | termination, security of personally identifiable information (PII), or the IT | ||
| 167 | systems involved in these processes. Company standards would be examined and | ||
| 168 | compared against how the processes are actually being performed day-to-day, as | ||
| 169 | well as compared against regulations such as the Equal Employment Opportunity | ||
| 170 | (EEO), American with Disabilities Act, and National Labor Relations Act. | ||
| 171 | |||
| 172 | ## 4. Reporting | ||
| 173 | |||
| 174 | Once all the tests have been completed, the audit will enter the reporting | ||
| 175 | phase. This is when the audit team will conclude on the evidence they've | ||
| 176 | collected, interviews they've held, and any opinions they've formed on the | ||
| 177 | controls in place. A summary of the audit findings, conclusions, and specific | ||
| 178 | recommendations are officially communicated to the client through a draft | ||
| 179 | report. Clients have the opportunity to respond to the report and submit an | ||
| 180 | action plan and time frame. These responses become part of the final report | ||
| 181 | which is distributed to the appropriate level of administration. | ||
| 182 | |||
| 183 | ## 5. Follow-Up | ||
| 184 | |||
| 185 | After audits have been completed and management has formed action plans and time | ||
| 186 | frames for audit issues, internal audit will follow up once that due date has | ||
| 187 | arrived. In most cases, the follow-up will simply consist of a meeting to | ||
| 188 | discuss how the action plan has been completed and to request documentation to | ||
| 189 | prove it. | ||
| 190 | |||
| 191 | # Audit Department Structure | ||
| 192 | |||
| 193 | While an internal audit department is most often thought of as a team of | ||
| 194 | full-time employees, there are actually many different ways in which a | ||
| 195 | department can be structured. As the world becomes more digital and fast-paced, | ||
| 196 | outsourcing has become a more attractive option for some organizations. Internal | ||
| 197 | audit can be fully outsourced or partially outsourced, allowing for flexibility | ||
| 198 | in cases where turnover is high. | ||
| 199 | |||
| 200 | In addition, departments can implement a rotational model. This allows for | ||
| 201 | interested employees around the organization to rotate into the internal audit | ||
| 202 | department for a period of time, allowing them to obtain knowledge of risks and | ||
| 203 | controls and allowing the internal audit team to obtain more business area | ||
| 204 | knowledge. This program is popular in very large organizations, but | ||
| 205 | organizations tend to rotate lower-level audit staff instead of managers. This | ||
| 206 | helps prevent any significant knowledge loss as auditors rotate out to business | ||
| 207 | areas. | ||
| 208 | |||
| 209 | # Consulting | ||
| 210 | |||
| 211 | Consulting is not an easy task at any organization, especially for a department | ||
| 212 | that can have negative perceptions within the organization as the "compliance | ||
| 213 | police." However, once an internal audit department has delivered value to | ||
| 214 | organization, adding consulting to their suite of services is a smart move. In | ||
| 215 | most cases, Internal Audit can insert themselves into a consulting role without | ||
| 216 | affecting the process of project management at the company. This means that | ||
| 217 | internal audit can add objective assurance and opinions to business areas as | ||
| 218 | they develop new processes, instead of coming in periodically to audit an area | ||
| 219 | and file issues that could have been fixed at the beginning. | ||
| 220 | |||
| 221 | # Data Science & Data Analytics | ||
| 222 | |||
| 223 | One major piece of the internal audit function in the modern world is data | ||
| 224 | science. While the process is data science, most auditors will refer to anything | ||
| 225 | in this realm as data analytics. Hot topics such as robotic process automation | ||
| 226 | (RPA), machine learning (ML), and data mining have taken over the auditing world | ||
| 227 | in recent years. These technologies have been immensely helpful with increasing | ||
| 228 | the effectiveness and efficiency of auditors. | ||
| 229 | |||
| 230 | For example, mundane and repetitive tasks can be automated in order for auditors | ||
| 231 | to make more room in their schedules for labor-intensive work. Further, auditors | ||
| 232 | will need to adapt technologies like machine learning in order to extract more | ||
| 233 | value from the data they're using to form conclusions. | ||
content/blog/2020-09-22-internal-audit.org added +248
| @@ -0,0 +1,248 @@ | |||
| 1 | #+date: <2020-09-22> | ||
| 2 | #+title: Who is Internal Audit? | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Definitions | ||
| 7 | |||
| 8 | One of the many reasons that Internal Audit needs such thorough | ||
| 9 | explaining to non-auditors is that Internal Audit can serve many | ||
| 10 | purposes, depending on the organization's size and needs. However, the | ||
| 11 | Institute of Internal Auditors (IIA) defines Internal Auditing as: | ||
| 12 | |||
| 13 | #+begin_quote | ||
| 14 | Internal auditing is an independent, objective assurance and consulting | ||
| 15 | activity designed to add value and improve an organization's operations. | ||
| 16 | It helps an organization accomplish its objectives by bringing a | ||
| 17 | systematic, disciplined approach to evaluate and improve the | ||
| 18 | effectiveness of risk management, control, and governance processes. | ||
| 19 | #+end_quote | ||
| 20 | |||
| 21 | However, this definition uses quite a few terms that aren't clear unless | ||
| 22 | the reader already has a solid understanding of the auditing profession. | ||
| 23 | To further explain, the following is a list of definitions that can help | ||
| 24 | supplement understanding of internal auditing. | ||
| 25 | |||
| 26 | ** Independent | ||
| 27 | |||
| 28 | Independence is the freedom from conditions that threaten the ability of | ||
| 29 | the internal audit activity to carry out internal audit responsibilities | ||
| 30 | in an unbiased manner. To achieve the degree of independence necessary | ||
| 31 | to effectively carry out the responsibilities of the internal audit | ||
| 32 | activity, the chief audit executive has direct and unrestricted access | ||
| 33 | to senior management and the board. This can be achieved through a | ||
| 34 | dual-reporting relationship. Threats to independence must be managed at | ||
| 35 | the individual auditor, engagement, functional, and organizational | ||
| 36 | levels. | ||
| 37 | |||
| 38 | ** Objective | ||
| 39 | |||
| 40 | Objectivity is an unbiased mental attitude that allows internal auditors | ||
| 41 | to perform engagements in such a manner that they believe in their work | ||
| 42 | product and that no quality compromises are made. Objectivity requires | ||
| 43 | that internal auditors do not subordinate their judgment on audit | ||
| 44 | matters to others. Threats to objectivity must be managed at the | ||
| 45 | individual auditor, engagement, functional, and organizational levels. | ||
| 46 | |||
| 47 | ** Assurance | ||
| 48 | |||
| 49 | Assurance services involve the internal auditor's objective assessment | ||
| 50 | of evidence to provide opinions or conclusions regarding an entity, | ||
| 51 | operation, function, process, system, or other subject matters. The | ||
| 52 | internal auditor determines the nature and scope of an assurance | ||
| 53 | engagement. Generally, three parties are participants in assurance | ||
| 54 | services: (1) the person or group directly involved with the entity, | ||
| 55 | operation, function, process, system, or other subject | ||
| 56 | |||
| 57 | - (the process owner), (2) the person or group making the assessment - | ||
| 58 | (the internal auditor), and (3) the person or group using the | ||
| 59 | assessment - (the user). | ||
| 60 | |||
| 61 | ** Consulting | ||
| 62 | |||
| 63 | Consulting services are advisory in nature and are generally performed | ||
| 64 | at the specific request of an engagement client. The nature and scope of | ||
| 65 | the consulting engagement are subject to agreement with the engagement | ||
| 66 | client. Consulting services generally involve two parties: (1) the | ||
| 67 | person or group offering the advice (the internal auditor), and (2) the | ||
| 68 | person or group seeking and receiving the advice (the engagement | ||
| 69 | client). When performing consulting services, the internal auditor | ||
| 70 | should maintain objectivity and not assume management responsibility. | ||
| 71 | |||
| 72 | ** Governance, Risk Management, & Compliance (GRC) | ||
| 73 | |||
| 74 | The integrated collection of capabilities that enable an organization to | ||
| 75 | reliably achieve objectives, address uncertainty and act with integrity. | ||
| 76 | |||
| 77 | * Audit Charter & Standards | ||
| 78 | |||
| 79 | First, it's important to note that not every organization needs internal | ||
| 80 | auditors. In fact, it's unwise for an organization to hire internal | ||
| 81 | auditors unless they have regulatory requirements for auditing and have | ||
| 82 | the capital to support the department. Internal audit is a cost center | ||
| 83 | that can only affect revenue indirectly. | ||
| 84 | |||
| 85 | Once an organization determines the need for internal assurance | ||
| 86 | services, they will hire a Chief Audit Executive and create the audit | ||
| 87 | charter. This charter is a document, approved by the company's governing | ||
| 88 | body, that will define internal audit's purpose, authority, | ||
| 89 | responsibility, and position within the organization. Fortunately, the | ||
| 90 | IIA has model charters available to IIA members for those developing or | ||
| 91 | improving their charter. | ||
| 92 | |||
| 93 | Beyond the charter and organizational documents, internal auditors | ||
| 94 | follow a few different standards in order to perform their job. First is | ||
| 95 | the International Professional Practices Framework (IPPF) by the IIA, | ||
| 96 | which is the model of standards for internal auditing. In addition, | ||
| 97 | ISACA's Information Technology Assurance Framework (ITAF) helps guide | ||
| 98 | auditors in reference to information technology (IT) compliance and | ||
| 99 | assurance. Finally, additional standards such as FASB, GAAP, and | ||
| 100 | industry-specific standards are used when performing internal audit | ||
| 101 | work. | ||
| 102 | |||
| 103 | * Three Lines of Defense | ||
| 104 | |||
| 105 | [[https://theiia.org][The IIA]] released the original Three Lines of | ||
| 106 | Defense model in 2013, but have released an updated version in 2020. | ||
| 107 | |||
| 108 | I won't go into depth about the changes made to the model in this | ||
| 109 | article. Instead, let's take a look at the most current model. | ||
| 110 | |||
| 111 | The updated model forgets the strict idea of areas performing their own | ||
| 112 | functions or line of defense. Instead of talking about management, risk, | ||
| 113 | and internal audit as 1-2-3, the new model creates a more fluid and | ||
| 114 | cooperative model. | ||
| 115 | |||
| 116 | Looking at this model from an auditing perspective shows us that | ||
| 117 | auditors will need to align, communicate, and collaborate with | ||
| 118 | management, including business area managers and chief officers, as well | ||
| 119 | as reporting to the governing body. The governing body will instruct | ||
| 120 | internal audit /functionally/ on their goals and track their progress | ||
| 121 | periodically. | ||
| 122 | |||
| 123 | However, the internal audit department will report /administratively/ to | ||
| 124 | a chief officer in the company for the purposes of collaboration, | ||
| 125 | direction, and assistance with the business. Note that in most | ||
| 126 | situations, the governing body is the audit committee on the company's | ||
| 127 | board of directors. | ||
| 128 | |||
| 129 | The result of this structure is that internal audit is an independent | ||
| 130 | and objective function that can provide assurance over the topics they | ||
| 131 | audit. | ||
| 132 | |||
| 133 | * Audit Process | ||
| 134 | |||
| 135 | A normal audit will generally follow the same process, regardless of the | ||
| 136 | topic. However, certain special projects or abnormal business areas may | ||
| 137 | call for changes to the audit process. The audit process is not set in | ||
| 138 | stone, it's simply a set of best practices so that audits can be | ||
| 139 | performed consistently. | ||
| 140 | |||
| 141 | While different organizations may tweak the process, it will generally | ||
| 142 | follow this flow: | ||
| 143 | |||
| 144 | ** 1. Risk Assessment | ||
| 145 | |||
| 146 | The risk assessment part of the process has historically been performed | ||
| 147 | annually, but many organizations have moved to performing this process | ||
| 148 | much more frequently. In fact, some organizations are moving to an agile | ||
| 149 | approach that can take new risks into the risk assessment and | ||
| 150 | re-prioritize risk areas on-the-go. To perform a risk assessment, | ||
| 151 | leaders in internal audit will research industry risks, consult with | ||
| 152 | business leaders around the company, and perform analyses on company | ||
| 153 | data. | ||
| 154 | |||
| 155 | Once a risk assessment has been documented, the audit department has a | ||
| 156 | prioritized list of risks that can be audited. This is usually in the | ||
| 157 | form of auditable entities, such as business areas or departments. | ||
| 158 | |||
| 159 | ** 2. Planning | ||
| 160 | |||
| 161 | During the planning phase of an audit, auditors will meet with the | ||
| 162 | business area to discuss the various processes, controls, and risks | ||
| 163 | applicable to the business. This helps the auditors determine the scope | ||
| 164 | limits for the audit, as well as timing and subject-matter experts. | ||
| 165 | Certain documents will be created in this phase that will be used to | ||
| 166 | keep the audit on-track an in-scope as it goes forward. | ||
| 167 | |||
| 168 | ** 3. Testing | ||
| 169 | |||
| 170 | The testing phase, also known as fieldwork or execution, is where | ||
| 171 | internal auditors will take the information they've discovered and test | ||
| 172 | it against regulations, industry standards, company rules, best | ||
| 173 | practices, as well as validating that any processes are complete and | ||
| 174 | accurate. For example, an audit of HR would most likely examine | ||
| 175 | processes such as employee on-boarding, employee termination, security | ||
| 176 | of personally identifiable information (PII), or the IT systems involved | ||
| 177 | in these processes. Company standards would be examined and compared | ||
| 178 | against how the processes are actually being performed day-to-day, as | ||
| 179 | well as compared against regulations such as the Equal Employment | ||
| 180 | Opportunity (EEO), American with Disabilities Act, and National Labor | ||
| 181 | Relations Act. | ||
| 182 | |||
| 183 | ** 4. Reporting | ||
| 184 | |||
| 185 | Once all the tests have been completed, the audit will enter the | ||
| 186 | reporting phase. This is when the audit team will conclude on the | ||
| 187 | evidence they've collected, interviews they've held, and any opinions | ||
| 188 | they've formed on the controls in place. A summary of the audit | ||
| 189 | findings, conclusions, and specific recommendations are officially | ||
| 190 | communicated to the client through a draft report. Clients have the | ||
| 191 | opportunity to respond to the report and submit an action plan and time | ||
| 192 | frame. These responses become part of the final report which is | ||
| 193 | distributed to the appropriate level of administration. | ||
| 194 | |||
| 195 | ** 5. Follow-Up | ||
| 196 | |||
| 197 | After audits have been completed and management has formed action plans | ||
| 198 | and time frames for audit issues, internal audit will follow up once | ||
| 199 | that due date has arrived. In most cases, the follow-up will simply | ||
| 200 | consist of a meeting to discuss how the action plan has been completed | ||
| 201 | and to request documentation to prove it. | ||
| 202 | |||
| 203 | * Audit Department Structure | ||
| 204 | |||
| 205 | While an internal audit department is most often thought of as a team of | ||
| 206 | full-time employees, there are actually many different ways in which a | ||
| 207 | department can be structured. As the world becomes more digital and | ||
| 208 | fast-paced, outsourcing has become a more attractive option for some | ||
| 209 | organizations. Internal audit can be fully outsourced or partially | ||
| 210 | outsourced, allowing for flexibility in cases where turnover is high. | ||
| 211 | |||
| 212 | In addition, departments can implement a rotational model. This allows | ||
| 213 | for interested employees around the organization to rotate into the | ||
| 214 | internal audit department for a period of time, allowing them to obtain | ||
| 215 | knowledge of risks and controls and allowing the internal audit team to | ||
| 216 | obtain more business area knowledge. This program is popular in very | ||
| 217 | large organizations, but organizations tend to rotate lower-level audit | ||
| 218 | staff instead of managers. This helps prevent any significant knowledge | ||
| 219 | loss as auditors rotate out to business areas. | ||
| 220 | |||
| 221 | * Consulting | ||
| 222 | |||
| 223 | Consulting is not an easy task at any organization, especially for a | ||
| 224 | department that can have negative perceptions within the organization as | ||
| 225 | the "compliance police." However, once an internal audit department has | ||
| 226 | delivered value to organization, adding consulting to their suite of | ||
| 227 | services is a smart move. In most cases, Internal Audit can insert | ||
| 228 | themselves into a consulting role without affecting the process of | ||
| 229 | project management at the company. This means that internal audit can | ||
| 230 | add objective assurance and opinions to business areas as they develop | ||
| 231 | new processes, instead of coming in periodically to audit an area and | ||
| 232 | file issues that could have been fixed at the beginning. | ||
| 233 | |||
| 234 | * Data Science & Data Analytics | ||
| 235 | |||
| 236 | One major piece of the internal audit function in the modern world is | ||
| 237 | data science. While the process is data science, most auditors will | ||
| 238 | refer to anything in this realm as data analytics. Hot topics such as | ||
| 239 | robotic process automation (RPA), machine learning (ML), and data mining | ||
| 240 | have taken over the auditing world in recent years. These technologies | ||
| 241 | have been immensely helpful with increasing the effectiveness and | ||
| 242 | efficiency of auditors. | ||
| 243 | |||
| 244 | For example, mundane and repetitive tasks can be automated in order for | ||
| 245 | auditors to make more room in their schedules for labor-intensive work. | ||
| 246 | Further, auditors will need to adapt technologies like machine learning | ||
| 247 | in order to extract more value from the data they're using to form | ||
| 248 | conclusions. | ||
content/blog/2020-09-25-happiness-map.md → content/blog/2020-09-25-happiness-map.org renamed +72 −71
| @@ -1,88 +1,87 @@ | |||
| 1 | +++ | 1 | #+date: <2020-09-25> |
| 2 | date = 2020-09-25 | 2 | #+title: Data Visualization: World Choropleth Map of Happiness |
| 3 | title = "Data Visualization: World Choropleth Map of Happiness" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Background Information | 5 | |
| 6 | * Background Information | ||
| 9 | 7 | ||
| 10 | The dataset (obtained from | 8 | The dataset (obtained from |
| 11 | [Kaggle](https://www.kaggle.com/unsdsn/world-happiness)) used in this article | 9 | [[https://www.kaggle.com/unsdsn/world-happiness][Kaggle]]) used in this |
| 12 | contains a list of countries around the world, their happiness rankings and | 10 | article contains a list of countries around the world, their happiness |
| 13 | scores, as well as other national scoring measures. | 11 | rankings and scores, as well as other national scoring measures. |
| 14 | 12 | ||
| 15 | Fields include: | 13 | Fields include: |
| 16 | 14 | ||
| 17 | - Overall rank | 15 | - Overall rank |
| 18 | - Country or region | 16 | - Country or region |
| 19 | - GDP per capita | 17 | - GDP per capita |
| 20 | - Social support | 18 | - Social support |
| 21 | - Healthy life expectancy | 19 | - Healthy life expectancy |
| 22 | - Freedom to make life choices | 20 | - Freedom to make life choices |
| 23 | - Generosity | 21 | - Generosity |
| 24 | - Perceptions of corruption | 22 | - Perceptions of corruption |
| 25 | 23 | ||
| 26 | There are 156 records. Since there are ~195 countries in the world, we can see | 24 | There are 156 records. Since there are ~195 countries in the world, we |
| 27 | that around 40 countries will be missing from this dataset. | 25 | can see that around 40 countries will be missing from this dataset. |
| 28 | 26 | ||
| 29 | # Install Packages | 27 | * Install Packages |
| 30 | 28 | ||
| 31 | As always, run the `install` command for all packages needed to perform | 29 | As always, run the =install= command for all packages needed to perform |
| 32 | analysis. | 30 | analysis. |
| 33 | 31 | ||
| 34 | ```python | 32 | #+begin_src python |
| 35 | !pip install folium geopandas matplotlib numpy pandas | 33 | !pip install folium geopandas matplotlib numpy pandas |
| 36 | ``` | 34 | #+end_src |
| 37 | 35 | ||
| 38 | # Import the Data | 36 | * Import the Data |
| 39 | 37 | ||
| 40 | We only need a couple packages to create a choropleth map. We will use | 38 | We only need a couple packages to create a choropleth map. We will use |
| 41 | [Folium](https://python-visualization.github.io/folium/), which provides map | 39 | [[https://python-visualization.github.io/folium/][Folium]], which |
| 42 | visualizations in Python. We will also use geopandas and pandas to wrangle our | 40 | provides map visualizations in Python. We will also use geopandas and |
| 43 | data before we put it on a map. | 41 | pandas to wrangle our data before we put it on a map. |
| 44 | 42 | ||
| 45 | ```python | 43 | #+begin_src python |
| 46 | # Import the necessary Python packages | 44 | # Import the necessary Python packages |
| 47 | import folium | 45 | import folium |
| 48 | import geopandas as gpd | 46 | import geopandas as gpd |
| 49 | import pandas as pd | 47 | import pandas as pd |
| 50 | ``` | 48 | #+end_src |
| 51 | 49 | ||
| 52 | To get anything to show up on a map, we need a file that will specify the | 50 | To get anything to show up on a map, we need a file that will specify |
| 53 | boundaries of each country. Luckily, GeoJSON files exist (for free!) on the | 51 | the boundaries of each country. Luckily, GeoJSON files exist (for free!) |
| 54 | internet. To get the boundaries of every country in the world, we will use the | 52 | on the internet. To get the boundaries of every country in the world, we |
| 55 | GeoJSON link shown below. | 53 | will use the GeoJSON link shown below. |
| 56 | 54 | ||
| 57 | GeoPandas will take this data and load it into a dataframe so that we can easily | 55 | GeoPandas will take this data and load it into a dataframe so that we |
| 58 | match it to the data we're trying to analyze. Let's look at the GeoJSON | 56 | can easily match it to the data we're trying to analyze. Let's look at |
| 59 | dataframe: | 57 | the GeoJSON dataframe: |
| 60 | 58 | ||
| 61 | ```python | 59 | #+begin_src python |
| 62 | # Load the GeoJSON data with geopandas | 60 | # Load the GeoJSON data with geopandas |
| 63 | geo_data = gpd.read_file('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson') | 61 | geo_data = gpd.read_file('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson') |
| 64 | geo_data.head() | 62 | geo_data.head() |
| 65 | ``` | 63 | #+end_src |
| 66 | 64 | ||
| 67 | Next, let's load the data from the Kaggle dataset. I've downloaded this file, so | 65 | Next, let's load the data from the Kaggle dataset. I've downloaded this |
| 68 | update the file path if you have it somewhere else. After loading, let's take a | 66 | file, so update the file path if you have it somewhere else. After |
| 69 | look at this dataframe: | 67 | loading, let's take a look at this dataframe: |
| 70 | 68 | ||
| 71 | ```python | 69 | #+begin_src python |
| 72 | # Load the world happiness data with pandas | 70 | # Load the world happiness data with pandas |
| 73 | happy_data = pd.read_csv(r'~/Downloads/world_happiness_data_2019.csv') | 71 | happy_data = pd.read_csv(r'~/Downloads/world_happiness_data_2019.csv') |
| 74 | happy_data.head() | 72 | happy_data.head() |
| 75 | ``` | 73 | #+end_src |
| 76 | 74 | ||
| 77 | # Clean the Data | 75 | * Clean the Data |
| 78 | 76 | ||
| 79 | Some countries need to be renamed, or they will be lost when you merge the | 77 | Some countries need to be renamed, or they will be lost when you merge |
| 80 | happiness and GeoJSON dataframes. This is something I discovered when the map | 78 | the happiness and GeoJSON dataframes. This is something I discovered |
| 81 | below showed empty countries. I searched both data frames for the missing | 79 | when the map below showed empty countries. I searched both data frames |
| 82 | countries to see the naming differences. Any countries that do not have records | 80 | for the missing countries to see the naming differences. Any countries |
| 83 | in the `happy_data` df will not show up on the map. | 81 | that do not have records in the =happy_data= df will not show up on the |
| 82 | map. | ||
| 84 | 83 | ||
| 85 | ```python | 84 | #+begin_src python |
| 86 | # Rename some countries to match our GeoJSON data | 85 | # Rename some countries to match our GeoJSON data |
| 87 | 86 | ||
| 88 | # Rename USA | 87 | # Rename USA |
| @@ -100,34 +99,35 @@ happy_data.at[republic_congo_index, 'Country or region'] = 'Republic of Congo' | |||
| 100 | # Rename the DRC | 99 | # Rename the DRC |
| 101 | democratic_congo_index = happy_data.index[happy_data['Country or region'] == 'Congo (Kinshasa)'] | 100 | democratic_congo_index = happy_data.index[happy_data['Country or region'] == 'Congo (Kinshasa)'] |
| 102 | happy_data.at[democratic_congo_index, 'Country or region'] = 'Democratic Republic of the Congo' | 101 | happy_data.at[democratic_congo_index, 'Country or region'] = 'Democratic Republic of the Congo' |
| 103 | ``` | 102 | #+end_src |
| 104 | 103 | ||
| 105 | # Merge the Data | 104 | * Merge the Data |
| 106 | 105 | ||
| 107 | Now that we have clean data, we need to merge the GeoJSON data with the | 106 | Now that we have clean data, we need to merge the GeoJSON data with the |
| 108 | happiness data. Since we've stored them both in dataframes, we just need to call | 107 | happiness data. Since we've stored them both in dataframes, we just need |
| 109 | the `.merge()` function. | 108 | to call the =.merge()= function. |
| 110 | 109 | ||
| 111 | We will also rename a couple columns, just so that they're a little easier to | 110 | We will also rename a couple columns, just so that they're a little |
| 112 | use when we create the map. | 111 | easier to use when we create the map. |
| 113 | 112 | ||
| 114 | ```python | 113 | #+begin_src python |
| 115 | # Merge the two previous dataframes into a single geopandas dataframe | 114 | # Merge the two previous dataframes into a single geopandas dataframe |
| 116 | merged_df = geo_data.merge(happy_data,left_on='ADMIN', right_on='Country or region') | 115 | merged_df = geo_data.merge(happy_data,left_on='ADMIN', right_on='Country or region') |
| 117 | 116 | ||
| 118 | # Rename columns for ease of use | 117 | # Rename columns for ease of use |
| 119 | merged_df = merged_df.rename(columns = {'ADMIN':'GeoJSON_Country'}) | 118 | merged_df = merged_df.rename(columns = {'ADMIN':'GeoJSON_Country'}) |
| 120 | merged_df = merged_df.rename(columns = {'Country or region':'Country'}) | 119 | merged_df = merged_df.rename(columns = {'Country or region':'Country'}) |
| 121 | ``` | 120 | #+end_src |
| 122 | 121 | ||
| 123 | # Create the Map | 122 | * Create the Map |
| 124 | 123 | ||
| 125 | The data is finally ready to be added to a map. The code below shows the | 124 | The data is finally ready to be added to a map. The code below shows the |
| 126 | simplest way to find the center of the map and create a Folium map object. The | 125 | simplest way to find the center of the map and create a Folium map |
| 127 | important part is to remember to reference the merged dataframe for our GeoJSON | 126 | object. The important part is to remember to reference the merged |
| 128 | data and value data. The columns specify which geo data and value data to use. | 127 | dataframe for our GeoJSON data and value data. The columns specify which |
| 128 | geo data and value data to use. | ||
| 129 | 129 | ||
| 130 | ```python | 130 | #+begin_src python |
| 131 | # Assign centroids to map | 131 | # Assign centroids to map |
| 132 | x_map = merged_df.centroid.x.mean() | 132 | x_map = merged_df.centroid.x.mean() |
| 133 | y_map = merged_df.centroid.y.mean() | 133 | y_map = merged_df.centroid.y.mean() |
| @@ -151,17 +151,18 @@ folium.Choropleth( | |||
| 151 | smooth_factor=0, | 151 | smooth_factor=0, |
| 152 | highlight=True | 152 | highlight=True |
| 153 | ).add_to(world_map) | 153 | ).add_to(world_map) |
| 154 | ``` | 154 | #+end_src |
| 155 | 155 | ||
| 156 | Let's look at the resulting map. | 156 | Let's look at the resulting map. |
| 157 | 157 | ||
| 158 | # Create a Tooltip on Hover | 158 | * Create a Tooltip on Hover |
| 159 | 159 | ||
| 160 | Now that we have a map set up, we could stop. However, I want to add a tooltip | 160 | Now that we have a map set up, we could stop. However, I want to add a |
| 161 | so that I can see more information about each country. The `tooltip_data` code | 161 | tooltip so that I can see more information about each country. The |
| 162 | below will show a popup on hover with all the data fields shown. | 162 | =tooltip_data= code below will show a popup on hover with all the data |
| 163 | fields shown. | ||
| 163 | 164 | ||
| 164 | ```python | 165 | #+begin_src python |
| 165 | # Adding labels to map | 166 | # Adding labels to map |
| 166 | style_function = lambda x: {'fillColor': '#ffffff', | 167 | style_function = lambda x: {'fillColor': '#ffffff', |
| 167 | 'color':'#000000', | 168 | 'color':'#000000', |
| @@ -202,6 +203,6 @@ folium.LayerControl().add_to(world_map) | |||
| 202 | 203 | ||
| 203 | # Display the map | 204 | # Display the map |
| 204 | world_map | 205 | world_map |
| 205 | ``` | 206 | #+end_src |
| 206 | 207 | ||
| 207 | The tooltip will now appear whenever you hover over a country. | 208 | The tooltip will now appear whenever you hover over a country. |
content/blog/2020-10-12-mediocrity.md deleted −116
| @@ -1,116 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-10-12 | ||
| 3 | title = "On the Pursuit of Mediocrity" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Perfect is the Enemy of Good | ||
| 9 | |||
| 10 | As the saying goes, "the best is the enemy of the good." As we strive for | ||
| 11 | perfection, we often fail to realize the implications of such an undertaking. | ||
| 12 | Attempting to reach perfection is often unrealistic. Even worse, it can get in | ||
| 13 | the way of achieving a good outcome. In certain situations, we try so hard to | ||
| 14 | achieve the ideal solution that we have burned the bridges that would have | ||
| 15 | allowed us to reach a lesser yet still superb solution. | ||
| 16 | |||
| 17 | Philosophers throughout history have inspected this plight from many viewpoints. | ||
| 18 | Greek mythology speaks of the [golden | ||
| 19 | mean](<https://en.wikipedia.org/wiki/Golden_mean_(philosophy)>), which uses the | ||
| 20 | story of Icarus to illustrate that sometimes "the middle course" is the best | ||
| 21 | solution. In this story, Daedalus, a famous artist of his time, built feathered | ||
| 22 | wings for himself and his son so that they might escape the clutches of King | ||
| 23 | Minos. Daedalus warns his beloved son whom he loved so much to "fly the middle | ||
| 24 | course", between the sea spray and the sun's heat. Icarus did not heed his | ||
| 25 | father; he flew up and up until the sun melted the wax off his wings. For not | ||
| 26 | heeding the middle course, he fell into the sea and drowned. | ||
| 27 | |||
| 28 | More recently, management scholars have explored the [Pareto | ||
| 29 | principle](https://en.wikipedia.org/wiki/Pareto_principle) and found that as we | ||
| 30 | increase the frequency of something, or strive to perform actions to achieve | ||
| 31 | some form of perfection, we run into [diminishing | ||
| 32 | returns](https://en.wikipedia.org/wiki/Diminishing_returns). | ||
| 33 | |||
| 34 | Even further, Harold Demsetz is noted as coining the term [the Nirvana | ||
| 35 | fallacy](https://en.wikipedia.org/wiki/Nirvana_fallacy) in 1969, which shows the | ||
| 36 | fallacy of comparing actual things with unrealistic, idealized alternatives. | ||
| 37 | This is another trap that we may fall into, where we are constantly thinking of | ||
| 38 | the ultimate solutions to problems, when something more realistic needs to be | ||
| 39 | considered. | ||
| 40 | |||
| 41 | Over and over throughout history, we've found that perfection is often | ||
| 42 | unrealistic and unachievable. However, we push ourselves and our peers to "give | ||
| 43 | 100%" or "go the extra mile," while it may be that the better course is to give | ||
| 44 | a valuable level of effort while considering the effects of further effort on | ||
| 45 | the outcome. Working harder does not always help us achieve loftier goals. | ||
| 46 | |||
| 47 | This has presented itself to me most recently during my time studying at my | ||
| 48 | university. I was anxious and feeling the stresses of my courses, career, and | ||
| 49 | personal life for quite a while, which was greatly affecting how well I was | ||
| 50 | doing at school and my level of effort at work. One day, I happened to be | ||
| 51 | talking to my father when he said something simple that hit home: | ||
| 52 | |||
| 53 | > All you can do is show up and do your best. Worrying about the outcomes won't | ||
| 54 | > affect the outcome itself. | ||
| 55 | |||
| 56 | The thought was extremely straightforward and uncomplicated, yet it was | ||
| 57 | something that I had lost sight of during my stress-filled years at school. Ever | ||
| 58 | since then, I've found myself pausing and remembering that quote every time I | ||
| 59 | get anxious or stressed. It helps to stop and think "Can I do anything to affect | ||
| 60 | the outcome, or am I simply worrying over something I can't change?" | ||
| 61 | |||
| 62 | # When Mediocrity Isn't Enough | ||
| 63 | |||
| 64 | One problem with the philosophies presented in this post is that they are | ||
| 65 | implemented far too often in situations where mediocrity simply isn't adequate. | ||
| 66 | For example, let's take a look at digital user data, specifically | ||
| 67 | personally-identifiable information (PII). As a cybersecurity auditor in the | ||
| 68 | United States, I have found that most companies are concerned more with | ||
| 69 | compliance than any actual safeguards over the privacy or protection of user | ||
| 70 | data. Other than companies who have built their reputation on privacy and | ||
| 71 | security, most companies will use | ||
| 72 | [satisficing](https://en.wikipedia.org/wiki/Satisficing) as their primary | ||
| 73 | decision-making strategy around user data. | ||
| 74 | |||
| 75 | > Satisficing is a decision-making strategy or cognitive heuristic that entails | ||
| 76 | > searching through the available alternatives until an acceptability threshold | ||
| 77 | > is met. | ||
| 78 | |||
| 79 | This means that each decision will be met with certain possible solutions until | ||
| 80 | one of the solutions meets their minimum acceptable standards. For companies | ||
| 81 | that deal with user data, the minimum-acceptable standards come from three | ||
| 82 | areas: | ||
| 83 | |||
| 84 | 1. Laws and regulations | ||
| 85 | 2. Competitive pressure | ||
| 86 | 3. Risk of monetary or reputation loss | ||
| 87 | |||
| 88 | Working with project management or auditing, the primary concern here is the | ||
| 89 | risk of legal ramifications. Since the primary risk comes from laws and | ||
| 90 | regulations, companies will require that any project that involves user data | ||
| 91 | must follow all the rules of those laws so that the company can protect itself | ||
| 92 | from fines or other penalties. | ||
| 93 | |||
| 94 | Following this, companies will consider best practices in order to place itself | ||
| 95 | in a competitive position (e.g. Google vs. Apple) and review any recent or | ||
| 96 | ongoing litigation against companies regarding user data. In a perfect company, | ||
| 97 | management would then consider the ethical responsibilities of their | ||
| 98 | organization and discuss their responsibilities over things like | ||
| 99 | personally-identifiable information. | ||
| 100 | |||
| 101 | However, as we mentioned above, most companies follow the idea of satisficing, | ||
| 102 | which states that they have met the minimum acceptable standards and can now | ||
| 103 | move on to other decisions. Modern business culture in the United States | ||
| 104 | dictates that profits are the golden measure of how well a company or manager is | ||
| 105 | performing, so we often don't think about our responsibilities beyond these | ||
| 106 | basic standards. | ||
| 107 | |||
| 108 | Not all situations demand excellence, but I believe that applying any philosophy | ||
| 109 | as a broad stroke across one's life can be a mistake. We must be able to think | ||
| 110 | critically about what we are doing as we do it and ask ourselves a few | ||
| 111 | questions. Have I done everything I can in this situation? Is mediocrity an | ||
| 112 | acceptable outcome, or should we strive for perfection, even if we can't attain | ||
| 113 | it? | ||
| 114 | |||
| 115 | Taking a few moments to think critically throughout our day, as we make | ||
| 116 | decisions, can have a tremendous effect on the outcomes we create. | ||
content/blog/2020-10-12-mediocrity.org added +124
| @@ -0,0 +1,124 @@ | |||
| 1 | #+date: <2020-10-12> | ||
| 2 | #+title: On the Pursuit of Mediocrity | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Perfect is the Enemy of Good | ||
| 7 | |||
| 8 | As the saying goes, "the best is the enemy of the good." As we strive | ||
| 9 | for perfection, we often fail to realize the implications of such an | ||
| 10 | undertaking. Attempting to reach perfection is often unrealistic. Even | ||
| 11 | worse, it can get in the way of achieving a good outcome. In certain | ||
| 12 | situations, we try so hard to achieve the ideal solution that we have | ||
| 13 | burned the bridges that would have allowed us to reach a lesser yet | ||
| 14 | still superb solution. | ||
| 15 | |||
| 16 | Philosophers throughout history have inspected this plight from many | ||
| 17 | viewpoints. Greek mythology speaks of the | ||
| 18 | [[https://en.wikipedia.org/wiki/Golden_mean_(philosophy)][golden mean]], | ||
| 19 | which uses the story of Icarus to illustrate that sometimes "the middle | ||
| 20 | course" is the best solution. In this story, Daedalus, a famous artist | ||
| 21 | of his time, built feathered wings for himself and his son so that they | ||
| 22 | might escape the clutches of King Minos. Daedalus warns his beloved son | ||
| 23 | whom he loved so much to "fly the middle course", between the sea spray | ||
| 24 | and the sun's heat. Icarus did not heed his father; he flew up and up | ||
| 25 | until the sun melted the wax off his wings. For not heeding the middle | ||
| 26 | course, he fell into the sea and drowned. | ||
| 27 | |||
| 28 | More recently, management scholars have explored the | ||
| 29 | [[https://en.wikipedia.org/wiki/Pareto_principle][Pareto principle]] and | ||
| 30 | found that as we increase the frequency of something, or strive to | ||
| 31 | perform actions to achieve some form of perfection, we run into | ||
| 32 | [[https://en.wikipedia.org/wiki/Diminishing_returns][diminishing | ||
| 33 | returns]]. | ||
| 34 | |||
| 35 | Even further, Harold Demsetz is noted as coining the term | ||
| 36 | [[https://en.wikipedia.org/wiki/Nirvana_fallacy][the Nirvana fallacy]] | ||
| 37 | in 1969, which shows the fallacy of comparing actual things with | ||
| 38 | unrealistic, idealized alternatives. This is another trap that we may | ||
| 39 | fall into, where we are constantly thinking of the ultimate solutions to | ||
| 40 | problems, when something more realistic needs to be considered. | ||
| 41 | |||
| 42 | Over and over throughout history, we've found that perfection is often | ||
| 43 | unrealistic and unachievable. However, we push ourselves and our peers | ||
| 44 | to "give 100%" or "go the extra mile," while it may be that the better | ||
| 45 | course is to give a valuable level of effort while considering the | ||
| 46 | effects of further effort on the outcome. Working harder does not always | ||
| 47 | help us achieve loftier goals. | ||
| 48 | |||
| 49 | This has presented itself to me most recently during my time studying at | ||
| 50 | my university. I was anxious and feeling the stresses of my courses, | ||
| 51 | career, and personal life for quite a while, which was greatly affecting | ||
| 52 | how well I was doing at school and my level of effort at work. One day, | ||
| 53 | I happened to be talking to my father when he said something simple that | ||
| 54 | hit home: | ||
| 55 | |||
| 56 | #+begin_quote | ||
| 57 | All you can do is show up and do your best. Worrying about the outcomes | ||
| 58 | won't affect the outcome itself. | ||
| 59 | #+end_quote | ||
| 60 | |||
| 61 | The thought was extremely straightforward and uncomplicated, yet it was | ||
| 62 | something that I had lost sight of during my stress-filled years at | ||
| 63 | school. Ever since then, I've found myself pausing and remembering that | ||
| 64 | quote every time I get anxious or stressed. It helps to stop and think | ||
| 65 | "Can I do anything to affect the outcome, or am I simply worrying over | ||
| 66 | something I can't change?" | ||
| 67 | |||
| 68 | * When Mediocrity Isn't Enough | ||
| 69 | |||
| 70 | One problem with the philosophies presented in this post is that they | ||
| 71 | are implemented far too often in situations where mediocrity simply | ||
| 72 | isn't adequate. For example, let's take a look at digital user data, | ||
| 73 | specifically personally-identifiable information (PII). As a | ||
| 74 | cybersecurity auditor in the United States, I have found that most | ||
| 75 | companies are concerned more with compliance than any actual safeguards | ||
| 76 | over the privacy or protection of user data. Other than companies who | ||
| 77 | have built their reputation on privacy and security, most companies will | ||
| 78 | use [[https://en.wikipedia.org/wiki/Satisficing][satisficing]] as their | ||
| 79 | primary decision-making strategy around user data. | ||
| 80 | |||
| 81 | #+begin_quote | ||
| 82 | Satisficing is a decision-making strategy or cognitive heuristic that | ||
| 83 | entails searching through the available alternatives until an | ||
| 84 | acceptability threshold is met. | ||
| 85 | #+end_quote | ||
| 86 | |||
| 87 | This means that each decision will be met with certain possible | ||
| 88 | solutions until one of the solutions meets their minimum acceptable | ||
| 89 | standards. For companies that deal with user data, the | ||
| 90 | minimum-acceptable standards come from three areas: | ||
| 91 | |||
| 92 | 1. Laws and regulations | ||
| 93 | 2. Competitive pressure | ||
| 94 | 3. Risk of monetary or reputation loss | ||
| 95 | |||
| 96 | Working with project management or auditing, the primary concern here is | ||
| 97 | the risk of legal ramifications. Since the primary risk comes from laws | ||
| 98 | and regulations, companies will require that any project that involves | ||
| 99 | user data must follow all the rules of those laws so that the company | ||
| 100 | can protect itself from fines or other penalties. | ||
| 101 | |||
| 102 | Following this, companies will consider best practices in order to place | ||
| 103 | itself in a competitive position (e.g. Google vs. Apple) and review any | ||
| 104 | recent or ongoing litigation against companies regarding user data. In a | ||
| 105 | perfect company, management would then consider the ethical | ||
| 106 | responsibilities of their organization and discuss their | ||
| 107 | responsibilities over things like personally-identifiable information. | ||
| 108 | |||
| 109 | However, as we mentioned above, most companies follow the idea of | ||
| 110 | satisficing, which states that they have met the minimum acceptable | ||
| 111 | standards and can now move on to other decisions. Modern business | ||
| 112 | culture in the United States dictates that profits are the golden | ||
| 113 | measure of how well a company or manager is performing, so we often | ||
| 114 | don't think about our responsibilities beyond these basic standards. | ||
| 115 | |||
| 116 | Not all situations demand excellence, but I believe that applying any | ||
| 117 | philosophy as a broad stroke across one's life can be a mistake. We must | ||
| 118 | be able to think critically about what we are doing as we do it and ask | ||
| 119 | ourselves a few questions. Have I done everything I can in this | ||
| 120 | situation? Is mediocrity an acceptable outcome, or should we strive for | ||
| 121 | perfection, even if we can't attain it? | ||
| 122 | |||
| 123 | Taking a few moments to think critically throughout our day, as we make | ||
| 124 | decisions, can have a tremendous effect on the outcomes we create. | ||
content/blog/2020-12-27-website-redesign.md deleted −94
| @@ -1,94 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-12-27 | ||
| 3 | title = "Redesigning My Website: The 5 KB Result" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # A Brief History | ||
| 9 | |||
| 10 | As a form of continuous learning and entertainment, I've been running a handful | ||
| 11 | of websites since 2016 when I took my first programming courses in college. I | ||
| 12 | maintain one main website, the place I consider the official website to | ||
| 13 | represent me. Under this site, I have a handful of subdirectories and | ||
| 14 | subdomains. | ||
| 15 | |||
| 16 | One of the parts I've enjoyed the most about web development is the aspect of | ||
| 17 | designing an identity for a web page and working to find exciting ways to | ||
| 18 | display the site's content. Inevitably, this means I've changed the designs for | ||
| 19 | my websites more times than I could possibly count. Since I don't really host | ||
| 20 | anything on my main webpage that's vital, it allows me the freedom to change | ||
| 21 | things as inspiration strikes. | ||
| 22 | |||
| 23 | Historically, I've relied on core utilities for spacing, components, and layouts | ||
| 24 | from [Bootstrap](https://getbootstrap.com) and added custom CSS for fonts, | ||
| 25 | accents, colors, and other items. I also tend to create sites with no border | ||
| 26 | radius on items, visible borders, and content that takes up the entire screen | ||
| 27 | (using whitespace inside components instead of whitespace around my components). | ||
| 28 | |||
| 29 | # The Redesign Process | ||
| 30 | |||
| 31 | About a week ago, I found myself wishing for a new design yet again. The prior | ||
| 32 | design was largely inspired by IBM's [Carbon Design | ||
| 33 | System](https://www.carbondesignsystem.com) and relied on jQuery, Bootstrap, | ||
| 34 | along with some compressed [.webp](https://en.wikipedia.org/wiki/WebP) images. | ||
| 35 | |||
| 36 | To anyone who knows my preferences toward web design - and even in my personal | ||
| 37 | life - it should be no surprise that I immediately started looking for | ||
| 38 | inspiration on minimalism. While there are some decent minimalistic designs on | ||
| 39 | sites like | ||
| 40 | [Dribbble](https://dribbble.com/search/shots/popular/web-design?q=minimalism), | ||
| 41 | people seem to mostly discuss [brutalist web | ||
| 42 | design](https://brutalist-web.design) when you ask about minimalism. While | ||
| 43 | brutalist web design doesn't have to be minimal, it often is. | ||
| 44 | |||
| 45 | I suppose, in a way, I did create a brutalist website since my HTML is semantic | ||
| 46 | and accessible, hyperlinks are colored and underlined, and all native browser | ||
| 47 | functions like scrolling and the back button work as expected. However, I didn't | ||
| 48 | think about brutalism while designing these sites. | ||
| 49 | |||
| 50 | The new design followed a simple design process. I walked through the screens on | ||
| 51 | my blog and asked myself: "Is this element necessary for a user?" This allowed | ||
| 52 | me to first start by removing all javascript, which had the sole purpose of | ||
| 53 | allowing users to open a collapsed navbar on mobile. Replacing the collapsible | ||
| 54 | navbar allowed me to remove both jQuery and Bootstrap's javascript. | ||
| 55 | |||
| 56 | Next, I removed things like author names (since I'm literally the only person | ||
| 57 | who will ever write on this site), multiple ways to click on a blog post card, | ||
| 58 | blog post descriptions, and the scroll-to-top button. It also helped to move all | ||
| 59 | categories to a single page, rather than have each category on its own page. | ||
| 60 | |||
| 61 | The final big piece to finish the | ||
| 62 | "[KonMari](https://en.wikipedia.org/wiki/Marie_Kondo#KonMari_method)"-like part | ||
| 63 | of my process was to remove Bootstrap CSS in its entirety. However, this meant | ||
| 64 | pulling out a few very useful classes, such as `.img-fluid` and the default font | ||
| 65 | stacks to keep in my custom CSS. | ||
| 66 | |||
| 67 | After removing all the unnecessary pieces, I was finally able to reorganize my | ||
| 68 | content and add a very small amount of custom CSS to make everything pretty. | ||
| 69 | This took a brief amount of time, effectively just consisting of me converting | ||
| 70 | `<div>` tags into things like `<ul>` lists and choosing accent colors. | ||
| 71 | |||
| 72 | # The Results | ||
| 73 | |||
| 74 | ## Reflection | ||
| 75 | |||
| 76 | So, what did all of this reorganizing do to my webpages? Well, first, my | ||
| 77 | websites are now **ridiculously fast**. Since the prior designs were also | ||
| 78 | minimal and didn't have many images, they measured up in Firefox's Network | ||
| 79 | Monitor around 300 KB - 600KB. After making the changes, my main site is at 5 KB | ||
| 80 | transferred (22 KB total), and my blog is at 6.5 KB transferred (13 KB total). | ||
| 81 | **That means the redesigned pages are less than 2% the size of the old | ||
| 82 | designs.** | ||
| 83 | |||
| 84 | Google Lighthouse ranks the new webpage as 100 in performance, accessibility, | ||
| 85 | and best practices, with SEO at 92 since they think tap targets are not sized | ||
| 86 | appropriately for mobile users. First contextual paints of the pages are under | ||
| 87 | 0.8 seconds with 0 ms of blocking time. However, the blog subdomain ranks at 100 | ||
| 88 | for all four categories! First contextual paints of the blog homepage are under | ||
| 89 | 1.0 seconds with 0 ms of blocking time, due to the fact that the CSS for my blog | ||
| 90 | is within a separate CSS file, and the CSS for my main website is simply | ||
| 91 | embedded in the HTML file. | ||
| 92 | |||
| 93 | Now that everything is complete, I can confidently say I'm happy with the result | ||
| 94 | and proud to look at the fastest set of websites I've created so far. | ||
content/blog/2020-12-27-website-redesign.org added +100
| @@ -0,0 +1,100 @@ | |||
| 1 | #+date: <2020-12-27> | ||
| 2 | #+title: Redesigning My Website: The 5 KB Result | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * A Brief History | ||
| 7 | |||
| 8 | As a form of continuous learning and entertainment, I've been running a | ||
| 9 | handful of websites since 2016 when I took my first programming courses | ||
| 10 | in college. I maintain one main website, the place I consider the | ||
| 11 | official website to represent me. Under this site, I have a handful of | ||
| 12 | subdirectories and subdomains. | ||
| 13 | |||
| 14 | One of the parts I've enjoyed the most about web development is the | ||
| 15 | aspect of designing an identity for a web page and working to find | ||
| 16 | exciting ways to display the site's content. Inevitably, this means I've | ||
| 17 | changed the designs for my websites more times than I could possibly | ||
| 18 | count. Since I don't really host anything on my main webpage that's | ||
| 19 | vital, it allows me the freedom to change things as inspiration strikes. | ||
| 20 | |||
| 21 | Historically, I've relied on core utilities for spacing, components, and | ||
| 22 | layouts from [[https://getbootstrap.com][Bootstrap]] and added custom | ||
| 23 | CSS for fonts, accents, colors, and other items. I also tend to create | ||
| 24 | sites with no border radius on items, visible borders, and content that | ||
| 25 | takes up the entire screen (using whitespace inside components instead | ||
| 26 | of whitespace around my components). | ||
| 27 | |||
| 28 | * The Redesign Process | ||
| 29 | |||
| 30 | About a week ago, I found myself wishing for a new design yet again. The | ||
| 31 | prior design was largely inspired by IBM's | ||
| 32 | [[https://www.carbondesignsystem.com][Carbon Design System]] and relied | ||
| 33 | on jQuery, Bootstrap, along with some compressed | ||
| 34 | [[https://en.wikipedia.org/wiki/WebP][.webp]] images. | ||
| 35 | |||
| 36 | To anyone who knows my preferences toward web design - and even in my | ||
| 37 | personal life - it should be no surprise that I immediately started | ||
| 38 | looking for inspiration on minimalism. While there are some decent | ||
| 39 | minimalistic designs on sites like | ||
| 40 | [[https://dribbble.com/search/shots/popular/web-design?q=minimalism][Dribbble]], | ||
| 41 | people seem to mostly discuss [[https://brutalist-web.design][brutalist | ||
| 42 | web design]] when you ask about minimalism. While brutalist web design | ||
| 43 | doesn't have to be minimal, it often is. | ||
| 44 | |||
| 45 | I suppose, in a way, I did create a brutalist website since my HTML is | ||
| 46 | semantic and accessible, hyperlinks are colored and underlined, and all | ||
| 47 | native browser functions like scrolling and the back button work as | ||
| 48 | expected. However, I didn't think about brutalism while designing these | ||
| 49 | sites. | ||
| 50 | |||
| 51 | The new design followed a simple design process. I walked through the | ||
| 52 | screens on my blog and asked myself: "Is this element necessary for a | ||
| 53 | user?" This allowed me to first start by removing all javascript, which | ||
| 54 | had the sole purpose of allowing users to open a collapsed navbar on | ||
| 55 | mobile. Replacing the collapsible navbar allowed me to remove both | ||
| 56 | jQuery and Bootstrap's javascript. | ||
| 57 | |||
| 58 | Next, I removed things like author names (since I'm literally the only | ||
| 59 | person who will ever write on this site), multiple ways to click on a | ||
| 60 | blog post card, blog post descriptions, and the scroll-to-top button. It | ||
| 61 | also helped to move all categories to a single page, rather than have | ||
| 62 | each category on its own page. | ||
| 63 | |||
| 64 | The final big piece to finish the | ||
| 65 | "[[https://en.wikipedia.org/wiki/Marie_Kondo#KonMari_method][KonMari]]"-like | ||
| 66 | part of my process was to remove Bootstrap CSS in its entirety. However, | ||
| 67 | this meant pulling out a few very useful classes, such as =.img-fluid= | ||
| 68 | and the default font stacks to keep in my custom CSS. | ||
| 69 | |||
| 70 | After removing all the unnecessary pieces, I was finally able to | ||
| 71 | reorganize my content and add a very small amount of custom CSS to make | ||
| 72 | everything pretty. This took a brief amount of time, effectively just | ||
| 73 | consisting of me converting =<div>= tags into things like =<ul>= lists | ||
| 74 | and choosing accent colors. | ||
| 75 | |||
| 76 | * The Results | ||
| 77 | |||
| 78 | ** Reflection | ||
| 79 | |||
| 80 | So, what did all of this reorganizing do to my webpages? Well, first, my | ||
| 81 | websites are now *ridiculously fast*. Since the prior designs were also | ||
| 82 | minimal and didn't have many images, they measured up in Firefox's | ||
| 83 | Network Monitor around 300 KB - 600KB. After making the changes, my main | ||
| 84 | site is at 5 KB transferred (22 KB total), and my blog is at 6.5 KB | ||
| 85 | transferred (13 KB total). *That means the redesigned pages are less | ||
| 86 | than 2% the size of the old designs.* | ||
| 87 | |||
| 88 | Google Lighthouse ranks the new webpage as 100 in performance, | ||
| 89 | accessibility, and best practices, with SEO at 92 since they think tap | ||
| 90 | targets are not sized appropriately for mobile users. First contextual | ||
| 91 | paints of the pages are under 0.8 seconds with 0 ms of blocking time. | ||
| 92 | However, the blog subdomain ranks at 100 for all four categories! First | ||
| 93 | contextual paints of the blog homepage are under 1.0 seconds with 0 ms | ||
| 94 | of blocking time, due to the fact that the CSS for my blog is within a | ||
| 95 | separate CSS file, and the CSS for my main website is simply embedded in | ||
| 96 | the HTML file. | ||
| 97 | |||
| 98 | Now that everything is complete, I can confidently say I'm happy with | ||
| 99 | the result and proud to look at the fastest set of websites I've created | ||
| 100 | so far. | ||
content/blog/2020-12-28-neon-drive.md deleted −81
| @@ -1,81 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-12-28 | ||
| 3 | title = "Neon Drive: A Nostalgic 80s Arcade Racing Game" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Game Description | ||
| 9 | |||
| 10 | [Neon Drive](https://store.steampowered.com/app/433910/Neon_Drive/) presents | ||
| 11 | itself as a simple arcade-style game inspired by the arcade race games of the | ||
| 12 | 1980s, yet it has managed to take up hours of my life without much effort. The | ||
| 13 | game description, directly from the Steam page, is intriguing enough to entice | ||
| 14 | anyone who's been looking for a good arcade racing game: | ||
| 15 | |||
| 16 | > Neon Drive is a slick retro-futuristic arcade game that will make your brain | ||
| 17 | > melt. You've been warned. From beautiful cityscapes and ocean roads to | ||
| 18 | > exploding enemy spaceships, Neon Drive has it all. | ||
| 19 | |||
| 20 | # Gameplay | ||
| 21 | |||
| 22 | The game holds true to the | ||
| 23 | [retro-futurism](https://en.wikipedia.org/wiki/Retrofuturism) style, including | ||
| 24 | chrome female robots, pixelated arcade machines, and | ||
| 25 | [outrun](https://teddit.net/r/outrun/) aesthetics. | ||
| 26 | |||
| 27 | Each level of the game is shown as a separate arcade machine. Each arcade | ||
| 28 | machine lets you play on Normal, Hard, Insane, Practice, and Free Run. To beat | ||
| 29 | each arcade, you must reach the end of the level without crashing your car into | ||
| 30 | the various obstacles on the course. Basic levels let you move left or right to | ||
| 31 | avoid blocks in the road. Later levels put you through other tests, such as | ||
| 32 | dodging traffic or blasting asteroids. | ||
| 33 | |||
| 34 | The game uses synthwave music to keep you on track to make the correct moves by | ||
| 35 | timing the beats of the songs to the correct moves on the screen. It reminds me | ||
| 36 | of the early Guitar Hero games, as well as mobile apps like VOEZ - repetition | ||
| 37 | and staying on-beat is the only way to win. | ||
| 38 | |||
| 39 | # In-Game Screenshots | ||
| 40 | |||
| 41 | Taking a look at the main menu, you can see that Neon Drive plays into every | ||
| 42 | stereotype you can think of around retro-futuristic, synthwave arcades (in a | ||
| 43 | good way). | ||
| 44 | |||
| 45 | Once you get into the first level, we see that the choice of car fits right in | ||
| 46 | with the stereotypical cars of the 80s, like the | ||
| 47 | [DeLorean](https://en.wikipedia.org/wiki/DMC_DeLorean) or the [Ferrari | ||
| 48 | F40](https://en.wikipedia.org/wiki/Ferrari_F40). Each new level comes with new | ||
| 49 | color schemes and cars, so you should never get tired of the aesthetic. | ||
| 50 | |||
| 51 | Personally, I love the orange and blue colors used in level 2: | ||
| 52 | |||
| 53 | If you're the competitive type and getting 100% on all arcade machines isn't | ||
| 54 | enough, there are leaderboards for the regular part of the game, and the | ||
| 55 | endurance game mode. | ||
| 56 | |||
| 57 | # Other Suggestions | ||
| 58 | |||
| 59 | Neon Drive sits nicely within the well-founded cult genre of Outrun. Other games | ||
| 60 | that I've enjoyed in this same spectrum are: | ||
| 61 | |||
| 62 | - [Far Cry 3: Blood | ||
| 63 | Dragon](https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/) | ||
| 64 | - [Retrowave](https://store.steampowered.com/app/1239690/Retrowave/) | ||
| 65 | - [Slipstream](https://store.steampowered.com/app/732810/Slipstream/) | ||
| 66 | |||
| 67 | Although these games aren't necessarily in the same genre, they do have aspects | ||
| 68 | that place them close enough to interest gamers that enjoyed Neon Drive: | ||
| 69 | |||
| 70 | - [Black Ice](https://store.steampowered.com/app/311800/Black_Ice/) | ||
| 71 | - [Cloudpunk](https://store.steampowered.com/app/746850/Cloudpunk/) | ||
| 72 | - [Need for Speed: | ||
| 73 | Heat](https://store.steampowered.com/app/1222680/Need_for_Speed_Heat/) | ||
| 74 | - [VirtuaVerse](https://store.steampowered.com/app/1019310/VirtuaVerse/) | ||
| 75 | |||
| 76 | Of course, if all you really care about is the arcade aspect of these games, you | ||
| 77 | can check out the [Atari | ||
| 78 | Vault](https://store.steampowered.com/app/400020/Atari_Vault/) or any of the | ||
| 79 | other classic games sold on Steam by companies like Namco, Atari. For something | ||
| 80 | like Nintendo, you'd have to settle for buying used classic consoles or delve | ||
| 81 | into the world of emulation. | ||
content/blog/2020-12-28-neon-drive.org added +84
| @@ -0,0 +1,84 @@ | |||
| 1 | #+date: <2020-12-28> | ||
| 2 | #+title: Neon Drive: A Nostalgic 80s Arcade Racing Game | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Game Description | ||
| 7 | |||
| 8 | [[https://store.steampowered.com/app/433910/Neon_Drive/][Neon Drive]] | ||
| 9 | presents itself as a simple arcade-style game inspired by the arcade | ||
| 10 | race games of the 1980s, yet it has managed to take up hours of my life | ||
| 11 | without much effort. The game description, directly from the Steam page, | ||
| 12 | is intriguing enough to entice anyone who's been looking for a good | ||
| 13 | arcade racing game: | ||
| 14 | |||
| 15 | #+begin_quote | ||
| 16 | Neon Drive is a slick retro-futuristic arcade game that will make your | ||
| 17 | brain melt. You've been warned. From beautiful cityscapes and ocean | ||
| 18 | roads to exploding enemy spaceships, Neon Drive has it all. | ||
| 19 | #+end_quote | ||
| 20 | |||
| 21 | * Gameplay | ||
| 22 | |||
| 23 | The game holds true to the | ||
| 24 | [[https://en.wikipedia.org/wiki/Retrofuturism][retro-futurism]] style, | ||
| 25 | including chrome female robots, pixelated arcade machines, and | ||
| 26 | [[https://teddit.net/r/outrun/][outrun]] aesthetics. | ||
| 27 | |||
| 28 | Each level of the game is shown as a separate arcade machine. Each | ||
| 29 | arcade machine lets you play on Normal, Hard, Insane, Practice, and Free | ||
| 30 | Run. To beat each arcade, you must reach the end of the level without | ||
| 31 | crashing your car into the various obstacles on the course. Basic levels | ||
| 32 | let you move left or right to avoid blocks in the road. Later levels put | ||
| 33 | you through other tests, such as dodging traffic or blasting asteroids. | ||
| 34 | |||
| 35 | The game uses synthwave music to keep you on track to make the correct | ||
| 36 | moves by timing the beats of the songs to the correct moves on the | ||
| 37 | screen. It reminds me of the early Guitar Hero games, as well as mobile | ||
| 38 | apps like VOEZ - repetition and staying on-beat is the only way to win. | ||
| 39 | |||
| 40 | * In-Game Screenshots | ||
| 41 | |||
| 42 | Taking a look at the main menu, you can see that Neon Drive plays into | ||
| 43 | every stereotype you can think of around retro-futuristic, synthwave | ||
| 44 | arcades (in a good way). | ||
| 45 | |||
| 46 | Once you get into the first level, we see that the choice of car fits | ||
| 47 | right in with the stereotypical cars of the 80s, like the | ||
| 48 | [[https://en.wikipedia.org/wiki/DMC_DeLorean][DeLorean]] or the | ||
| 49 | [[https://en.wikipedia.org/wiki/Ferrari_F40][Ferrari F40]]. Each new | ||
| 50 | level comes with new color schemes and cars, so you should never get | ||
| 51 | tired of the aesthetic. | ||
| 52 | |||
| 53 | Personally, I love the orange and blue colors used in level 2: | ||
| 54 | |||
| 55 | If you're the competitive type and getting 100% on all arcade machines | ||
| 56 | isn't enough, there are leaderboards for the regular part of the game, | ||
| 57 | and the endurance game mode. | ||
| 58 | |||
| 59 | * Other Suggestions | ||
| 60 | |||
| 61 | Neon Drive sits nicely within the well-founded cult genre of Outrun. | ||
| 62 | Other games that I've enjoyed in this same spectrum are: | ||
| 63 | |||
| 64 | - [[https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/][Far | ||
| 65 | Cry 3: Blood Dragon]] | ||
| 66 | - [[https://store.steampowered.com/app/1239690/Retrowave/][Retrowave]] | ||
| 67 | - [[https://store.steampowered.com/app/732810/Slipstream/][Slipstream]] | ||
| 68 | |||
| 69 | Although these games aren't necessarily in the same genre, they do have | ||
| 70 | aspects that place them close enough to interest gamers that enjoyed | ||
| 71 | Neon Drive: | ||
| 72 | |||
| 73 | - [[https://store.steampowered.com/app/311800/Black_Ice/][Black Ice]] | ||
| 74 | - [[https://store.steampowered.com/app/746850/Cloudpunk/][Cloudpunk]] | ||
| 75 | - [[https://store.steampowered.com/app/1222680/Need_for_Speed_Heat/][Need | ||
| 76 | for Speed: Heat]] | ||
| 77 | - [[https://store.steampowered.com/app/1019310/VirtuaVerse/][VirtuaVerse]] | ||
| 78 | |||
| 79 | Of course, if all you really care about is the arcade aspect of these | ||
| 80 | games, you can check out the | ||
| 81 | [[https://store.steampowered.com/app/400020/Atari_Vault/][Atari Vault]] | ||
| 82 | or any of the other classic games sold on Steam by companies like Namco, | ||
| 83 | Atari. For something like Nintendo, you'd have to settle for buying used | ||
| 84 | classic consoles or delve into the world of emulation. | ||
content/blog/2020-12-29-zork.md deleted −80
| @@ -1,80 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2020-12-29 | ||
| 3 | title = "Zork: Let's Explore a Classic" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Download (Free) | ||
| 9 | |||
| 10 | Before we get into the game itself, you should know that you can download Zork | ||
| 11 | for free from Infocom's [download | ||
| 12 | page](http://infocom-if.org/downloads/downloads.html). So feel free to boot it | ||
| 13 | up and take a ride back to the 1980s with this masterpiece. | ||
| 14 | |||
| 15 | # Game Description | ||
| 16 | |||
| 17 | Zork is an interactive, text-based computer game originally released in 1980. | ||
| 18 | This series, split into three separate games, introduced a robust and | ||
| 19 | sophisticated text parser to gamers. People were largely used to the simple | ||
| 20 | commands used in the popular game [Colossal Cave | ||
| 21 | Adventure](https://en.wikipedia.org/wiki/Colossal_Cave_Adventure), but Zork | ||
| 22 | allowed users to send more complex commands that included prepositions and | ||
| 23 | conjunctions. | ||
| 24 | |||
| 25 | Zork tracks your score as you explore the map, find tools, and collect trophy | ||
| 26 | items (e.g., a jewel-encrusted egg). When you place your trophy items in the | ||
| 27 | trophy case found in the Living Room area, you gain score points. Collecting the | ||
| 28 | Twenty Treasures of Zork and placing them within the trophy case wins the game. | ||
| 29 | However, you must explore the map, solve puzzles, and avoid being eaten by a | ||
| 30 | grue to collect these treasures. | ||
| 31 | |||
| 32 | # The Map | ||
| 33 | |||
| 34 | Since Zork is a vast and complex game, it helps to have a map as you explore and | ||
| 35 | collect your trophies. However, if you want to play the game as it was truly | ||
| 36 | intended, you should try to play it without using the map. | ||
| 37 | |||
| 38 |  | ||
| 39 | |||
| 40 | _[Map Source](https://www.filfre.net/2012/01/exploring-zork-part-1/)_ | ||
| 41 | |||
| 42 | # In-Game Screenshots | ||
| 43 | |||
| 44 | After playing the game (for the first time ever) for several weeks around 2014, | ||
| 45 | I was finally able to beat the game with some online help to find the last | ||
| 46 | couple items. As I was writing this post, I installed the game again to grab | ||
| 47 | some screenshots to show off the true glory of this game. As noted in [Jimmy | ||
| 48 | Maher's playthrough](https://www.filfre.net/2012/01/exploring-zork-part-1/), the | ||
| 49 | original Zork games looked quite a bit different due to the older hardware of | ||
| 50 | computers like the Apple II and multiple bug fixes that Infocom pushed out after | ||
| 51 | the game's initial release. My play-through uses the [Zork | ||
| 52 | Anthology](https://store.steampowered.com/app/570580/Zork_Anthology/) version, | ||
| 53 | which utilizes DOSBox on Windows. | ||
| 54 | |||
| 55 | The first screenshot here shows the introductory information, which doesn't | ||
| 56 | include instructions of any kind for the player. If you haven't played text | ||
| 57 | adventures before, try to use simple commands like "go west," "look around," or | ||
| 58 | "hit troll with elvish sword." | ||
| 59 | |||
| 60 | In this second screenshot, we see the player has entered the house and found the | ||
| 61 | trophy case in the living room. The lantern and sword in this room allow the | ||
| 62 | player to explore dark areas and attack enemies. If you don't use the lantern, | ||
| 63 | you won't be able to see anything in dark areas, and you may be eaten by a grue. | ||
| 64 | |||
| 65 | Finally, we see that the player has found the first treasure: a jewel-encrusted | ||
| 66 | egg. These treasures can be taken back to the house and placed in the trophy | ||
| 67 | case or carried until you feel like you want to put things away. | ||
| 68 | |||
| 69 | # Conclusion | ||
| 70 | |||
| 71 | It's been quite a few years since I first played Zork, but I clearly remember | ||
| 72 | the late nights and bloodshot eyes that helped me find all the treasures. This | ||
| 73 | game is well worth the time and effort, even though the text-based aspect may be | ||
| 74 | off-putting to gamers who didn't have to grow up playing games without graphics. | ||
| 75 | However, I believe that the strategy and skills learned in early video games | ||
| 76 | like Zork can actually help you, even when playing newer games. | ||
| 77 | |||
| 78 | If you do decide to play Zork, you can download Zork I, II, and III from | ||
| 79 | Infocom's [download page](http://infocom-if.org/downloads/downloads.html) for | ||
| 80 | free or search the internet for an online version. | ||
content/blog/2020-12-29-zork.org added +86
| @@ -0,0 +1,86 @@ | |||
| 1 | #+date: <2020-12-29> | ||
| 2 | #+title: Zork: Let's Explore a Classic | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Download (Free) | ||
| 7 | |||
| 8 | Before we get into the game itself, you should know that you can | ||
| 9 | download Zork for free from Infocom's | ||
| 10 | [[http://infocom-if.org/downloads/downloads.html][download page]]. So | ||
| 11 | feel free to boot it up and take a ride back to the 1980s with this | ||
| 12 | masterpiece. | ||
| 13 | |||
| 14 | * Game Description | ||
| 15 | |||
| 16 | Zork is an interactive, text-based computer game originally released | ||
| 17 | in 1980. This series, split into three separate games, introduced a | ||
| 18 | robust and sophisticated text parser to gamers. People were largely used | ||
| 19 | to the simple commands used in the popular game | ||
| 20 | [[https://en.wikipedia.org/wiki/Colossal_Cave_Adventure][Colossal Cave | ||
| 21 | Adventure]], but Zork allowed users to send more complex commands that | ||
| 22 | included prepositions and conjunctions. | ||
| 23 | |||
| 24 | Zork tracks your score as you explore the map, find tools, and collect | ||
| 25 | trophy items (e.g., a jewel-encrusted egg). When you place your trophy | ||
| 26 | items in the trophy case found in the Living Room area, you gain score | ||
| 27 | points. Collecting the Twenty Treasures of Zork and placing them within | ||
| 28 | the trophy case wins the game. However, you must explore the map, solve | ||
| 29 | puzzles, and avoid being eaten by a grue to collect these treasures. | ||
| 30 | |||
| 31 | * The Map | ||
| 32 | |||
| 33 | Since Zork is a vast and complex game, it helps to have a map as you | ||
| 34 | explore and collect your trophies. However, if you want to play the game | ||
| 35 | as it was truly intended, you should try to play it without using the | ||
| 36 | map. | ||
| 37 | |||
| 38 | #+caption: Zork Map | ||
| 39 | [[https://img.cleberg.net/blog/20201229-zork/zork_map.jpg]] | ||
| 40 | |||
| 41 | /[[https://www.filfre.net/2012/01/exploring-zork-part-1/][Map Source]]/ | ||
| 42 | |||
| 43 | * In-Game Screenshots | ||
| 44 | |||
| 45 | After playing the game (for the first time ever) for several weeks | ||
| 46 | around 2014, I was finally able to beat the game with some online help | ||
| 47 | to find the last couple items. As I was writing this post, I installed | ||
| 48 | the game again to grab some screenshots to show off the true glory of | ||
| 49 | this game. As noted in | ||
| 50 | [[https://www.filfre.net/2012/01/exploring-zork-part-1/][Jimmy Maher's | ||
| 51 | playthrough]], the original Zork games looked quite a bit different due | ||
| 52 | to the older hardware of computers like the Apple II and multiple bug | ||
| 53 | fixes that Infocom pushed out after the game's initial release. My | ||
| 54 | play-through uses the | ||
| 55 | [[https://store.steampowered.com/app/570580/Zork_Anthology/][Zork | ||
| 56 | Anthology]] version, which utilizes DOSBox on Windows. | ||
| 57 | |||
| 58 | The first screenshot here shows the introductory information, which | ||
| 59 | doesn't include instructions of any kind for the player. If you haven't | ||
| 60 | played text adventures before, try to use simple commands like "go | ||
| 61 | west," "look around," or "hit troll with elvish sword." | ||
| 62 | |||
| 63 | In this second screenshot, we see the player has entered the house and | ||
| 64 | found the trophy case in the living room. The lantern and sword in this | ||
| 65 | room allow the player to explore dark areas and attack enemies. If you | ||
| 66 | don't use the lantern, you won't be able to see anything in dark areas, | ||
| 67 | and you may be eaten by a grue. | ||
| 68 | |||
| 69 | Finally, we see that the player has found the first treasure: a | ||
| 70 | jewel-encrusted egg. These treasures can be taken back to the house and | ||
| 71 | placed in the trophy case or carried until you feel like you want to put | ||
| 72 | things away. | ||
| 73 | |||
| 74 | * Conclusion | ||
| 75 | |||
| 76 | It's been quite a few years since I first played Zork, but I clearly | ||
| 77 | remember the late nights and bloodshot eyes that helped me find all the | ||
| 78 | treasures. This game is well worth the time and effort, even though the | ||
| 79 | text-based aspect may be off-putting to gamers who didn't have to grow | ||
| 80 | up playing games without graphics. However, I believe that the strategy | ||
| 81 | and skills learned in early video games like Zork can actually help you, | ||
| 82 | even when playing newer games. | ||
| 83 | |||
| 84 | If you do decide to play Zork, you can download Zork I, II, and III from | ||
| 85 | Infocom's [[http://infocom-if.org/downloads/downloads.html][download | ||
| 86 | page]] for free or search the internet for an online version. | ||
content/blog/2021-01-01-seum.md deleted −71
| @@ -1,71 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-01-01 | ||
| 3 | title = "SEUM: Spreedrunners from Hell" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Game Description | ||
| 9 | |||
| 10 | [SEUM: Speedrunners from | ||
| 11 | Hell](https://store.steampowered.com/app/457210/SEUM_Speedrunners_from_Hell/) is | ||
| 12 | an incredibly fast-paced mashup of a puzzle game and a free-running game. | ||
| 13 | Created by [Pine Studio](https://pinestudio.co) in early 2016 and first released | ||
| 14 | as a [demo on | ||
| 15 | GameJolt](https://gamejolt.com/games/seum-speedrunners-from-hell-demo/154868), | ||
| 16 | this game was successfully green-lit on Steam and has amassed a cult following | ||
| 17 | on multiple consoles. | ||
| 18 | |||
| 19 | Here's the game description straight from the developers: | ||
| 20 | |||
| 21 | > Like a bastard child of Quake 3 and Super Meat Boy, SEUM: Speedrunners from | ||
| 22 | > Hell is truly hardcore and focuses on speed and fast reaction. | ||
| 23 | |||
| 24 | # Story | ||
| 25 | |||
| 26 | SEUM does a great job setting the stage when you open the game for the first | ||
| 27 | time, playing an animated video in the form of comic book images. You see Marty, | ||
| 28 | the protagonist, sitting around drinking his beer as Satan busts down the door, | ||
| 29 | cuts his hand off, and steals all his beer and bacon. As Satan is leaving, Marty | ||
| 30 | whips a vinyl record across the room and cuts off one of Satan's hands. This | ||
| 31 | hand is what allows you to use all the powers in the game. | ||
| 32 | |||
| 33 | # Gameplay | ||
| 34 | |||
| 35 | To accomplish each level, you will need to get to the final blue portal under a | ||
| 36 | certain time limit. You can beat a level by getting to the portal before the | ||
| 37 | time meter runs out or "Dominate" a level by beating it within a shorter time | ||
| 38 | limit (noted by a bright red/orange color in the time meter). | ||
| 39 | |||
| 40 | The portal is usually set behind locked gates, protected by dangerous defenses, | ||
| 41 | or in hard-to-reach places. To reach the portal, you'll need to go through any | ||
| 42 | existing orange portals, light all yellow beacons, avoid things like fireballs | ||
| 43 | and blades, or use any satanic power orbs lying around. These special abilities | ||
| 44 | include: | ||
| 45 | |||
| 46 | - Gravity | ||
| 47 | - Teleport | ||
| 48 | - Rewind | ||
| 49 | - Spawn platform | ||
| 50 | - Roar (DLC) | ||
| 51 | - Rocket (DLC) | ||
| 52 | - Shadow world (DLC) | ||
| 53 | |||
| 54 | For the main storyline, there are nine floors to beat. Each floor contains nine | ||
| 55 | regular levels, one boss level, and one bonus level; although you don't | ||
| 56 | technically need to beat all levels to advance to the next floor. | ||
| 57 | # In-Game Screenshots | ||
| 58 | |||
| 59 | The main menu gives you plenty of options for tuning your system, playing main | ||
| 60 | levels, playing the DLC, or exploring the other game modes. | ||
| 61 | |||
| 62 | Once you enter a level, you can look around and strategize before starting. | ||
| 63 | Clicking any button will start the menu, and you'll have to restart if you die. | ||
| 64 | One of the great things about SEUM is that it has great keyboard shortcuts. You | ||
| 65 | can quickly smash `R` to restart the level or `M` to return to the level menu. | ||
| 66 | |||
| 67 | Once you're able to win a level, you'll see the high scores and can watch | ||
| 68 | replays of the best scores. | ||
| 69 | |||
| 70 | Each regular level also contains a beer in a disguised location that may take | ||
| 71 | some out-of-the-box thinking. | ||
content/blog/2021-01-01-seum.org added +73
| @@ -0,0 +1,73 @@ | |||
| 1 | #+date: <2021-01-01> | ||
| 2 | #+title: SEUM: Spreedrunners from Hell | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Game Description | ||
| 7 | |||
| 8 | [[https://store.steampowered.com/app/457210/SEUM_Speedrunners_from_Hell/][SEUM: | ||
| 9 | Speedrunners from Hell]] is an incredibly fast-paced mashup of a puzzle | ||
| 10 | game and a free-running game. Created by [[https://pinestudio.co][Pine | ||
| 11 | Studio]] in early 2016 and first released as a | ||
| 12 | [[https://gamejolt.com/games/seum-speedrunners-from-hell-demo/154868][demo | ||
| 13 | on GameJolt]], this game was successfully green-lit on Steam and has | ||
| 14 | amassed a cult following on multiple consoles. | ||
| 15 | |||
| 16 | Here's the game description straight from the developers: | ||
| 17 | |||
| 18 | #+begin_quote | ||
| 19 | Like a bastard child of Quake 3 and Super Meat Boy, SEUM: Speedrunners | ||
| 20 | from Hell is truly hardcore and focuses on speed and fast reaction. | ||
| 21 | #+end_quote | ||
| 22 | |||
| 23 | * Story | ||
| 24 | |||
| 25 | SEUM does a great job setting the stage when you open the game for the | ||
| 26 | first time, playing an animated video in the form of comic book images. | ||
| 27 | You see Marty, the protagonist, sitting around drinking his beer as | ||
| 28 | Satan busts down the door, cuts his hand off, and steals all his beer | ||
| 29 | and bacon. As Satan is leaving, Marty whips a vinyl record across the | ||
| 30 | room and cuts off one of Satan's hands. This hand is what allows you to | ||
| 31 | use all the powers in the game. | ||
| 32 | |||
| 33 | * Gameplay | ||
| 34 | |||
| 35 | To accomplish each level, you will need to get to the final blue portal | ||
| 36 | under a certain time limit. You can beat a level by getting to the | ||
| 37 | portal before the time meter runs out or "Dominate" a level by beating | ||
| 38 | it within a shorter time limit (noted by a bright red/orange color in | ||
| 39 | the time meter). | ||
| 40 | |||
| 41 | The portal is usually set behind locked gates, protected by dangerous | ||
| 42 | defenses, or in hard-to-reach places. To reach the portal, you'll need | ||
| 43 | to go through any existing orange portals, light all yellow beacons, | ||
| 44 | avoid things like fireballs and blades, or use any satanic power orbs | ||
| 45 | lying around. These special abilities include: | ||
| 46 | |||
| 47 | - Gravity | ||
| 48 | - Teleport | ||
| 49 | - Rewind | ||
| 50 | - Spawn platform | ||
| 51 | - Roar (DLC) | ||
| 52 | - Rocket (DLC) | ||
| 53 | - Shadow world (DLC) | ||
| 54 | |||
| 55 | For the main storyline, there are nine floors to beat. Each floor | ||
| 56 | contains nine regular levels, one boss level, and one bonus level; | ||
| 57 | although you don't technically need to beat all levels to advance to the | ||
| 58 | next floor. # In-Game Screenshots | ||
| 59 | |||
| 60 | The main menu gives you plenty of options for tuning your system, | ||
| 61 | playing main levels, playing the DLC, or exploring the other game modes. | ||
| 62 | |||
| 63 | Once you enter a level, you can look around and strategize before | ||
| 64 | starting. Clicking any button will start the menu, and you'll have to | ||
| 65 | restart if you die. One of the great things about SEUM is that it has | ||
| 66 | great keyboard shortcuts. You can quickly smash =R= to restart the level | ||
| 67 | or =M= to return to the level menu. | ||
| 68 | |||
| 69 | Once you're able to win a level, you'll see the high scores and can | ||
| 70 | watch replays of the best scores. | ||
| 71 | |||
| 72 | Each regular level also contains a beer in a disguised location that may | ||
| 73 | take some out-of-the-box thinking. | ||
content/blog/2021-01-04-fediverse.md deleted −100
| @@ -1,100 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-01-04 | ||
| 3 | title = "A Simple Guide to the Fediverse" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is the Fediverse? | ||
| 9 | |||
| 10 | The fediverse is a federated universe of servers commonly used for sharing | ||
| 11 | content, like social media. So, instead of having to rely on a single | ||
| 12 | organization to run the server (e.g. Facebook), the fediverse is a giant | ||
| 13 | collection of servers across the world, owned by many people and organizations. | ||
| 14 | |||
| 15 | Take a look at this depiction of a federated network. Each server in this photo | ||
| 16 | is owned and run by different administrators/owners. Federated networks are best | ||
| 17 | explained as email servers: you have an email account that exists on a server | ||
| 18 | (e.g. Outlook), your friend has an account on a different server (e.g. GMail), | ||
| 19 | and another friend has an account on a third server (e.g. ProtonMail). All three | ||
| 20 | of you can talk and communicate back and forth without having to be on the same | ||
| 21 | server. However, responsible email admins are there to set rules and control the | ||
| 22 | traffic going in/out of the server. | ||
| 23 | |||
| 24 | The main objective of this architecture is to decentralize the control within | ||
| 25 | the internet connections. For example, if you run your own Mastodon instance, | ||
| 26 | you and your users can't be censored or impacted in any way by authorities of | ||
| 27 | another Mastodon instance. Some users have praised these features due to recent | ||
| 28 | criticism of popular social media websites that may be over-censoring their | ||
| 29 | users. | ||
| 30 | |||
| 31 | This strategy is great for making sure control of the social web isn't | ||
| 32 | controlled by a single organization, but it also has some downsides. If I create | ||
| 33 | a Mastodon instance and get a ton of users to sign up, I can shut the server | ||
| 34 | down at any time. That means you're at risk of losing the content you've created | ||
| 35 | unless you back it up, or the server backs it up for you. Also, depending on the | ||
| 36 | software used (e.g. Mastodon, Pixelfed, etc.), censorship may still be an issue | ||
| 37 | if the server admins decide they want to censor their users. Now, censorship | ||
| 38 | isn't always a bad thing and can even benefit the community as a whole, but | ||
| 39 | you'll want to determine which servers align with your idea of proper | ||
| 40 | censorship. | ||
| 41 | |||
| 42 | However, these are risks that we take when we sign up for any online platform. | ||
| 43 | Whatever your reason is for trying out federated social networks, they are part | ||
| 44 | of the future of the internet. However, the popularity of these services is | ||
| 45 | still yet to be determined, especially with the increased difficulty | ||
| 46 | understanding and signing up for these platforms. Perhaps increased regulation | ||
| 47 | and litigation against current social media sites will push more users into the | ||
| 48 | fediverse. | ||
| 49 | |||
| 50 | # Federated Alternatives to Popular Sites | ||
| 51 | |||
| 52 | The list below is a small guide that will show you federated alternatives to | ||
| 53 | current popular websites. There are many more out there, so go and explore: you | ||
| 54 | might just find the perfect home. | ||
| 55 | |||
| 56 | |||
| 57 | |||
| 58 | - [Lemmy](https://lemmy.ml/instances) | ||
| 59 | |||
| 60 | ## Twitter/Facebook/Tumblr | ||
| 61 | |||
| 62 | - [Mastodon](https://joinmastodon.org) | ||
| 63 | - [Diaspora](https://diasporafoundation.org) | ||
| 64 | - [Friendica](https://friendi.ca) | ||
| 65 | - [GNU Social](https://gnusocial.network) | ||
| 66 | - [Pleroma](https://pleroma.social) | ||
| 67 | |||
| 68 | |||
| 69 | |||
| 70 | - [Pixelfed](https://pixelfed.org) | ||
| 71 | |||
| 72 | ## Slack/Discord | ||
| 73 | |||
| 74 | - [Matrix](https://element.io) | ||
| 75 | |||
| 76 | ## Youtube/Vimeo | ||
| 77 | |||
| 78 | - [Peertube](https://joinpeertube.org) | ||
| 79 | |||
| 80 | ## Spotify/Soundcloud | ||
| 81 | |||
| 82 | - [Funkwhale](https://funkwhale.audio) | ||
| 83 | |||
| 84 | ## Podcasting | ||
| 85 | |||
| 86 | - [Pubcast](https://pubcast.pub) | ||
| 87 | |||
| 88 | ## Medium/Blogger | ||
| 89 | |||
| 90 | - [WriteFreely](https://writefreely.org) | ||
| 91 | |||
| 92 | # Get Started | ||
| 93 | |||
| 94 | The best way to get started is to simply sign up and learn as you go. If you're | ||
| 95 | comfortable signing up through a Mastodon, Pleroma, or Friendica server, here is | ||
| 96 | [a list of themed servers](https://fediverse.party/en/portal/servers) to choose | ||
| 97 | from. If you're looking for something else, try a web search for a federated | ||
| 98 | alternative to your favorite sites. | ||
| 99 | |||
| 100 | Find a server that focuses on your passions and start there! | ||
content/blog/2021-01-04-fediverse.org added +100
| @@ -0,0 +1,100 @@ | |||
| 1 | #+date: <2021-01-04> | ||
| 2 | #+title: A Simple Guide to the Fediverse | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is the Fediverse? | ||
| 7 | |||
| 8 | The fediverse is a federated universe of servers commonly used for | ||
| 9 | sharing content, like social media. So, instead of having to rely on a | ||
| 10 | single organization to run the server (e.g. Facebook), the fediverse is | ||
| 11 | a giant collection of servers across the world, owned by many people and | ||
| 12 | organizations. | ||
| 13 | |||
| 14 | Take a look at this depiction of a federated network. Each server in | ||
| 15 | this photo is owned and run by different administrators/owners. | ||
| 16 | Federated networks are best explained as email servers: you have an | ||
| 17 | email account that exists on a server (e.g. Outlook), your friend has an | ||
| 18 | account on a different server (e.g. GMail), and another friend has an | ||
| 19 | account on a third server (e.g. ProtonMail). All three of you can talk | ||
| 20 | and communicate back and forth without having to be on the same server. | ||
| 21 | However, responsible email admins are there to set rules and control the | ||
| 22 | traffic going in/out of the server. | ||
| 23 | |||
| 24 | The main objective of this architecture is to decentralize the control | ||
| 25 | within the internet connections. For example, if you run your own | ||
| 26 | Mastodon instance, you and your users can't be censored or impacted in | ||
| 27 | any way by authorities of another Mastodon instance. Some users have | ||
| 28 | praised these features due to recent criticism of popular social media | ||
| 29 | websites that may be over-censoring their users. | ||
| 30 | |||
| 31 | This strategy is great for making sure control of the social web isn't | ||
| 32 | controlled by a single organization, but it also has some downsides. If | ||
| 33 | I create a Mastodon instance and get a ton of users to sign up, I can | ||
| 34 | shut the server down at any time. That means you're at risk of losing | ||
| 35 | the content you've created unless you back it up, or the server backs it | ||
| 36 | up for you. Also, depending on the software used (e.g. Mastodon, | ||
| 37 | Pixelfed, etc.), censorship may still be an issue if the server admins | ||
| 38 | decide they want to censor their users. Now, censorship isn't always a | ||
| 39 | bad thing and can even benefit the community as a whole, but you'll want | ||
| 40 | to determine which servers align with your idea of proper censorship. | ||
| 41 | |||
| 42 | However, these are risks that we take when we sign up for any online | ||
| 43 | platform. Whatever your reason is for trying out federated social | ||
| 44 | networks, they are part of the future of the internet. However, the | ||
| 45 | popularity of these services is still yet to be determined, especially | ||
| 46 | with the increased difficulty understanding and signing up for these | ||
| 47 | platforms. Perhaps increased regulation and litigation against current | ||
| 48 | social media sites will push more users into the fediverse. | ||
| 49 | |||
| 50 | * Federated Alternatives to Popular Sites | ||
| 51 | |||
| 52 | The list below is a small guide that will show you federated | ||
| 53 | alternatives to current popular websites. There are many more out there, | ||
| 54 | so go and explore: you might just find the perfect home. | ||
| 55 | |||
| 56 | |||
| 57 | |||
| 58 | - [[https://lemmy.ml/instances][Lemmy]] | ||
| 59 | |||
| 60 | ** Twitter/Facebook/Tumblr | ||
| 61 | |||
| 62 | - [[https://joinmastodon.org][Mastodon]] | ||
| 63 | - [[https://diasporafoundation.org][Diaspora]] | ||
| 64 | - [[https://friendi.ca][Friendica]] | ||
| 65 | - [[https://gnusocial.network][GNU Social]] | ||
| 66 | - [[https://pleroma.social][Pleroma]] | ||
| 67 | |||
| 68 | |||
| 69 | |||
| 70 | - [[https://pixelfed.org][Pixelfed]] | ||
| 71 | |||
| 72 | ** Slack/Discord | ||
| 73 | |||
| 74 | - [[https://element.io][Matrix]] | ||
| 75 | |||
| 76 | ** Youtube/Vimeo | ||
| 77 | |||
| 78 | - [[https://joinpeertube.org][Peertube]] | ||
| 79 | |||
| 80 | ** Spotify/Soundcloud | ||
| 81 | |||
| 82 | - [[https://funkwhale.audio][Funkwhale]] | ||
| 83 | |||
| 84 | ** Podcasting | ||
| 85 | |||
| 86 | - [[https://pubcast.pub][Pubcast]] | ||
| 87 | |||
| 88 | ** Medium/Blogger | ||
| 89 | |||
| 90 | - [[https://writefreely.org][WriteFreely]] | ||
| 91 | |||
| 92 | * Get Started | ||
| 93 | |||
| 94 | The best way to get started is to simply sign up and learn as you go. If | ||
| 95 | you're comfortable signing up through a Mastodon, Pleroma, or Friendica | ||
| 96 | server, here is [[https://fediverse.party/en/portal/servers][a list of | ||
| 97 | themed servers]] to choose from. If you're looking for something else, | ||
| 98 | try a web search for a federated alternative to your favorite sites. | ||
| 99 | |||
| 100 | Find a server that focuses on your passions and start there! | ||
content/blog/2021-01-07-ufw.md deleted −217
| @@ -1,217 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-01-07 | ||
| 3 | title = "Secure Your Network with the Uncomplicated Firewall (ufw)" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Uncomplicated Firewall | ||
| 9 | |||
| 10 | Uncomplicated Firewall, also known as ufw, is a convenient and beginner-friendly | ||
| 11 | way to enforce OS-level firewall rules. For those who are hosting servers or any | ||
| 12 | device that is accessible to the world (i.e., by public IP or domain name), it's | ||
| 13 | critical that a firewall is properly implemented and active. | ||
| 14 | |||
| 15 | Ufw is available by default in all Ubuntu installations after 8.04 LTS. For | ||
| 16 | other distributions, you can look to install ufw or check if there are | ||
| 17 | alternative firewalls installed already. There are usually alternatives | ||
| 18 | available, such as Fedora's `firewall` and the package available on most | ||
| 19 | distributions: `iptables`. Ufw is considered a beginner-friendly front-end to | ||
| 20 | iptables. | ||
| 21 | |||
| 22 | [Gufw](https://gufw.org) is available as a graphical user interface (GUI) | ||
| 23 | application for users who are uncomfortable setting up a firewall through a | ||
| 24 | terminal. | ||
| 25 | |||
| 26 | # Getting Help | ||
| 27 | |||
| 28 | If you need help figuring out commands, remember that you can run the `--help` | ||
| 29 | flag to get a list of options. | ||
| 30 | |||
| 31 | ```sh | ||
| 32 | sudo ufw --help | ||
| 33 | ``` | ||
| 34 | |||
| 35 | # Set Default State | ||
| 36 | |||
| 37 | The proper way to run a firewall is to set a strict default state and slowly | ||
| 38 | open up ports that you want to allow. This helps prevent anything malicious from | ||
| 39 | slipping through the cracks. The following command prevents all incoming traffic | ||
| 40 | (other than the rules we specify later), but you can also set this for outgoing | ||
| 41 | connections, if necessary. | ||
| 42 | |||
| 43 | ```sh | ||
| 44 | sudo ufw default deny incoming | ||
| 45 | ``` | ||
| 46 | |||
| 47 | You should also allow outgoing traffic if you want to allow the device to | ||
| 48 | communicate back to you or other parties. For example, media servers like Plex | ||
| 49 | need to be able to send out data related to streaming the media. | ||
| 50 | |||
| 51 | ```sh | ||
| 52 | sudo ufw default allow outgoing | ||
| 53 | ``` | ||
| 54 | |||
| 55 | # Adding Port Rules | ||
| 56 | |||
| 57 | Now that we've disabled all incoming traffic by default, we need to open up some | ||
| 58 | ports (or else no traffic would be able to come in). If you need to be able to | ||
| 59 | `ssh` into the machine, you'll need to open up port 22. | ||
| 60 | |||
| 61 | ```sh | ||
| 62 | sudo ufw allow 22 | ||
| 63 | ``` | ||
| 64 | |||
| 65 | You can also issue more restrictive rules. The following rule will allow `ssh` | ||
| 66 | connections only from machines on the local subnet. | ||
| 67 | |||
| 68 | ```sh | ||
| 69 | sudo ufw allow proto tcp from 192.168.0.0/24 to any port 22 | ||
| 70 | ``` | ||
| 71 | |||
| 72 | If you need to set a rule that isn't tcp, just append your connection type to | ||
| 73 | the end of the rule. | ||
| 74 | |||
| 75 | ```sh | ||
| 76 | sudo ufw allow 1900/udp | ||
| 77 | ``` | ||
| 78 | |||
| 79 | # Enable ufw | ||
| 80 | |||
| 81 | Now that the firewall is configured and ready to go, you can enable the | ||
| 82 | firewall. | ||
| 83 | |||
| 84 | ```sh | ||
| 85 | sudo ufw enable | ||
| 86 | ``` | ||
| 87 | |||
| 88 | A restart may be required for the firewall to begin operating. | ||
| 89 | |||
| 90 | ```sh | ||
| 91 | sudo reboot now | ||
| 92 | ``` | ||
| 93 | |||
| 94 | # Checking Status | ||
| 95 | |||
| 96 | Now that the firewall is enabled, let's check and see what the rules look like. | ||
| 97 | |||
| 98 | ```sh | ||
| 99 | sudo ufw status numbered | ||
| 100 | ``` | ||
| 101 | |||
| 102 | ```txt | ||
| 103 | Status: active | ||
| 104 | |||
| 105 | To Action From | ||
| 106 | -- ------ ---- | ||
| 107 | [ 1] 22 ALLOW IN Anywhere | ||
| 108 | [ 2] 22 (v6) ALLOW IN Anywhere (v6) | ||
| 109 | ``` | ||
| 110 | |||
| 111 | # Deleting Rules | ||
| 112 | |||
| 113 | If you need to delete a rule, you need to know the number associated with that | ||
| 114 | rule. Let's delete the first rule in the table above. You'll be asked to confirm | ||
| 115 | the deletion as part of this process. | ||
| 116 | |||
| 117 | ```sh | ||
| 118 | sudo ufw delete 1 | ||
| 119 | ``` | ||
| 120 | |||
| 121 | # Managing App Rules | ||
| 122 | |||
| 123 | Luckily, there's a convenient way for installed applications to create files | ||
| 124 | that ufw can easily implement so that you don't have to search and find which | ||
| 125 | ports your application requires. To see if your device has any applications with | ||
| 126 | pre-installed ufw rules, execute the following command: | ||
| 127 | |||
| 128 | ```sh | ||
| 129 | sudo ufw app list | ||
| 130 | ``` | ||
| 131 | |||
| 132 | The results should look something like this: | ||
| 133 | |||
| 134 | ```txt | ||
| 135 | Available applications: | ||
| 136 | OpenSSH | ||
| 137 | Samba | ||
| 138 | plexmediaserver | ||
| 139 | plexmediaserver-all | ||
| 140 | plexmediaserver-dlna | ||
| 141 | ``` | ||
| 142 | |||
| 143 | If you want to get more information on a specific app rule, use the `info` | ||
| 144 | command. | ||
| 145 | |||
| 146 | ```sh | ||
| 147 | sudo ufw app info plexmediaserver-dlna | ||
| 148 | ``` | ||
| 149 | |||
| 150 | You'll get a blurb of info back like this: | ||
| 151 | |||
| 152 | ```txt | ||
| 153 | Profile: plexmediaserver-dlna | ||
| 154 | Title: Plex Media Server (DLNA) | ||
| 155 | Description: The Plex Media Server (additional DLNA capability only) | ||
| 156 | |||
| 157 | Ports: | ||
| 158 | 1900/udp | ||
| 159 | 32469/tcp | ||
| 160 | ``` | ||
| 161 | |||
| 162 | You can add or delete app rules the same way that you'd add or delete specific | ||
| 163 | port rules. | ||
| 164 | |||
| 165 | ```sh | ||
| 166 | sudo ufw allow plexmediaserver-dlna | ||
| 167 | ``` | ||
| 168 | |||
| 169 | ```sh | ||
| 170 | sudo ufw delete RULE|NUM | ||
| 171 | ``` | ||
| 172 | |||
| 173 | # Creating App Rules | ||
| 174 | |||
| 175 | If you'd like to create you own app rule, you'll need to create a file in the | ||
| 176 | `/etc/ufw/applications.d` directory. Within the file you create, you need to | ||
| 177 | make sure the content is properly formatted. | ||
| 178 | |||
| 179 | For example, here are the contents my `plexmediaserver` file, which creates | ||
| 180 | three distinct app rules for ufw: | ||
| 181 | |||
| 182 | ```config | ||
| 183 | [plexmediaserver] | ||
| 184 | title=Plex Media Server (Standard) | ||
| 185 | description=The Plex Media Server | ||
| 186 | ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp | ||
| 187 | |||
| 188 | [plexmediaserver-dlna] | ||
| 189 | title=Plex Media Server (DLNA) | ||
| 190 | description=The Plex Media Server (additional DLNA capability only) | ||
| 191 | ports=1900/udp|32469/tcp | ||
| 192 | |||
| 193 | [plexmediaserver-all] | ||
| 194 | title=Plex Media Server (Standard + DLNA) | ||
| 195 | description=The Plex Media Server (with additional DLNA capability) | ||
| 196 | ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp|1900/udp|32469/tcp | ||
| 197 | ``` | ||
| 198 | |||
| 199 | So, if I wanted to create a custom app rule called "mycustomrule," I'd create a | ||
| 200 | file and add my content like this: | ||
| 201 | |||
| 202 | ```sh | ||
| 203 | sudo nano /etc/ufw/applications.d/mycustomrule | ||
| 204 | ``` | ||
| 205 | |||
| 206 | ```config | ||
| 207 | [mycustomrule] | ||
| 208 | title=My Custom Rule | ||
| 209 | description=This is a temporary ufw app rule. | ||
| 210 | ports=88/tcp|9100/udp | ||
| 211 | ``` | ||
| 212 | |||
| 213 | Then, I would just enable this rule in ufw. | ||
| 214 | |||
| 215 | ```sh | ||
| 216 | sudo ufw allow mycustomrule | ||
| 217 | ``` | ||
content/blog/2021-01-07-ufw.org added +219
| @@ -0,0 +1,219 @@ | |||
| 1 | #+date: <2021-01-07> | ||
| 2 | #+title: Secure Your Network with the Uncomplicated Firewall (ufw) | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Uncomplicated Firewall | ||
| 7 | |||
| 8 | Uncomplicated Firewall, also known as ufw, is a convenient and | ||
| 9 | beginner-friendly way to enforce OS-level firewall rules. For those who | ||
| 10 | are hosting servers or any device that is accessible to the world (i.e., | ||
| 11 | by public IP or domain name), it's critical that a firewall is properly | ||
| 12 | implemented and active. | ||
| 13 | |||
| 14 | Ufw is available by default in all Ubuntu installations after 8.04 LTS. | ||
| 15 | For other distributions, you can look to install ufw or check if there | ||
| 16 | are alternative firewalls installed already. There are usually | ||
| 17 | alternatives available, such as Fedora's =firewall= and the package | ||
| 18 | available on most distributions: =iptables=. Ufw is considered a | ||
| 19 | beginner-friendly front-end to iptables. | ||
| 20 | |||
| 21 | [[https://gufw.org][Gufw]] is available as a graphical user interface | ||
| 22 | (GUI) application for users who are uncomfortable setting up a firewall | ||
| 23 | through a terminal. | ||
| 24 | |||
| 25 | * Getting Help | ||
| 26 | |||
| 27 | If you need help figuring out commands, remember that you can run the | ||
| 28 | =--help= flag to get a list of options. | ||
| 29 | |||
| 30 | #+begin_src sh | ||
| 31 | sudo ufw --help | ||
| 32 | #+end_src | ||
| 33 | |||
| 34 | * Set Default State | ||
| 35 | |||
| 36 | The proper way to run a firewall is to set a strict default state and | ||
| 37 | slowly open up ports that you want to allow. This helps prevent anything | ||
| 38 | malicious from slipping through the cracks. The following command | ||
| 39 | prevents all incoming traffic (other than the rules we specify later), | ||
| 40 | but you can also set this for outgoing connections, if necessary. | ||
| 41 | |||
| 42 | #+begin_src sh | ||
| 43 | sudo ufw default deny incoming | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | You should also allow outgoing traffic if you want to allow the device | ||
| 47 | to communicate back to you or other parties. For example, media servers | ||
| 48 | like Plex need to be able to send out data related to streaming the | ||
| 49 | media. | ||
| 50 | |||
| 51 | #+begin_src sh | ||
| 52 | sudo ufw default allow outgoing | ||
| 53 | #+end_src | ||
| 54 | |||
| 55 | * Adding Port Rules | ||
| 56 | |||
| 57 | Now that we've disabled all incoming traffic by default, we need to open | ||
| 58 | up some ports (or else no traffic would be able to come in). If you need | ||
| 59 | to be able to =ssh= into the machine, you'll need to open up port 22. | ||
| 60 | |||
| 61 | #+begin_src sh | ||
| 62 | sudo ufw allow 22 | ||
| 63 | #+end_src | ||
| 64 | |||
| 65 | You can also issue more restrictive rules. The following rule will allow | ||
| 66 | =ssh= connections only from machines on the local subnet. | ||
| 67 | |||
| 68 | #+begin_src sh | ||
| 69 | sudo ufw allow proto tcp from 192.168.0.0/24 to any port 22 | ||
| 70 | #+end_src | ||
| 71 | |||
| 72 | If you need to set a rule that isn't tcp, just append your connection | ||
| 73 | type to the end of the rule. | ||
| 74 | |||
| 75 | #+begin_src sh | ||
| 76 | sudo ufw allow 1900/udp | ||
| 77 | #+end_src | ||
| 78 | |||
| 79 | * Enable ufw | ||
| 80 | |||
| 81 | Now that the firewall is configured and ready to go, you can enable the | ||
| 82 | firewall. | ||
| 83 | |||
| 84 | #+begin_src sh | ||
| 85 | sudo ufw enable | ||
| 86 | #+end_src | ||
| 87 | |||
| 88 | A restart may be required for the firewall to begin operating. | ||
| 89 | |||
| 90 | #+begin_src sh | ||
| 91 | sudo reboot now | ||
| 92 | #+end_src | ||
| 93 | |||
| 94 | * Checking Status | ||
| 95 | |||
| 96 | Now that the firewall is enabled, let's check and see what the rules | ||
| 97 | look like. | ||
| 98 | |||
| 99 | #+begin_src sh | ||
| 100 | sudo ufw status numbered | ||
| 101 | #+end_src | ||
| 102 | |||
| 103 | #+begin_src txt | ||
| 104 | Status: active | ||
| 105 | |||
| 106 | To Action From | ||
| 107 | -- ------ ---- | ||
| 108 | [ 1] 22 ALLOW IN Anywhere | ||
| 109 | [ 2] 22 (v6) ALLOW IN Anywhere (v6) | ||
| 110 | #+end_src | ||
| 111 | |||
| 112 | * Deleting Rules | ||
| 113 | |||
| 114 | If you need to delete a rule, you need to know the number associated | ||
| 115 | with that rule. Let's delete the first rule in the table above. You'll | ||
| 116 | be asked to confirm the deletion as part of this process. | ||
| 117 | |||
| 118 | #+begin_src sh | ||
| 119 | sudo ufw delete 1 | ||
| 120 | #+end_src | ||
| 121 | |||
| 122 | * Managing App Rules | ||
| 123 | |||
| 124 | Luckily, there's a convenient way for installed applications to create | ||
| 125 | files that ufw can easily implement so that you don't have to search and | ||
| 126 | find which ports your application requires. To see if your device has | ||
| 127 | any applications with pre-installed ufw rules, execute the following | ||
| 128 | command: | ||
| 129 | |||
| 130 | #+begin_src sh | ||
| 131 | sudo ufw app list | ||
| 132 | #+end_src | ||
| 133 | |||
| 134 | The results should look something like this: | ||
| 135 | |||
| 136 | #+begin_src txt | ||
| 137 | Available applications: | ||
| 138 | OpenSSH | ||
| 139 | Samba | ||
| 140 | plexmediaserver | ||
| 141 | plexmediaserver-all | ||
| 142 | plexmediaserver-dlna | ||
| 143 | #+end_src | ||
| 144 | |||
| 145 | If you want to get more information on a specific app rule, use the | ||
| 146 | =info= command. | ||
| 147 | |||
| 148 | #+begin_src sh | ||
| 149 | sudo ufw app info plexmediaserver-dlna | ||
| 150 | #+end_src | ||
| 151 | |||
| 152 | You'll get a blurb of info back like this: | ||
| 153 | |||
| 154 | #+begin_src txt | ||
| 155 | Profile: plexmediaserver-dlna | ||
| 156 | Title: Plex Media Server (DLNA) | ||
| 157 | Description: The Plex Media Server (additional DLNA capability only) | ||
| 158 | |||
| 159 | Ports: | ||
| 160 | 1900/udp | ||
| 161 | 32469/tcp | ||
| 162 | #+end_src | ||
| 163 | |||
| 164 | You can add or delete app rules the same way that you'd add or delete | ||
| 165 | specific port rules. | ||
| 166 | |||
| 167 | #+begin_src sh | ||
| 168 | sudo ufw allow plexmediaserver-dlna | ||
| 169 | #+end_src | ||
| 170 | |||
| 171 | #+begin_src sh | ||
| 172 | sudo ufw delete RULE|NUM | ||
| 173 | #+end_src | ||
| 174 | |||
| 175 | * Creating App Rules | ||
| 176 | |||
| 177 | If you'd like to create you own app rule, you'll need to create a file | ||
| 178 | in the =/etc/ufw/applications.d= directory. Within the file you create, | ||
| 179 | you need to make sure the content is properly formatted. | ||
| 180 | |||
| 181 | For example, here are the contents my =plexmediaserver= file, which | ||
| 182 | creates three distinct app rules for ufw: | ||
| 183 | |||
| 184 | #+begin_src config | ||
| 185 | [plexmediaserver] | ||
| 186 | title=Plex Media Server (Standard) | ||
| 187 | description=The Plex Media Server | ||
| 188 | ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp | ||
| 189 | |||
| 190 | [plexmediaserver-dlna] | ||
| 191 | title=Plex Media Server (DLNA) | ||
| 192 | description=The Plex Media Server (additional DLNA capability only) | ||
| 193 | ports=1900/udp|32469/tcp | ||
| 194 | |||
| 195 | [plexmediaserver-all] | ||
| 196 | title=Plex Media Server (Standard + DLNA) | ||
| 197 | description=The Plex Media Server (with additional DLNA capability) | ||
| 198 | ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp|1900/udp|32469/tcp | ||
| 199 | #+end_src | ||
| 200 | |||
| 201 | So, if I wanted to create a custom app rule called "mycustomrule," I'd | ||
| 202 | create a file and add my content like this: | ||
| 203 | |||
| 204 | #+begin_src sh | ||
| 205 | sudo nano /etc/ufw/applications.d/mycustomrule | ||
| 206 | #+end_src | ||
| 207 | |||
| 208 | #+begin_src config | ||
| 209 | [mycustomrule] | ||
| 210 | title=My Custom Rule | ||
| 211 | description=This is a temporary ufw app rule. | ||
| 212 | ports=88/tcp|9100/udp | ||
| 213 | #+end_src | ||
| 214 | |||
| 215 | Then, I would just enable this rule in ufw. | ||
| 216 | |||
| 217 | #+begin_src sh | ||
| 218 | sudo ufw allow mycustomrule | ||
| 219 | #+end_src | ||
content/blog/2021-02-19-macos.md deleted −197
| @@ -1,197 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-02-19 | ||
| 3 | title = "macOS: Testing Out a New OS" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Diving into macOS | ||
| 9 | |||
| 10 | After spending nearly 15 years working with Windows and 8 years on Linux, I have | ||
| 11 | experienced macOS for the first time. By chance, my spouse happened to buy a new | ||
| 12 | MacBook and gifted me their 2013 model. Of course, I still consider my Linux | ||
| 13 | desktop to be my daily driver and keep Windows around for gaming needs, but over | ||
| 14 | the past week I've found myself using the MacBook more and more for things that | ||
| 15 | don't require gaming specs or advanced dev tools. | ||
| 16 | |||
| 17 | # Initial Thoughts | ||
| 18 | |||
| 19 | Before I move on to the technical aspects of my set-up, I want to take some time | ||
| 20 | and express my thoughts on the overall OS. | ||
| 21 | |||
| 22 | As expected, the initial computer setup is a breeze with Mac's guided GUI | ||
| 23 | installer. | ||
| 24 | |||
| 25 | The desktop itself reminds me of GNOME more than anything else I've seen: even | ||
| 26 | Pantheon from [ElementaryOS](https://elementary.io/), which people commonly | ||
| 27 | refer to as the closest Linux distro to macOS. The desktop toolbar is great and | ||
| 28 | far surpasses the utility of the GNOME toolbar due to the fact that the | ||
| 29 | extensions and icons _actually work_. I launch macOS and immediately see my | ||
| 30 | shortcuts for Tresorit, Bitwarden, and Mullvad pop up as the computer loads. | ||
| 31 | |||
| 32 | Even further, the app dock is very useful and will be yet another familiarity | ||
| 33 | for GNOME users. I know many people like panels instead of docks, but I've | ||
| 34 | always found docks to have a more pleasing UI. However, I had to disable the | ||
| 35 | "Show recent applications in Dock" preference; I can't stand items taking up | ||
| 36 | precious screen space if I'm not currently using them. On that same note, it's | ||
| 37 | taking me some time to get use to the fact that I have to manually quit an app | ||
| 38 | or else it will still stay open/active in the dock, even if I've closed out all | ||
| 39 | windows for that app (e.g. Firefox). | ||
| 40 | |||
| 41 | Overall, I'm having a lot of fun and for users who spend a large majority of | ||
| 42 | their time performing basic tasks like web browsing, writing, watching media, | ||
| 43 | etc., macOS is a fantastic option. | ||
| 44 | |||
| 45 | The rest of this post explains the technicalities of how I set up my CLI | ||
| 46 | environment to make me feel more at-home, similar to the environments I set up | ||
| 47 | on Fedora, Ubuntu, etc. | ||
| 48 | |||
| 49 | # Making it Feel Like Home | ||
| 50 | |||
| 51 | If you're someone who uses Linux primarily, no doubt your first thought when | ||
| 52 | booting macOS will be the same as mine was: "Where is the terminal and how do I | ||
| 53 | set up my favorite utilities?" | ||
| 54 | |||
| 55 | Luckily, macOS hasn't completely hidden away the development tools from the | ||
| 56 | average user. You can easily find the Terminal app in the Launchpad area, but | ||
| 57 | it's probably not what you're used to. I was surprised (and happy) to see that | ||
| 58 | the default shell is `zsh`, the shell I use on all of my Linux distros. However, | ||
| 59 | the commands are not the same - even the ones you may think are native to the | ||
| 60 | shell. Commands like `dir` do not exist, so other native commands like `ls -la` | ||
| 61 | or `pwd` are more useful here. | ||
| 62 | |||
| 63 | With only a few minutes of installing and tweaking a few packages, I was able to | ||
| 64 | recreate a terminal environment that I feel very comfortable using. See the | ||
| 65 | image below for a preview of the iTerm2 app with a split view between my macOS | ||
| 66 | desktop shell and an SSH session into my server. | ||
| 67 | |||
| 68 | # Xcode | ||
| 69 | |||
| 70 | My first step was to search the web for any hints on how to get `zsh` back up to | ||
| 71 | the state I like, with extensions, themes, etc. My first step was to install the | ||
| 72 | CLI tools for [Xcode](https://developer.apple.com/xcode/), Apple's suite of | ||
| 73 | development tools. | ||
| 74 | |||
| 75 | ```sh | ||
| 76 | sudo xcode-select -r | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ```sh | ||
| 80 | sudo xcode-select --install | ||
| 81 | ``` | ||
| 82 | |||
| 83 | # Homebrew | ||
| 84 | |||
| 85 | Next up is to install [Homebrew](https://brew.sh), a nifty package manager for | ||
| 86 | macOS. | ||
| 87 | |||
| 88 | ```sh | ||
| 89 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | ||
| 90 | ``` | ||
| 91 | |||
| 92 | I ran into a permission error when installing Homebrew: | ||
| 93 | |||
| 94 | ```sh | ||
| 95 | Error: Failed to link all completions, docs and manpages: | ||
| 96 | Permission denied @ rb_file_s_symlink - (../../../Homebrew/completions/zsh/_brew, /usr/local/share/zsh/site-functions/_brew) | ||
| 97 | Failed during: /usr/local/bin/brew update --force --quiet | ||
| 98 | ``` | ||
| 99 | |||
| 100 | I found that the following permission modification worked like a charm. However, | ||
| 101 | I noted that some users online discussed the fact that this solution may not | ||
| 102 | work if your system has multiple users who use Homebrew. | ||
| 103 | |||
| 104 | ```sh | ||
| 105 | sudo chown -R $(whoami) $(brew --prefix)/* | ||
| 106 | ``` | ||
| 107 | |||
| 108 | Next up is to ensure Homebrew is updated and cleaned. | ||
| 109 | |||
| 110 | ```sh | ||
| 111 | brew update | ||
| 112 | ``` | ||
| 113 | |||
| 114 | ```sh | ||
| 115 | brew cleanup | ||
| 116 | ``` | ||
| 117 | |||
| 118 | # iTerm2 | ||
| 119 | |||
| 120 | Now that I've installed the basic utilities for development, I moved onto | ||
| 121 | installing iTerm2, a much better terminal than the default. | ||
| 122 | |||
| 123 | ```sh | ||
| 124 | brew install --cask iterm2 | ||
| 125 | ``` | ||
| 126 | |||
| 127 | I also used the `Make iTerm2 Default Term` and `Install Shell Integration` | ||
| 128 | options in the iTerm2 application menu to make sure I don't run into any issues | ||
| 129 | later on with different terminals. | ||
| 130 | |||
| 131 | We will also install `zsh` so we can use it in iTerm2. | ||
| 132 | |||
| 133 | ```sh | ||
| 134 | brew install zsh | ||
| 135 | ``` | ||
| 136 | |||
| 137 | # Oh-My-Zsh | ||
| 138 | |||
| 139 | I've shown the great aspects of [Oh My Zsh](https://ohmyz.sh) in other blog | ||
| 140 | posts, so I'll skip over that speech for now. Simply install it and run an | ||
| 141 | update. | ||
| 142 | |||
| 143 | ```sh | ||
| 144 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" | ||
| 145 | ``` | ||
| 146 | |||
| 147 | ```sh | ||
| 148 | omz update | ||
| 149 | ``` | ||
| 150 | |||
| 151 | Finally, restart the iTerm2 application to ensure all changes go into effect. | ||
| 152 | |||
| 153 | # Oh-My-Zsh Themes | ||
| 154 | |||
| 155 | Let's change the theme of the terminal to make it a little more friendly. | ||
| 156 | |||
| 157 | ```sh | ||
| 158 | open ~/.zshrc | ||
| 159 | ``` | ||
| 160 | |||
| 161 | The third section of this file should contain a line like the code below. Change | ||
| 162 | that theme to [any theme you | ||
| 163 | want](https://github.com/ohmyzsh/ohmyzsh/wiki/Themes), save the file, and exit. | ||
| 164 | |||
| 165 | ```sh | ||
| 166 | ZSH_THEME="af-magic" | ||
| 167 | ``` | ||
| 168 | |||
| 169 | After changing the `.zshrc` file, you'll need to close your terminal and re-open | ||
| 170 | it to see the changes. Optionally, just open a new tab if you're using iTerm2, | ||
| 171 | and you'll see the new shell config. | ||
| 172 | |||
| 173 | # Oh-My-Zsh Plugins | ||
| 174 | |||
| 175 | Of course, my customization of `zsh` would not be complete without | ||
| 176 | [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions). This | ||
| 177 | will bring up commands you've run in the past as you type them. For example, if | ||
| 178 | you've run `ssh user@192.168.1.99` before, the terminal will show this command | ||
| 179 | as soon as you start typing it (e.g. `zsh u`), and you can hit the right arrow | ||
| 180 | to autocomplete the command. | ||
| 181 | |||
| 182 | ```sh | ||
| 183 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | ||
| 184 | ``` | ||
| 185 | |||
| 186 | ```sh | ||
| 187 | open ~/.zshrc | ||
| 188 | ``` | ||
| 189 | |||
| 190 | ```sh | ||
| 191 | # Scroll down the script and edit this line to add zsh-autosuggestions | ||
| 192 | plugins=(git zsh-autosuggestions) | ||
| 193 | ``` | ||
| 194 | |||
| 195 | Remember: After changing the `.zshrc` file, you'll need to close your terminal | ||
| 196 | and re-open it to see the changes. Optionally, just open a new tab if you're | ||
| 197 | using iTerm2, and you'll see the new shell config. | ||
content/blog/2021-02-19-macos.org added +203
| @@ -0,0 +1,203 @@ | |||
| 1 | #+date: <2021-02-19> | ||
| 2 | #+title: macOS: Testing Out a New OS | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Diving into macOS | ||
| 7 | |||
| 8 | After spending nearly 15 years working with Windows and 8 years on | ||
| 9 | Linux, I have experienced macOS for the first time. By chance, my spouse | ||
| 10 | happened to buy a new MacBook and gifted me their 2013 model. Of course, | ||
| 11 | I still consider my Linux desktop to be my daily driver and keep Windows | ||
| 12 | around for gaming needs, but over the past week I've found myself using | ||
| 13 | the MacBook more and more for things that don't require gaming specs or | ||
| 14 | advanced dev tools. | ||
| 15 | |||
| 16 | * Initial Thoughts | ||
| 17 | |||
| 18 | Before I move on to the technical aspects of my set-up, I want to take | ||
| 19 | some time and express my thoughts on the overall OS. | ||
| 20 | |||
| 21 | As expected, the initial computer setup is a breeze with Mac's guided | ||
| 22 | GUI installer. | ||
| 23 | |||
| 24 | The desktop itself reminds me of GNOME more than anything else I've | ||
| 25 | seen: even Pantheon from [[https://elementary.io/][ElementaryOS]], which | ||
| 26 | people commonly refer to as the closest Linux distro to macOS. The | ||
| 27 | desktop toolbar is great and far surpasses the utility of the GNOME | ||
| 28 | toolbar due to the fact that the extensions and icons /actually work/. I | ||
| 29 | launch macOS and immediately see my shortcuts for Tresorit, Bitwarden, | ||
| 30 | and Mullvad pop up as the computer loads. | ||
| 31 | |||
| 32 | Even further, the app dock is very useful and will be yet another | ||
| 33 | familiarity for GNOME users. I know many people like panels instead of | ||
| 34 | docks, but I've always found docks to have a more pleasing UI. However, | ||
| 35 | I had to disable the "Show recent applications in Dock" preference; I | ||
| 36 | can't stand items taking up precious screen space if I'm not currently | ||
| 37 | using them. On that same note, it's taking me some time to get use to | ||
| 38 | the fact that I have to manually quit an app or else it will still stay | ||
| 39 | open/active in the dock, even if I've closed out all windows for that | ||
| 40 | app (e.g. Firefox). | ||
| 41 | |||
| 42 | Overall, I'm having a lot of fun and for users who spend a large | ||
| 43 | majority of their time performing basic tasks like web browsing, | ||
| 44 | writing, watching media, etc., macOS is a fantastic option. | ||
| 45 | |||
| 46 | The rest of this post explains the technicalities of how I set up my CLI | ||
| 47 | environment to make me feel more at-home, similar to the environments I | ||
| 48 | set up on Fedora, Ubuntu, etc. | ||
| 49 | |||
| 50 | * Making it Feel Like Home | ||
| 51 | |||
| 52 | If you're someone who uses Linux primarily, no doubt your first thought | ||
| 53 | when booting macOS will be the same as mine was: "Where is the terminal | ||
| 54 | and how do I set up my favorite utilities?" | ||
| 55 | |||
| 56 | Luckily, macOS hasn't completely hidden away the development tools from | ||
| 57 | the average user. You can easily find the Terminal app in the Launchpad | ||
| 58 | area, but it's probably not what you're used to. I was surprised (and | ||
| 59 | happy) to see that the default shell is =zsh=, the shell I use on all of | ||
| 60 | my Linux distros. However, the commands are not the same - even the ones | ||
| 61 | you may think are native to the shell. Commands like =dir= do not exist, | ||
| 62 | so other native commands like =ls -la= or =pwd= are more useful here. | ||
| 63 | |||
| 64 | With only a few minutes of installing and tweaking a few packages, I was | ||
| 65 | able to recreate a terminal environment that I feel very comfortable | ||
| 66 | using. See the image below for a preview of the iTerm2 app with a split | ||
| 67 | view between my macOS desktop shell and an SSH session into my server. | ||
| 68 | |||
| 69 | * Xcode | ||
| 70 | |||
| 71 | My first step was to search the web for any hints on how to get =zsh= | ||
| 72 | back up to the state I like, with extensions, themes, etc. My first step | ||
| 73 | was to install the CLI tools for | ||
| 74 | [[https://developer.apple.com/xcode/][Xcode]], Apple's suite of | ||
| 75 | development tools. | ||
| 76 | |||
| 77 | #+begin_src sh | ||
| 78 | sudo xcode-select -r | ||
| 79 | #+end_src | ||
| 80 | |||
| 81 | #+begin_src sh | ||
| 82 | sudo xcode-select --install | ||
| 83 | #+end_src | ||
| 84 | |||
| 85 | * Homebrew | ||
| 86 | |||
| 87 | Next up is to install [[https://brew.sh][Homebrew]], a nifty package | ||
| 88 | manager for macOS. | ||
| 89 | |||
| 90 | #+begin_src sh | ||
| 91 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | ||
| 92 | #+end_src | ||
| 93 | |||
| 94 | I ran into a permission error when installing Homebrew: | ||
| 95 | |||
| 96 | #+begin_src sh | ||
| 97 | Error: Failed to link all completions, docs and manpages: | ||
| 98 | Permission denied @ rb_file_s_symlink - (../../../Homebrew/completions/zsh/_brew, /usr/local/share/zsh/site-functions/_brew) | ||
| 99 | Failed during: /usr/local/bin/brew update --force --quiet | ||
| 100 | #+end_src | ||
| 101 | |||
| 102 | I found that the following permission modification worked like a charm. | ||
| 103 | However, I noted that some users online discussed the fact that this | ||
| 104 | solution may not work if your system has multiple users who use | ||
| 105 | Homebrew. | ||
| 106 | |||
| 107 | #+begin_src sh | ||
| 108 | sudo chown -R $(whoami) $(brew --prefix)/* | ||
| 109 | #+end_src | ||
| 110 | |||
| 111 | Next up is to ensure Homebrew is updated and cleaned. | ||
| 112 | |||
| 113 | #+begin_src sh | ||
| 114 | brew update | ||
| 115 | #+end_src | ||
| 116 | |||
| 117 | #+begin_src sh | ||
| 118 | brew cleanup | ||
| 119 | #+end_src | ||
| 120 | |||
| 121 | * iTerm2 | ||
| 122 | |||
| 123 | Now that I've installed the basic utilities for development, I moved | ||
| 124 | onto installing iTerm2, a much better terminal than the default. | ||
| 125 | |||
| 126 | #+begin_src sh | ||
| 127 | brew install --cask iterm2 | ||
| 128 | #+end_src | ||
| 129 | |||
| 130 | I also used the =Make iTerm2 Default Term= and | ||
| 131 | =Install Shell Integration= options in the iTerm2 application menu to | ||
| 132 | make sure I don't run into any issues later on with different terminals. | ||
| 133 | |||
| 134 | We will also install =zsh= so we can use it in iTerm2. | ||
| 135 | |||
| 136 | #+begin_src sh | ||
| 137 | brew install zsh | ||
| 138 | #+end_src | ||
| 139 | |||
| 140 | * Oh-My-Zsh | ||
| 141 | |||
| 142 | I've shown the great aspects of [[https://ohmyz.sh][Oh My Zsh]] in other | ||
| 143 | blog posts, so I'll skip over that speech for now. Simply install it and | ||
| 144 | run an update. | ||
| 145 | |||
| 146 | #+begin_src sh | ||
| 147 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" | ||
| 148 | #+end_src | ||
| 149 | |||
| 150 | #+begin_src sh | ||
| 151 | omz update | ||
| 152 | #+end_src | ||
| 153 | |||
| 154 | Finally, restart the iTerm2 application to ensure all changes go into | ||
| 155 | effect. | ||
| 156 | |||
| 157 | * Oh-My-Zsh Themes | ||
| 158 | |||
| 159 | Let's change the theme of the terminal to make it a little more | ||
| 160 | friendly. | ||
| 161 | |||
| 162 | #+begin_src sh | ||
| 163 | open ~/.zshrc | ||
| 164 | #+end_src | ||
| 165 | |||
| 166 | The third section of this file should contain a line like the code | ||
| 167 | below. Change that theme to | ||
| 168 | [[https://github.com/ohmyzsh/ohmyzsh/wiki/Themes][any theme you want]], | ||
| 169 | save the file, and exit. | ||
| 170 | |||
| 171 | #+begin_src sh | ||
| 172 | ZSH_THEME="af-magic" | ||
| 173 | #+end_src | ||
| 174 | |||
| 175 | After changing the =.zshrc= file, you'll need to close your terminal and | ||
| 176 | re-open it to see the changes. Optionally, just open a new tab if you're | ||
| 177 | using iTerm2, and you'll see the new shell config. | ||
| 178 | |||
| 179 | * Oh-My-Zsh Plugins | ||
| 180 | |||
| 181 | Of course, my customization of =zsh= would not be complete without | ||
| 182 | [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]]. | ||
| 183 | This will bring up commands you've run in the past as you type them. For | ||
| 184 | example, if you've run =ssh user@192.168.1.99= before, the terminal will | ||
| 185 | show this command as soon as you start typing it (e.g. =zsh u=), and you | ||
| 186 | can hit the right arrow to autocomplete the command. | ||
| 187 | |||
| 188 | #+begin_src sh | ||
| 189 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | ||
| 190 | #+end_src | ||
| 191 | |||
| 192 | #+begin_src sh | ||
| 193 | open ~/.zshrc | ||
| 194 | #+end_src | ||
| 195 | |||
| 196 | #+begin_src sh | ||
| 197 | # Scroll down the script and edit this line to add zsh-autosuggestions | ||
| 198 | plugins=(git zsh-autosuggestions) | ||
| 199 | #+end_src | ||
| 200 | |||
| 201 | Remember: After changing the =.zshrc= file, you'll need to close your | ||
| 202 | terminal and re-open it to see the changes. Optionally, just open a new | ||
| 203 | tab if you're using iTerm2, and you'll see the new shell config. | ||
content/blog/2021-03-19-clone-github-repos.md → content/blog/2021-03-19-clone-github-repos.org renamed +64 −64
| @@ -1,58 +1,57 @@ | |||
| 1 | +++ | 1 | #+date: <2021-03-19> |
| 2 | date = 2021-03-19 | 2 | #+title: How to Clonse All Repositories from a GitHub or Sourcehut Account |
| 3 | title = "How to Clonse All Repositories from a GitHub or Sourcehut Account" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Cloning from GitHub | ||
| 9 | 5 | ||
| 10 | If you're like me and use a lot of different devices (and sometimes decide to | 6 | * Cloning from GitHub |
| 11 | just wipe your device and start with a new OS), you probably know the pain of | ||
| 12 | cloning all your old code repositories down to your local file system. | ||
| 13 | 7 | ||
| 14 | If you're using GitHub, you can easily clone all of your code back down in just | 8 | If you're like me and use a lot of different devices (and sometimes |
| 15 | seconds. First, create a bash script. I do so by opening a new file in `nano`, | 9 | decide to just wipe your device and start with a new OS), you probably |
| 16 | but you can use `gedit`, `vim`, or something else: | 10 | know the pain of cloning all your old code repositories down to your |
| 11 | local file system. | ||
| 17 | 12 | ||
| 18 | ```sh | 13 | If you're using GitHub, you can easily clone all of your code back down |
| 14 | in just seconds. First, create a bash script. I do so by opening a new | ||
| 15 | file in =nano=, but you can use =gedit=, =vim=, or something else: | ||
| 16 | |||
| 17 | #+begin_src sh | ||
| 19 | nano clone_github_repos.sh | 18 | nano clone_github_repos.sh |
| 20 | ``` | 19 | #+end_src |
| 21 | 20 | ||
| 22 | Next, paste in the following information. Note that you can replace the word | 21 | Next, paste in the following information. Note that you can replace the |
| 23 | `users` in the first line with `orgs` and type an organization's name instead of | 22 | word =users= in the first line with =orgs= and type an organization's |
| 24 | a user's name. | 23 | name instead of a user's name. |
| 25 | 24 | ||
| 26 | ```sh | 25 | #+begin_src sh |
| 27 | CNTX=users; NAME=YOUR-USERNAME; PAGE=1 | 26 | CNTX=users; NAME=YOUR-USERNAME; PAGE=1 |
| 28 | curl "https://api.github.com/$CNTX/$NAME/repos?page=$PAGE&per_page=100" | | 27 | curl "https://api.github.com/$CNTX/$NAME/repos?page=$PAGE&per_page=100" | |
| 29 | grep -e 'git_url*' | | 28 | grep -e 'git_url*' | |
| 30 | cut -d " -f 4 | | 29 | cut -d " -f 4 | |
| 31 | xargs -L1 git clone | 30 | xargs -L1 git clone |
| 32 | ``` | 31 | #+end_src |
| 33 | 32 | ||
| 34 | Finally, save the bash script and make it executable. | 33 | Finally, save the bash script and make it executable. |
| 35 | 34 | ||
| 36 | ```sh | 35 | #+begin_src sh |
| 37 | chmod a+x clone_github_repos.sh | 36 | chmod a+x clone_github_repos.sh |
| 38 | ``` | 37 | #+end_src |
| 39 | 38 | ||
| 40 | Now you can run the script and should see the cloning process begin. | 39 | Now you can run the script and should see the cloning process begin. |
| 41 | 40 | ||
| 42 | ```sh | 41 | #+begin_src sh |
| 43 | ./clone_github_repos.sh | 42 | ./clone_github_repos.sh |
| 44 | ``` | 43 | #+end_src |
| 45 | 44 | ||
| 46 | # Cloning from Sourcehut | 45 | * Cloning from Sourcehut |
| 47 | 46 | ||
| 48 | I haven't fully figured out how to directly incorporate Sourcehut's GraphQL API | 47 | I haven't fully figured out how to directly incorporate Sourcehut's |
| 49 | into a bash script yet, so this one will take two steps. | 48 | GraphQL API into a bash script yet, so this one will take two steps. |
| 50 | 49 | ||
| 51 | First, log-in to Sourcehut and go to their [GraphQL playground for | 50 | First, log-in to Sourcehut and go to their |
| 52 | Git](https://git.sr.ht/graphql). Next, paste the following query into the left | 51 | [[https://git.sr.ht/graphql][GraphQL playground for Git]]. Next, paste |
| 53 | box: | 52 | the following query into the left box: |
| 54 | 53 | ||
| 55 | ```sh | 54 | #+begin_src sh |
| 56 | query { | 55 | query { |
| 57 | me { | 56 | me { |
| 58 | canonicalName | 57 | canonicalName |
| @@ -64,26 +63,27 @@ query { | |||
| 64 | } | 63 | } |
| 65 | } | 64 | } |
| 66 | } | 65 | } |
| 67 | ``` | 66 | #+end_src |
| 68 | 67 | ||
| 69 | The output on the right side will give you an object of all your repositories. | 68 | The output on the right side will give you an object of all your |
| 70 | Just grab that text and remove all the characters such as quotation marks and | 69 | repositories. Just grab that text and remove all the characters such as |
| 71 | curly brackets. You will need a single-line list of space-separated values for | 70 | quotation marks and curly brackets. You will need a single-line list of |
| 72 | the next step. | 71 | space-separated values for the next step. |
| 73 | 72 | ||
| 74 | Now let's create the bash script: | 73 | Now let's create the bash script: |
| 75 | 74 | ||
| 76 | ```sh | 75 | #+begin_src sh |
| 77 | nano clone_sourcehut_repos.sh | 76 | nano clone_sourcehut_repos.sh |
| 78 | ``` | 77 | #+end_src |
| 79 | 78 | ||
| 80 | Next, paste the following bash script in with the list of repositories you | 79 | Next, paste the following bash script in with the list of repositories |
| 81 | obtained above and replace `your-username` with your username. | 80 | you obtained above and replace =your-username= with your username. |
| 82 | 81 | ||
| 83 | Note that this uses the SSH-based Git cloning method (e.g. `git@git...`), so | 82 | Note that this uses the SSH-based Git cloning method |
| 84 | you'll need to ensure you have set up Sourcehut with your SSH key. | 83 | (e.g. =git@git...=), so you'll need to ensure you have set up Sourcehut |
| 84 | with your SSH key. | ||
| 85 | 85 | ||
| 86 | ```sh | 86 | #+begin_src sh |
| 87 | repos=(repo1 repo2 repo3) | 87 | repos=(repo1 repo2 repo3) |
| 88 | 88 | ||
| 89 | # List all sub-directories in the current directory | 89 | # List all sub-directories in the current directory |
| @@ -92,36 +92,36 @@ do | |||
| 92 | # Clone | 92 | # Clone |
| 93 | git clone git@git.sr.ht:~your-username/$repo | 93 | git clone git@git.sr.ht:~your-username/$repo |
| 94 | done | 94 | done |
| 95 | ``` | 95 | #+end_src |
| 96 | 96 | ||
| 97 | Finally, save the bash script and make it executable. | 97 | Finally, save the bash script and make it executable. |
| 98 | 98 | ||
| 99 | ```sh | 99 | #+begin_src sh |
| 100 | chmod a+x clone_sourcehut_repos.sh | 100 | chmod a+x clone_sourcehut_repos.sh |
| 101 | ``` | 101 | #+end_src |
| 102 | 102 | ||
| 103 | Now you can run the script and should see the cloning process begin. | 103 | Now you can run the script and should see the cloning process begin. |
| 104 | 104 | ||
| 105 | ```sh | 105 | #+begin_src sh |
| 106 | ./clone_sourcehut_repos.sh | 106 | ./clone_sourcehut_repos.sh |
| 107 | ``` | 107 | #+end_src |
| 108 | 108 | ||
| 109 | # Moving Repositories to a New Host | 109 | * Moving Repositories to a New Host |
| 110 | 110 | ||
| 111 | Now that you have all of your code repositories cloned to your local computer, | 111 | Now that you have all of your code repositories cloned to your local |
| 112 | you may want to change the remote host (e.g., moving from GitHub to GitLab). To | 112 | computer, you may want to change the remote host (e.g., moving from |
| 113 | do this, let's create another bash script: | 113 | GitHub to GitLab). To do this, let's create another bash script: |
| 114 | 114 | ||
| 115 | ```sh | 115 | #+begin_src sh |
| 116 | nano change_remote_urls.sh | 116 | nano change_remote_urls.sh |
| 117 | ``` | 117 | #+end_src |
| 118 | 118 | ||
| 119 | Past the following information and be sure to change the URL information to | 119 | Past the following information and be sure to change the URL information |
| 120 | whichever host you are moving to. For this example, I am looping through all of | 120 | to whichever host you are moving to. For this example, I am looping |
| 121 | my cloned GitHub directories and changing them to Sourcehut (e.g. | 121 | through all of my cloned GitHub directories and changing them to |
| 122 | `<YOUR_NEW_REMOTE_URL>` -> `git@git.sr.ht:~myusername`). | 122 | Sourcehut (e.g. =<YOUR_NEW_REMOTE_URL>= -> =git@git.sr.ht:~myusername=). |
| 123 | 123 | ||
| 124 | ```sh | 124 | #+begin_src sh |
| 125 | # List all sub-directories in the current directory | 125 | # List all sub-directories in the current directory |
| 126 | for dir in */ | 126 | for dir in */ |
| 127 | do | 127 | do |
| @@ -136,16 +136,16 @@ do | |||
| 136 | # Go back to main directory | 136 | # Go back to main directory |
| 137 | cd .. | 137 | cd .. |
| 138 | done | 138 | done |
| 139 | ``` | 139 | #+end_src |
| 140 | 140 | ||
| 141 | Finally, save the bash script and make it executable. | 141 | Finally, save the bash script and make it executable. |
| 142 | 142 | ||
| 143 | ```sh | 143 | #+begin_src sh |
| 144 | chmod a+x change_remote_urls.sh | 144 | chmod a+x change_remote_urls.sh |
| 145 | ``` | 145 | #+end_src |
| 146 | 146 | ||
| 147 | Now you can run the script and should see the cloning process begin. | 147 | Now you can run the script and should see the cloning process begin. |
| 148 | 148 | ||
| 149 | ```sh | 149 | #+begin_src sh |
| 150 | ./change_remote_urls.sh | 150 | ./change_remote_urls.sh |
| 151 | ``` | 151 | #+end_src |
content/blog/2021-03-28-gemini-capsule.md deleted −171
| @@ -1,171 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-03-28 | ||
| 3 | title = "Launching a Gemini Capsule" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is Gemini? | ||
| 9 | |||
| 10 | [Gemini](https://gemini.circumlunar.space/) is an internet protocol introduced | ||
| 11 | in June 2019 as an alternative to HTTP(S) or Gopher. In layman's terms, it's an | ||
| 12 | alternative way to browse sites (called capsules) that requires a special | ||
| 13 | browser. Since Gemini is not standardized as an internet standard, normal web | ||
| 14 | browsers won't be able to load a Gemini capsule. Instead, you'll need to use [a | ||
| 15 | Gemini-specific browser](https://gemini.circumlunar.space/clients.html). | ||
| 16 | |||
| 17 | The content found within a Gemini page is called | ||
| 18 | [Gemtext](https://gemini.circumlunar.space/docs/cheatsheet.gmi) and is | ||
| 19 | _extremely_ basic (on purpose). Gemini only processes the text, no media content | ||
| 20 | like images. However, you're able to style 3 levels of headings, regular text, | ||
| 21 | links (which will display on their own line), quotes, and an unordered list. | ||
| 22 | |||
| 23 | Here's a complete listing of valid Gemtext: | ||
| 24 | |||
| 25 | ````txt | ||
| 26 | # Heading 1 | ||
| 27 | ## Heading 2 | ||
| 28 | ### Heading 3 | ||
| 29 | |||
| 30 | Regular text! Lorem ipsum dolor sit amet. | ||
| 31 | |||
| 32 | => https://example.com My Website | ||
| 33 | => gemini://example.com My Gemini Capsule | ||
| 34 | |||
| 35 | > "If life were predictable it would cease to be life, and be without flavor." - Eleanor Roosevelt | ||
| 36 | |||
| 37 | My List: | ||
| 38 | ** Item | ||
| 39 | ** Item | ||
| 40 | |||
| 41 | ```Anything between three backticks will be rendered as code.``` | ||
| 42 | ```` | ||
| 43 | |||
| 44 | ### Free Option | ||
| 45 | |||
| 46 | There are probably numerous websites that allow you to create your personal | ||
| 47 | Gemini capsule, but I'm going to focus on the two sites that I have personally | ||
| 48 | tested. The first option below, Midnight Pub, allows you to create/edit any | ||
| 49 | Gemini files you want in your account. This is essentially a GUI option with a | ||
| 50 | built-in text box for editing. The second option below, Sourcehut, allows you to | ||
| 51 | use a Git repository and automatic build process to deploy your personal Gemini | ||
| 52 | capsule every time you push a commit. | ||
| 53 | |||
| 54 | ## Midnight Pub - Beginner Friendly | ||
| 55 | |||
| 56 | [Midnight Pub](https://midnight.pub/) is a small, virtual community meant to | ||
| 57 | reflect the atmosphere of wandering into a small alley pub. The site is built in | ||
| 58 | Gemtext and has a server-side process to convert Gemtext to HTML if someone | ||
| 59 | loads the site in an HTTP(S) browser. | ||
| 60 | |||
| 61 | To create an account, you'll need to email the owner of the website to obtain a | ||
| 62 | key. You can find their email on the Midnight Pub homepage. Once registered, | ||
| 63 | head to [your account](https://midnight.pub/account) and select [manage | ||
| 64 | site](https://midnight.pub/site). This is the screen where you can upload or | ||
| 65 | create any files to be displayed on the internet. | ||
| 66 | |||
| 67 | For example, I've created both an HTML file and a Gemini file. Remember that | ||
| 68 | Gemini is automatically converted to HTML on the Pub, so you don't need an HTML | ||
| 69 | version. For example, I created an HTML version to add in some extra styling. | ||
| 70 | |||
| 71 | All you need to do is create a page like `index.gmi` and use your Gemini browser | ||
| 72 | to head over to your-username.midnight.pub to see the result. | ||
| 73 | |||
| 74 | That's all there is to it! Easy enough, right? Let's check out a more advanced | ||
| 75 | version in the next section. | ||
| 76 | |||
| 77 | # Paid Option | ||
| 78 | |||
| 79 | As of 2021, Sourcehut has decided to require users to have a paid account in | ||
| 80 | order to utilize their automated build system. For now, paid accounts can be as | ||
| 81 | low as $2/month. | ||
| 82 | |||
| 83 | ## Sourcehut | ||
| 84 | |||
| 85 | [Sourcehut](https://sourcehut.org/) is a collection of software development | ||
| 86 | tools, but mostly surrounds their hosted Git repository service. Simply put, | ||
| 87 | it's a minimal and more private alternative to services like GitHub. | ||
| 88 | |||
| 89 | This walkthrough is more advanced and involves things like Git, SSH, the command | ||
| 90 | line. If you don't think you know enough to do this, check out my walkthrough on | ||
| 91 | creating a Gemini capsule for the Midnight Pub instead. | ||
| 92 | |||
| 93 | The first thing you'll need to do is create an SSH key pair, if you don't | ||
| 94 | already have one on your system. Once created, grab the contents of `id_rsa.pub` | ||
| 95 | and add it to your Sourcehut account settings - this will allow you to push and | ||
| 96 | pull code changes without using a username/password. | ||
| 97 | |||
| 98 | ```sh | ||
| 99 | ssh keygen | ||
| 100 | ``` | ||
| 101 | |||
| 102 | Next up, let's create a repository with the proper name so that the Sourcehut | ||
| 103 | build system will know we want them to host a website for us. Use the following | ||
| 104 | format exactly: | ||
| 105 | |||
| 106 | ```sh | ||
| 107 | mkdir your-username.srht.site && cd your-username.srht.site | ||
| 108 | ``` | ||
| 109 | |||
| 110 | Now that we've created the repo, let's initialize Git and add the proper remote | ||
| 111 | URL. | ||
| 112 | |||
| 113 | ```sh | ||
| 114 | git init | ||
| 115 | ``` | ||
| 116 | |||
| 117 | ```sh | ||
| 118 | git remote add origin git@git.sr.ht:~your-username/your-username.srht.site | ||
| 119 | ``` | ||
| 120 | |||
| 121 | Now that our repository is set up and configured, we will need to create at | ||
| 122 | least two files: | ||
| 123 | |||
| 124 | - `index.gmi` | ||
| 125 | - `.build.yml` | ||
| 126 | |||
| 127 | For your `.build.yml` file, use the following content and be sure to update the | ||
| 128 | `site` line with your username! | ||
| 129 | |||
| 130 | ```yaml | ||
| 131 | image: alpine/latest | ||
| 132 | oauth: pages.sr.ht/PAGES:RW | ||
| 133 | environment: | ||
| 134 | site: your-username.srht.site | ||
| 135 | tasks: | ||
| 136 | - package: | | ||
| 137 | cd $site | ||
| 138 | tar -cvz . > ../site.tar.gz | ||
| 139 | - upload: | | ||
| 140 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI | ||
| 141 | ``` | ||
| 142 | |||
| 143 | For the `index.gmi` file, put whatever you want in there and save it. You could | ||
| 144 | even just copy and paste the Gemtext cheatsheet. | ||
| 145 | |||
| 146 | If you want to serve both HTML and Gemini files from this repository, just add a | ||
| 147 | second command to the `upload` section: | ||
| 148 | |||
| 149 | ```yaml | ||
| 150 | - upload: | | ||
| 151 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI | ||
| 152 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz | ||
| 153 | ``` | ||
| 154 | |||
| 155 | Lastly, commit your changes and push them to the remote repo. | ||
| 156 | |||
| 157 | ```sh | ||
| 158 | git add .; git commit -m "initial commit"; git push --set-upstream origin HEAD | ||
| 159 | ``` | ||
| 160 | |||
| 161 | If you've successfully created the files with the proper format, you'll see the | ||
| 162 | terminal print a message that lets you know where the automatic build is taking | ||
| 163 | place. For example, here's what the terminal tells me: | ||
| 164 | |||
| 165 | ```sh | ||
| 166 | remote: Build started: | ||
| 167 | remote: https://builds.sr.ht/~user/job/689803 [.build.yml] | ||
| 168 | ``` | ||
| 169 | |||
| 170 | Now that you've properly built your Sourcehut page, you can browse to | ||
| 171 | your-username.srht.site in a Gemini browser and view the final results. | ||
content/blog/2021-03-28-gemini-capsule.org added +178
| @@ -0,0 +1,178 @@ | |||
| 1 | #+date: <2021-03-28> | ||
| 2 | #+title: Launching a Gemini Capsule | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is Gemini? | ||
| 7 | |||
| 8 | [[https://gemini.circumlunar.space/][Gemini]] is an internet protocol | ||
| 9 | introduced in June 2019 as an alternative to HTTP(S) or Gopher. In | ||
| 10 | layman's terms, it's an alternative way to browse sites (called | ||
| 11 | capsules) that requires a special browser. Since Gemini is not | ||
| 12 | standardized as an internet standard, normal web browsers won't be able | ||
| 13 | to load a Gemini capsule. Instead, you'll need to use | ||
| 14 | [[https://gemini.circumlunar.space/clients.html][a Gemini-specific | ||
| 15 | browser]]. | ||
| 16 | |||
| 17 | The content found within a Gemini page is called | ||
| 18 | [[https://gemini.circumlunar.space/docs/cheatsheet.gmi][Gemtext]] and is | ||
| 19 | /extremely/ basic (on purpose). Gemini only processes the text, no media | ||
| 20 | content like images. However, you're able to style 3 levels of headings, | ||
| 21 | regular text, links (which will display on their own line), quotes, and | ||
| 22 | an unordered list. | ||
| 23 | |||
| 24 | Here's a complete listing of valid Gemtext: | ||
| 25 | |||
| 26 | #+begin_src txt | ||
| 27 | # Heading 1 | ||
| 28 | ## Heading 2 | ||
| 29 | ### Heading 3 | ||
| 30 | |||
| 31 | Regular text! Lorem ipsum dolor sit amet. | ||
| 32 | |||
| 33 | => https://example.com My Website | ||
| 34 | => gemini://example.com My Gemini Capsule | ||
| 35 | |||
| 36 | > "If life were predictable it would cease to be life, and be without flavor." - Eleanor Roosevelt | ||
| 37 | |||
| 38 | My List: | ||
| 39 | ,** Item | ||
| 40 | ,** Item | ||
| 41 | |||
| 42 | ```Anything between three backticks will be rendered as code.``` | ||
| 43 | #+end_src | ||
| 44 | |||
| 45 | *** Free Option | ||
| 46 | |||
| 47 | There are probably numerous websites that allow you to create your | ||
| 48 | personal Gemini capsule, but I'm going to focus on the two sites that I | ||
| 49 | have personally tested. The first option below, Midnight Pub, allows you | ||
| 50 | to create/edit any Gemini files you want in your account. This is | ||
| 51 | essentially a GUI option with a built-in text box for editing. The | ||
| 52 | second option below, Sourcehut, allows you to use a Git repository and | ||
| 53 | automatic build process to deploy your personal Gemini capsule every | ||
| 54 | time you push a commit. | ||
| 55 | |||
| 56 | ** Midnight Pub - Beginner Friendly | ||
| 57 | |||
| 58 | [[https://midnight.pub/][Midnight Pub]] is a small, virtual community | ||
| 59 | meant to reflect the atmosphere of wandering into a small alley pub. The | ||
| 60 | site is built in Gemtext and has a server-side process to convert | ||
| 61 | Gemtext to HTML if someone loads the site in an HTTP(S) browser. | ||
| 62 | |||
| 63 | To create an account, you'll need to email the owner of the website to | ||
| 64 | obtain a key. You can find their email on the Midnight Pub homepage. | ||
| 65 | Once registered, head to [[https://midnight.pub/account][your account]] | ||
| 66 | and select [[https://midnight.pub/site][manage site]]. This is the | ||
| 67 | screen where you can upload or create any files to be displayed on the | ||
| 68 | internet. | ||
| 69 | |||
| 70 | For example, I've created both an HTML file and a Gemini file. Remember | ||
| 71 | that Gemini is automatically converted to HTML on the Pub, so you don't | ||
| 72 | need an HTML version. For example, I created an HTML version to add in | ||
| 73 | some extra styling. | ||
| 74 | |||
| 75 | All you need to do is create a page like =index.gmi= and use your Gemini | ||
| 76 | browser to head over to your-username.midnight.pub to see the result. | ||
| 77 | |||
| 78 | That's all there is to it! Easy enough, right? Let's check out a more | ||
| 79 | advanced version in the next section. | ||
| 80 | |||
| 81 | * Paid Option | ||
| 82 | |||
| 83 | As of 2021, Sourcehut has decided to require users to have a paid | ||
| 84 | account in order to utilize their automated build system. For now, paid | ||
| 85 | accounts can be as low as $2/month. | ||
| 86 | |||
| 87 | ** Sourcehut | ||
| 88 | |||
| 89 | [[https://sourcehut.org/][Sourcehut]] is a collection of software | ||
| 90 | development tools, but mostly surrounds their hosted Git repository | ||
| 91 | service. Simply put, it's a minimal and more private alternative to | ||
| 92 | services like GitHub. | ||
| 93 | |||
| 94 | This walkthrough is more advanced and involves things like Git, SSH, the | ||
| 95 | command line. If you don't think you know enough to do this, check out | ||
| 96 | my walkthrough on creating a Gemini capsule for the Midnight Pub | ||
| 97 | instead. | ||
| 98 | |||
| 99 | The first thing you'll need to do is create an SSH key pair, if you | ||
| 100 | don't already have one on your system. Once created, grab the contents | ||
| 101 | of =id_rsa.pub= and add it to your Sourcehut account settings - this | ||
| 102 | will allow you to push and pull code changes without using a | ||
| 103 | username/password. | ||
| 104 | |||
| 105 | #+begin_src sh | ||
| 106 | ssh keygen | ||
| 107 | #+end_src | ||
| 108 | |||
| 109 | Next up, let's create a repository with the proper name so that the | ||
| 110 | Sourcehut build system will know we want them to host a website for us. | ||
| 111 | Use the following format exactly: | ||
| 112 | |||
| 113 | #+begin_src sh | ||
| 114 | mkdir your-username.srht.site && cd your-username.srht.site | ||
| 115 | #+end_src | ||
| 116 | |||
| 117 | Now that we've created the repo, let's initialize Git and add the proper | ||
| 118 | remote URL. | ||
| 119 | |||
| 120 | #+begin_src sh | ||
| 121 | git init | ||
| 122 | #+end_src | ||
| 123 | |||
| 124 | #+begin_src sh | ||
| 125 | git remote add origin git@git.sr.ht:~your-username/your-username.srht.site | ||
| 126 | #+end_src | ||
| 127 | |||
| 128 | Now that our repository is set up and configured, we will need to create | ||
| 129 | at least two files: | ||
| 130 | |||
| 131 | - =index.gmi= | ||
| 132 | - =.build.yml= | ||
| 133 | |||
| 134 | For your =.build.yml= file, use the following content and be sure to | ||
| 135 | update the =site= line with your username! | ||
| 136 | |||
| 137 | #+begin_src yaml | ||
| 138 | image: alpine/latest | ||
| 139 | oauth: pages.sr.ht/PAGES:RW | ||
| 140 | environment: | ||
| 141 | site: your-username.srht.site | ||
| 142 | tasks: | ||
| 143 | - package: | | ||
| 144 | cd $site | ||
| 145 | tar -cvz . > ../site.tar.gz | ||
| 146 | - upload: | | ||
| 147 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI | ||
| 148 | #+end_src | ||
| 149 | |||
| 150 | For the =index.gmi= file, put whatever you want in there and save it. | ||
| 151 | You could even just copy and paste the Gemtext cheatsheet. | ||
| 152 | |||
| 153 | If you want to serve both HTML and Gemini files from this repository, | ||
| 154 | just add a second command to the =upload= section: | ||
| 155 | |||
| 156 | #+begin_src yaml | ||
| 157 | - upload: | | ||
| 158 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI | ||
| 159 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz | ||
| 160 | #+end_src | ||
| 161 | |||
| 162 | Lastly, commit your changes and push them to the remote repo. | ||
| 163 | |||
| 164 | #+begin_src sh | ||
| 165 | git add .; git commit -m "initial commit"; git push --set-upstream origin HEAD | ||
| 166 | #+end_src | ||
| 167 | |||
| 168 | If you've successfully created the files with the proper format, you'll | ||
| 169 | see the terminal print a message that lets you know where the automatic | ||
| 170 | build is taking place. For example, here's what the terminal tells me: | ||
| 171 | |||
| 172 | #+begin_src sh | ||
| 173 | remote: Build started: | ||
| 174 | remote: https://builds.sr.ht/~user/job/689803 [.build.yml] | ||
| 175 | #+end_src | ||
| 176 | |||
| 177 | Now that you've properly built your Sourcehut page, you can browse to | ||
| 178 | your-username.srht.site in a Gemini browser and view the final results. | ||
content/blog/2021-03-28-vaporwave-vs-outrun.md deleted −126
| @@ -1,126 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-03-28 | ||
| 3 | title = "Vaporware vs. Outrun" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | When it comes to an aesthetic that originated primarily online, there tends to | ||
| 11 | be a lot of confusion around what falls into each specific genre. This post | ||
| 12 | discusses Vaporwave and Outrun, which have become almost synonymous in the minds | ||
| 13 | of those online who aren't hardcore into these genres of media. More | ||
| 14 | specifically, Outrun is largely an unknown term while all aesthetics in these | ||
| 15 | two genres are usually attributed to Vaporwave. For example, take a look at the | ||
| 16 | image search results for Vaporwave: the results include **a lot** of | ||
| 17 | Outrun-themed images. You'll find a similar trend almost everywhere. | ||
| 18 | |||
| 19 |  | ||
| 21 | |||
| 22 | # Vaporwave | ||
| 23 | |||
| 24 |  | ||
| 26 | |||
| 27 | Okay, so what is Vaporwave? I'm going to just copy-and-paste some general info | ||
| 28 | from the Wikipedia article on | ||
| 29 | [Vaporwave](https://en.wikipedia.org/wiki/Vaporwave), so that I'm not repeating | ||
| 30 | everything you can already search for online: | ||
| 31 | |||
| 32 | > Vaporwave is a microgenre of electronic music, a visual art style, and an | ||
| 33 | > Internet meme that emerged in the early 2010s. It is defined partly by its | ||
| 34 | > slowed-down, chopped and screwed samples of smooth jazz, elevator, R&B, and | ||
| 35 | > lounge music from the 1980s and 1990s. The surrounding subculture is sometimes | ||
| 36 | > associated with an ambiguous or satirical take on consumer capitalism and pop | ||
| 37 | > culture, and tends to be characterized by a nostalgic or surrealist engagement | ||
| 38 | > with the popular entertainment, technology and advertising of previous | ||
| 39 | > decades. Visually, it incorporates early Internet imagery, late 1990s web | ||
| 40 | > design, glitch art, anime, 3D-rendered objects, and cyberpunk tropes in its | ||
| 41 | > cover artwork and music videos. | ||
| 42 | |||
| 43 | This is an excellent summary, and it helps address my point here: there are | ||
| 44 | specific aspects that make Vaporwave unique: | ||
| 45 | |||
| 46 | ## Time Frame | ||
| 47 | |||
| 48 | The time frame for references, logos, etc. focuses mostly on the 1990s in | ||
| 49 | Vaporwave. You'll see old school Pepsi logos, Microsoft 95 screens, tropical | ||
| 50 | plants, classic marble sculptures, and many references from Japan's influence in | ||
| 51 | the 90s. | ||
| 52 | |||
| 53 | ## Art | ||
| 54 | |||
| 55 | The color scheme is generally a soft, light palette that uses pastel colors. The | ||
| 56 | backdrop will often be in a light-pink or teal blue. | ||
| 57 | |||
| 58 | ## Music | ||
| 59 | |||
| 60 | The musical genre of Vaporwave incorporates soft tunes, lounge music, and sound | ||
| 61 | effects that will make the listener reminisce of the 90s. The sounds of the | ||
| 62 | genre are generally slower-paced and calming. The major breakthrough artist for | ||
| 63 | Vaporwave was [Macintosh Plus](https://en.wikipedia.org/wiki/Vektroid), who | ||
| 64 | released the album [Floral | ||
| 65 | Shoppe](https://archive.org/details/MACINTOSHPLUS-FLORALSHOPPE_complete) in | ||
| 66 | 2011. Another more recent example is the artist | ||
| 67 | [sadbert](https://sadbert.bandcamp.com/), whose latest album incorporates the | ||
| 68 | upbeat tones of the 1999 Dilbert TV series. | ||
| 69 | |||
| 70 | Notice that Vaporwave doesn't include things like racing cars, futuristic | ||
| 71 | technology, chrome, or the deep orange/purple color scheme. Vaporwave is a focus | ||
| 72 | on the idyllic state of the world when technology was becoming common in | ||
| 73 | households, a reality that we have already experienced. Focus on the most | ||
| 74 | aesthetically-pleasing parts of that past is a large part of Vaporwave. | ||
| 75 | |||
| 76 | # Outrun | ||
| 77 | |||
| 78 |  | ||
| 79 | |||
| 80 | Now, let's get to Outrun. This one is a little trickier since the genre has | ||
| 81 | largely been lumped under the Vaporwave title for so long. However, it stems | ||
| 82 | from the [Synthwave](https://en.wikipedia.org/wiki/Synthwave) music genre and is | ||
| 83 | likely named after the 1986 racer game, [Out | ||
| 84 | Run](https://en.wikipedia.org/wiki/Out_Run). | ||
| 85 | |||
| 86 | ## Time Frame | ||
| 87 | |||
| 88 | Outrun can be thought of as a retro-futuristic aesthetic born from the 1980s. | ||
| 89 | |||
| 90 | ## Art | ||
| 91 | |||
| 92 | The color scheme uses a very dark color palette with the main colors being deep | ||
| 93 | oranges, blues, and purples. Red edges are common around objects in Outrun art. | ||
| 94 | The background of the Outrun aesthetic is almost always a neon grid like you'd | ||
| 95 | expect to see in Tron or a 1980s arcade machine. | ||
| 96 | |||
| 97 | Classic sports cars, chrome robots, computer generated graphics and fonts, and | ||
| 98 | the occasional use of rain or palm trees can be found in Outrun art. | ||
| 99 | |||
| 100 | ## Music | ||
| 101 | |||
| 102 | This aesthetic has a more aggressive and fast-paced style of music, which tends | ||
| 103 | to match the subject of the art in this aesthetic. | ||
| 104 | |||
| 105 | Outrun enthusiasts love what people in the 1980s thought the future would look | ||
| 106 | like. Take a look at a common video game discussed in Outrun circles, Far Cry 3: | ||
| 107 | Blood Dragon: | ||
| 108 | |||
| 109 |  | ||
| 111 | |||
| 112 | Another example that doesn't force the color scheme as hard as some online art | ||
| 113 | does is Kung Fury: | ||
| 114 | |||
| 115 |  | ||
| 117 | |||
| 118 |  | ||
| 120 | |||
| 121 | # Conclusion | ||
| 122 | |||
| 123 | While Vaporwave and Outrun share similarities, they are two distinct aesthetics | ||
| 124 | with many important distinctions. Someone who enjoys one may not necessarily | ||
| 125 | enjoy the other, so it's important to make sure we properly describe the | ||
| 126 | aesthetic we're looking for. | ||
content/blog/2021-03-28-vaporwave-vs-outrun.org added +134
| @@ -0,0 +1,134 @@ | |||
| 1 | #+date: <2021-03-28> | ||
| 2 | #+title: Vaporware vs. Outrun | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | When it comes to an aesthetic that originated primarily online, there | ||
| 9 | tends to be a lot of confusion around what falls into each specific | ||
| 10 | genre. This post discusses Vaporwave and Outrun, which have become | ||
| 11 | almost synonymous in the minds of those online who aren't hardcore into | ||
| 12 | these genres of media. More specifically, Outrun is largely an unknown | ||
| 13 | term while all aesthetics in these two genres are usually attributed to | ||
| 14 | Vaporwave. For example, take a look at the image search results for | ||
| 15 | Vaporwave: the results include *a lot* of Outrun-themed images. You'll | ||
| 16 | find a similar trend almost everywhere. | ||
| 17 | |||
| 18 | #+caption: Vaporwave Search Results | ||
| 19 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/vaporwave-search-results.png]] | ||
| 20 | |||
| 21 | * Vaporwave | ||
| 22 | |||
| 23 | #+caption: Macintosh Plus Album Cover | ||
| 24 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/macintosh-plus.png]] | ||
| 25 | |||
| 26 | Okay, so what is Vaporwave? I'm going to just copy-and-paste some | ||
| 27 | general info from the Wikipedia article on | ||
| 28 | [[https://en.wikipedia.org/wiki/Vaporwave][Vaporwave]], so that I'm not | ||
| 29 | repeating everything you can already search for online: | ||
| 30 | |||
| 31 | #+begin_quote | ||
| 32 | Vaporwave is a microgenre of electronic music, a visual art style, and | ||
| 33 | an Internet meme that emerged in the early 2010s. It is defined partly | ||
| 34 | by its slowed-down, chopped and screwed samples of smooth jazz, | ||
| 35 | elevator, R&B, and lounge music from the 1980s and 1990s. The | ||
| 36 | surrounding subculture is sometimes associated with an ambiguous or | ||
| 37 | satirical take on consumer capitalism and pop culture, and tends to be | ||
| 38 | characterized by a nostalgic or surrealist engagement with the popular | ||
| 39 | entertainment, technology and advertising of previous decades. Visually, | ||
| 40 | it incorporates early Internet imagery, late 1990s web design, glitch | ||
| 41 | art, anime, 3D-rendered objects, and cyberpunk tropes in its cover | ||
| 42 | artwork and music videos. | ||
| 43 | #+end_quote | ||
| 44 | |||
| 45 | This is an excellent summary, and it helps address my point here: there | ||
| 46 | are specific aspects that make Vaporwave unique: | ||
| 47 | |||
| 48 | ** Time Frame | ||
| 49 | |||
| 50 | The time frame for references, logos, etc. focuses mostly on the 1990s | ||
| 51 | in Vaporwave. You'll see old school Pepsi logos, Microsoft 95 screens, | ||
| 52 | tropical plants, classic marble sculptures, and many references from | ||
| 53 | Japan's influence in the 90s. | ||
| 54 | |||
| 55 | ** Art | ||
| 56 | |||
| 57 | The color scheme is generally a soft, light palette that uses pastel | ||
| 58 | colors. The backdrop will often be in a light-pink or teal blue. | ||
| 59 | |||
| 60 | ** Music | ||
| 61 | |||
| 62 | The musical genre of Vaporwave incorporates soft tunes, lounge music, | ||
| 63 | and sound effects that will make the listener reminisce of the 90s. The | ||
| 64 | sounds of the genre are generally slower-paced and calming. The major | ||
| 65 | breakthrough artist for Vaporwave was | ||
| 66 | [[https://en.wikipedia.org/wiki/Vektroid][Macintosh Plus]], who released | ||
| 67 | the album | ||
| 68 | [[https://archive.org/details/MACINTOSHPLUS-FLORALSHOPPE_complete][Floral | ||
| 69 | Shoppe]] in 2011. Another more recent example is the artist | ||
| 70 | [[https://sadbert.bandcamp.com/][sadbert]], whose latest album | ||
| 71 | incorporates the upbeat tones of the 1999 Dilbert TV series. | ||
| 72 | |||
| 73 | Notice that Vaporwave doesn't include things like racing cars, | ||
| 74 | futuristic technology, chrome, or the deep orange/purple color scheme. | ||
| 75 | Vaporwave is a focus on the idyllic state of the world when technology | ||
| 76 | was becoming common in households, a reality that we have already | ||
| 77 | experienced. Focus on the most aesthetically-pleasing parts of that past | ||
| 78 | is a large part of Vaporwave. | ||
| 79 | |||
| 80 | * Outrun | ||
| 81 | |||
| 82 | #+caption: Outrun | ||
| 83 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/outrun.png]] | ||
| 84 | |||
| 85 | Now, let's get to Outrun. This one is a little trickier since the genre | ||
| 86 | has largely been lumped under the Vaporwave title for so long. However, | ||
| 87 | it stems from the [[https://en.wikipedia.org/wiki/Synthwave][Synthwave]] | ||
| 88 | music genre and is likely named after the 1986 racer game, | ||
| 89 | [[https://en.wikipedia.org/wiki/Out_Run][Out Run]]. | ||
| 90 | |||
| 91 | ** Time Frame | ||
| 92 | |||
| 93 | Outrun can be thought of as a retro-futuristic aesthetic born from the | ||
| 94 | 1980s. | ||
| 95 | |||
| 96 | ** Art | ||
| 97 | |||
| 98 | The color scheme uses a very dark color palette with the main colors | ||
| 99 | being deep oranges, blues, and purples. Red edges are common around | ||
| 100 | objects in Outrun art. The background of the Outrun aesthetic is almost | ||
| 101 | always a neon grid like you'd expect to see in Tron or a 1980s arcade | ||
| 102 | machine. | ||
| 103 | |||
| 104 | Classic sports cars, chrome robots, computer generated graphics and | ||
| 105 | fonts, and the occasional use of rain or palm trees can be found in | ||
| 106 | Outrun art. | ||
| 107 | |||
| 108 | ** Music | ||
| 109 | |||
| 110 | This aesthetic has a more aggressive and fast-paced style of music, | ||
| 111 | which tends to match the subject of the art in this aesthetic. | ||
| 112 | |||
| 113 | Outrun enthusiasts love what people in the 1980s thought the future | ||
| 114 | would look like. Take a look at a common video game discussed in Outrun | ||
| 115 | circles, Far Cry 3: Blood Dragon: | ||
| 116 | |||
| 117 | #+caption: Fry Cry 3: Blood Dragon | ||
| 118 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/far-cry.png]] | ||
| 119 | |||
| 120 | Another example that doesn't force the color scheme as hard as some | ||
| 121 | online art does is Kung Fury: | ||
| 122 | |||
| 123 | #+caption: Kung Fury | ||
| 124 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury.png]] | ||
| 125 | |||
| 126 | #+caption: Kung Fury Hacker Scene | ||
| 127 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury-hacker.png]] | ||
| 128 | |||
| 129 | * Conclusion | ||
| 130 | |||
| 131 | While Vaporwave and Outrun share similarities, they are two distinct | ||
| 132 | aesthetics with many important distinctions. Someone who enjoys one may | ||
| 133 | not necessarily enjoy the other, so it's important to make sure we | ||
| 134 | properly describe the aesthetic we're looking for. | ||
content/blog/2021-03-30-vps-web-server.md deleted −396
| @@ -1,396 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-03-30 | ||
| 3 | title = "How to Set Up a VPS Web Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Shared Hosting vs. VPS | ||
| 9 | |||
| 10 | Choosing a place to host a website is one of the most confusing decisions for | ||
| 11 | beginner web developers. Even for experienced web devs, choosing between | ||
| 12 | different forms of web hosting can be a daunting choice. | ||
| 13 | |||
| 14 | First, let's take a look at [shared web | ||
| 15 | hosting](https://en.wikipedia.org/wiki/Shared_web_hosting_service). Shared web | ||
| 16 | hosting is a product where you are purchasing a small piece of a web server that | ||
| 17 | is being shared between many websites. As a result, the cost of shared hosting | ||
| 18 | is extremely low. You won't have access to the server itself, which means you | ||
| 19 | can't install your own software on the server, such as Docker. Usually, you are | ||
| 20 | simply allowed to connect your domains to the server, set up domain security, | ||
| 21 | and other small utilities. | ||
| 22 | |||
| 23 | In contrast, a [virtual private | ||
| 24 | server](https://en.wikipedia.org/wiki/Virtual_private_server) (VPS) is a virtual | ||
| 25 | machine that replicates the environment of having a dedicated server to | ||
| 26 | yourself. You are able to control the virtual server's host names, base file | ||
| 27 | system, package manager, etc. Another great upside of a VPS is that since it's | ||
| 28 | virtual, the company providing the VPS can dynamically increase the disk size, | ||
| 29 | RAM size, or number of CPUs at any time. However, the virtual server is still | ||
| 30 | physically located on a server that is shared between multiple virtual servers. | ||
| 31 | |||
| 32 | The choice between shared hosting and VPS mostly depends on your skill level | ||
| 33 | with system administration. If you're comforting working on a server that is | ||
| 34 | mostly left up to you (or you're willing to learn), then a VPS is usually a | ||
| 35 | better option. However, shared hosting is a fantastic option for people who | ||
| 36 | don't want to have to learn how to manage their server. | ||
| 37 | |||
| 38 | # My Situation | ||
| 39 | |||
| 40 | I had used shared hosting for approximately 5 years before trying my first VPS. | ||
| 41 | I manage a homelab and have had success running a server and performing typical | ||
| 42 | sysadmin duties, but I was still hesitant to get a VPS. One fear was that I | ||
| 43 | always struggled to properly set up the networking part of a server - DNS and | ||
| 44 | hostname configurations were not my friend. | ||
| 45 | |||
| 46 | As a little bit of background, I originally used | ||
| 47 | [Siteground](https://www.siteground.com) for my initially shared hosting and | ||
| 48 | stayed on that platform for at least a year. However, the UI was clunky, and I | ||
| 49 | didn't like how they handled certain technical aspects, so I switched to | ||
| 50 | [Namecheap](https://www.namecheap.com). Namecheap was great because it is the | ||
| 51 | service I primarily use for purchasing domain names, which made it incredibly | ||
| 52 | easy to link them to my hosting service. However, it was still mediocre shared | ||
| 53 | hosting, and Namecheap is notorious for not letting you use [Let's | ||
| 54 | Encrypt](https://letsencrypt.org) to obtain free SSL/TLS certificates; Namecheap | ||
| 55 | wants to make you purchase certificates through their store. | ||
| 56 | |||
| 57 | Finally, I settled down with [iWebFusion](https://www.iwebfusion.net) for about | ||
| 58 | the last year of my shared hosting. This service was pretty great, came with | ||
| 59 | free SSL/TLS, and I never had any complaints. | ||
| 60 | |||
| 61 | However, I finally grew tired of not being able to install software on my own | ||
| 62 | web server. I wanted to be able to try out things like | ||
| 63 | [Postmill](https://postmill.xyz) or [Matrix](https://matrix.org). This is | ||
| 64 | possible with a VPS, so I decided to grab a new domain name to try it out. | ||
| 65 | |||
| 66 | # Getting Started: Buying a VPS | ||
| 67 | |||
| 68 | The first step to moving over to a VPS is (you guessed it): finding a VPS | ||
| 69 | provider. For my VPSs, I use [1984](https://1984hosting.com) and prefer their | ||
| 70 | services much more than any alternative, due to their location (Iceland), their | ||
| 71 | [privacy policy](https://1984hosting.com/GDPR/), their respect for GDPR, and the | ||
| 72 | ability to remain anonymous if you pay in Bitcoin or Monero. | ||
| 73 | |||
| 74 | [Njalla](https://njal.la) is another good, privacy-oriented option for VPS | ||
| 75 | services. | ||
| 76 | |||
| 77 | You'll have to decide what specifications you want on your VPS. For me, I only | ||
| 78 | build and deploy low-resource HTML, PHP, and Python websites. This means I can | ||
| 79 | survive on the smallest VPS: 1 CPU, 1GB of RAM, and 25GB SSD for $5.00 per | ||
| 80 | month. | ||
| 81 | |||
| 82 | As noted above, the great thing about a VPS is you can request your provider to | ||
| 83 | increase the resources at any time. | ||
| 84 | |||
| 85 | # Configuring DNS Settings | ||
| 86 | |||
| 87 | Okay, so now let's get into some actual work that has to be done to get content | ||
| 88 | moved from a shared host to a VPS. At this point, I'm assuming you have a shared | ||
| 89 | host with website content that you can still access, and you've purchased a new | ||
| 90 | VPS and can SSH into that server. | ||
| 91 | |||
| 92 | The first change is minor, but it should be done immediately in order to get | ||
| 93 | things moving: DNS settings. Go to wherever your DNS settings are handled. If | ||
| 94 | your shared host also managed your DNS settings, you'll need to first move that | ||
| 95 | DNS over to your new VPS provider. For me, I route my DNS through | ||
| 96 | [Gandi](https://www.gandi.net). | ||
| 97 | |||
| 98 | Once you know where your DNS settings are, go ahead and update the `A` records | ||
| 99 | to match the public IP address of your VPS. For example: | ||
| 100 | |||
| 101 | ```txt | ||
| 102 | A example.com xxx.xxx.xxx.xxx | ||
| 103 | A subdomain xxx.xxx.xxx.xxx | ||
| 104 | CNAME www example.com. | ||
| 105 | ``` | ||
| 106 | |||
| 107 | If you have any other records that require updates, such as MX or TXT records | ||
| 108 | for a mail server, be sure to update those accordingly. Personally, I don't host | ||
| 109 | my own mail server. I route all mail on my custom domains to | ||
| 110 | [Migadu](https://www.migadu.com). Hosting your own email server can become | ||
| 111 | complex quickly and is not for beginners. | ||
| 112 | |||
| 113 | DNS changes can take up to 48 hours to propagate, so be sure to give it some | ||
| 114 | time before assuming you've made an error. | ||
| 115 | |||
| 116 | # Server Updates and Packages | ||
| 117 | |||
| 118 | Now that the DNS settings have been changed, let's set up our server while we | ||
| 119 | wait for the DNS to propagate. First up is to ssh into your server. If you've | ||
| 120 | signed up with a service like DigitalOcean, you can add your SSH key to your | ||
| 121 | account and to your VPS droplet so that you don't need a password in order to | ||
| 122 | SSH. | ||
| 123 | |||
| 124 | ```sh | ||
| 125 | ssh root@xxx.xxx.xxx.xxx | ||
| 126 | ``` | ||
| 127 | |||
| 128 | The VPS that is used in this blog post runs Ubuntu 20.04 with an Apache web | ||
| 129 | server. If you're working on a different operating system (OS) or want a | ||
| 130 | different web server, such as Nginx, you'll have to use different commands to | ||
| 131 | set it up. | ||
| 132 | |||
| 133 | First, let's update and upgrade our server. | ||
| 134 | |||
| 135 | **NOTE:** Since we have logged in to the server as `root` for now, we don't need | ||
| 136 | to use the `sudo` modifier before our commands. | ||
| 137 | |||
| 138 | ```sh | ||
| 139 | apt update && apt upgrade -y | ||
| 140 | ``` | ||
| 141 | |||
| 142 | # Create A User Account | ||
| 143 | |||
| 144 | While being able to use `root` can be beneficial at times, you shouldn't use | ||
| 145 | `root` unless you have to. | ||
| 146 | |||
| 147 | So let's set up a new user in our system. The `-m` option below tells the OS to | ||
| 148 | create a home directory for the new user. | ||
| 149 | |||
| 150 | ```sh | ||
| 151 | adduser USERNAME | ||
| 152 | ``` | ||
| 153 | |||
| 154 | Now, create a password for that user. | ||
| 155 | |||
| 156 | ```sh | ||
| 157 | passwd USERNAME | ||
| 158 | ``` | ||
| 159 | |||
| 160 | Finally, add the user to the sudoers file, so they can perform priveleged | ||
| 161 | commands. | ||
| 162 | |||
| 163 | ```sh | ||
| 164 | usermod -a -G sudo USERNAME | ||
| 165 | ``` | ||
| 166 | |||
| 167 | If you are using SSH keys and not passwords, you'll need to copy your SSH key | ||
| 168 | from your local machine to the VPS. If you haven't disabled password-based SSH | ||
| 169 | yet, the easiest way to do this is `ssh-copy-id` from your local computer (not | ||
| 170 | from the VPS): | ||
| 171 | |||
| 172 | ```sh | ||
| 173 | ssh-copy-id testuser@xxx.xxx.xxx.xxx | ||
| 174 | ``` | ||
| 175 | |||
| 176 | If you've disabled password-based SSH, you'll need to manually copy your SSH key | ||
| 177 | into the `~/.ssh/authorized_keys` file. | ||
| 178 | |||
| 179 | # Install Software | ||
| 180 | |||
| 181 | Our goal here is to host a web server, so the next step is to install the Apache | ||
| 182 | web server and any other packages we need. | ||
| 183 | |||
| 184 | From this point on, I will be logged in as a user (not `root`) and will need to | ||
| 185 | use the `sudo` modifier for most commands. | ||
| 186 | |||
| 187 | ```sh | ||
| 188 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | ||
| 189 | sudo apt install apache2 | ||
| 190 | ``` | ||
| 191 | |||
| 192 | If you need other language support, such as PHP, you'll need to install that | ||
| 193 | too. | ||
| 194 | |||
| 195 | ```sh | ||
| 196 | sudo apt install libapache2-mod-php php-dom | ||
| 197 | sudo a2enmod php | ||
| 198 | sudo systemctl restart apache2 | ||
| 199 | ``` | ||
| 200 | |||
| 201 | # Website Files & Folders | ||
| 202 | |||
| 203 | Next up is to create the directories for the domain(s) we want to be hosted on | ||
| 204 | this web server. | ||
| 205 | |||
| 206 | ```sh | ||
| 207 | cd /var/www | ||
| 208 | sudo mkdir example.com | ||
| 209 | ``` | ||
| 210 | |||
| 211 | We have a folder for `example.com` now, so let's add an `index.html` file and | ||
| 212 | put it within a specific `public_html` folder. You don't need this `public_html` | ||
| 213 | if you don't want it, but it helps with organizing items related to | ||
| 214 | `example.com` that you don't want to publish to the internet. | ||
| 215 | |||
| 216 | ```sh | ||
| 217 | cd example.com | ||
| 218 | sudo mkdir public_html && cd public_html | ||
| 219 | sudo nano index.html | ||
| 220 | ``` | ||
| 221 | |||
| 222 | You can put anything you want in this `index.html` file. If you can't think of | ||
| 223 | anything, paste this in there: | ||
| 224 | |||
| 225 | ```html | ||
| 226 | <!DOCTYPE html> | ||
| 227 | <html lang="en"> | ||
| 228 | <head> | ||
| 229 | <meta charset="utf-8" /> | ||
| 230 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 231 | <title>Hello, world!</title> | ||
| 232 | </head> | ||
| 233 | <body> | ||
| 234 | <h1>Hello, world!</h1> | ||
| 235 | </body> | ||
| 236 | </html> | ||
| 237 | ``` | ||
| 238 | |||
| 239 | If you want something to be served at `example.com/page01/file.txt`, you'll have | ||
| 240 | to create the `page01` directory under the `example.com` directory. For example: | ||
| 241 | |||
| 242 | ```sh | ||
| 243 | cd /var/www/example.com/public_html | ||
| 244 | sudo mkdir page01 | ||
| 245 | sudo nano file.txt | ||
| 246 | ``` | ||
| 247 | |||
| 248 | # Apache Configuration | ||
| 249 | |||
| 250 | Now, let's set up the files that will tell the server where to find the files | ||
| 251 | for `example.com`. We will copy the default configuration file and create our | ||
| 252 | own. | ||
| 253 | |||
| 254 | ```sh | ||
| 255 | cd /etc/apache2/sites-available | ||
| 256 | sudo cp 000-default.conf example.com.conf | ||
| 257 | sudo nano example.com.conf | ||
| 258 | ``` | ||
| 259 | |||
| 260 | This configuration file will have a few default lines, but you'll need to edit | ||
| 261 | it to look similar to this (settings may change based on your personal needs): | ||
| 262 | |||
| 263 | ```config | ||
| 264 | <VirtualHost *:80> | ||
| 265 | ServerAdmin your-email@email-provider.com | ||
| 266 | ServerName example.com | ||
| 267 | ServerAlias www.example.com | ||
| 268 | DocumentRoot /var/www/example.com/public_html | ||
| 269 | ErrorLog ${APACHE_LOG_DIR}/error.log | ||
| 270 | CustomLog ${APACHE_LOG_DIR}/access.log combined | ||
| 271 | </VirtualHost> | ||
| 272 | ``` | ||
| 273 | |||
| 274 | Now, enable the configuration for your new site, disable the default | ||
| 275 | configuration, and reload the web server. | ||
| 276 | |||
| 277 | ```sh | ||
| 278 | sudo a2ensite example.com.conf | ||
| 279 | sudo a2dissite 000-default.conf | ||
| 280 | sudo systemctl reload apache2 | ||
| 281 | ``` | ||
| 282 | |||
| 283 | You can always run a test to make sure no errors or warnings are found in your | ||
| 284 | configuration files. | ||
| 285 | |||
| 286 | ```sh | ||
| 287 | sudo apache2ctl configtest | ||
| 288 | ``` | ||
| 289 | |||
| 290 | Now, restart the web server entirely. After this, you should be able to browse | ||
| 291 | to `http://example.com` and see the HTML content you provided earlier. Note that | ||
| 292 | SSL/TLS has not been enabled yet, so you won't be able to use the secure version | ||
| 293 | yet (`https://example.com`). | ||
| 294 | |||
| 295 | ```sh | ||
| 296 | sudo systemctl restart apache2 | ||
| 297 | ``` | ||
| 298 | |||
| 299 | You can repeat this for as many websites as you need. Just create the domain | ||
| 300 | folders in `/var/www/`, add the configuration file, enable the configuration, | ||
| 301 | and restart `apache2`. | ||
| 302 | |||
| 303 | # SSL/TLS Certificates: Serve Websites Over HTTPS | ||
| 304 | |||
| 305 | In order to serve secure content, you'll need to obtain SSL/TLS certificates. | ||
| 306 | Luckily, there's a free tool called [Certbot](https://certbot.eff.org) that | ||
| 307 | helps us with the process. | ||
| 308 | |||
| 309 | The first step is to install `snapd` and `core` for Ubuntu. | ||
| 310 | |||
| 311 | ```sh | ||
| 312 | sudo apt install snapd | ||
| 313 | sudo snap install core | ||
| 314 | sudo snap refresh core | ||
| 315 | ``` | ||
| 316 | |||
| 317 | Next, install the `certbot` snap package. | ||
| 318 | |||
| 319 | ```sh | ||
| 320 | sudo snap install --classic certbot | ||
| 321 | ``` | ||
| 322 | |||
| 323 | Execute the following command to ensure that the `certbot` command can be run. | ||
| 324 | |||
| 325 | ```sh | ||
| 326 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 327 | ``` | ||
| 328 | |||
| 329 | Finally, you can run `certbot` one of two ways: | ||
| 330 | |||
| 331 | 1. run it and let it alter your Apache configuration files automatically to | ||
| 332 | enable HTTPS redirects. | ||
| 333 | 2. run it and only allow it to create certificates. You'll need to manually | ||
| 334 | alter the config files to enable HTTPS redirects. | ||
| 335 | |||
| 336 | Run certbot and allow automatic config changes: | ||
| 337 | |||
| 338 | ```sh | ||
| 339 | sudo certbot --apache | ||
| 340 | ``` | ||
| 341 | |||
| 342 | Run certbot for certificates only and don't allow it to alter config files: | ||
| 343 | |||
| 344 | ```sh | ||
| 345 | sudo certbot certonly --apache | ||
| 346 | ``` | ||
| 347 | |||
| 348 | The Certbot packages on your system come with a cron job or systemd timer that | ||
| 349 | will renew your certificates automatically before they expire. You will not need | ||
| 350 | to run Certbot again unless you change your configuration. You can test | ||
| 351 | automatic renewal for your certificates by running this command: | ||
| 352 | |||
| 353 | ```sh | ||
| 354 | sudo certbot renew --dry-run | ||
| 355 | ``` | ||
| 356 | |||
| 357 | Now, test your domains by going to `https://example.com`. | ||
| 358 | |||
| 359 | # Firewall Security | ||
| 360 | |||
| 361 | To enable better security on your server, you'll need to enable a basic | ||
| 362 | firewall. For Ubuntu, we'll use [the uncomplicated | ||
| 363 | firewall](../secure-your-network-with-the-uncomplicated-firewall/). | ||
| 364 | |||
| 365 | Now, add the following rules to the firewall allow SSH, Apache, and HTTP(S) | ||
| 366 | connections. If you need to, you can enable different ports for specifics | ||
| 367 | applications, SFTP, etc. | ||
| 368 | |||
| 369 | ```sh | ||
| 370 | sudo ufw default deny incoming | ||
| 371 | sudo ufw default allow outgoing | ||
| 372 | sudo ufw allow OpenSSH | ||
| 373 | sudo ufw allow Apache | ||
| 374 | sudo ufw allow proto tcp from any to any port 80,443 | ||
| 375 | ``` | ||
| 376 | |||
| 377 | Once you've added all the rules you need, enable the firewall. | ||
| 378 | |||
| 379 | ```sh | ||
| 380 | sudo ufw enable | ||
| 381 | ``` | ||
| 382 | |||
| 383 | # Troubleshooting | ||
| 384 | |||
| 385 | If you run into any issues during your VPS set-up, be sure to walk back through | ||
| 386 | your actions and make sure you didn't miss any steps. | ||
| 387 | |||
| 388 | Many websites have fantastic guides to setting up various web servers. This is | ||
| 389 | one of the areas [where DigitalOcean | ||
| 390 | shines](https://www.digitalocean.com/community/tutorials). For simpler or | ||
| 391 | more Linux-oriented questions, I suggest using [Linuxize] | ||
| 392 | (<https://linuxize.com>). | ||
| 393 | |||
| 394 | If you're getting certain errors (e.g. `500 Internal Server Error`) and need to | ||
| 395 | debug locally, you can view the `access.log` and `error.log` files in the | ||
| 396 | `/var/log/apache/` directory. | ||
content/blog/2021-03-30-vps-web-server.org added +411
| @@ -0,0 +1,411 @@ | |||
| 1 | #+date: <2021-03-30> | ||
| 2 | #+title: How to Set Up a VPS Web Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Shared Hosting vs. VPS | ||
| 7 | |||
| 8 | Choosing a place to host a website is one of the most confusing | ||
| 9 | decisions for beginner web developers. Even for experienced web devs, | ||
| 10 | choosing between different forms of web hosting can be a daunting | ||
| 11 | choice. | ||
| 12 | |||
| 13 | First, let's take a look at | ||
| 14 | [[https://en.wikipedia.org/wiki/Shared_web_hosting_service][shared web | ||
| 15 | hosting]]. Shared web hosting is a product where you are purchasing a | ||
| 16 | small piece of a web server that is being shared between many websites. | ||
| 17 | As a result, the cost of shared hosting is extremely low. You won't have | ||
| 18 | access to the server itself, which means you can't install your own | ||
| 19 | software on the server, such as Docker. Usually, you are simply allowed | ||
| 20 | to connect your domains to the server, set up domain security, and other | ||
| 21 | small utilities. | ||
| 22 | |||
| 23 | In contrast, a | ||
| 24 | [[https://en.wikipedia.org/wiki/Virtual_private_server][virtual private | ||
| 25 | server]] (VPS) is a virtual machine that replicates the environment of | ||
| 26 | having a dedicated server to yourself. You are able to control the | ||
| 27 | virtual server's host names, base file system, package manager, etc. | ||
| 28 | Another great upside of a VPS is that since it's virtual, the company | ||
| 29 | providing the VPS can dynamically increase the disk size, RAM size, or | ||
| 30 | number of CPUs at any time. However, the virtual server is still | ||
| 31 | physically located on a server that is shared between multiple virtual | ||
| 32 | servers. | ||
| 33 | |||
| 34 | The choice between shared hosting and VPS mostly depends on your skill | ||
| 35 | level with system administration. If you're comforting working on a | ||
| 36 | server that is mostly left up to you (or you're willing to learn), then | ||
| 37 | a VPS is usually a better option. However, shared hosting is a fantastic | ||
| 38 | option for people who don't want to have to learn how to manage their | ||
| 39 | server. | ||
| 40 | |||
| 41 | * My Situation | ||
| 42 | |||
| 43 | I had used shared hosting for approximately 5 years before trying my | ||
| 44 | first VPS. I manage a homelab and have had success running a server and | ||
| 45 | performing typical sysadmin duties, but I was still hesitant to get a | ||
| 46 | VPS. One fear was that I always struggled to properly set up the | ||
| 47 | networking part of a server - DNS and hostname configurations were not | ||
| 48 | my friend. | ||
| 49 | |||
| 50 | As a little bit of background, I originally used | ||
| 51 | [[https://www.siteground.com][Siteground]] for my initially shared | ||
| 52 | hosting and stayed on that platform for at least a year. However, the UI | ||
| 53 | was clunky, and I didn't like how they handled certain technical | ||
| 54 | aspects, so I switched to [[https://www.namecheap.com][Namecheap]]. | ||
| 55 | Namecheap was great because it is the service I primarily use for | ||
| 56 | purchasing domain names, which made it incredibly easy to link them to | ||
| 57 | my hosting service. However, it was still mediocre shared hosting, and | ||
| 58 | Namecheap is notorious for not letting you use | ||
| 59 | [[https://letsencrypt.org][Let's Encrypt]] to obtain free SSL/TLS | ||
| 60 | certificates; Namecheap wants to make you purchase certificates through | ||
| 61 | their store. | ||
| 62 | |||
| 63 | Finally, I settled down with [[https://www.iwebfusion.net][iWebFusion]] | ||
| 64 | for about the last year of my shared hosting. This service was pretty | ||
| 65 | great, came with free SSL/TLS, and I never had any complaints. | ||
| 66 | |||
| 67 | However, I finally grew tired of not being able to install software on | ||
| 68 | my own web server. I wanted to be able to try out things like | ||
| 69 | [[https://postmill.xyz][Postmill]] or [[https://matrix.org][Matrix]]. | ||
| 70 | This is possible with a VPS, so I decided to grab a new domain name to | ||
| 71 | try it out. | ||
| 72 | |||
| 73 | * Getting Started: Buying a VPS | ||
| 74 | |||
| 75 | The first step to moving over to a VPS is (you guessed it): finding a | ||
| 76 | VPS provider. For my VPSs, I use [[https://1984hosting.com][1984]] and | ||
| 77 | prefer their services much more than any alternative, due to their | ||
| 78 | location (Iceland), their [[https://1984hosting.com/GDPR/][privacy | ||
| 79 | policy]], their respect for GDPR, and the ability to remain anonymous if | ||
| 80 | you pay in Bitcoin or Monero. | ||
| 81 | |||
| 82 | [[https://njal.la][Njalla]] is another good, privacy-oriented option for | ||
| 83 | VPS services. | ||
| 84 | |||
| 85 | You'll have to decide what specifications you want on your VPS. For me, | ||
| 86 | I only build and deploy low-resource HTML, PHP, and Python websites. | ||
| 87 | This means I can survive on the smallest VPS: 1 CPU, 1GB of RAM, and | ||
| 88 | 25GB SSD for $5.00 per month. | ||
| 89 | |||
| 90 | As noted above, the great thing about a VPS is you can request your | ||
| 91 | provider to increase the resources at any time. | ||
| 92 | |||
| 93 | * Configuring DNS Settings | ||
| 94 | |||
| 95 | Okay, so now let's get into some actual work that has to be done to get | ||
| 96 | content moved from a shared host to a VPS. At this point, I'm assuming | ||
| 97 | you have a shared host with website content that you can still access, | ||
| 98 | and you've purchased a new VPS and can SSH into that server. | ||
| 99 | |||
| 100 | The first change is minor, but it should be done immediately in order to | ||
| 101 | get things moving: DNS settings. Go to wherever your DNS settings are | ||
| 102 | handled. If your shared host also managed your DNS settings, you'll need | ||
| 103 | to first move that DNS over to your new VPS provider. For me, I route my | ||
| 104 | DNS through [[https://www.gandi.net][Gandi]]. | ||
| 105 | |||
| 106 | Once you know where your DNS settings are, go ahead and update the =A= | ||
| 107 | records to match the public IP address of your VPS. For example: | ||
| 108 | |||
| 109 | #+begin_src txt | ||
| 110 | A example.com xxx.xxx.xxx.xxx | ||
| 111 | A subdomain xxx.xxx.xxx.xxx | ||
| 112 | CNAME www example.com. | ||
| 113 | #+end_src | ||
| 114 | |||
| 115 | If you have any other records that require updates, such as MX or TXT | ||
| 116 | records for a mail server, be sure to update those accordingly. | ||
| 117 | Personally, I don't host my own mail server. I route all mail on my | ||
| 118 | custom domains to [[https://www.migadu.com][Migadu]]. Hosting your own | ||
| 119 | email server can become complex quickly and is not for beginners. | ||
| 120 | |||
| 121 | DNS changes can take up to 48 hours to propagate, so be sure to give it | ||
| 122 | some time before assuming you've made an error. | ||
| 123 | |||
| 124 | * Server Updates and Packages | ||
| 125 | |||
| 126 | Now that the DNS settings have been changed, let's set up our server | ||
| 127 | while we wait for the DNS to propagate. First up is to ssh into your | ||
| 128 | server. If you've signed up with a service like DigitalOcean, you can | ||
| 129 | add your SSH key to your account and to your VPS droplet so that you | ||
| 130 | don't need a password in order to SSH. | ||
| 131 | |||
| 132 | #+begin_src sh | ||
| 133 | ssh root@xxx.xxx.xxx.xxx | ||
| 134 | #+end_src | ||
| 135 | |||
| 136 | The VPS that is used in this blog post runs Ubuntu 20.04 with an Apache | ||
| 137 | web server. If you're working on a different operating system (OS) or | ||
| 138 | want a different web server, such as Nginx, you'll have to use different | ||
| 139 | commands to set it up. | ||
| 140 | |||
| 141 | First, let's update and upgrade our server. | ||
| 142 | |||
| 143 | *NOTE:* Since we have logged in to the server as =root= for now, we | ||
| 144 | don't need to use the =sudo= modifier before our commands. | ||
| 145 | |||
| 146 | #+begin_src sh | ||
| 147 | apt update && apt upgrade -y | ||
| 148 | #+end_src | ||
| 149 | |||
| 150 | * Create A User Account | ||
| 151 | |||
| 152 | While being able to use =root= can be beneficial at times, you shouldn't | ||
| 153 | use =root= unless you have to. | ||
| 154 | |||
| 155 | So let's set up a new user in our system. The =-m= option below tells | ||
| 156 | the OS to create a home directory for the new user. | ||
| 157 | |||
| 158 | #+begin_src sh | ||
| 159 | adduser USERNAME | ||
| 160 | #+end_src | ||
| 161 | |||
| 162 | Now, create a password for that user. | ||
| 163 | |||
| 164 | #+begin_src sh | ||
| 165 | passwd USERNAME | ||
| 166 | #+end_src | ||
| 167 | |||
| 168 | Finally, add the user to the sudoers file, so they can perform | ||
| 169 | priveleged commands. | ||
| 170 | |||
| 171 | #+begin_src sh | ||
| 172 | usermod -a -G sudo USERNAME | ||
| 173 | #+end_src | ||
| 174 | |||
| 175 | If you are using SSH keys and not passwords, you'll need to copy your | ||
| 176 | SSH key from your local machine to the VPS. If you haven't disabled | ||
| 177 | password-based SSH yet, the easiest way to do this is =ssh-copy-id= from | ||
| 178 | your local computer (not from the VPS): | ||
| 179 | |||
| 180 | #+begin_src sh | ||
| 181 | ssh-copy-id testuser@xxx.xxx.xxx.xxx | ||
| 182 | #+end_src | ||
| 183 | |||
| 184 | If you've disabled password-based SSH, you'll need to manually copy your | ||
| 185 | SSH key into the =~/.ssh/authorized_keys= file. | ||
| 186 | |||
| 187 | * Install Software | ||
| 188 | |||
| 189 | Our goal here is to host a web server, so the next step is to install | ||
| 190 | the Apache web server and any other packages we need. | ||
| 191 | |||
| 192 | From this point on, I will be logged in as a user (not =root=) and will | ||
| 193 | need to use the =sudo= modifier for most commands. | ||
| 194 | |||
| 195 | #+begin_src sh | ||
| 196 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | ||
| 197 | sudo apt install apache2 | ||
| 198 | #+end_src | ||
| 199 | |||
| 200 | If you need other language support, such as PHP, you'll need to install | ||
| 201 | that too. | ||
| 202 | |||
| 203 | #+begin_src sh | ||
| 204 | sudo apt install libapache2-mod-php php-dom | ||
| 205 | sudo a2enmod php | ||
| 206 | sudo systemctl restart apache2 | ||
| 207 | #+end_src | ||
| 208 | |||
| 209 | * Website Files & Folders | ||
| 210 | |||
| 211 | Next up is to create the directories for the domain(s) we want to be | ||
| 212 | hosted on this web server. | ||
| 213 | |||
| 214 | #+begin_src sh | ||
| 215 | cd /var/www | ||
| 216 | sudo mkdir example.com | ||
| 217 | #+end_src | ||
| 218 | |||
| 219 | We have a folder for =example.com= now, so let's add an =index.html= | ||
| 220 | file and put it within a specific =public_html= folder. You don't need | ||
| 221 | this =public_html= if you don't want it, but it helps with organizing | ||
| 222 | items related to =example.com= that you don't want to publish to the | ||
| 223 | internet. | ||
| 224 | |||
| 225 | #+begin_src sh | ||
| 226 | cd example.com | ||
| 227 | sudo mkdir public_html && cd public_html | ||
| 228 | sudo nano index.html | ||
| 229 | #+end_src | ||
| 230 | |||
| 231 | You can put anything you want in this =index.html= file. If you can't | ||
| 232 | think of anything, paste this in there: | ||
| 233 | |||
| 234 | #+begin_src html | ||
| 235 | <!DOCTYPE html> | ||
| 236 | <html lang="en"> | ||
| 237 | <head> | ||
| 238 | <meta charset="utf-8" /> | ||
| 239 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 240 | <title>Hello, world!</title> | ||
| 241 | </head> | ||
| 242 | <body> | ||
| 243 | <h1>Hello, world!</h1> | ||
| 244 | </body> | ||
| 245 | </html> | ||
| 246 | #+end_src | ||
| 247 | |||
| 248 | If you want something to be served at =example.com/page01/file.txt=, | ||
| 249 | you'll have to create the =page01= directory under the =example.com= | ||
| 250 | directory. For example: | ||
| 251 | |||
| 252 | #+begin_src sh | ||
| 253 | cd /var/www/example.com/public_html | ||
| 254 | sudo mkdir page01 | ||
| 255 | sudo nano file.txt | ||
| 256 | #+end_src | ||
| 257 | |||
| 258 | * Apache Configuration | ||
| 259 | |||
| 260 | Now, let's set up the files that will tell the server where to find the | ||
| 261 | files for =example.com=. We will copy the default configuration file and | ||
| 262 | create our own. | ||
| 263 | |||
| 264 | #+begin_src sh | ||
| 265 | cd /etc/apache2/sites-available | ||
| 266 | sudo cp 000-default.conf example.com.conf | ||
| 267 | sudo nano example.com.conf | ||
| 268 | #+end_src | ||
| 269 | |||
| 270 | This configuration file will have a few default lines, but you'll need | ||
| 271 | to edit it to look similar to this (settings may change based on your | ||
| 272 | personal needs): | ||
| 273 | |||
| 274 | #+begin_src config | ||
| 275 | <VirtualHost *:80> | ||
| 276 | ServerAdmin your-email@email-provider.com | ||
| 277 | ServerName example.com | ||
| 278 | ServerAlias www.example.com | ||
| 279 | DocumentRoot /var/www/example.com/public_html | ||
| 280 | ErrorLog ${APACHE_LOG_DIR}/error.log | ||
| 281 | CustomLog ${APACHE_LOG_DIR}/access.log combined | ||
| 282 | </VirtualHost> | ||
| 283 | #+end_src | ||
| 284 | |||
| 285 | Now, enable the configuration for your new site, disable the default | ||
| 286 | configuration, and reload the web server. | ||
| 287 | |||
| 288 | #+begin_src sh | ||
| 289 | sudo a2ensite example.com.conf | ||
| 290 | sudo a2dissite 000-default.conf | ||
| 291 | sudo systemctl reload apache2 | ||
| 292 | #+end_src | ||
| 293 | |||
| 294 | You can always run a test to make sure no errors or warnings are found | ||
| 295 | in your configuration files. | ||
| 296 | |||
| 297 | #+begin_src sh | ||
| 298 | sudo apache2ctl configtest | ||
| 299 | #+end_src | ||
| 300 | |||
| 301 | Now, restart the web server entirely. After this, you should be able to | ||
| 302 | browse to =http://example.com= and see the HTML content you provided | ||
| 303 | earlier. Note that SSL/TLS has not been enabled yet, so you won't be | ||
| 304 | able to use the secure version yet (=https://example.com=). | ||
| 305 | |||
| 306 | #+begin_src sh | ||
| 307 | sudo systemctl restart apache2 | ||
| 308 | #+end_src | ||
| 309 | |||
| 310 | You can repeat this for as many websites as you need. Just create the | ||
| 311 | domain folders in =/var/www/=, add the configuration file, enable the | ||
| 312 | configuration, and restart =apache2=. | ||
| 313 | |||
| 314 | * SSL/TLS Certificates: Serve Websites Over HTTPS | ||
| 315 | |||
| 316 | In order to serve secure content, you'll need to obtain SSL/TLS | ||
| 317 | certificates. Luckily, there's a free tool called | ||
| 318 | [[https://certbot.eff.org][Certbot]] that helps us with the process. | ||
| 319 | |||
| 320 | The first step is to install =snapd= and =core= for Ubuntu. | ||
| 321 | |||
| 322 | #+begin_src sh | ||
| 323 | sudo apt install snapd | ||
| 324 | sudo snap install core | ||
| 325 | sudo snap refresh core | ||
| 326 | #+end_src | ||
| 327 | |||
| 328 | Next, install the =certbot= snap package. | ||
| 329 | |||
| 330 | #+begin_src sh | ||
| 331 | sudo snap install --classic certbot | ||
| 332 | #+end_src | ||
| 333 | |||
| 334 | Execute the following command to ensure that the =certbot= command can | ||
| 335 | be run. | ||
| 336 | |||
| 337 | #+begin_src sh | ||
| 338 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 339 | #+end_src | ||
| 340 | |||
| 341 | Finally, you can run =certbot= one of two ways: | ||
| 342 | |||
| 343 | 1. run it and let it alter your Apache configuration files automatically | ||
| 344 | to enable HTTPS redirects. | ||
| 345 | 2. run it and only allow it to create certificates. You'll need to | ||
| 346 | manually alter the config files to enable HTTPS redirects. | ||
| 347 | |||
| 348 | Run certbot and allow automatic config changes: | ||
| 349 | |||
| 350 | #+begin_src sh | ||
| 351 | sudo certbot --apache | ||
| 352 | #+end_src | ||
| 353 | |||
| 354 | Run certbot for certificates only and don't allow it to alter config | ||
| 355 | files: | ||
| 356 | |||
| 357 | #+begin_src sh | ||
| 358 | sudo certbot certonly --apache | ||
| 359 | #+end_src | ||
| 360 | |||
| 361 | The Certbot packages on your system come with a cron job or systemd | ||
| 362 | timer that will renew your certificates automatically before they | ||
| 363 | expire. You will not need to run Certbot again unless you change your | ||
| 364 | configuration. You can test automatic renewal for your certificates by | ||
| 365 | running this command: | ||
| 366 | |||
| 367 | #+begin_src sh | ||
| 368 | sudo certbot renew --dry-run | ||
| 369 | #+end_src | ||
| 370 | |||
| 371 | Now, test your domains by going to =https://example.com=. | ||
| 372 | |||
| 373 | * Firewall Security | ||
| 374 | |||
| 375 | To enable better security on your server, you'll need to enable a basic | ||
| 376 | firewall. For Ubuntu, we'll use | ||
| 377 | [[../secure-your-network-with-the-uncomplicated-firewall/][the | ||
| 378 | uncomplicated firewall]]. | ||
| 379 | |||
| 380 | Now, add the following rules to the firewall allow SSH, Apache, and | ||
| 381 | HTTP(S) connections. If you need to, you can enable different ports for | ||
| 382 | specifics applications, SFTP, etc. | ||
| 383 | |||
| 384 | #+begin_src sh | ||
| 385 | sudo ufw default deny incoming | ||
| 386 | sudo ufw default allow outgoing | ||
| 387 | sudo ufw allow OpenSSH | ||
| 388 | sudo ufw allow Apache | ||
| 389 | sudo ufw allow proto tcp from any to any port 80,443 | ||
| 390 | #+end_src | ||
| 391 | |||
| 392 | Once you've added all the rules you need, enable the firewall. | ||
| 393 | |||
| 394 | #+begin_src sh | ||
| 395 | sudo ufw enable | ||
| 396 | #+end_src | ||
| 397 | |||
| 398 | * Troubleshooting | ||
| 399 | |||
| 400 | If you run into any issues during your VPS set-up, be sure to walk back | ||
| 401 | through your actions and make sure you didn't miss any steps. | ||
| 402 | |||
| 403 | Many websites have fantastic guides to setting up various web servers. | ||
| 404 | This is one of the areas | ||
| 405 | [[https://www.digitalocean.com/community/tutorials][where DigitalOcean | ||
| 406 | shines]]. For simpler or more Linux-oriented questions, I suggest using | ||
| 407 | [Linuxize] ([[https://linuxize.com]]). | ||
| 408 | |||
| 409 | If you're getting certain errors (e.g. =500 Internal Server Error=) and | ||
| 410 | need to debug locally, you can view the =access.log= and =error.log= | ||
| 411 | files in the =/var/log/apache/= directory. | ||
content/blog/2021-04-17-gemini-server.md deleted −150
| @@ -1,150 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-04-17 | ||
| 3 | title = "Hosting a Gemini Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Similar Article Available | ||
| 9 | |||
| 10 | To read more about Gemini and ways to test out this new protocol without your | ||
| 11 | own server, see my previous post [Launching a Gemini | ||
| 12 | Capsule](../launching-a-gemini-capsule/). | ||
| 13 | |||
| 14 | # Preparation | ||
| 15 | |||
| 16 | This guide assumes you have access to a server accessible to the world through a | ||
| 17 | public IP address and that you own a domain name used for this Gemini capsule. | ||
| 18 | |||
| 19 | # Getting Started with Agate | ||
| 20 | |||
| 21 | We are going to use [Agate](https://github.com/mbrubeck/agate) for this | ||
| 22 | tutorial. This is a basic Gemini server written in Rust. It takes very little | ||
| 23 | time and maintenance to get it running. | ||
| 24 | |||
| 25 | # Install Dependencies | ||
| 26 | |||
| 27 | First, you will need to install the Rust package for your system. On Ubuntu, use | ||
| 28 | the following commands (remember to use `sudo` if you are not the root user). | ||
| 29 | The Rust installation will give you options to customize the installation; I | ||
| 30 | used the default installation options. | ||
| 31 | |||
| 32 | ```sh | ||
| 33 | sudo apt update && sudo apt upgrade -y | ||
| 34 | curl https://sh.rustup.rs -sSf | sh | ||
| 35 | ``` | ||
| 36 | |||
| 37 | Remember to configure your shell with the new configuration: | ||
| 38 | |||
| 39 | ```sh | ||
| 40 | source $HOME/.cargo/env | ||
| 41 | ``` | ||
| 42 | |||
| 43 | Before we install agate, make sure you have the `gcc` package installed: | ||
| 44 | |||
| 45 | ```sh | ||
| 46 | sudo apt install gcc | ||
| 47 | ``` | ||
| 48 | |||
| 49 | Next, you'll need to install the agate executable with Rust's Cargo package | ||
| 50 | maintainer: | ||
| 51 | |||
| 52 | ```sh | ||
| 53 | cargo install agate | ||
| 54 | ``` | ||
| 55 | |||
| 56 | # Create Symlinks | ||
| 57 | |||
| 58 | Once Cargo has finished installing all the required packages, symlink the | ||
| 59 | executable to your $PATH. | ||
| 60 | |||
| 61 | ```sh | ||
| 62 | sudo ln -s $HOME/.cargo/bin/agate /usr/local/bin/agate | ||
| 63 | ``` | ||
| 64 | |||
| 65 | # Using Agate's Built-In Installation Tool | ||
| 66 | |||
| 67 | If you're running Ubuntu or Debian, use the Debian installation script found in | ||
| 68 | Agate's GitHub repository, under the `tools/debian` folder. | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | git clone https://github.com/mbrubeck/agate | ||
| 72 | cd agate/tools/debian | ||
| 73 | sudo ./install.sh | ||
| 74 | ``` | ||
| 75 | |||
| 76 | # Configure the Gemini Service | ||
| 77 | |||
| 78 | We have a little more to do, but since this script tries to immediately run the | ||
| 79 | service, it will likely fail with an exit code. Let's add our finishing touches. | ||
| 80 | Edit the following file and replace the hostname with your desired URL. You can | ||
| 81 | also change the directory where content will be served. | ||
| 82 | |||
| 83 | ```sh | ||
| 84 | sudo nano /etc/systemd/system/gemini.service | ||
| 85 | ``` | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | # Edit these lines to whatever you want - see the next code block for my personal configuration. | ||
| 89 | WorkingDirectory=/srv/gemini | ||
| 90 | ExecStart=agate --hostname $(uname -n) --lang en | ||
| 91 | ``` | ||
| 92 | |||
| 93 | This is my personal config: | ||
| 94 | |||
| 95 | ```sh | ||
| 96 | WorkingDirectory=/var/gemini/ | ||
| 97 | ExecStart=agate --hostname gemini.example.com --lang en | ||
| 98 | ``` | ||
| 99 | |||
| 100 | Since we've altered the systemd configuration files, we have to reload the | ||
| 101 | daemon. Let's do that, restart our service, and check its status. | ||
| 102 | |||
| 103 | ```sh | ||
| 104 | sudo systemctl daemon-reload | ||
| 105 | sudo systemctl restart gemini.service | ||
| 106 | sudo systemctl status gemini.service | ||
| 107 | ``` | ||
| 108 | |||
| 109 | # Fixing Systemd Errors | ||
| 110 | |||
| 111 | If you're still getting errors, the installation process may not have properly | ||
| 112 | enabled the gemini service. Fix it with the following commands. | ||
| 113 | |||
| 114 | ```sh | ||
| 115 | sudo systemctl enable gemini.service | ||
| 116 | sudo systemctl restart gemini.service | ||
| 117 | sudo systemctl status gemini.service | ||
| 118 | ``` | ||
| 119 | |||
| 120 | # Firewall Rules | ||
| 121 | |||
| 122 | Great! Our server is now functional and running. The first consideration now is | ||
| 123 | that you need to be able to access port 1965 on the server. If you have a | ||
| 124 | firewall enabled, you'll need to open that port up. | ||
| 125 | |||
| 126 | ```sh | ||
| 127 | sudo ufw allow 1965 | ||
| 128 | sudo ufw reload | ||
| 129 | ``` | ||
| 130 | |||
| 131 | # Creating Content | ||
| 132 | |||
| 133 | Let's create the Gemini capsule. Note that wherever you set the WorkingDirectory | ||
| 134 | variable to earlier, Agate will expect you to put your Gemini capsule contents | ||
| 135 | in a sub-folder called "content." So, I place my files in "/var/gmi/content." | ||
| 136 | I'm going to create that folder now and put a file in there. | ||
| 137 | |||
| 138 | ```sh | ||
| 139 | sudo mkdir /var/gemini/content | ||
| 140 | sudo nano /var/gemini/content/index.gmi | ||
| 141 | ``` | ||
| 142 | |||
| 143 | You can put whatever you want in the "index.gmi" file, just make sure it's valid | ||
| 144 | Gemtext. | ||
| 145 | |||
| 146 | # The Results | ||
| 147 | |||
| 148 | To view the results, you can use a Gemini browser, such as | ||
| 149 | [Lagrange](https://gmi.skyjake.fi/lagrange/) or | ||
| 150 | [amfora](https://github.com/makeworld-the-better-one/amfora). | ||
content/blog/2021-04-17-gemini-server.org added +151
| @@ -0,0 +1,151 @@ | |||
| 1 | #+date: <2021-04-17> | ||
| 2 | #+title: Hosting a Gemini Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Similar Article Available | ||
| 7 | |||
| 8 | To read more about Gemini and ways to test out this new protocol without | ||
| 9 | your own server, see my previous post | ||
| 10 | [[../launching-a-gemini-capsule/][Launching a Gemini Capsule]]. | ||
| 11 | |||
| 12 | * Preparation | ||
| 13 | |||
| 14 | This guide assumes you have access to a server accessible to the world | ||
| 15 | through a public IP address and that you own a domain name used for this | ||
| 16 | Gemini capsule. | ||
| 17 | |||
| 18 | * Getting Started with Agate | ||
| 19 | |||
| 20 | We are going to use [[https://github.com/mbrubeck/agate][Agate]] for | ||
| 21 | this tutorial. This is a basic Gemini server written in Rust. It takes | ||
| 22 | very little time and maintenance to get it running. | ||
| 23 | |||
| 24 | * Install Dependencies | ||
| 25 | |||
| 26 | First, you will need to install the Rust package for your system. On | ||
| 27 | Ubuntu, use the following commands (remember to use =sudo= if you are | ||
| 28 | not the root user). The Rust installation will give you options to | ||
| 29 | customize the installation; I used the default installation options. | ||
| 30 | |||
| 31 | #+begin_src sh | ||
| 32 | sudo apt update && sudo apt upgrade -y | ||
| 33 | curl https://sh.rustup.rs -sSf | sh | ||
| 34 | #+end_src | ||
| 35 | |||
| 36 | Remember to configure your shell with the new configuration: | ||
| 37 | |||
| 38 | #+begin_src sh | ||
| 39 | source $HOME/.cargo/env | ||
| 40 | #+end_src | ||
| 41 | |||
| 42 | Before we install agate, make sure you have the =gcc= package installed: | ||
| 43 | |||
| 44 | #+begin_src sh | ||
| 45 | sudo apt install gcc | ||
| 46 | #+end_src | ||
| 47 | |||
| 48 | Next, you'll need to install the agate executable with Rust's Cargo | ||
| 49 | package maintainer: | ||
| 50 | |||
| 51 | #+begin_src sh | ||
| 52 | cargo install agate | ||
| 53 | #+end_src | ||
| 54 | |||
| 55 | * Create Symlinks | ||
| 56 | |||
| 57 | Once Cargo has finished installing all the required packages, symlink | ||
| 58 | the executable to your $PATH. | ||
| 59 | |||
| 60 | #+begin_src sh | ||
| 61 | sudo ln -s $HOME/.cargo/bin/agate /usr/local/bin/agate | ||
| 62 | #+end_src | ||
| 63 | |||
| 64 | * Using Agate's Built-In Installation Tool | ||
| 65 | |||
| 66 | If you're running Ubuntu or Debian, use the Debian installation script | ||
| 67 | found in Agate's GitHub repository, under the =tools/debian= folder. | ||
| 68 | |||
| 69 | #+begin_src sh | ||
| 70 | git clone https://github.com/mbrubeck/agate | ||
| 71 | cd agate/tools/debian | ||
| 72 | sudo ./install.sh | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | * Configure the Gemini Service | ||
| 76 | |||
| 77 | We have a little more to do, but since this script tries to immediately | ||
| 78 | run the service, it will likely fail with an exit code. Let's add our | ||
| 79 | finishing touches. Edit the following file and replace the hostname with | ||
| 80 | your desired URL. You can also change the directory where content will | ||
| 81 | be served. | ||
| 82 | |||
| 83 | #+begin_src sh | ||
| 84 | sudo nano /etc/systemd/system/gemini.service | ||
| 85 | #+end_src | ||
| 86 | |||
| 87 | #+begin_src sh | ||
| 88 | # Edit these lines to whatever you want - see the next code block for my personal configuration. | ||
| 89 | WorkingDirectory=/srv/gemini | ||
| 90 | ExecStart=agate --hostname $(uname -n) --lang en | ||
| 91 | #+end_src | ||
| 92 | |||
| 93 | This is my personal config: | ||
| 94 | |||
| 95 | #+begin_src sh | ||
| 96 | WorkingDirectory=/var/gemini/ | ||
| 97 | ExecStart=agate --hostname gemini.example.com --lang en | ||
| 98 | #+end_src | ||
| 99 | |||
| 100 | Since we've altered the systemd configuration files, we have to reload | ||
| 101 | the daemon. Let's do that, restart our service, and check its status. | ||
| 102 | |||
| 103 | #+begin_src sh | ||
| 104 | sudo systemctl daemon-reload | ||
| 105 | sudo systemctl restart gemini.service | ||
| 106 | sudo systemctl status gemini.service | ||
| 107 | #+end_src | ||
| 108 | |||
| 109 | * Fixing Systemd Errors | ||
| 110 | |||
| 111 | If you're still getting errors, the installation process may not have | ||
| 112 | properly enabled the gemini service. Fix it with the following commands. | ||
| 113 | |||
| 114 | #+begin_src sh | ||
| 115 | sudo systemctl enable gemini.service | ||
| 116 | sudo systemctl restart gemini.service | ||
| 117 | sudo systemctl status gemini.service | ||
| 118 | #+end_src | ||
| 119 | |||
| 120 | * Firewall Rules | ||
| 121 | |||
| 122 | Great! Our server is now functional and running. The first consideration | ||
| 123 | now is that you need to be able to access port 1965 on the server. If | ||
| 124 | you have a firewall enabled, you'll need to open that port up. | ||
| 125 | |||
| 126 | #+begin_src sh | ||
| 127 | sudo ufw allow 1965 | ||
| 128 | sudo ufw reload | ||
| 129 | #+end_src | ||
| 130 | |||
| 131 | * Creating Content | ||
| 132 | |||
| 133 | Let's create the Gemini capsule. Note that wherever you set the | ||
| 134 | WorkingDirectory variable to earlier, Agate will expect you to put your | ||
| 135 | Gemini capsule contents in a sub-folder called "content." So, I place my | ||
| 136 | files in "/var/gmi/content." I'm going to create that folder now and put | ||
| 137 | a file in there. | ||
| 138 | |||
| 139 | #+begin_src sh | ||
| 140 | sudo mkdir /var/gemini/content | ||
| 141 | sudo nano /var/gemini/content/index.gmi | ||
| 142 | #+end_src | ||
| 143 | |||
| 144 | You can put whatever you want in the "index.gmi" file, just make sure | ||
| 145 | it's valid Gemtext. | ||
| 146 | |||
| 147 | * The Results | ||
| 148 | |||
| 149 | To view the results, you can use a Gemini browser, such as | ||
| 150 | [[https://gmi.skyjake.fi/lagrange/][Lagrange]] or | ||
| 151 | [[https://github.com/makeworld-the-better-one/amfora][amfora]]. | ||
content/blog/2021-04-23-php-comment-system.md deleted −261
| @@ -1,261 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-04-23 | ||
| 3 | title = "Roll Your Own Static Commenting System in PHP" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Terrible-ness of Commenting Systems | ||
| 9 | |||
| 10 | The current state of affairs regarding interactive comment systems is, well, | ||
| 11 | terrible. It is especially awful if you're a privacy conscious person who does | ||
| 12 | not generally load third-party scripts or frames on the websites you visit. | ||
| 13 | |||
| 14 | Even further, many comment systems are charging exorbitant fees for something | ||
| 15 | that should be standard. | ||
| 16 | |||
| 17 | Of course, there are some really terrible options: | ||
| 18 | |||
| 19 | - Facebook Comments | ||
| 20 | - Discourse | ||
| 21 | |||
| 22 | There are some options that are better but still use too many scripts, frames, | ||
| 23 | or social integrations on your web page that could impact some users: | ||
| 24 | |||
| 25 | - Disqus | ||
| 26 | - Isso | ||
| 27 | - Remark42 | ||
| 28 | |||
| 29 | Lastly, I looked into a few unique ways of generating blog comments, such as | ||
| 30 | using Twitter threads or GitHub issues to automatically post issues. However, | ||
| 31 | these both rely on external third-party sites that I don't currently use. | ||
| 32 | |||
| 33 | # Stay Static with Server-Side Comments | ||
| 34 | |||
| 35 | The main issue for my personal use-case is that my blog is completely, 100% | ||
| 36 | static. I use PHP on the back-end but website visitors only see HTML and a | ||
| 37 | single CSS file. No external javascript and no embedded frames. | ||
| 38 | |||
| 39 | So, how do we keep a site static and still allow users to interact with blog | ||
| 40 | posts? The key actually pretty simple - I'm already using PHP, so why not rely | ||
| 41 | on the classic HTML `<form>` and a PHP script to save the comments somewhere? As | ||
| 42 | it turns out, this was a perfect solution for me. | ||
| 43 | |||
| 44 | The second issue for my personal use-case is that I am trying to keep the | ||
| 45 | contents of my website accessible over time, as described by @brandur, in his | ||
| 46 | post entitled [Blog with Markdown + Git, and degrade gracefully through | ||
| 47 | time](https://brandur.org/fragments/graceful-degradation-time) . | ||
| 48 | |||
| 49 | This means I cannot rely on a database for comments, since I do not rely on a | ||
| 50 | database for any other part of my websites. | ||
| 51 | |||
| 52 | I blog in plain Markdown files, commit all articles to Git, and ensure that | ||
| 53 | future readers will be able to see the source data long after I'm gone, or the | ||
| 54 | website has gone offline. However, I still haven't committed any images served | ||
| 55 | on my blog to Git, as I'm not entirely sold on Git LFS yet - for now, images can | ||
| 56 | be found at [img.cleberg.net](https://img.cleberg.net). | ||
| 57 | |||
| 58 | Saving my comments back to the Git repository ensures that another aspect of my | ||
| 59 | site will degrade gracefully. | ||
| 60 | |||
| 61 | # Create a Comment Form | ||
| 62 | |||
| 63 | Okay, let's get started. The first step is to create an HTML form that users can | ||
| 64 | see and utilize to submit comments. This is fairly easy and can be changed | ||
| 65 | depending on your personal preferences. | ||
| 66 | |||
| 67 | Take a look at the code block below for the form I currently use. Note that | ||
| 68 | `<current-url>` is replaced automatically in PHP with the current post's URL, so | ||
| 69 | that my PHP script used later will know which blog post the comment is related | ||
| 70 | to. | ||
| 71 | |||
| 72 | The form contains the following structure: | ||
| 73 | |||
| 74 | 1. `<form>` - This is the form and will determine which PHP script to send the | ||
| 75 | comment to. | ||
| 76 | 2. `<section hidden>` - This section is hidden from the user and is used to | ||
| 77 | ensure that we know which blog post sent the comment. | ||
| 78 | 3. `<section>` Display Name (Optional) - Used to accept a display name, if | ||
| 79 | entered. | ||
| 80 | 4. `<section>` Comment (Required) - Used to accept the user's full comment. | ||
| 81 | Markdown is allowed. | ||
| 82 | 5. `<button>` - A button to submit the form. | ||
| 83 | |||
| 84 | ```html | ||
| 85 | <form action="/comment.php" method="POST"> | ||
| 86 | <h3>Leave a Comment</h3> | ||
| 87 | <section hidden> | ||
| 88 | <label class="form-label" for="postURL">Post URL</label> | ||
| 89 | <input | ||
| 90 | class="form-control" | ||
| 91 | id="postURL" | ||
| 92 | name="postURL" | ||
| 93 | type="text" | ||
| 94 | value="<current-url>" | ||
| 95 | /> | ||
| 96 | </section> | ||
| 97 | <section> | ||
| 98 | <label class="form-label" for="userName">Display Name</label> | ||
| 99 | <input | ||
| 100 | class="form-control" | ||
| 101 | id="userName" | ||
| 102 | name="userName" | ||
| 103 | placeholder="John Doe" | ||
| 104 | type="text" | ||
| 105 | /> | ||
| 106 | </section> | ||
| 107 | <section> | ||
| 108 | <label class="form-label" for="userContent">Your Comment</label> | ||
| 109 | <textarea | ||
| 110 | class="form-control" | ||
| 111 | id="userContent" | ||
| 112 | name="userContent" | ||
| 113 | rows="3" | ||
| 114 | placeholder="# Feel free to use Markdown" | ||
| 115 | aria-describedby="commentHelp" | ||
| 116 | required | ||
| 117 | ></textarea> | ||
| 118 | <div id="commentHelp" class="form-text"> | ||
| 119 | Comments are saved as Markdown and cannot be edited or deleted. | ||
| 120 | </div> | ||
| 121 | </section> | ||
| 122 | <button type="submit">Submit</button> | ||
| 123 | </form> | ||
| 124 | ``` | ||
| 125 | |||
| 126 | # Handle Comments via POST | ||
| 127 | |||
| 128 | Now that we have a form and can submit comments, we need to create a PHP script | ||
| 129 | so that the server can fetch the comment data and save it. Make sure your PHP | ||
| 130 | script name matches the name you entered in the `action` field in your form. | ||
| 131 | |||
| 132 | See the code block below for the contents of my `comment.php` script. We perform | ||
| 133 | the following tasks in this script: | ||
| 134 | |||
| 135 | 1. Grab the POST data from the HTML form. | ||
| 136 | 2. Sanitize the comment data with `htmlentities`. | ||
| 137 | 3. Set the display name to `Anonymous` if it was left blank. | ||
| 138 | 4. Create a PHP object that combines all of this data. | ||
| 139 | 5. Check if our destination file `comments.json` exists. | ||
| 140 | 6. If so, convert the PHP object to JSON and save it to the file. | ||
| 141 | 7. If the `comments.json` file does not exist, the script will exit with an | ||
| 142 | error. You can alter this to ensure it creates the script, but my source | ||
| 143 | code includes the file by default, so it will always exist. | ||
| 144 | 8. Finally, send the user back to the `#comments` section of the blog post they | ||
| 145 | just read. | ||
| 146 | |||
| 147 | ```php | ||
| 148 | // Get the content sent from the comment form | ||
| 149 | $comment = htmlentities($_POST['userContent']); | ||
| 150 | $post_url = $_POST['postURL']; | ||
| 151 | |||
| 152 | // Set default values if blank | ||
| 153 | if (isset($_POST['userName']) && trim($_POST['userName']) !== "") { | ||
| 154 | $username = $_POST['userName']; | ||
| 155 | } else { | ||
| 156 | $username = 'Anonymous'; | ||
| 157 | } | ||
| 158 | |||
| 159 | // Create an empty PHP object | ||
| 160 | $user_object = new stdClass(); | ||
| 161 | |||
| 162 | // Add object content | ||
| 163 | $user_object->timestamp = date('Y-m-d H:i:s'); | ||
| 164 | $user_object->username = $username; | ||
| 165 | $user_object->comment = $comment; | ||
| 166 | $user_object->post_url = $post_url; | ||
| 167 | |||
| 168 | // Append JSON to file | ||
| 169 | $file_name = 'comments.json'; | ||
| 170 | if (file_exists($file_name)) { | ||
| 171 | $source_data = file_get_contents($file_name); | ||
| 172 | $temp_array = json_decode($source_data); | ||
| 173 | array_push($temp_array, $user_object); | ||
| 174 | $json_data = json_encode($temp_array, JSON_PRETTY_PRINT); | ||
| 175 | file_put_contents($file_name, $json_data); | ||
| 176 | } else { | ||
| 177 | die('Error: The "comments.json" file does not exist.'); | ||
| 178 | } | ||
| 179 | |||
| 180 | // Send the user back | ||
| 181 | header('Location: ' . $post_url . '#comments'); | ||
| 182 | ``` | ||
| 183 | |||
| 184 | If you're using Apache, make sure the `www-data` user on your server has the | ||
| 185 | correct permissions to your website directory or else it will not be able to | ||
| 186 | write to `comments.json`. | ||
| 187 | |||
| 188 | ```sh | ||
| 189 | chgrp -R www-data /path/to/website/ | ||
| 190 | chmod -R g+w comments.json | ||
| 191 | ``` | ||
| 192 | |||
| 193 | # Display User Comments | ||
| 194 | |||
| 195 | Now that we can submit comments, and they are saved to the `comments.json` file, | ||
| 196 | let's make sure we can show those comments in each blog post. | ||
| 197 | |||
| 198 | The code block below shows the function I use to decode my `comments.json` file, | ||
| 199 | check if the comments apply to the current post, and then display them. | ||
| 200 | |||
| 201 | This piece of code should **really** be inside a function (or at least in an | ||
| 202 | organized PHP workflow). Don't just copy-and-paste and expect it to work. You | ||
| 203 | need to at least supply a `$query` variable depending on the page visited. | ||
| 204 | |||
| 205 | ```php | ||
| 206 | $query = 'your-blog-post.html'; | ||
| 207 | |||
| 208 | // Load saved comments | ||
| 209 | $comments_file = 'comments.json'; | ||
| 210 | $comments_raw = file_get_contents($comments_file); | ||
| 211 | $comments = json_decode($comments_raw); | ||
| 212 | $comment_section = '<section id="comments" class="comments"><h3>Comments</h3>'; | ||
| 213 | foreach ($comments as $comment) { | ||
| 214 | if ($comment->post_url == "https://example.com/post/" . $query) { | ||
| 215 | // Assign metadata to variables | ||
| 216 | $comment_timestamp = $comment->timestamp; | ||
| 217 | $comment_username = $comment->username; | ||
| 218 | $comment_content = $comment->comment; | ||
| 219 | |||
| 220 | // Parse the comment, in case it contains Markdown | ||
| 221 | $comment_md = Parsedown::instance()->text($comment_content); | ||
| 222 | $comment_html = new DOMDocument(); | ||
| 223 | $comment_html->loadHTML($comment_md); | ||
| 224 | $comment_html_links = $comment_html->getElementsByTagName('a'); | ||
| 225 | foreach ($comment_html_links as $comment_html_link) { | ||
| 226 | $comment_html_link->setAttribute('rel', 'noreferrer'); | ||
| 227 | $comment_html_link->setAttribute('target', '_blank'); | ||
| 228 | } | ||
| 229 | $comment_secured_html = $comment_html->saveHTML(); | ||
| 230 | |||
| 231 | // Apply metadata to comments section | ||
| 232 | $comment_section .= '<div class="user-comment"><div class="row"><label>Timestamp:</label><p>' . $comment_timestamp . '</p></div><div class="row"><label>Name:</label><p>' . $comment_username . '</p></div><div class="row markdown"><label>Comment:</label><div class="comment-markdown">' . $comment_secured_html . '</div></div></div>'; | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | echo $comment_section; | ||
| 237 | ``` | ||
| 238 | |||
| 239 | # Bonus: Create a 'Recent Comments' Page | ||
| 240 | |||
| 241 | Finally, the last part of my current system is to create a Recent Comments page | ||
| 242 | so that I can easily check-in on my blog and moderate any spam. As an | ||
| 243 | alternative, you could use PHP's `mail()` function to email you for each blog | ||
| 244 | comment. | ||
| 245 | |||
| 246 | The code to do this is literally the same code as the previous section, I just | ||
| 247 | make sure it is printed when someone visits `https://example.com/comments/`. | ||
| 248 | |||
| 249 | # Possible Enhancements | ||
| 250 | |||
| 251 | This comment system is by no means a fully-developed system. I have noted a few | ||
| 252 | possible enhancements here that I may implement in the future: | ||
| 253 | |||
| 254 | - Create a secure moderator page with user authentication at | ||
| 255 | `https://blog.example.com/mod/`. This page could have the option to edit or | ||
| 256 | delete any comment found in `comments.json`. | ||
| 257 | - Create a temporary file, such as `pending_comments.json`, that will store | ||
| 258 | newly-submitted comments and won't display on blog posts until approved by a | ||
| 259 | moderator. | ||
| 260 | - Create a `/modlog/` page with a chronological log, showing which moderator | ||
| 261 | approved which comments and why certain comments were rejected. | ||
content/blog/2021-04-23-php-comment-system.org added +272
| @@ -0,0 +1,272 @@ | |||
| 1 | #+date: <2021-04-23> | ||
| 2 | #+title: Roll Your Own Static Commenting System in PHP | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Terrible-ness of Commenting Systems | ||
| 7 | |||
| 8 | The current state of affairs regarding interactive comment systems is, | ||
| 9 | well, terrible. It is especially awful if you're a privacy conscious | ||
| 10 | person who does not generally load third-party scripts or frames on the | ||
| 11 | websites you visit. | ||
| 12 | |||
| 13 | Even further, many comment systems are charging exorbitant fees for | ||
| 14 | something that should be standard. | ||
| 15 | |||
| 16 | Of course, there are some really terrible options: | ||
| 17 | |||
| 18 | - Facebook Comments | ||
| 19 | - Discourse | ||
| 20 | |||
| 21 | There are some options that are better but still use too many scripts, | ||
| 22 | frames, or social integrations on your web page that could impact some | ||
| 23 | users: | ||
| 24 | |||
| 25 | - Disqus | ||
| 26 | - Isso | ||
| 27 | - Remark42 | ||
| 28 | |||
| 29 | Lastly, I looked into a few unique ways of generating blog comments, | ||
| 30 | such as using Twitter threads or GitHub issues to automatically post | ||
| 31 | issues. However, these both rely on external third-party sites that I | ||
| 32 | don't currently use. | ||
| 33 | |||
| 34 | * Stay Static with Server-Side Comments | ||
| 35 | |||
| 36 | The main issue for my personal use-case is that my blog is completely, | ||
| 37 | 100% static. I use PHP on the back-end but website visitors only see | ||
| 38 | HTML and a single CSS file. No external javascript and no embedded | ||
| 39 | frames. | ||
| 40 | |||
| 41 | So, how do we keep a site static and still allow users to interact with | ||
| 42 | blog posts? The key actually pretty simple - I'm already using PHP, so | ||
| 43 | why not rely on the classic HTML =<form>= and a PHP script to save the | ||
| 44 | comments somewhere? As it turns out, this was a perfect solution for me. | ||
| 45 | |||
| 46 | The second issue for my personal use-case is that I am trying to keep | ||
| 47 | the contents of my website accessible over time, as described by | ||
| 48 | [cite/t:@brandur], in his post entitled | ||
| 49 | [[https://brandur.org/fragments/graceful-degradation-time][Blog with | ||
| 50 | Markdown + Git, and degrade gracefully through time]] . | ||
| 51 | |||
| 52 | This means I cannot rely on a database for comments, since I do not rely | ||
| 53 | on a database for any other part of my websites. | ||
| 54 | |||
| 55 | I blog in plain Markdown files, commit all articles to Git, and ensure | ||
| 56 | that future readers will be able to see the source data long after I'm | ||
| 57 | gone, or the website has gone offline. However, I still haven't | ||
| 58 | committed any images served on my blog to Git, as I'm not entirely sold | ||
| 59 | on Git LFS yet - for now, images can be found at | ||
| 60 | [[https://img.cleberg.net][img.cleberg.net]]. | ||
| 61 | |||
| 62 | Saving my comments back to the Git repository ensures that another | ||
| 63 | aspect of my site will degrade gracefully. | ||
| 64 | |||
| 65 | * Create a Comment Form | ||
| 66 | |||
| 67 | Okay, let's get started. The first step is to create an HTML form that | ||
| 68 | users can see and utilize to submit comments. This is fairly easy and | ||
| 69 | can be changed depending on your personal preferences. | ||
| 70 | |||
| 71 | Take a look at the code block below for the form I currently use. Note | ||
| 72 | that =<current-url>= is replaced automatically in PHP with the current | ||
| 73 | post's URL, so that my PHP script used later will know which blog post | ||
| 74 | the comment is related to. | ||
| 75 | |||
| 76 | The form contains the following structure: | ||
| 77 | |||
| 78 | 1. =<form>= - This is the form and will determine which PHP script to | ||
| 79 | send the comment to. | ||
| 80 | 2. =<section hidden>= - This section is hidden from the user and is used | ||
| 81 | to ensure that we know which blog post sent the comment. | ||
| 82 | 3. =<section>= Display Name (Optional) - Used to accept a display name, | ||
| 83 | if entered. | ||
| 84 | 4. =<section>= Comment (Required) - Used to accept the user's full | ||
| 85 | comment. Markdown is allowed. | ||
| 86 | 5. =<button>= - A button to submit the form. | ||
| 87 | |||
| 88 | #+begin_src html | ||
| 89 | <form action="/comment.php" method="POST"> | ||
| 90 | <h3>Leave a Comment</h3> | ||
| 91 | <section hidden> | ||
| 92 | <label class="form-label" for="postURL">Post URL</label> | ||
| 93 | <input | ||
| 94 | class="form-control" | ||
| 95 | id="postURL" | ||
| 96 | name="postURL" | ||
| 97 | type="text" | ||
| 98 | value="<current-url>" | ||
| 99 | /> | ||
| 100 | </section> | ||
| 101 | <section> | ||
| 102 | <label class="form-label" for="userName">Display Name</label> | ||
| 103 | <input | ||
| 104 | class="form-control" | ||
| 105 | id="userName" | ||
| 106 | name="userName" | ||
| 107 | placeholder="John Doe" | ||
| 108 | type="text" | ||
| 109 | /> | ||
| 110 | </section> | ||
| 111 | <section> | ||
| 112 | <label class="form-label" for="userContent">Your Comment</label> | ||
| 113 | <textarea | ||
| 114 | class="form-control" | ||
| 115 | id="userContent" | ||
| 116 | name="userContent" | ||
| 117 | rows="3" | ||
| 118 | placeholder="# Feel free to use Markdown" | ||
| 119 | aria-describedby="commentHelp" | ||
| 120 | required | ||
| 121 | ></textarea> | ||
| 122 | <div id="commentHelp" class="form-text"> | ||
| 123 | Comments are saved as Markdown and cannot be edited or deleted. | ||
| 124 | </div> | ||
| 125 | </section> | ||
| 126 | <button type="submit">Submit</button> | ||
| 127 | </form> | ||
| 128 | #+end_src | ||
| 129 | |||
| 130 | * Handle Comments via POST | ||
| 131 | |||
| 132 | Now that we have a form and can submit comments, we need to create a PHP | ||
| 133 | script so that the server can fetch the comment data and save it. Make | ||
| 134 | sure your PHP script name matches the name you entered in the =action= | ||
| 135 | field in your form. | ||
| 136 | |||
| 137 | See the code block below for the contents of my =comment.php= script. We | ||
| 138 | perform the following tasks in this script: | ||
| 139 | |||
| 140 | 1. Grab the POST data from the HTML form. | ||
| 141 | 2. Sanitize the comment data with =htmlentities=. | ||
| 142 | 3. Set the display name to =Anonymous= if it was left blank. | ||
| 143 | 4. Create a PHP object that combines all of this data. | ||
| 144 | 5. Check if our destination file =comments.json= exists. | ||
| 145 | 6. If so, convert the PHP object to JSON and save it to the file. | ||
| 146 | 7. If the =comments.json= file does not exist, the script will exit with | ||
| 147 | an error. You can alter this to ensure it creates the script, but my | ||
| 148 | source code includes the file by default, so it will always exist. | ||
| 149 | 8. Finally, send the user back to the =#comments= section of the blog | ||
| 150 | post they just read. | ||
| 151 | |||
| 152 | #+begin_src php | ||
| 153 | // Get the content sent from the comment form | ||
| 154 | $comment = htmlentities($_POST['userContent']); | ||
| 155 | $post_url = $_POST['postURL']; | ||
| 156 | |||
| 157 | // Set default values if blank | ||
| 158 | if (isset($_POST['userName']) && trim($_POST['userName']) !== "") { | ||
| 159 | $username = $_POST['userName']; | ||
| 160 | } else { | ||
| 161 | $username = 'Anonymous'; | ||
| 162 | } | ||
| 163 | |||
| 164 | // Create an empty PHP object | ||
| 165 | $user_object = new stdClass(); | ||
| 166 | |||
| 167 | // Add object content | ||
| 168 | $user_object->timestamp = date('Y-m-d H:i:s'); | ||
| 169 | $user_object->username = $username; | ||
| 170 | $user_object->comment = $comment; | ||
| 171 | $user_object->post_url = $post_url; | ||
| 172 | |||
| 173 | // Append JSON to file | ||
| 174 | $file_name = 'comments.json'; | ||
| 175 | if (file_exists($file_name)) { | ||
| 176 | $source_data = file_get_contents($file_name); | ||
| 177 | $temp_array = json_decode($source_data); | ||
| 178 | array_push($temp_array, $user_object); | ||
| 179 | $json_data = json_encode($temp_array, JSON_PRETTY_PRINT); | ||
| 180 | file_put_contents($file_name, $json_data); | ||
| 181 | } else { | ||
| 182 | die('Error: The "comments.json" file does not exist.'); | ||
| 183 | } | ||
| 184 | |||
| 185 | // Send the user back | ||
| 186 | header('Location: ' . $post_url . '#comments'); | ||
| 187 | #+end_src | ||
| 188 | |||
| 189 | If you're using Apache, make sure the =www-data= user on your server has | ||
| 190 | the correct permissions to your website directory or else it will not be | ||
| 191 | able to write to =comments.json=. | ||
| 192 | |||
| 193 | #+begin_src sh | ||
| 194 | chgrp -R www-data /path/to/website/ | ||
| 195 | chmod -R g+w comments.json | ||
| 196 | #+end_src | ||
| 197 | |||
| 198 | * Display User Comments | ||
| 199 | |||
| 200 | Now that we can submit comments, and they are saved to the | ||
| 201 | =comments.json= file, let's make sure we can show those comments in each | ||
| 202 | blog post. | ||
| 203 | |||
| 204 | The code block below shows the function I use to decode my | ||
| 205 | =comments.json= file, check if the comments apply to the current post, | ||
| 206 | and then display them. | ||
| 207 | |||
| 208 | This piece of code should *really* be inside a function (or at least in | ||
| 209 | an organized PHP workflow). Don't just copy-and-paste and expect it to | ||
| 210 | work. You need to at least supply a =$query= variable depending on the | ||
| 211 | page visited. | ||
| 212 | |||
| 213 | #+begin_src php | ||
| 214 | $query = 'your-blog-post.html'; | ||
| 215 | |||
| 216 | // Load saved comments | ||
| 217 | $comments_file = 'comments.json'; | ||
| 218 | $comments_raw = file_get_contents($comments_file); | ||
| 219 | $comments = json_decode($comments_raw); | ||
| 220 | $comment_section = '<section id="comments" class="comments"><h3>Comments</h3>'; | ||
| 221 | foreach ($comments as $comment) { | ||
| 222 | if ($comment->post_url == "https://example.com/post/" . $query) { | ||
| 223 | // Assign metadata to variables | ||
| 224 | $comment_timestamp = $comment->timestamp; | ||
| 225 | $comment_username = $comment->username; | ||
| 226 | $comment_content = $comment->comment; | ||
| 227 | |||
| 228 | // Parse the comment, in case it contains Markdown | ||
| 229 | $comment_md = Parsedown::instance()->text($comment_content); | ||
| 230 | $comment_html = new DOMDocument(); | ||
| 231 | $comment_html->loadHTML($comment_md); | ||
| 232 | $comment_html_links = $comment_html->getElementsByTagName('a'); | ||
| 233 | foreach ($comment_html_links as $comment_html_link) { | ||
| 234 | $comment_html_link->setAttribute('rel', 'noreferrer'); | ||
| 235 | $comment_html_link->setAttribute('target', '_blank'); | ||
| 236 | } | ||
| 237 | $comment_secured_html = $comment_html->saveHTML(); | ||
| 238 | |||
| 239 | // Apply metadata to comments section | ||
| 240 | $comment_section .= '<div class="user-comment"><div class="row"><label>Timestamp:</label><p>' . $comment_timestamp . '</p></div><div class="row"><label>Name:</label><p>' . $comment_username . '</p></div><div class="row markdown"><label>Comment:</label><div class="comment-markdown">' . $comment_secured_html . '</div></div></div>'; | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | echo $comment_section; | ||
| 245 | #+end_src | ||
| 246 | |||
| 247 | * Bonus: Create a 'Recent Comments' Page | ||
| 248 | |||
| 249 | Finally, the last part of my current system is to create a Recent | ||
| 250 | Comments page so that I can easily check-in on my blog and moderate any | ||
| 251 | spam. As an alternative, you could use PHP's =mail()= function to email | ||
| 252 | you for each blog comment. | ||
| 253 | |||
| 254 | The code to do this is literally the same code as the previous section, | ||
| 255 | I just make sure it is printed when someone visits | ||
| 256 | =https://example.com/comments/=. | ||
| 257 | |||
| 258 | * Possible Enhancements | ||
| 259 | |||
| 260 | This comment system is by no means a fully-developed system. I have | ||
| 261 | noted a few possible enhancements here that I may implement in the | ||
| 262 | future: | ||
| 263 | |||
| 264 | - Create a secure moderator page with user authentication at | ||
| 265 | =https://blog.example.com/mod/=. This page could have the option to | ||
| 266 | edit or delete any comment found in =comments.json=. | ||
| 267 | - Create a temporary file, such as =pending_comments.json=, that will | ||
| 268 | store newly-submitted comments and won't display on blog posts until | ||
| 269 | approved by a moderator. | ||
| 270 | - Create a =/modlog/= page with a chronological log, showing which | ||
| 271 | moderator approved which comments and why certain comments were | ||
| 272 | rejected. | ||
content/blog/2021-04-28-photography.md deleted −68
| @@ -1,68 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-04-28 | ||
| 3 | title = "Jumping Back Into Photography" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Why Photography? | ||
| 9 | |||
| 10 | I've often wondered why photography is as enticing as it is. You can see | ||
| 11 | billions of people around the world taking photographs every single moment of | ||
| 12 | the day. New technology often boasts about their photographic capabilities, | ||
| 13 | especially smartphones. I would even assume that we live in a world where there | ||
| 14 | is never a moment in which a photograph is not being taken somewhere on Earth. | ||
| 15 | |||
| 16 | As for myself, I would simply say that I enjoy preserving a memory in physical | ||
| 17 | (or digital) form. I've never had the best memory when it comes to recalling | ||
| 18 | details of places and people gone by, so it helps to have a frame of reference | ||
| 19 | lying around. | ||
| 20 | |||
| 21 | Regardless of the reason, I think most people would agree that you simply cannot | ||
| 22 | have too many hobbies. | ||
| 23 | |||
| 24 | # Older Cameras | ||
| 25 | |||
| 26 | I started playing around with the idea of photography when my family purchased a | ||
| 27 | Fujifilm camera for family-specific events. I don't recall the specific model, | ||
| 28 | but I do recall it was a point-and-shoot camera without an interchangeable lens. | ||
| 29 | However, it was of great value to someone, like myself, who couldn't afford any | ||
| 30 | other camera. I took about 10,000 shots with that camera over a 3-5 year span. | ||
| 31 | Most notably, all of my trips to California were documented through this camera. | ||
| 32 | |||
| 33 | When possible, I would borrow my sister's camera, which is a Sony SLT-A58. This | ||
| 34 | camera was great and allowed for some of my best early shots, especially those | ||
| 35 | taken in Utah's and Nevada's parks. | ||
| 36 | |||
| 37 | # My Current Kit | ||
| 38 | |||
| 39 | I've finally come to a point in my life where I have the disposable income to | ||
| 40 | invest in a solid photography kit. I played around with the idea of a lot of | ||
| 41 | different cameras, different types, new vs used, etc. Finally, I settled on the | ||
| 42 | [Sony α7 III](https://en.wikipedia.org/wiki/Sony_%CE%B17_III). This camera is | ||
| 43 | mirror-less and uses a full-frame image sensor at 24 megapixels. I don't create | ||
| 44 | large prints, and I am mostly focused on preserving memories in high quality for | ||
| 45 | the next 5-10 years with this camera, so the specifications here are just | ||
| 46 | perfect for me. | ||
| 47 | |||
| 48 | For lenses, I decided to buy two lenses that could carry me through most | ||
| 49 | situations: | ||
| 50 | |||
| 51 | - [Vario-Tessar T\*\* FE 24-70 mm F4 ZA | ||
| 52 | OSS](https://electronics.sony.com/imaging/lenses/full-frame-e-mount/p/sel2470z) | ||
| 53 | - [Tamron 70-300mm f4.5-6.3 Di III | ||
| 54 | RXD](https://www.tamron-usa.com/product/lenses/a047.html) | ||
| 55 | |||
| 56 | In addition, I grabbed a couple [HGX Prime | ||
| 57 | 67mm](https://www.promaster.com/Product/6725) protection filters for the lenses. | ||
| 58 | |||
| 59 | As I delve further into photography and pick up more skills, I will most likely | ||
| 60 | go back and grab a lens with a higher f-stop value, such as f/1.8. I toyed with | ||
| 61 | the idea of grabbing a 50 mm at `f/1.8`, but decided to keep things in a | ||
| 62 | reasonable price range instead. | ||
| 63 | |||
| 64 | Finally, I made sure to buy a photography-specific backpack with a rain guard, | ||
| 65 | and the zipper on the back panel, to protect the equipment while wearing the | ||
| 66 | bag. If you've ever had to haul around a DSLR (or camera of similar heft) in a | ||
| 67 | bag that only has a shoulder strap, you'll know the pain it can cause. Putting | ||
| 68 | all my equipment in a backpack was an easy decision. | ||
content/blog/2021-04-28-photography.org added +71
| @@ -0,0 +1,71 @@ | |||
| 1 | #+date: <2021-04-28> | ||
| 2 | #+title: Jumping Back Into Photography | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Why Photography? | ||
| 7 | |||
| 8 | I've often wondered why photography is as enticing as it is. You can see | ||
| 9 | billions of people around the world taking photographs every single | ||
| 10 | moment of the day. New technology often boasts about their photographic | ||
| 11 | capabilities, especially smartphones. I would even assume that we live | ||
| 12 | in a world where there is never a moment in which a photograph is not | ||
| 13 | being taken somewhere on Earth. | ||
| 14 | |||
| 15 | As for myself, I would simply say that I enjoy preserving a memory in | ||
| 16 | physical (or digital) form. I've never had the best memory when it comes | ||
| 17 | to recalling details of places and people gone by, so it helps to have a | ||
| 18 | frame of reference lying around. | ||
| 19 | |||
| 20 | Regardless of the reason, I think most people would agree that you | ||
| 21 | simply cannot have too many hobbies. | ||
| 22 | |||
| 23 | * Older Cameras | ||
| 24 | |||
| 25 | I started playing around with the idea of photography when my family | ||
| 26 | purchased a Fujifilm camera for family-specific events. I don't recall | ||
| 27 | the specific model, but I do recall it was a point-and-shoot camera | ||
| 28 | without an interchangeable lens. However, it was of great value to | ||
| 29 | someone, like myself, who couldn't afford any other camera. I took about | ||
| 30 | 10,000 shots with that camera over a 3-5 year span. Most notably, all of | ||
| 31 | my trips to California were documented through this camera. | ||
| 32 | |||
| 33 | When possible, I would borrow my sister's camera, which is a Sony | ||
| 34 | SLT-A58. This camera was great and allowed for some of my best early | ||
| 35 | shots, especially those taken in Utah's and Nevada's parks. | ||
| 36 | |||
| 37 | * My Current Kit | ||
| 38 | |||
| 39 | I've finally come to a point in my life where I have the disposable | ||
| 40 | income to invest in a solid photography kit. I played around with the | ||
| 41 | idea of a lot of different cameras, different types, new vs used, etc. | ||
| 42 | Finally, I settled on the | ||
| 43 | [[https://en.wikipedia.org/wiki/Sony_%CE%B17_III][Sony α7 III]]. This | ||
| 44 | camera is mirror-less and uses a full-frame image sensor at 24 | ||
| 45 | megapixels. I don't create large prints, and I am mostly focused on | ||
| 46 | preserving memories in high quality for the next 5-10 years with this | ||
| 47 | camera, so the specifications here are just perfect for me. | ||
| 48 | |||
| 49 | For lenses, I decided to buy two lenses that could carry me through most | ||
| 50 | situations: | ||
| 51 | |||
| 52 | - [[https://electronics.sony.com/imaging/lenses/full-frame-e-mount/p/sel2470z][Vario-Tessar | ||
| 53 | T** FE 24-70 mm F4 ZA OSS]] | ||
| 54 | - [[https://www.tamron-usa.com/product/lenses/a047.html][Tamron 70-300mm | ||
| 55 | f4.5-6.3 Di III RXD]] | ||
| 56 | |||
| 57 | In addition, I grabbed a couple | ||
| 58 | [[https://www.promaster.com/Product/6725][HGX Prime 67mm]] protection | ||
| 59 | filters for the lenses. | ||
| 60 | |||
| 61 | As I delve further into photography and pick up more skills, I will most | ||
| 62 | likely go back and grab a lens with a higher f-stop value, such as | ||
| 63 | f/1.8. I toyed with the idea of grabbing a 50 mm at =f/1.8=, but decided | ||
| 64 | to keep things in a reasonable price range instead. | ||
| 65 | |||
| 66 | Finally, I made sure to buy a photography-specific backpack with a rain | ||
| 67 | guard, and the zipper on the back panel, to protect the equipment while | ||
| 68 | wearing the bag. If you've ever had to haul around a DSLR (or camera of | ||
| 69 | similar heft) in a bag that only has a shoulder strap, you'll know the | ||
| 70 | pain it can cause. Putting all my equipment in a backpack was an easy | ||
| 71 | decision. | ||
content/blog/2021-05-30-changing-git-authors.md → content/blog/2021-05-30-changing-git-authors.org renamed +26 −28
| @@ -1,33 +1,31 @@ | |||
| 1 | +++ | 1 | #+date: <2021-05-30> |
| 2 | date = 2021-05-30 | 2 | #+title: Changing Git Authors |
| 3 | title = "Changing Git Authors" | 3 | #+description: |
| 4 | description = "" | 4 | |
| 5 | draft = false | 5 | |
| 6 | +++ | 6 | * Changing Git Author/Email Based on Previously Committed Email |
| 7 | 7 | ||
| 8 | # Changing Git Author/Email Based on Previously Committed Email | 8 | Here's the dilemma: You've been committing changes to your git |
| 9 | 9 | repository with an incorrect name or email (or multiple repositories), | |
| 10 | Here's the dilemma: You've been committing changes to your git repository with | 10 | and now you want to fix it. Luckily, there's a semi-reliable way to fix |
| 11 | an incorrect name or email (or multiple repositories), and now you want to fix | 11 | that. While I have never experienced issues with this method, some |
| 12 | it. Luckily, there's a semi-reliable way to fix that. While I have never | 12 | people have warned that it can mess with historical hashes and integrity |
| 13 | experienced issues with this method, some people have warned that it can mess | 13 | of commits, so use this method only if you're okay accepting that risk. |
| 14 | with historical hashes and integrity of commits, so use this method only if | ||
| 15 | you're okay accepting that risk. | ||
| 16 | 14 | ||
| 17 | Okay, let's create the bash script: | 15 | Okay, let's create the bash script: |
| 18 | 16 | ||
| 19 | ```sh | 17 | #+begin_src sh |
| 20 | nano change_git_authors.sh | 18 | nano change_git_authors.sh |
| 21 | ``` | 19 | #+end_src |
| 22 | 20 | ||
| 23 | The following information can be pasted directly into your bash script. The only | 21 | The following information can be pasted directly into your bash script. |
| 24 | changes you need to make are to the following variables: | 22 | The only changes you need to make are to the following variables: |
| 25 | 23 | ||
| 26 | - `OLD_EMAIL` | 24 | - =OLD_EMAIL= |
| 27 | - `CORRECT_NAME` | 25 | - =CORRECT_NAME= |
| 28 | - `CORRECT_EMAIL` | 26 | - =CORRECT_EMAIL= |
| 29 | 27 | ||
| 30 | ```sh | 28 | #+begin_src sh |
| 31 | #!/bin/sh | 29 | #!/bin/sh |
| 32 | 30 | ||
| 33 | # List all sub-directories in the current directory | 31 | # List all sub-directories in the current directory |
| @@ -60,16 +58,16 @@ do | |||
| 60 | 58 | ||
| 61 | cd .. | 59 | cd .. |
| 62 | done | 60 | done |
| 63 | ``` | 61 | #+end_src |
| 64 | 62 | ||
| 65 | Finally, save the bash script and make it executable. | 63 | Finally, save the bash script and make it executable. |
| 66 | 64 | ||
| 67 | ```sh | 65 | #+begin_src sh |
| 68 | chmod a+x change_git_authors.sh | 66 | chmod a+x change_git_authors.sh |
| 69 | ``` | 67 | #+end_src |
| 70 | 68 | ||
| 71 | Now you can run the script and should see the process begin. | 69 | Now you can run the script and should see the process begin. |
| 72 | 70 | ||
| 73 | ```sh | 71 | #+begin_src sh |
| 74 | ./change_git_authors.sh | 72 | ./change_git_authors.sh |
| 75 | ``` | 73 | #+end_src |
content/blog/2021-07-15-delete-gitlab-repos.md → content/blog/2021-07-15-delete-gitlab-repos.org renamed +41 −42
| @@ -1,53 +1,51 @@ | |||
| 1 | +++ | 1 | #+date: <2021-07-15> |
| 2 | date = 2021-07-15 | 2 | #+title: How to Delete All GitLab Repositories |
| 3 | title = "How t oDelete All GitLab Repositories" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Background | ||
| 9 | 5 | ||
| 10 | Have you ever used GitLab to host your source code, moved to a different host, | 6 | * Background |
| 11 | and wanted to delete everything from your GitLab account? Well, this post covers | ||
| 12 | any scenario where you would want to delete all repositories from your GitLab | ||
| 13 | account. | ||
| 14 | 7 | ||
| 15 | For me, I currently maintain around 30 repositories and don't like to manually | 8 | Have you ever used GitLab to host your source code, moved to a different |
| 16 | delete them whenever I switch host. GitHub has a few different tools online to | 9 | host, and wanted to delete everything from your GitLab account? Well, |
| 17 | delete all repositories for you, but I have not found anything similar for | 10 | this post covers any scenario where you would want to delete all |
| 18 | GitLab, so I needed an alternative solution. | 11 | repositories from your GitLab account. |
| 19 | 12 | ||
| 20 | # Use a Python Script | 13 | For me, I currently maintain around 30 repositories and don't like to |
| 14 | manually delete them whenever I switch host. GitHub has a few different | ||
| 15 | tools online to delete all repositories for you, but I have not found | ||
| 16 | anything similar for GitLab, so I needed an alternative solution. | ||
| 21 | 17 | ||
| 22 | ## Requirements | 18 | * Use a Python Script |
| 23 | 19 | ||
| 24 | Before we look at the script, make sure you know your GitLab username. Next, | 20 | ** Requirements |
| 25 | [create an authorization | ||
| 26 | token](https://gitlab.com/-/profile/personal_access_tokens) so that the Python | ||
| 27 | script can delete your repositories. Don't lose this token or else you'll need | ||
| 28 | to create a new one. | ||
| 29 | 21 | ||
| 30 | ## Create the Script | 22 | Before we look at the script, make sure you know your GitLab username. |
| 23 | Next, [[https://gitlab.com/-/profile/personal_access_tokens][create an | ||
| 24 | authorization token]] so that the Python script can delete your | ||
| 25 | repositories. Don't lose this token or else you'll need to create a new | ||
| 26 | one. | ||
| 31 | 27 | ||
| 32 | To run a Python script, you must first create it. Open a terminal and enter the | 28 | ** Create the Script |
| 33 | following commands in whichever directory you prefer to store the script. You | ||
| 34 | can do the same things in a file manager if you prefer. | ||
| 35 | 29 | ||
| 36 | ```sh | 30 | To run a Python script, you must first create it. Open a terminal and |
| 31 | enter the following commands in whichever directory you prefer to store | ||
| 32 | the script. You can do the same things in a file manager if you prefer. | ||
| 33 | |||
| 34 | #+begin_src sh | ||
| 37 | mkdir delete-gitlab | 35 | mkdir delete-gitlab |
| 38 | ``` | 36 | #+end_src |
| 39 | 37 | ||
| 40 | ```sh | 38 | #+begin_src sh |
| 41 | cd delete-gitlab | 39 | cd delete-gitlab |
| 42 | ``` | 40 | #+end_src |
| 43 | 41 | ||
| 44 | ```sh | 42 | #+begin_src sh |
| 45 | nano main.py | 43 | nano main.py |
| 46 | ``` | 44 | #+end_src |
| 47 | 45 | ||
| 48 | Enter the following code into your `main.py` script. | 46 | Enter the following code into your =main.py= script. |
| 49 | 47 | ||
| 50 | ```python | 48 | #+begin_src python |
| 51 | import request | 49 | import request |
| 52 | import json | 50 | import json |
| 53 | 51 | ||
| @@ -101,15 +99,16 @@ def main(): | |||
| 101 | 99 | ||
| 102 | if __name__ == "__main__": | 100 | if __name__ == "__main__": |
| 103 | main() | 101 | main() |
| 104 | ``` | 102 | #+end_src |
| 105 | 103 | ||
| 106 | Now that you have the proper information, replace `{user-id}` with your GitLab | 104 | Now that you have the proper information, replace ={user-id}= with your |
| 107 | username and `{auth-token}` with the authorization token you created earlier. | 105 | GitLab username and ={auth-token}= with the authorization token you |
| 106 | created earlier. | ||
| 108 | 107 | ||
| 109 | Finally, simply run the script and watch the output. You can also use PyCharm | 108 | Finally, simply run the script and watch the output. You can also use |
| 110 | Community Edition to edit and run the Python script if you don't want to work in | 109 | PyCharm Community Edition to edit and run the Python script if you don't |
| 111 | a terminal. | 110 | want to work in a terminal. |
| 112 | 111 | ||
| 113 | ```sh | 112 | #+begin_src sh |
| 114 | python3 main.py | 113 | python3 main.py |
| 115 | ``` | 114 | #+end_src |
content/blog/2021-08-25-audit-sampling.md → content/blog/2021-08-25-audit-sampling.org renamed +126 −124
| @@ -1,52 +1,51 @@ | |||
| 1 | +++ | 1 | #+date: <2021-08-25> |
| 2 | date = 2021-08-25 | 2 | #+title: Audit Sampling with Python |
| 3 | title = "Audit Sampling with Python" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Introduction | 5 | |
| 6 | * Introduction | ||
| 9 | 7 | ||
| 10 | For anyone who is familiar with internal auditing, external auditing, or | 8 | For anyone who is familiar with internal auditing, external auditing, or |
| 11 | consulting, you will understand how tedious audit testing can become when you | 9 | consulting, you will understand how tedious audit testing can become |
| 12 | are required to test large swaths of data. When we cannot establish an automated | 10 | when you are required to test large swaths of data. When we cannot |
| 13 | means of testing an entire population, we generate samples to represent the | 11 | establish an automated means of testing an entire population, we |
| 14 | population of data. This helps ensure we can have a small enough data pool to | 12 | generate samples to represent the population of data. This helps ensure |
| 15 | test and that our results still represent the population. | 13 | we can have a small enough data pool to test and that our results still |
| 16 | 14 | represent the population. | |
| 17 | However, sampling data within the world of audit still seems to confuse quite a | 15 | |
| 18 | lot of people. While some audit-focused tools have introduced sampling | 16 | However, sampling data within the world of audit still seems to confuse |
| 19 | functionality (e.g. Wdesk), many audit departments and firms cannot use software | 17 | quite a lot of people. While some audit-focused tools have introduced |
| 20 | like this due to certain constraints, such as the team's budget or knowledge. | 18 | sampling functionality (e.g. Wdesk), many audit departments and firms |
| 21 | Here is where this article comes in: we're going to use | 19 | cannot use software like this due to certain constraints, such as the |
| 22 | [Python](https://www.python.org), a free and open-source programming language, | 20 | team's budget or knowledge. Here is where this article comes in: we're |
| 23 | to generate random samples from a dataset in order to suffice numerous audit | 21 | going to use [[https://www.python.org][Python]], a free and open-source |
| 24 | situations. | 22 | programming language, to generate random samples from a dataset in order |
| 25 | 23 | to suffice numerous audit situations. | |
| 26 | # Audit Requirements for Sampling | 24 | |
| 27 | 25 | * Audit Requirements for Sampling | |
| 28 | Before we get into the details of how to sample with Python, I want to make sure | 26 | |
| 29 | I discuss the different requirements that auditors may have of samples used | 27 | Before we get into the details of how to sample with Python, I want to |
| 30 | within their projects. | 28 | make sure I discuss the different requirements that auditors may have of |
| 31 | 29 | samples used within their projects. | |
| 32 | ## Randomness | 30 | |
| 33 | 31 | ** Randomness | |
| 34 | First, let's discuss randomness. When testing out new technology to help assist | 32 | |
| 35 | with audit sampling, you need to understand exactly how your samples are being | 33 | First, let's discuss randomness. When testing out new technology to help |
| 36 | generated. For example, if the underlying function is just picking every 57th | 34 | assist with audit sampling, you need to understand exactly how your |
| 37 | element from a list, that's not truly random; it's a systematic form of | 35 | samples are being generated. For example, if the underlying function is |
| 38 | sampling. Luckily, since Python is open-source, we have access to its codebase. | 36 | just picking every 57th element from a list, that's not truly random; |
| 39 | Through this blog post, I will be using the [pandas](https://pandas.pydata.org) | 37 | it's a systematic form of sampling. Luckily, since Python is |
| 40 | module in order to generate the random samples. More specifically, I will be | 38 | open-source, we have access to its codebase. Through this blog post, I |
| 41 | using the | 39 | will be using the [[https://pandas.pydata.org][pandas]] module in order |
| 42 | [pandas.DataFrame.sample](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html) | 40 | to generate the random samples. More specifically, I will be using the |
| 41 | [[https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html][pandas.DataFrame.sample]] | ||
| 43 | function provided by Pandas. | 42 | function provided by Pandas. |
| 44 | 43 | ||
| 45 | Now that you know what you're using, you can always check out the code behind | 44 | Now that you know what you're using, you can always check out the code |
| 46 | `pandas.DataFrame.sample`. This function does a lot of work, but we really only | 45 | behind =pandas.DataFrame.sample=. This function does a lot of work, but |
| 47 | care about the following snippets of code: | 46 | we really only care about the following snippets of code: |
| 48 | 47 | ||
| 49 | ```python | 48 | #+begin_src python |
| 50 | # Process random_state argument | 49 | # Process random_state argument |
| 51 | rs = com.random_state(random_state) | 50 | rs = com.random_state(random_state) |
| 52 | 51 | ||
| @@ -58,62 +57,65 @@ if ignore_index: | |||
| 58 | result.index = ibase.default_index(len(result)) | 57 | result.index = ibase.default_index(len(result)) |
| 59 | 58 | ||
| 60 | return result | 59 | return result |
| 61 | ``` | 60 | #+end_src |
| 62 | 61 | ||
| 63 | The block of code above shows you that if you assign a `random_state` argument | 62 | The block of code above shows you that if you assign a =random_state= |
| 64 | when you run the function, that will be used as a seed number in the random | 63 | argument when you run the function, that will be used as a seed number |
| 65 | generation and will allow you to reproduce a sample, given that nothing else | 64 | in the random generation and will allow you to reproduce a sample, given |
| 66 | changes. This is critical to the posterity of audit work. After all, how can you | 65 | that nothing else changes. This is critical to the posterity of audit |
| 67 | say your audit process is adequately documented if the next person can't run the | 66 | work. After all, how can you say your audit process is adequately |
| 68 | code and get the same sample? The final piece here on randomness is to look at | 67 | documented if the next person can't run the code and get the same |
| 69 | the [choice](https://docs.python.org/3/library/random.html#random.choice) | 68 | sample? The final piece here on randomness is to look at the |
| 70 | function used above. This is the crux of the generation and can also be examined | 69 | [[https://docs.python.org/3/library/random.html#random.choice][choice]] |
| 71 | for more detailed analysis on its reliability. As far as auditing goes, we will | 70 | function used above. This is the crux of the generation and can also be |
| 72 | trust that these functions are mathematically random. | 71 | examined for more detailed analysis on its reliability. As far as |
| 73 | 72 | auditing goes, we will trust that these functions are mathematically | |
| 74 | ## Sample Sizes | 73 | random. |
| 75 | 74 | ||
| 76 | As mentioned in the intro, sampling is only an effective method of auditing when | 75 | ** Sample Sizes |
| 77 | it truly represents the entire population. While some audit departments or firms | 76 | |
| 78 | may consider certain judgmental sample sizes to be adequate, you may need to | 77 | As mentioned in the intro, sampling is only an effective method of |
| 79 | rely on statistically-significant confidence levels of sample testing at certain | 78 | auditing when it truly represents the entire population. While some |
| 80 | points. I will demonstrate both here. For statistically-significant confidence | 79 | audit departments or firms may consider certain judgmental sample sizes |
| 81 | levels, most people will assume a 90% - 99% confidence level. In order to | 80 | to be adequate, you may need to rely on statistically-significant |
| 82 | actually calculate the correct sample size, it is best to use statistical tools | 81 | confidence levels of sample testing at certain points. I will |
| 83 | due to the tedious math work required. For example, for a population of 1000, | 82 | demonstrate both here. For statistically-significant confidence levels, |
| 84 | and a 90% confidence level that no more than 5% of the items are nonconforming, | 83 | most people will assume a 90% - 99% confidence level. In order to |
| 85 | you would sample 45 items. | 84 | actually calculate the correct sample size, it is best to use |
| 86 | 85 | statistical tools due to the tedious math work required. For example, | |
| 87 | However, in my personal experience, many audit departments and firms do not use | 86 | for a population of 1000, and a 90% confidence level that no more than |
| 88 | statistical sampling. Most people use a predetermined, often proprietary, table | 87 | 5% of the items are nonconforming, you would sample 45 items. |
| 89 | that will instruct auditors which sample sizes to choose. This allows for | 88 | |
| 90 | uniform testing and reduces overall workload. See the table below for a common | 89 | However, in my personal experience, many audit departments and firms do |
| 91 | implementation of sample sizes: | 90 | not use statistical sampling. Most people use a predetermined, often |
| 91 | proprietary, table that will instruct auditors which sample sizes to | ||
| 92 | choose. This allows for uniform testing and reduces overall workload. | ||
| 93 | See the table below for a common implementation of sample sizes: | ||
| 92 | 94 | ||
| 93 | Control Frequency Sample Size - High Risk Sample Size - Low Risk | 95 | Control Frequency Sample Size - High Risk Sample Size - Low Risk |
| 94 | 96 | ||
| 95 | --- | 97 | -------------- |
| 96 | 98 | ||
| 97 | More Than Daily 40 25 Daily 40 25 Weekly 12 5 Monthly 5 3 Quarterly 2 2 | 99 | More Than Daily 40 25 Daily 40 25 Weekly 12 5 Monthly 5 3 Quarterly 2 2 |
| 98 | Semi-Annually 1 1 Annually 1 1 Ad-hoc 1 1 | 100 | Semi-Annually 1 1 Annually 1 1 Ad-hoc 1 1 |
| 99 | 101 | ||
| 100 | ### Sampling with Python & Pandas | 102 | *** Sampling with Python & Pandas |
| 101 | 103 | ||
| 102 | In this section, I am going to cover a few basic audit situations that require | 104 | In this section, I am going to cover a few basic audit situations that |
| 103 | sampling. While some situations may require more effort, the syntax, | 105 | require sampling. While some situations may require more effort, the |
| 104 | organization, and intellect used remain largely the same. If you've never used | 106 | syntax, organization, and intellect used remain largely the same. If |
| 105 | Python before, note that lines starting with a '`#`' symbol are called comments, | 107 | you've never used Python before, note that lines starting with a '=#=' |
| 106 | and they will be skipped by Python. I highly recommend taking a quick tutorial | 108 | symbol are called comments, and they will be skipped by Python. I highly |
| 107 | online to understand the basics of Python if any of the code below is confusing | 109 | recommend taking a quick tutorial online to understand the basics of |
| 108 | to you. | 110 | Python if any of the code below is confusing to you. |
| 109 | 111 | ||
| 110 | ## Simple Random Sample | 112 | ** Simple Random Sample |
| 111 | 113 | ||
| 112 | First, let's look at a simple, random sample. The code block below will import | 114 | First, let's look at a simple, random sample. The code block below will |
| 113 | the `pandas` module, load a data file, sample the data, and export the sample to | 115 | import the =pandas= module, load a data file, sample the data, and |
| 114 | a file. | 116 | export the sample to a file. |
| 115 | 117 | ||
| 116 | ```python | 118 | #+begin_src python |
| 117 | # Import the Pandas module | 119 | # Import the Pandas module |
| 118 | import pandas | 120 | import pandas |
| 119 | 121 | ||
| @@ -131,14 +133,14 @@ sample = df.sample(n=25, random_state=0) | |||
| 131 | 133 | ||
| 132 | # Save the sample to Excel | 134 | # Save the sample to Excel |
| 133 | sample.to_excel(file_output) | 135 | sample.to_excel(file_output) |
| 134 | ``` | 136 | #+end_src |
| 135 | 137 | ||
| 136 | ## Simple Random Sample: Using Multiple Input Files | 138 | ** Simple Random Sample: Using Multiple Input Files |
| 137 | 139 | ||
| 138 | Now that we've created a simple sample, let's create a sample from multiple | 140 | Now that we've created a simple sample, let's create a sample from |
| 139 | files. | 141 | multiple files. |
| 140 | 142 | ||
| 141 | ```python | 143 | #+begin_src python |
| 142 | # Import the Pandas module | 144 | # Import the Pandas module |
| 143 | import pandas | 145 | import pandas |
| 144 | 146 | ||
| @@ -165,15 +167,15 @@ sample = pandas.concat([sample_01, sample_02, sample_03], ignore_index=True) | |||
| 165 | 167 | ||
| 166 | # Save the sample to Excel | 168 | # Save the sample to Excel |
| 167 | sample.to_excel(file_output) | 169 | sample.to_excel(file_output) |
| 168 | ``` | 170 | #+end_src |
| 169 | 171 | ||
| 170 | ## Stratified Random Sample | 172 | ** Stratified Random Sample |
| 171 | 173 | ||
| 172 | Well, what if you need to sample distinct parts of a single file? For example, | 174 | Well, what if you need to sample distinct parts of a single file? For |
| 173 | let's write some code to separate our data by "Region" and sample those regions | 175 | example, let's write some code to separate our data by "Region" and |
| 174 | independently. | 176 | sample those regions independently. |
| 175 | 177 | ||
| 176 | ```python | 178 | #+begin_src python |
| 177 | # Import the Pandas module | 179 | # Import the Pandas module |
| 178 | import pandas | 180 | import pandas |
| 179 | 181 | ||
| @@ -199,16 +201,16 @@ sample = pandas.concat([sample_east, sample_west], ignore_index=True) | |||
| 199 | 201 | ||
| 200 | # Save the sample to Excel | 202 | # Save the sample to Excel |
| 201 | sample.to_excel(file_output) | 203 | sample.to_excel(file_output) |
| 202 | ``` | 204 | #+end_src |
| 203 | 205 | ||
| 204 | ## Stratified Systematic Sample | 206 | ** Stratified Systematic Sample |
| 205 | 207 | ||
| 206 | This next example is quite useful if you need audit coverage over a certain time | 208 | This next example is quite useful if you need audit coverage over a |
| 207 | period. This code will generate samples for each month in the data and combine | 209 | certain time period. This code will generate samples for each month in |
| 208 | them all together at the end. Obviously, this code can be modified to stratify | 210 | the data and combine them all together at the end. Obviously, this code |
| 209 | by something other than months, if needed. | 211 | can be modified to stratify by something other than months, if needed. |
| 210 | 212 | ||
| 211 | ```python | 213 | #+begin_src python |
| 212 | # Import the Pandas module | 214 | # Import the Pandas module |
| 213 | import pandas | 215 | import pandas |
| 214 | 216 | ||
| @@ -248,21 +250,21 @@ def monthly_stratified_sample(df: pandas.DataFrame, date_column: str, num_select | |||
| 248 | sample_size = 3 | 250 | sample_size = 3 |
| 249 | sample = monthly_stratified_sample(df, 'Date of Sale', sample_size) | 251 | sample = monthly_stratified_sample(df, 'Date of Sale', sample_size) |
| 250 | sample.to_excel(file_output) | 252 | sample.to_excel(file_output) |
| 251 | ``` | 253 | #+end_src |
| 252 | 254 | ||
| 253 | ### Documenting the Results | 255 | *** Documenting the Results |
| 254 | 256 | ||
| 255 | Once you've generated a proper sample, there are a few things left to do in | 257 | Once you've generated a proper sample, there are a few things left to do |
| 256 | order to properly ensure your process is reproducible. | 258 | in order to properly ensure your process is reproducible. |
| 257 | 259 | ||
| 258 | 1. Document the sample. Make sure the resulting file is readable and includes | 260 | 1. Document the sample. Make sure the resulting file is readable and |
| 259 | the documentation listed in the next bullet. | 261 | includes the documentation listed in the next bullet. |
| 260 | 2. Include documentation around the data source, extraction techniques, any | 262 | 2. Include documentation around the data source, extraction techniques, |
| 261 | modifications made to the data, and be sure to include a copy of the script | 263 | any modifications made to the data, and be sure to include a copy of |
| 262 | itself. | 264 | the script itself. |
| 263 | 3. Whenever possible, perform a completeness and accuracy test to ensure your | 265 | 3. Whenever possible, perform a completeness and accuracy test to ensure |
| 264 | sample is coming from a complete and accurate population. To ensure | 266 | your sample is coming from a complete and accurate population. To |
| 265 | completeness, compare the record count from the data source to the record | 267 | ensure completeness, compare the record count from the data source to |
| 266 | count loaded into Python. To ensure accuracy, test a small sample against the | 268 | the record count loaded into Python. To ensure accuracy, test a small |
| 267 | source data (e.g., test 5 sales against the database to see if the details | 269 | sample against the source data (e.g., test 5 sales against the |
| 268 | are accurate). | 270 | database to see if the details are accurate). |
content/blog/2021-10-09-apache-redirect.md deleted −45
| @@ -1,45 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-10-09 | ||
| 3 | title = "Apache Redirect HTML Files to a Directory" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Problem | ||
| 9 | |||
| 10 | After recently switching static site generators (SSG), my blog URLs changed with | ||
| 11 | no option to preserve the classic `.html` extension at the end of my blog post | ||
| 12 | URLs. | ||
| 13 | |||
| 14 | I really disliked using my old SSG ([Jekyll](https://jekyllrb.com)) and prefer | ||
| 15 | my new tool ([Zola](https://www.getzola.org)) much more, so I was determined to | ||
| 16 | figure out a way to get the proper redirect set up so that people who find my | ||
| 17 | posts online aren't constantly met by 404 errors. | ||
| 18 | |||
| 19 | # The Solution | ||
| 20 | |||
| 21 | To solve this problem, I really needed to solve two pieces: | ||
| 22 | |||
| 23 | 1. Redirect all blog post URL requests from `/blog/some-post.html` to | ||
| 24 | `/blog/some-post/`. | ||
| 25 | 2. Ensure that no other `.html` files are redirected, such as `index.html`. | ||
| 26 | |||
| 27 | After _a lot_ of tweaking and testing, I believe I have finally found the | ||
| 28 | solution. The solution is shown below. | ||
| 29 | |||
| 30 | ```conf | ||
| 31 | RewriteEngine On | ||
| 32 | RewriteCond %{REQUEST_URI} !\index.html$ [NC] | ||
| 33 | RewriteRule ^(.*).html$ https://example.com/$1 [R=301,L] | ||
| 34 | ``` | ||
| 35 | |||
| 36 | This piece of code in the Apache `.conf` or `.htaccess` file will do the | ||
| 37 | following: | ||
| 38 | |||
| 39 | 1. Turn on the RewriteEngine so that we can modify URLs. | ||
| 40 | 2. Ignore any `index.html` files from the rule we are about to specify. | ||
| 41 | 3. Find any `.html` files within the website directory and redirect it to | ||
| 42 | exclude the file extension. | ||
| 43 | 4. The final piece is adding the trailing slash (`/`) at the end of the URL - | ||
| 44 | you'll notice that I don't have an Apache rule for that since Apache handles | ||
| 45 | that automatically. | ||
content/blog/2021-10-09-apache-redirect.org added +45
| @@ -0,0 +1,45 @@ | |||
| 1 | #+date: <2021-10-09> | ||
| 2 | #+title: Apache Redirect HTML Files to a Directory | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Problem | ||
| 7 | |||
| 8 | After recently switching static site generators (SSG), my blog URLs | ||
| 9 | changed with no option to preserve the classic =.html= extension at the | ||
| 10 | end of my blog post URLs. | ||
| 11 | |||
| 12 | I really disliked using my old SSG ([[https://jekyllrb.com][Jekyll]]) | ||
| 13 | and prefer my new tool ([[https://www.getzola.org][Zola]]) much more, so | ||
| 14 | I was determined to figure out a way to get the proper redirect set up | ||
| 15 | so that people who find my posts online aren't constantly met by 404 | ||
| 16 | errors. | ||
| 17 | |||
| 18 | * The Solution | ||
| 19 | |||
| 20 | To solve this problem, I really needed to solve two pieces: | ||
| 21 | |||
| 22 | 1. Redirect all blog post URL requests from =/blog/some-post.html= to | ||
| 23 | =/blog/some-post/=. | ||
| 24 | 2. Ensure that no other =.html= files are redirected, such as | ||
| 25 | =index.html=. | ||
| 26 | |||
| 27 | After /a lot/ of tweaking and testing, I believe I have finally found | ||
| 28 | the solution. The solution is shown below. | ||
| 29 | |||
| 30 | #+begin_src conf | ||
| 31 | RewriteEngine On | ||
| 32 | RewriteCond %{REQUEST_URI} !\index.html$ [NC] | ||
| 33 | RewriteRule ^(.*).html$ https://example.com/$1 [R=301,L] | ||
| 34 | #+end_src | ||
| 35 | |||
| 36 | This piece of code in the Apache =.conf= or =.htaccess= file will do the | ||
| 37 | following: | ||
| 38 | |||
| 39 | 1. Turn on the RewriteEngine so that we can modify URLs. | ||
| 40 | 2. Ignore any =index.html= files from the rule we are about to specify. | ||
| 41 | 3. Find any =.html= files within the website directory and redirect it | ||
| 42 | to exclude the file extension. | ||
| 43 | 4. The final piece is adding the trailing slash (=/=) at the end of the | ||
| 44 | URL - you'll notice that I don't have an Apache rule for that since | ||
| 45 | Apache handles that automatically. | ||
content/blog/2021-12-04-cisa.md deleted −193
| @@ -1,193 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2021-12-04 | ||
| 3 | title = "I Passed the CISA!" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is the CISA? | ||
| 9 | |||
| 10 | For those of you lucky enough not to be knee-deep in the world of IT/IS | ||
| 11 | Auditing, [CISA](https://www.isaca.org/credentialing/cisa) stands for Certified | ||
| 12 | Information Systems Auditor. This certification and exam are part of ISACA's | ||
| 13 | suite of certifications. As I often explain it to people like my family, it | ||
| 14 | basically means you're employed to use your knowledge of information systems, | ||
| 15 | regulations, common threats, risks, etc. in order to assess an organization's | ||
| 16 | current control of their risk. If a risk isn't controlled (and the company | ||
| 17 | doesn't want to accept the risk), an IS auditor will suggest implementing a | ||
| 18 | control to address that risk. | ||
| 19 | |||
| 20 | Now, the CISA certification itself is, in my opinion, the main certification for | ||
| 21 | this career. While certifications such as the CPA or CISSP are beneficial, | ||
| 22 | nothing matches the power of the CISA for an IS auditor when it comes to getting | ||
| 23 | hired, getting a raise/bonus, or earning respect in the field. | ||
| 24 | |||
| 25 | However, to be honest, I am a skeptic of most certifications. I understand the | ||
| 26 | value they hold in terms of how much you need to commit to studying or learning | ||
| 27 | on the job, as well as the market value for certifications such as the CISA. But | ||
| 28 | I also have known some very ~~incompetent~~ _less than stellar_ auditors who | ||
| 29 | have CPAs, CISAs, CIAs, etc. | ||
| 30 | |||
| 31 | The same goes for most industries: if a person is good at studying, they can | ||
| 32 | earn the certification. However, that knowledge means nothing unless you're | ||
| 33 | actually able to use it in real life and perform as expected of a certification | ||
| 34 | holder. The challenge comes when people are hired or connected strictly because | ||
| 35 | of their certifications or resume; you need to see a person work before you can | ||
| 36 | assume them having a CISA means they're better than someone without the CISA. | ||
| 37 | |||
| 38 | Okay, rant over. Certifications are generally accepted as a measuring stick of | ||
| 39 | commitment and quality of an employee, so I am accepting it too. | ||
| 40 | |||
| 41 | # Exam Content | ||
| 42 | |||
| 43 | The CISA is broken down into five sections, each weighted with a percentage of | ||
| 44 | test questions that may appear. | ||
| 45 | |||
| 46 | Since the exam contains 150 questions, here's how those sections break down: | ||
| 47 | |||
| 48 | | Exam Section | Percentage of Exam | Questions | | ||
| 49 | | --------------- | ------------------ | --------- | | ||
| 50 | | 1 | 21% | 32 | | ||
| 51 | | 2 | 17% | 26 | | ||
| 52 | | 3 | 12% | 18 | | ||
| 53 | | 4 | 23% | 34 | | ||
| 54 | | 5 | 27% | 40 | | ||
| 55 | | **Grand Total** | **100%** | **150** | | ||
| 56 | |||
| 57 | # My Studying Habits | ||
| 58 | |||
| 59 | This part is a little hard for me to break down into specific detail due to the | ||
| 60 | craziness of the last year. While I officially purchased my studying materials | ||
| 61 | in December 2020 and opened them to "start studying" in January 2021, I really | ||
| 62 | wasn't able to study much due to the demands of my job and personal life. | ||
| 63 | |||
| 64 | Let me approach this from a few different viewpoints. | ||
| 65 | |||
| 66 | ## Study Materials | ||
| 67 | |||
| 68 | Let's start by discussing the study materials I purchased. I'll be referring to | ||
| 69 | #1 as the CRM and #2 as the QAE. | ||
| 70 | |||
| 71 | 1. [CISA Review Manual, 27th Edition | | ||
| 72 | Print](https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCbEAK) | ||
| 73 | 2. [CISA Review Questions, Answers & Explanations Manual, 12th Edition | | ||
| 74 | Print](https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCcEAK) | ||
| 75 | |||
| 76 | The CRM is an excellent source of information and could honestly be used as a | ||
| 77 | reference for most IS auditors as a learning reference during their daily audit | ||
| 78 | responsibilities. However, it is **full** of information and can be overloading | ||
| 79 | if you're not good at filtering out useless information while studying. | ||
| 80 | |||
| 81 | The QAE is the real star of the show here. This book contains 1000 questions, | ||
| 82 | separated by exam section, and a practice exam. My only complaint about the QAE | ||
| 83 | is that each question is immediately followed with the correct answer and | ||
| 84 | explanations below it, which means I had to use something to constantly cover | ||
| 85 | the answers while I was studying. | ||
| 86 | |||
| 87 | I didn't use the online database version of the QAE, but I've heard that it's | ||
| 88 | easier to use than the printed book. However, it is more expensive ($299 | ||
| 89 | database vs $129 book) which might be important if you're paying for materials | ||
| 90 | yourself. | ||
| 91 | |||
| 92 | In terms of question difficulty, I felt that the QAE was a good representation | ||
| 93 | of the actual exam. I've seen a lot of people online say it wasn't accurate to | ||
| 94 | the exam or that it was much easier/harder, but I disagree with all of those. | ||
| 95 | The exam was fairly similar to the QAE, just focusing on whichever topics they | ||
| 96 | chose for my version of the exam. | ||
| 97 | |||
| 98 | If you understand the concepts, skim the CRM (and read in-depth on topics you | ||
| 99 | struggle with), and use the QAE to continue practicing exam-like questions, you | ||
| 100 | should be fine. I didn't use any online courses, videos, etc. - the ISACA | ||
| 101 | materials are more than enough. | ||
| 102 | |||
| 103 | ## Studying Process | ||
| 104 | |||
| 105 | While I was able to briefly read through sections 1 and 2 in early 2021, I had | ||
| 106 | to stop and take a break from February/March to September. I switched jobs in | ||
| 107 | September, which allowed me a lot more free time to study. | ||
| 108 | |||
| 109 | In September, I studied sections 3-5, took notes, and did a quick review of the | ||
| 110 | section topics. Once I felt comfortable with my notes, I took a practice exam | ||
| 111 | from the QAE manual and scored 70% (105/150). | ||
| 112 | |||
| 113 | Here's a breakdown of my initial practice exam: | ||
| 114 | |||
| 115 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | ||
| 116 | | --------------- | --------- | ------- | ----------- | ------- | | ||
| 117 | | 1 | 8 | 25 | 33 | 76% | | ||
| 118 | | 2 | 5 | 20 | 25 | 80% | | ||
| 119 | | 3 | 6 | 12 | 18 | 67% | | ||
| 120 | | 4 | 10 | 23 | 33 | 70% | | ||
| 121 | | 5 | 16 | 25 | 41 | 61% | | ||
| 122 | | **Grand Total** | **45** | **105** | **150** | **70%** | | ||
| 123 | |||
| 124 | As I expected, my toughest sections were related to project management, | ||
| 125 | development, implementation, and security. | ||
| 126 | |||
| 127 | This just leaves October and November. For these months, I tried to practice | ||
| 128 | every few days, doing 10 questions for each section, until the exam. This came | ||
| 129 | out to 13 practice sessions, ~140 questions per section, and ~700 questions | ||
| 130 | total. | ||
| 131 | |||
| 132 | While some practice sessions were worse and some were better, the final results | ||
| 133 | were similar to my practice exam results. As you can see below, my averages were | ||
| 134 | slightly worse than my practice exam. However, I got in over 700 questions of | ||
| 135 | practice and, most importantly, _I read through the explanations every time I | ||
| 136 | answered incorrectly and learned from my mistakes_. | ||
| 137 | |||
| 138 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | ||
| 139 | | --------------- | --------- | ------- | ----------- | ------- | | ||
| 140 | | 1 | 33 | 108 | 141 | 77% | | ||
| 141 | | 2 | 33 | 109 | 142 | 77% | | ||
| 142 | | 3 | 55 | 89 | 144 | 62% | | ||
| 143 | | 4 | 52 | 88 | 140 | 63% | | ||
| 144 | | 5 | 55 | 85 | 140 | 61% | | ||
| 145 | | **Grand Total** | **228** | **479** | **707** | **68%** | | ||
| 146 | |||
| 147 | # Results | ||
| 148 | |||
| 149 | Now, how do the practice scores reflect my actual results? After all, it's hard | ||
| 150 | to tell how good a practice regimen is unless you see how it turns out. | ||
| 151 | |||
| 152 | | Exam Section | Section Name | Score | | ||
| 153 | | ------------ | ---------------------------------------------------------------- | ----- | | ||
| 154 | | 1 | Information Systems Auditing Process | 678 | | ||
| 155 | | 2 | Governance and Management of IT | 590 | | ||
| 156 | | 3 | Information Systems Acquisition, Development, and Implementation | 721 | | ||
| 157 | | 4 | Information Systems Operations and Business Resilience | 643 | | ||
| 158 | | 5 | Protection of Information Assets | 511 | | ||
| 159 | |||
| 160 | Now, in order to pass the CISA, you need at least 450 on a sliding scale of | ||
| 161 | 200-800. Personally, I really have no clue what an average CISA score is. After | ||
| 162 | a _very_ brief look online, I can see that the high end is usually in the low | ||
| 163 | 700s. In addition, only about 50-60% of people pass the exam. | ||
| 164 | |||
| 165 | Given this information, I feel great about my scores. 616 may not be phenomenal, | ||
| 166 | and I wish I had done better on sections 2 & 5, but my practicing seems to have | ||
| 167 | worked very well overall. | ||
| 168 | |||
| 169 | However, the practice results do not conform to the actual results. Section 2 | ||
| 170 | was one of my highest practice sections and was my second-lowest score in the | ||
| 171 | exam. Conversely, section 3 was my second-lowest practice section and turned out | ||
| 172 | to be my highest actual score! | ||
| 173 | |||
| 174 | After reflecting, it is obvious that if you have any background on the CISA | ||
| 175 | topics at all, the most important part of studying is doing practice questions. | ||
| 176 | You really need to understand how to read the questions critically and pick the | ||
| 177 | best answer. | ||
| 178 | |||
| 179 | # Looking Forward | ||
| 180 | |||
| 181 | I am extremely happy that I was finally able to pass the CISA. Looking to the | ||
| 182 | future, I'm not sure what's next in terms of professional learning. My current | ||
| 183 | company offers internal learning courses, so I will most likely focus on that if | ||
| 184 | I need to gain more knowledge in certain areas. | ||
| 185 | |||
| 186 | To be fair, even if you pass the CISA, it's hard to become an expert on any | ||
| 187 | specific topic found within. My career may take me in a different direction, and | ||
| 188 | I might need to focus more on security or networking certifications (or possibly | ||
| 189 | building a better analysis/visualization portfolio if I want to go into data | ||
| 190 | analysis/science). | ||
| 191 | |||
| 192 | All I know is that I am content at the moment and extremely proud of my | ||
| 193 | accomplishment. | ||
content/blog/2021-12-04-cisa.org added +205
| @@ -0,0 +1,205 @@ | |||
| 1 | #+date: <2021-12-04> | ||
| 2 | #+title: I Passed the CISA! | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is the CISA? | ||
| 7 | |||
| 8 | For those of you lucky enough not to be knee-deep in the world of IT/IS | ||
| 9 | Auditing, [[https://www.isaca.org/credentialing/cisa][CISA]] stands for | ||
| 10 | Certified Information Systems Auditor. This certification and exam are | ||
| 11 | part of ISACA's suite of certifications. As I often explain it to people | ||
| 12 | like my family, it basically means you're employed to use your knowledge | ||
| 13 | of information systems, regulations, common threats, risks, etc. in | ||
| 14 | order to assess an organization's current control of their risk. If a | ||
| 15 | risk isn't controlled (and the company doesn't want to accept the risk), | ||
| 16 | an IS auditor will suggest implementing a control to address that risk. | ||
| 17 | |||
| 18 | Now, the CISA certification itself is, in my opinion, the main | ||
| 19 | certification for this career. While certifications such as the CPA or | ||
| 20 | CISSP are beneficial, nothing matches the power of the CISA for an IS | ||
| 21 | auditor when it comes to getting hired, getting a raise/bonus, or | ||
| 22 | earning respect in the field. | ||
| 23 | |||
| 24 | However, to be honest, I am a skeptic of most certifications. I | ||
| 25 | understand the value they hold in terms of how much you need to commit | ||
| 26 | to studying or learning on the job, as well as the market value for | ||
| 27 | certifications such as the CISA. But I also have known some very | ||
| 28 | +incompetent+ /less than stellar/ auditors who have CPAs, CISAs, CIAs, | ||
| 29 | etc. | ||
| 30 | |||
| 31 | The same goes for most industries: if a person is good at studying, they | ||
| 32 | can earn the certification. However, that knowledge means nothing unless | ||
| 33 | you're actually able to use it in real life and perform as expected of a | ||
| 34 | certification holder. The challenge comes when people are hired or | ||
| 35 | connected strictly because of their certifications or resume; you need | ||
| 36 | to see a person work before you can assume them having a CISA means | ||
| 37 | they're better than someone without the CISA. | ||
| 38 | |||
| 39 | Okay, rant over. Certifications are generally accepted as a measuring | ||
| 40 | stick of commitment and quality of an employee, so I am accepting it | ||
| 41 | too. | ||
| 42 | |||
| 43 | * Exam Content | ||
| 44 | |||
| 45 | The CISA is broken down into five sections, each weighted with a | ||
| 46 | percentage of test questions that may appear. | ||
| 47 | |||
| 48 | Since the exam contains 150 questions, here's how those sections break | ||
| 49 | down: | ||
| 50 | |||
| 51 | | Exam Section | Percentage of Exam | Questions | | ||
| 52 | |---------------+--------------------+-----------| | ||
| 53 | | 1 | 21% | 32 | | ||
| 54 | | 2 | 17% | 26 | | ||
| 55 | | 3 | 12% | 18 | | ||
| 56 | | 4 | 23% | 34 | | ||
| 57 | | 5 | 27% | 40 | | ||
| 58 | | *Grand Total* | *100%* | *150* | | ||
| 59 | |||
| 60 | * My Studying Habits | ||
| 61 | |||
| 62 | This part is a little hard for me to break down into specific detail due | ||
| 63 | to the craziness of the last year. While I officially purchased my | ||
| 64 | studying materials in December 2020 and opened them to "start studying" | ||
| 65 | in January 2021, I really wasn't able to study much due to the demands | ||
| 66 | of my job and personal life. | ||
| 67 | |||
| 68 | Let me approach this from a few different viewpoints. | ||
| 69 | |||
| 70 | ** Study Materials | ||
| 71 | |||
| 72 | Let's start by discussing the study materials I purchased. I'll be | ||
| 73 | referring to #1 as the CRM and #2 as the QAE. | ||
| 74 | |||
| 75 | 1. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCbEAK][CISA | ||
| 76 | Review Manual, 27th Edition | Print]] | ||
| 77 | 2. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCcEAK][CISA | ||
| 78 | Review Questions, Answers & Explanations Manual, 12th Edition | | ||
| 79 | Print]] | ||
| 80 | |||
| 81 | The CRM is an excellent source of information and could honestly be used | ||
| 82 | as a reference for most IS auditors as a learning reference during their | ||
| 83 | daily audit responsibilities. However, it is *full* of information and | ||
| 84 | can be overloading if you're not good at filtering out useless | ||
| 85 | information while studying. | ||
| 86 | |||
| 87 | The QAE is the real star of the show here. This book contains 1000 | ||
| 88 | questions, separated by exam section, and a practice exam. My only | ||
| 89 | complaint about the QAE is that each question is immediately followed | ||
| 90 | with the correct answer and explanations below it, which means I had to | ||
| 91 | use something to constantly cover the answers while I was studying. | ||
| 92 | |||
| 93 | I didn't use the online database version of the QAE, but I've heard that | ||
| 94 | it's easier to use than the printed book. However, it is more expensive | ||
| 95 | ($299 database vs $129 book) which might be important if you're paying | ||
| 96 | for materials yourself. | ||
| 97 | |||
| 98 | In terms of question difficulty, I felt that the QAE was a good | ||
| 99 | representation of the actual exam. I've seen a lot of people online say | ||
| 100 | it wasn't accurate to the exam or that it was much easier/harder, but I | ||
| 101 | disagree with all of those. The exam was fairly similar to the QAE, just | ||
| 102 | focusing on whichever topics they chose for my version of the exam. | ||
| 103 | |||
| 104 | If you understand the concepts, skim the CRM (and read in-depth on | ||
| 105 | topics you struggle with), and use the QAE to continue practicing | ||
| 106 | exam-like questions, you should be fine. I didn't use any online | ||
| 107 | courses, videos, etc. - the ISACA materials are more than enough. | ||
| 108 | |||
| 109 | ** Studying Process | ||
| 110 | |||
| 111 | While I was able to briefly read through sections 1 and 2 in early 2021, | ||
| 112 | I had to stop and take a break from February/March to September. I | ||
| 113 | switched jobs in September, which allowed me a lot more free time to | ||
| 114 | study. | ||
| 115 | |||
| 116 | In September, I studied sections 3-5, took notes, and did a quick review | ||
| 117 | of the section topics. Once I felt comfortable with my notes, I took a | ||
| 118 | practice exam from the QAE manual and scored 70% (105/150). | ||
| 119 | |||
| 120 | Here's a breakdown of my initial practice exam: | ||
| 121 | |||
| 122 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | ||
| 123 | |---------------+-----------+---------+-------------+---------| | ||
| 124 | | 1 | 8 | 25 | 33 | 76% | | ||
| 125 | | 2 | 5 | 20 | 25 | 80% | | ||
| 126 | | 3 | 6 | 12 | 18 | 67% | | ||
| 127 | | 4 | 10 | 23 | 33 | 70% | | ||
| 128 | | 5 | 16 | 25 | 41 | 61% | | ||
| 129 | | *Grand Total* | *45* | *105* | *150* | *70%* | | ||
| 130 | |||
| 131 | As I expected, my toughest sections were related to project management, | ||
| 132 | development, implementation, and security. | ||
| 133 | |||
| 134 | This just leaves October and November. For these months, I tried to | ||
| 135 | practice every few days, doing 10 questions for each section, until the | ||
| 136 | exam. This came out to 13 practice sessions, ~140 questions per section, | ||
| 137 | and ~700 questions total. | ||
| 138 | |||
| 139 | While some practice sessions were worse and some were better, the final | ||
| 140 | results were similar to my practice exam results. As you can see below, | ||
| 141 | my averages were slightly worse than my practice exam. However, I got in | ||
| 142 | over 700 questions of practice and, most importantly, /I read through | ||
| 143 | the explanations every time I answered incorrectly and learned from my | ||
| 144 | mistakes/. | ||
| 145 | |||
| 146 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | ||
| 147 | |---------------+-----------+---------+-------------+---------| | ||
| 148 | | 1 | 33 | 108 | 141 | 77% | | ||
| 149 | | 2 | 33 | 109 | 142 | 77% | | ||
| 150 | | 3 | 55 | 89 | 144 | 62% | | ||
| 151 | | 4 | 52 | 88 | 140 | 63% | | ||
| 152 | | 5 | 55 | 85 | 140 | 61% | | ||
| 153 | | *Grand Total* | *228* | *479* | *707* | *68%* | | ||
| 154 | |||
| 155 | * Results | ||
| 156 | |||
| 157 | Now, how do the practice scores reflect my actual results? After all, | ||
| 158 | it's hard to tell how good a practice regimen is unless you see how it | ||
| 159 | turns out. | ||
| 160 | |||
| 161 | | Exam Section | Section Name | Score | | ||
| 162 | |--------------+------------------------------------------------------------------+-------| | ||
| 163 | | 1 | Information Systems Auditing Process | 678 | | ||
| 164 | | 2 | Governance and Management of IT | 590 | | ||
| 165 | | 3 | Information Systems Acquisition, Development, and Implementation | 721 | | ||
| 166 | | 4 | Information Systems Operations and Business Resilience | 643 | | ||
| 167 | | 5 | Protection of Information Assets | 511 | | ||
| 168 | |||
| 169 | Now, in order to pass the CISA, you need at least 450 on a sliding scale | ||
| 170 | of 200-800. Personally, I really have no clue what an average CISA score | ||
| 171 | is. After a /very/ brief look online, I can see that the high end is | ||
| 172 | usually in the low 700s. In addition, only about 50-60% of people pass | ||
| 173 | the exam. | ||
| 174 | |||
| 175 | Given this information, I feel great about my scores. 616 may not be | ||
| 176 | phenomenal, and I wish I had done better on sections 2 & 5, but my | ||
| 177 | practicing seems to have worked very well overall. | ||
| 178 | |||
| 179 | However, the practice results do not conform to the actual results. | ||
| 180 | Section 2 was one of my highest practice sections and was my | ||
| 181 | second-lowest score in the exam. Conversely, section 3 was my | ||
| 182 | second-lowest practice section and turned out to be my highest actual | ||
| 183 | score! | ||
| 184 | |||
| 185 | After reflecting, it is obvious that if you have any background on the | ||
| 186 | CISA topics at all, the most important part of studying is doing | ||
| 187 | practice questions. You really need to understand how to read the | ||
| 188 | questions critically and pick the best answer. | ||
| 189 | |||
| 190 | * Looking Forward | ||
| 191 | |||
| 192 | I am extremely happy that I was finally able to pass the CISA. Looking | ||
| 193 | to the future, I'm not sure what's next in terms of professional | ||
| 194 | learning. My current company offers internal learning courses, so I will | ||
| 195 | most likely focus on that if I need to gain more knowledge in certain | ||
| 196 | areas. | ||
| 197 | |||
| 198 | To be fair, even if you pass the CISA, it's hard to become an expert on | ||
| 199 | any specific topic found within. My career may take me in a different | ||
| 200 | direction, and I might need to focus more on security or networking | ||
| 201 | certifications (or possibly building a better analysis/visualization | ||
| 202 | portfolio if I want to go into data analysis/science). | ||
| 203 | |||
| 204 | All I know is that I am content at the moment and extremely proud of my | ||
| 205 | accomplishment. | ||
content/blog/2022-02-10-leaving-the-office.md deleted −207
| @@ -1,207 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-02-10 | ||
| 3 | title = "Leaving Office-Based Work in the Past" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Working World is Changing | ||
| 9 | |||
| 10 | There has been a trend for the past few years of companies slowly realizing that | ||
| 11 | the pandemic is not just a temporary state that will go away eventually and let | ||
| 12 | everything return to the way it was before. In terms of business and employment, | ||
| 13 | this means that more and more jobs are being offered as permanently remote | ||
| 14 | roles. | ||
| 15 | |||
| 16 | I had always dreamt of working from home but thought of it as a fantasy, | ||
| 17 | especially since I did not want to move over into the software development | ||
| 18 | field. However, I have found that almost all roles being sent to me via | ||
| 19 | recruiters are permanently remote (although most are limited to US citizens or | ||
| 20 | even region-locked for companies who only operate in select states). | ||
| 21 | |||
| 22 | I decided to take a look back at my relatively short career so far and compare | ||
| 23 | the positive and negative effects of the different work environments I've been | ||
| 24 | in. | ||
| 25 | |||
| 26 | # In-Person Offices | ||
| 27 | |||
| 28 | ## Retail Internship | ||
| 29 | |||
| 30 | I started my first job as a management intern at a busy retail pharmacy, working | ||
| 31 | my 40-hour weeks on my feet. As these retail stores don't believe in resting or | ||
| 32 | sitting down, you can guarantee that you will spend entire shifts standing, | ||
| 33 | walking, or running around the store. Unfortunately, I worked at a time when our | ||
| 34 | store didn't have enough managers, so I spent the majority of my tenure at the | ||
| 35 | store running and breaking a sweat. | ||
| 36 | |||
| 37 | Now, things aren't all bad in retail stores like this. It is definitely tiring | ||
| 38 | and inefficient to force employees to work constantly, or pretend to work if | ||
| 39 | there's nothing to do, and not allow anyone to sit down. However, if you are | ||
| 40 | able to operate a retail store with a limited crew and provide enough comfort | ||
| 41 | and support, I believe these jobs could be both comfortable and efficient. | ||
| 42 | |||
| 43 | ## Semi-Private Cubicles | ||
| 44 | |||
| 45 | After about a year, I was able to find another internship - this time, it was in | ||
| 46 | my field of interest: internal auditing. This was for a life insurance company | ||
| 47 | that was well over 100 years old. The age of the company shows if you work | ||
| 48 | there, as most people in management are well into their 40s-60s with little to | ||
| 49 | no youthful leadership in the company. Likewise, they owned a large headquarters | ||
| 50 | in a nice area of town with plenty of space, parking, etc. | ||
| 51 | |||
| 52 | One upside is that each person gets their own large L-shaped desk, formed into | ||
| 53 | cubicles that house 4 desks/employees. These "pods" of 4-person cubicles are | ||
| 54 | linked throughout each floor of the headquarters (except the sales people, who | ||
| 55 | had that open-floor concept going on). The walls of the cubicle were tall and | ||
| 56 | provided a lot of privacy and sound-proofing, except when I used the standing | ||
| 57 | desk feature (I'm over 6 feet tall, so probably not an issue for most people). | ||
| 58 | |||
| 59 | I loved this environment, it allowed me to focus on my work with minimal | ||
| 60 | distractions, but also allowed easy access, so I could spin around in my chair | ||
| 61 | and chat with my friends without leaving my chair. This is the closest I've been | ||
| 62 | to a home office environment (which is my personal favorite, as I'll get to | ||
| 63 | later in this post). | ||
| 64 | |||
| 65 | ## Semi-Open Floor Concept | ||
| 66 | |||
| 67 | When I shifted to my first full-time internal audit job out of college, I was | ||
| 68 | working at a company that was headquartered on a floor in a downtown high-rise | ||
| 69 | building. The company was only about 20 years old when I worked there and were | ||
| 70 | trying a lot of new things to attract young talent, one of which was a semi-open | ||
| 71 | floor concept for the office. My department worked just around the hallway | ||
| 72 | corner from the executive offices and used that "modern" layout young tech | ||
| 73 | companies started using in the 2000s/2010s. | ||
| 74 | |||
| 75 | Each desk was brief, and you could look most coworkers in the face without | ||
| 76 | moving from your chair, I hated this so much. Directly to my left was the Chief | ||
| 77 | Audit Executive (our department's leading boss), and his desk was pointed so | ||
| 78 | that his face would stare straight at my desk all day. I spent more time | ||
| 79 | thinking about who was looking at me or checking on me than actually working. | ||
| 80 | |||
| 81 | The other annoying part of the open concept they used was that the kitchen area | ||
| 82 | and pathways were too close to everyone's desks (since the desks were spread | ||
| 83 | out, to provide space or something), so noise and conversation would be constant | ||
| 84 | throughout the day while you try to work. For someone like me, who needs silence | ||
| 85 | to get work done, that was a non-starter. | ||
| 86 | |||
| 87 | ## Hotel Office Concept | ||
| 88 | |||
| 89 | I currently work for a company remotely (for now) and travel to the office every | ||
| 90 | once in a while for events and to help coach the staff underneath me. The office | ||
| 91 | I visit uses the hotel desk concept, where you need to check in at a touch | ||
| 92 | screen when you enter the office and "rent" a desk for the day. The same goes | ||
| 93 | for offices and meeting rooms. | ||
| 94 | |||
| 95 | These desks are flat-top only and do not have any walls at all. In addition, | ||
| 96 | they're stacked with one row of 4 desks facing another row of 4 desks. These | ||
| 97 | pairs of desk rows are repeated through the office. | ||
| 98 | |||
| 99 | This means that when I go, I need to rent a random desk or try to remember the | ||
| 100 | unique ID numbers on desks I like. Once I rent it, I have to make sure no one | ||
| 101 | sat down in that desk without renting it. Then, I can sit down and work, but | ||
| 102 | will probably need to adjust the monitors so that I'm not staring in the face of | ||
| 103 | the person across from me all day. Finally, I need to wear headphones as this | ||
| 104 | environment does nothing to provide you with peace or quiet. | ||
| 105 | |||
| 106 | Luckily, you can rent offices with doors that offer quiet and privacy, which can | ||
| 107 | be very nice if you have a lot of meetings or webinars on a certain day. | ||
| 108 | |||
| 109 | # Home Office | ||
| 110 | |||
| 111 | Okay, now let's finally get to the home office concept. I have worked from home | ||
| 112 | for a little over two years at this point, across three different | ||
| 113 | jobs/employers. Over this time, I have experimented with a plethora of different | ||
| 114 | organizational ideas, desks, and room layouts to find what works best for me. | ||
| 115 | |||
| 116 | These things might not apply to you, and that's fine. Everyone has a different | ||
| 117 | situation, and I really don't think you'll know what works until you try. | ||
| 118 | |||
| 119 | ## Tip #1 | ||
| 120 | |||
| 121 | Let's start with my top rule for a home office: | ||
| 122 | |||
| 123 | > If you live with others, working in a shared space is not effective. | ||
| 124 | |||
| 125 | It just does not work. If you have another person sleeping in your bedroom, it | ||
| 126 | is difficult to manage your work schedule with their sleeping/work/school | ||
| 127 | schedule. If they wake up after you need to start work, you might wake them up | ||
| 128 | or have to suffer the agony of staring at bright screens in a dark room. | ||
| 129 | |||
| 130 | In a similar vein, working from a location such as the living room likely won't | ||
| 131 | work either. Distractions will come far more frequently: televisions, cooking, | ||
| 132 | cleaning, deliveries, etc. If you're like me, you'll end up playing a game | ||
| 133 | instead of actually doing any work. | ||
| 134 | |||
| 135 | ## Tip #2 | ||
| 136 | |||
| 137 | Okay, the second thing I've discovered that works for me: | ||
| 138 | |||
| 139 | > Use the pomodoro method (or something similar) to balance work tasks with | ||
| 140 | > personal tasks. | ||
| 141 | |||
| 142 | I use a very casual version of the pomodoro method where I will work for 1-2 | ||
| 143 | hours (usually set in strict intervals like 1, 1.5, 2 hours) and then will allow | ||
| 144 | myself 30-60 minutes for personal tasks. This schedule works for me, since my | ||
| 145 | work schedule really only comes to 3-6 hours of work per day. | ||
| 146 | |||
| 147 | In this case, I'll work through my list of tasks for an hour or two and then | ||
| 148 | give myself personal time to get drinks and food, wash dishes, put clothes in | ||
| 149 | the washer, get the mail, etc. If you're in a convenient location, this usually | ||
| 150 | gives time for things like getting groceries (as long as you're not a slow | ||
| 151 | shopper). | ||
| 152 | |||
| 153 | ## Tip #3 | ||
| 154 | |||
| 155 | While I listed this one as number three, I don't think I'd accomplish anything | ||
| 156 | without it: | ||
| 157 | |||
| 158 | > Document everything: even things you didn't before - such as task lists and | ||
| 159 | > notes from casual calls or meetings. | ||
| 160 | |||
| 161 | I've noticed that staying in an office gave me more constant reminders of | ||
| 162 | outstanding tasks or facts I had learned in a conversation. Translating | ||
| 163 | everything to a digital world has made me lose a bit of that focus (perhaps | ||
| 164 | since I don't have visual reminders?). | ||
| 165 | |||
| 166 | Keeping a running task list of all things I have to do - even potential tasks! - | ||
| 167 | has helped me keep up without missing anything small. Likewise, keeping notes | ||
| 168 | for ALL meetings and calls, no matter how casual/quick, has improved my | ||
| 169 | retention immensely. Beyond helping my mental recollection, it has saved me | ||
| 170 | numerous times when I need to do a keyword search for some topic that was | ||
| 171 | discussed 6+ months ago. | ||
| 172 | |||
| 173 | ## Tip #4 | ||
| 174 | |||
| 175 | Okay, last one for now. | ||
| 176 | |||
| 177 | > Keep your work area clean. | ||
| 178 | |||
| 179 | This one is straightforward, but I know some people struggle with cleanliness or | ||
| 180 | may not believe it makes a difference. Trust me, keeping your desk area clean | ||
| 181 | and organized makes a huge difference, both mentally and emotionally. | ||
| 182 | |||
| 183 | Just think about it, you walk into your home office and see a clean desk with a | ||
| 184 | laptop, dock, monitors, keyboard, mouse, and a notepad with a pen on top. | ||
| 185 | |||
| 186 | Now imagine the opposite, there's an office with the same equipment, but there | ||
| 187 | are clothes hanging on the chair, empty drink bottles, candy wrappers and dirty | ||
| 188 | plates. This can take both a mental and emotional toll by bringing constant | ||
| 189 | disarray and stress into your working environment. | ||
| 190 | |||
| 191 | Just keep things clean each day, and you won't need to do any big cleaning days | ||
| 192 | to recover. | ||
| 193 | |||
| 194 | # My Preferences | ||
| 195 | |||
| 196 | I've talked about the different environments I've worked in and expressed some | ||
| 197 | honest thoughts on pros or cons to each, but what do I prefer? Well, if you're | ||
| 198 | reading along, you should be able to tell that I much prefer a home office above | ||
| 199 | all else. | ||
| 200 | |||
| 201 | Being able to control my own day and allot my time as needed has brought a | ||
| 202 | calmness to my life and has allowed me to maximize each day. I feel far more | ||
| 203 | effective and efficient in a home office than any other office, especially | ||
| 204 | open-office layouts. | ||
| 205 | |||
| 206 | If I do need to return to an office part-time in the future, I really hope the | ||
| 207 | office will have privacy and quietness in order for me to get my work done. | ||
content/blog/2022-02-10-leaving-the-office.org added +231
| @@ -0,0 +1,231 @@ | |||
| 1 | #+date: <2022-02-10> | ||
| 2 | #+title: Leaving Office-Based Work in the Past | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Working World is Changing | ||
| 7 | |||
| 8 | There has been a trend for the past few years of companies slowly | ||
| 9 | realizing that the pandemic is not just a temporary state that will go | ||
| 10 | away eventually and let everything return to the way it was before. In | ||
| 11 | terms of business and employment, this means that more and more jobs are | ||
| 12 | being offered as permanently remote roles. | ||
| 13 | |||
| 14 | I had always dreamt of working from home but thought of it as a fantasy, | ||
| 15 | especially since I did not want to move over into the software | ||
| 16 | development field. However, I have found that almost all roles being | ||
| 17 | sent to me via recruiters are permanently remote (although most are | ||
| 18 | limited to US citizens or even region-locked for companies who only | ||
| 19 | operate in select states). | ||
| 20 | |||
| 21 | I decided to take a look back at my relatively short career so far and | ||
| 22 | compare the positive and negative effects of the different work | ||
| 23 | environments I've been in. | ||
| 24 | |||
| 25 | * In-Person Offices | ||
| 26 | |||
| 27 | ** Retail Internship | ||
| 28 | |||
| 29 | I started my first job as a management intern at a busy retail pharmacy, | ||
| 30 | working my 40-hour weeks on my feet. As these retail stores don't | ||
| 31 | believe in resting or sitting down, you can guarantee that you will | ||
| 32 | spend entire shifts standing, walking, or running around the store. | ||
| 33 | Unfortunately, I worked at a time when our store didn't have enough | ||
| 34 | managers, so I spent the majority of my tenure at the store running and | ||
| 35 | breaking a sweat. | ||
| 36 | |||
| 37 | Now, things aren't all bad in retail stores like this. It is definitely | ||
| 38 | tiring and inefficient to force employees to work constantly, or pretend | ||
| 39 | to work if there's nothing to do, and not allow anyone to sit down. | ||
| 40 | However, if you are able to operate a retail store with a limited crew | ||
| 41 | and provide enough comfort and support, I believe these jobs could be | ||
| 42 | both comfortable and efficient. | ||
| 43 | |||
| 44 | ** Semi-Private Cubicles | ||
| 45 | |||
| 46 | After about a year, I was able to find another internship - this time, | ||
| 47 | it was in my field of interest: internal auditing. This was for a life | ||
| 48 | insurance company that was well over 100 years old. The age of the | ||
| 49 | company shows if you work there, as most people in management are well | ||
| 50 | into their 40s-60s with little to no youthful leadership in the company. | ||
| 51 | Likewise, they owned a large headquarters in a nice area of town with | ||
| 52 | plenty of space, parking, etc. | ||
| 53 | |||
| 54 | One upside is that each person gets their own large L-shaped desk, | ||
| 55 | formed into cubicles that house 4 desks/employees. These "pods" of | ||
| 56 | 4-person cubicles are linked throughout each floor of the headquarters | ||
| 57 | (except the sales people, who had that open-floor concept going on). The | ||
| 58 | walls of the cubicle were tall and provided a lot of privacy and | ||
| 59 | sound-proofing, except when I used the standing desk feature (I'm over 6 | ||
| 60 | feet tall, so probably not an issue for most people). | ||
| 61 | |||
| 62 | I loved this environment, it allowed me to focus on my work with minimal | ||
| 63 | distractions, but also allowed easy access, so I could spin around in my | ||
| 64 | chair and chat with my friends without leaving my chair. This is the | ||
| 65 | closest I've been to a home office environment (which is my personal | ||
| 66 | favorite, as I'll get to later in this post). | ||
| 67 | |||
| 68 | ** Semi-Open Floor Concept | ||
| 69 | |||
| 70 | When I shifted to my first full-time internal audit job out of college, | ||
| 71 | I was working at a company that was headquartered on a floor in a | ||
| 72 | downtown high-rise building. The company was only about 20 years old | ||
| 73 | when I worked there and were trying a lot of new things to attract young | ||
| 74 | talent, one of which was a semi-open floor concept for the office. My | ||
| 75 | department worked just around the hallway corner from the executive | ||
| 76 | offices and used that "modern" layout young tech companies started using | ||
| 77 | in the 2000s/2010s. | ||
| 78 | |||
| 79 | Each desk was brief, and you could look most coworkers in the face | ||
| 80 | without moving from your chair, I hated this so much. Directly to my | ||
| 81 | left was the Chief Audit Executive (our department's leading boss), and | ||
| 82 | his desk was pointed so that his face would stare straight at my desk | ||
| 83 | all day. I spent more time thinking about who was looking at me or | ||
| 84 | checking on me than actually working. | ||
| 85 | |||
| 86 | The other annoying part of the open concept they used was that the | ||
| 87 | kitchen area and pathways were too close to everyone's desks (since the | ||
| 88 | desks were spread out, to provide space or something), so noise and | ||
| 89 | conversation would be constant throughout the day while you try to work. | ||
| 90 | For someone like me, who needs silence to get work done, that was a | ||
| 91 | non-starter. | ||
| 92 | |||
| 93 | ** Hotel Office Concept | ||
| 94 | |||
| 95 | I currently work for a company remotely (for now) and travel to the | ||
| 96 | office every once in a while for events and to help coach the staff | ||
| 97 | underneath me. The office I visit uses the hotel desk concept, where you | ||
| 98 | need to check in at a touch screen when you enter the office and "rent" | ||
| 99 | a desk for the day. The same goes for offices and meeting rooms. | ||
| 100 | |||
| 101 | These desks are flat-top only and do not have any walls at all. In | ||
| 102 | addition, they're stacked with one row of 4 desks facing another row of | ||
| 103 | 4 desks. These pairs of desk rows are repeated through the office. | ||
| 104 | |||
| 105 | This means that when I go, I need to rent a random desk or try to | ||
| 106 | remember the unique ID numbers on desks I like. Once I rent it, I have | ||
| 107 | to make sure no one sat down in that desk without renting it. Then, I | ||
| 108 | can sit down and work, but will probably need to adjust the monitors so | ||
| 109 | that I'm not staring in the face of the person across from me all day. | ||
| 110 | Finally, I need to wear headphones as this environment does nothing to | ||
| 111 | provide you with peace or quiet. | ||
| 112 | |||
| 113 | Luckily, you can rent offices with doors that offer quiet and privacy, | ||
| 114 | which can be very nice if you have a lot of meetings or webinars on a | ||
| 115 | certain day. | ||
| 116 | |||
| 117 | * Home Office | ||
| 118 | |||
| 119 | Okay, now let's finally get to the home office concept. I have worked | ||
| 120 | from home for a little over two years at this point, across three | ||
| 121 | different jobs/employers. Over this time, I have experimented with a | ||
| 122 | plethora of different organizational ideas, desks, and room layouts to | ||
| 123 | find what works best for me. | ||
| 124 | |||
| 125 | These things might not apply to you, and that's fine. Everyone has a | ||
| 126 | different situation, and I really don't think you'll know what works | ||
| 127 | until you try. | ||
| 128 | |||
| 129 | ** Tip #1 | ||
| 130 | |||
| 131 | Let's start with my top rule for a home office: | ||
| 132 | |||
| 133 | #+begin_quote | ||
| 134 | If you live with others, working in a shared space is not effective. | ||
| 135 | #+end_quote | ||
| 136 | |||
| 137 | It just does not work. If you have another person sleeping in your | ||
| 138 | bedroom, it is difficult to manage your work schedule with their | ||
| 139 | sleeping/work/school schedule. If they wake up after you need to start | ||
| 140 | work, you might wake them up or have to suffer the agony of staring at | ||
| 141 | bright screens in a dark room. | ||
| 142 | |||
| 143 | In a similar vein, working from a location such as the living room | ||
| 144 | likely won't work either. Distractions will come far more frequently: | ||
| 145 | televisions, cooking, cleaning, deliveries, etc. If you're like me, | ||
| 146 | you'll end up playing a game instead of actually doing any work. | ||
| 147 | |||
| 148 | ** Tip #2 | ||
| 149 | |||
| 150 | Okay, the second thing I've discovered that works for me: | ||
| 151 | |||
| 152 | #+begin_quote | ||
| 153 | Use the pomodoro method (or something similar) to balance work tasks | ||
| 154 | with personal tasks. | ||
| 155 | #+end_quote | ||
| 156 | |||
| 157 | I use a very casual version of the pomodoro method where I will work for | ||
| 158 | 1-2 hours (usually set in strict intervals like 1, 1.5, 2 hours) and | ||
| 159 | then will allow myself 30-60 minutes for personal tasks. This schedule | ||
| 160 | works for me, since my work schedule really only comes to 3-6 hours of | ||
| 161 | work per day. | ||
| 162 | |||
| 163 | In this case, I'll work through my list of tasks for an hour or two and | ||
| 164 | then give myself personal time to get drinks and food, wash dishes, put | ||
| 165 | clothes in the washer, get the mail, etc. If you're in a convenient | ||
| 166 | location, this usually gives time for things like getting groceries (as | ||
| 167 | long as you're not a slow shopper). | ||
| 168 | |||
| 169 | ** Tip #3 | ||
| 170 | |||
| 171 | While I listed this one as number three, I don't think I'd accomplish | ||
| 172 | anything without it: | ||
| 173 | |||
| 174 | #+begin_quote | ||
| 175 | Document everything: even things you didn't before - such as task lists | ||
| 176 | and notes from casual calls or meetings. | ||
| 177 | #+end_quote | ||
| 178 | |||
| 179 | I've noticed that staying in an office gave me more constant reminders | ||
| 180 | of outstanding tasks or facts I had learned in a conversation. | ||
| 181 | Translating everything to a digital world has made me lose a bit of that | ||
| 182 | focus (perhaps since I don't have visual reminders?). | ||
| 183 | |||
| 184 | Keeping a running task list of all things I have to do - even potential | ||
| 185 | tasks! - has helped me keep up without missing anything small. Likewise, | ||
| 186 | keeping notes for ALL meetings and calls, no matter how casual/quick, | ||
| 187 | has improved my retention immensely. Beyond helping my mental | ||
| 188 | recollection, it has saved me numerous times when I need to do a keyword | ||
| 189 | search for some topic that was discussed 6+ months ago. | ||
| 190 | |||
| 191 | ** Tip #4 | ||
| 192 | |||
| 193 | Okay, last one for now. | ||
| 194 | |||
| 195 | #+begin_quote | ||
| 196 | Keep your work area clean. | ||
| 197 | #+end_quote | ||
| 198 | |||
| 199 | This one is straightforward, but I know some people struggle with | ||
| 200 | cleanliness or may not believe it makes a difference. Trust me, keeping | ||
| 201 | your desk area clean and organized makes a huge difference, both | ||
| 202 | mentally and emotionally. | ||
| 203 | |||
| 204 | Just think about it, you walk into your home office and see a clean desk | ||
| 205 | with a laptop, dock, monitors, keyboard, mouse, and a notepad with a pen | ||
| 206 | on top. | ||
| 207 | |||
| 208 | Now imagine the opposite, there's an office with the same equipment, but | ||
| 209 | there are clothes hanging on the chair, empty drink bottles, candy | ||
| 210 | wrappers and dirty plates. This can take both a mental and emotional | ||
| 211 | toll by bringing constant disarray and stress into your working | ||
| 212 | environment. | ||
| 213 | |||
| 214 | Just keep things clean each day, and you won't need to do any big | ||
| 215 | cleaning days to recover. | ||
| 216 | |||
| 217 | * My Preferences | ||
| 218 | |||
| 219 | I've talked about the different environments I've worked in and | ||
| 220 | expressed some honest thoughts on pros or cons to each, but what do I | ||
| 221 | prefer? Well, if you're reading along, you should be able to tell that I | ||
| 222 | much prefer a home office above all else. | ||
| 223 | |||
| 224 | Being able to control my own day and allot my time as needed has brought | ||
| 225 | a calmness to my life and has allowed me to maximize each day. I feel | ||
| 226 | far more effective and efficient in a home office than any other office, | ||
| 227 | especially open-office layouts. | ||
| 228 | |||
| 229 | If I do need to return to an office part-time in the future, I really | ||
| 230 | hope the office will have privacy and quietness in order for me to get | ||
| 231 | my work done. | ||
content/blog/2022-02-10-njalla-dns-api.md → content/blog/2022-02-10-njalla-dns-api.org renamed +81 −79
| @@ -1,104 +1,106 @@ | |||
| 1 | +++ | 1 | #+date: <2022-02-10> |
| 2 | date = 2022-02-10 | 2 | #+title: Dynamic DNS with Njalla API |
| 3 | title = "Dynamic DNS with Njalla API" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Njalla's API | ||
| 9 | 5 | ||
| 10 | As noted in my recent post about [switching to Njalla from | 6 | * Njalla's API |
| 11 | Cloudflare](file:///blog/ditching-cloudflare/), I was searching for a way to | ||
| 12 | replace my very easy-to-use bash script to [update Cloudflare's DNS via their | ||
| 13 | API](file:///blog/cloudflare-dns-api/). | ||
| 14 | 7 | ||
| 15 | To reiterate what I said in those posts, this is a common necessity for those of | 8 | As noted in my recent post about |
| 16 | us who have non-static IP addresses that can change at any moment due to ISP | 9 | [[file:///blog/ditching-cloudflare/][switching to Njalla from |
| 17 | policy. | 10 | Cloudflare]], I was searching for a way to replace my very easy-to-use |
| 11 | bash script to [[file:///blog/cloudflare-dns-api/][update Cloudflare's | ||
| 12 | DNS via their API]]. | ||
| 18 | 13 | ||
| 19 | In order to keep a home server running smoothly, the server admin needs to have | 14 | To reiterate what I said in those posts, this is a common necessity for |
| 20 | a process to constantly monitor their public IP address and update their | 15 | those of us who have non-static IP addresses that can change at any |
| 21 | domain's DNS records if it changes. | 16 | moment due to ISP policy. |
| 22 | 17 | ||
| 23 | This post explains how to use Python to update Njalla's DNS records whenever a | 18 | In order to keep a home server running smoothly, the server admin needs |
| 24 | machine's public IP address changes. | 19 | to have a process to constantly monitor their public IP address and |
| 20 | update their domain's DNS records if it changes. | ||
| 25 | 21 | ||
| 26 | ## Creating a Token | 22 | This post explains how to use Python to update Njalla's DNS records |
| 23 | whenever a machine's public IP address changes. | ||
| 27 | 24 | ||
| 28 | To use Njalla's API, you will first need to create a token that will be used to | 25 | ** Creating a Token |
| 29 | authenticate you every time you call the API. Luckily, this is very easy to do | ||
| 30 | if you have an account with Njalla. | ||
| 31 | 26 | ||
| 32 | Simply go the [API Settings](https://njal.la/settings/api/) page and click the | 27 | To use Njalla's API, you will first need to create a token that will be |
| 33 | `Add Token` button. Next, enter a name for the token and click `Add`. | 28 | used to authenticate you every time you call the API. Luckily, this is |
| 29 | very easy to do if you have an account with Njalla. | ||
| 34 | 30 | ||
| 35 | Finally, click the `Manage` button next to your newly created token and copy the | 31 | Simply go the [[https://njal.la/settings/api/][API Settings]] page and |
| 36 | `API Token` field. | 32 | click the =Add Token= button. Next, enter a name for the token and click |
| 33 | =Add=. | ||
| 37 | 34 | ||
| 38 | ## Finding the Correct API Request | 35 | Finally, click the =Manage= button next to your newly created token and |
| 36 | copy the =API Token= field. | ||
| 39 | 37 | ||
| 40 | Once you have a token, you're ready to call the Njalla API for any number of | 38 | ** Finding the Correct API Request |
| 41 | requests. For a full listing of available requests, see the [Njalla API | ||
| 42 | Documentation](https://njal.la/api/). | ||
| 43 | 39 | ||
| 44 | For this demo, we are using the `list-records` and `edit-record` requests. | 40 | Once you have a token, you're ready to call the Njalla API for any |
| 41 | number of requests. For a full listing of available requests, see the | ||
| 42 | [[https://njal.la/api/][Njalla API Documentation]]. | ||
| 45 | 43 | ||
| 46 | The `list-records` request requires the following payload to be sent when | 44 | For this demo, we are using the =list-records= and =edit-record= |
| 47 | calling the API: | 45 | requests. |
| 46 | |||
| 47 | The =list-records= request requires the following payload to be sent | ||
| 48 | when calling the API: | ||
| 48 | 49 | ||
| 49 | ```txt | 50 | #+begin_src txt |
| 50 | params: { | 51 | params: { |
| 51 | domain: string | 52 | domain: string |
| 52 | } | 53 | } |
| 53 | ``` | 54 | #+end_src |
| 54 | 55 | ||
| 55 | The `edit-record` request requires the following payload to be sent when calling | 56 | The =edit-record= request requires the following payload to be sent when |
| 56 | the API: | 57 | calling the API: |
| 57 | 58 | ||
| 58 | ```txt | 59 | #+begin_src txt |
| 59 | params: { | 60 | params: { |
| 60 | domain: string | 61 | domain: string |
| 61 | id: int | 62 | id: int |
| 62 | content: string | 63 | content: string |
| 63 | } | 64 | } |
| 64 | ``` | 65 | #+end_src |
| 65 | 66 | ||
| 66 | # Server Set-Up | 67 | * Server Set-Up |
| 67 | 68 | ||
| 68 | To create this script, we will be using Python. By default, I use Python 3 on my | 69 | To create this script, we will be using Python. By default, I use Python |
| 69 | servers, so please note that I did not test this in Python 2, and I do not know | 70 | 3 on my servers, so please note that I did not test this in Python 2, |
| 70 | if Python 2 will work for this. | 71 | and I do not know if Python 2 will work for this. |
| 71 | 72 | ||
| 72 | ## Creating the Script | 73 | ** Creating the Script |
| 73 | 74 | ||
| 74 | First, find a suitable place to create your script. Personally, I just create a | 75 | First, find a suitable place to create your script. Personally, I just |
| 75 | directory called `ddns` in my home directory: | 76 | create a directory called =ddns= in my home directory: |
| 76 | 77 | ||
| 77 | ```sh | 78 | #+begin_src sh |
| 78 | mkdir ~/ddns | 79 | mkdir ~/ddns |
| 79 | ``` | 80 | #+end_src |
| 80 | 81 | ||
| 81 | Next, create a Python script file: | 82 | Next, create a Python script file: |
| 82 | 83 | ||
| 83 | ```sh | 84 | #+begin_src sh |
| 84 | nano ~/ddns/ddns.py | 85 | nano ~/ddns/ddns.py |
| 85 | ``` | 86 | #+end_src |
| 86 | 87 | ||
| 87 | The following code snippet is quite long, so I won't go into depth on each part. | 88 | The following code snippet is quite long, so I won't go into depth on |
| 88 | However, I suggest you read through the entire script before running it; it is | 89 | each part. However, I suggest you read through the entire script before |
| 89 | quite simple and contains comments to help explain each code block. | 90 | running it; it is quite simple and contains comments to help explain |
| 91 | each code block. | ||
| 90 | 92 | ||
| 91 | :warning: **Note**: You will need to update the following variables for this to | 93 | :warning: *Note*: You will need to update the following variables for |
| 92 | work: | 94 | this to work: |
| 93 | 95 | ||
| 94 | - `token`: This is the Njalla API token you created earlier. | 96 | - =token=: This is the Njalla API token you created earlier. |
| 95 | - `user_domain`: This is the top-level domain you want to modify. | 97 | - =user_domain=: This is the top-level domain you want to modify. |
| 96 | - `include_subdomains`: Set this to `True` if you also want to modify | 98 | - =include_subdomains=: Set this to =True= if you also want to modify |
| 97 | subdomains found under the TLD. | 99 | subdomains found under the TLD. |
| 98 | - `subdomains`: If `include_subdomains` = `True`, you can include your list of | 100 | - =subdomains=: If =include_subdomains= = =True=, you can include your |
| 99 | subdomains to be modified here. | 101 | list of subdomains to be modified here. |
| 100 | 102 | ||
| 101 | ```python | 103 | #+begin_src python |
| 102 | #!/usr/bin/python | 104 | #!/usr/bin/python |
| 103 | # -*- coding: utf-8 -*- | 105 | # -*- coding: utf-8 -*- |
| 104 | # Import Python modules | 106 | # Import Python modules |
| @@ -177,29 +179,29 @@ for record in data['records']: | |||
| 177 | 'does not match Njalla's value of', | 179 | 'does not match Njalla's value of', |
| 178 | record['content'], '. Updating...') | 180 | record['content'], '. Updating...') |
| 179 | update_record(user_domain, record['id'], ipv6) | 181 | update_record(user_domain, record['id'], ipv6) |
| 180 | ``` | 182 | #+end_src |
| 181 | 183 | ||
| 182 | ## Running the Script | 184 | ** Running the Script |
| 183 | 185 | ||
| 184 | Once you've created the script and are ready to test it, run the following | 186 | Once you've created the script and are ready to test it, run the |
| 185 | command: | 187 | following command: |
| 186 | 188 | ||
| 187 | ```sh | 189 | #+begin_src sh |
| 188 | python3 ~/ddns/ddns.py | 190 | python3 ~/ddns/ddns.py |
| 189 | ``` | 191 | #+end_src |
| 190 | 192 | ||
| 191 | ## Setting the Script to Run Automatically | 193 | ** Setting the Script to Run Automatically |
| 192 | 194 | ||
| 193 | To make sure the scripts run automatically, add it to the `cron` file so that it | 195 | To make sure the scripts run automatically, add it to the =cron= file so |
| 194 | will run on a schedule. To do this, open the `cron` file: | 196 | that it will run on a schedule. To do this, open the =cron= file: |
| 195 | 197 | ||
| 196 | ```sh | 198 | #+begin_src sh |
| 197 | crontab -e | 199 | crontab -e |
| 198 | ``` | 200 | #+end_src |
| 199 | 201 | ||
| 200 | In the cron file, paste the following at the bottom of the editor in order to | 202 | In the cron file, paste the following at the bottom of the editor in |
| 201 | check the IP every five minutes: | 203 | order to check the IP every five minutes: |
| 202 | 204 | ||
| 203 | ```sh | 205 | #+begin_src sh |
| 204 | */5 ** ** ** ** python3 /home/<your_username>/ddns/ddns.py | 206 | ,*/5 ** ** ** ** python3 /home/<your_username>/ddns/ddns.py |
| 205 | ``` | 207 | #+end_src |
content/blog/2022-02-16-debian-and-nginx.md deleted −164
| @@ -1,164 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-02-16 | ||
| 3 | title = "Migrating to a New Web Server Setup with Debian, Nginx, and Agate" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Server OS: Debian | ||
| 9 | |||
| 10 | I've used various Linux distributions throughout the years, but I've never used | ||
| 11 | anything except Ubuntu for my servers. Why? I really have no idea, mostly just | ||
| 12 | comfort around the commands and software availability. | ||
| 13 | |||
| 14 | However, I have always wanted to try Debian as a server OS after testing it out | ||
| 15 | in a VM a few years ago (side-note: I'd love to try Alpine too, but I always | ||
| 16 | struggle with compatibility). So, I decided to launch a new VPS and use | ||
| 17 | [Debian](https://www.debian.org) 11 as the OS. Spoiler alert: it feels identical | ||
| 18 | to Ubuntu for my purposes. | ||
| 19 | |||
| 20 | I did the normal things when first launching the VPS, such as adding a new user, | ||
| 21 | locking down SSH, etc. If you want to see that level of detail, read my other | ||
| 22 | post about [How to Set Up a VPS Web | ||
| 23 | Server](https://cleberg.net/blog/how-to-set-up-a-vps-web-server/). | ||
| 24 | |||
| 25 | All of this has been similar, apart from small things such as the location of | ||
| 26 | users' home folders. No complaints at all from me - Debian seems great. | ||
| 27 | |||
| 28 | # Web Server: Nginx | ||
| 29 | |||
| 30 | Once I had the baseline server configuration set-up for Debian, I moved on to | ||
| 31 | trying out [Nginx](https://nginx.org) as my web server software. This required | ||
| 32 | me to install the `nginx` and `ufw` packages, as well as setting up the initial | ||
| 33 | UFW config: | ||
| 34 | |||
| 35 | ```sh | ||
| 36 | sudo apt install nginx ufw | ||
| 37 | sudo ufw allow 'Nginx Full' | ||
| 38 | sudo ufw allow SSH | ||
| 39 | sudo ufw enable | ||
| 40 | sudo ufw status | ||
| 41 | sudo systemctl status nginx | ||
| 42 | ``` | ||
| 43 | |||
| 44 | Once I had the firewall set, I moved on to creating the directories and files | ||
| 45 | for my website. This is very easy and is basically the same as setting up an | ||
| 46 | Apache server, so no struggles here. | ||
| 47 | |||
| 48 | ```sh | ||
| 49 | sudo mkdir -p /var/www/your_domain/html | ||
| 50 | sudo chown -R $USER:$USER /var/www/your_domain/html | ||
| 51 | sudo chmod -R 755 /var/www/your_domain | ||
| 52 | nano /var/www/your_domain/html/index.html | ||
| 53 | ``` | ||
| 54 | |||
| 55 | The next part, creating the Nginx configuration files, is quite a bit different | ||
| 56 | from Apache. First, you need to create the files in the `sites-available` folder | ||
| 57 | and symlink it the `sites-enabled` folder. | ||
| 58 | |||
| 59 | Creating the config file for your domain: | ||
| 60 | |||
| 61 | ```sh | ||
| 62 | sudo nano /etc/nginx/sites-available/your_domain | ||
| 63 | ``` | ||
| 64 | |||
| 65 | Default content for an Nginx config file: | ||
| 66 | |||
| 67 | ```sh | ||
| 68 | server { | ||
| 69 | listen 80; | ||
| 70 | listen [::]:80; | ||
| 71 | |||
| 72 | root /var/www/your_domain/html; | ||
| 73 | index index.html index.htm index.nginx-debian.html; | ||
| 74 | |||
| 75 | server_name your_domain www.your_domain; | ||
| 76 | |||
| 77 | location / { | ||
| 78 | try_files $uri $uri/ =404; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | ``` | ||
| 82 | |||
| 83 | Finally, symlink it together: | ||
| 84 | |||
| 85 | ```sh | ||
| 86 | sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ | ||
| 87 | ``` | ||
| 88 | |||
| 89 | This will make your site available to the public (as long as you have | ||
| 90 | `your_domain` DNS records pointed at the server's IP address)! | ||
| 91 | |||
| 92 | Next, I used [certbot](https://certbot.eff.org/) to issue an HTTPS certificate | ||
| 93 | for my domains using the following commands: | ||
| 94 | |||
| 95 | ```sh | ||
| 96 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | ||
| 97 | sudo snap install --classic certbot | ||
| 98 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 99 | sudo certbot --nginx | ||
| 100 | ``` | ||
| 101 | |||
| 102 | Now that certbot ran successfully and updated my Nginx config files to include a | ||
| 103 | `443` server block of code, I went back in and edited the config file to include | ||
| 104 | security HTTP headers. This part is optional, but is recommended for security | ||
| 105 | purposes; you can even test a website's HTTP header security at [Security | ||
| 106 | Headers](https://securityheaders.com/). | ||
| 107 | |||
| 108 | The configuration below shows a set-up where you only want your website to serve | ||
| 109 | content from its own domain, except for images and scripts, which may come from | ||
| 110 | `nullitics.com`. All other content would be blocked from loading in a browser. | ||
| 111 | |||
| 112 | ```sh | ||
| 113 | sudo nano /etc/nginx/sites-available/your_domain | ||
| 114 | ``` | ||
| 115 | |||
| 116 | ```sh | ||
| 117 | server { | ||
| 118 | ... | ||
| 119 | add_header Content-Security-Policy "default-src 'none'; img-src 'self' https://nullitics.com; script-src 'self' https://nullitics.com; style-src 'self'; font-src 'self'"; | ||
| 120 | add_header X-Content-Type-Options "nosniff"; | ||
| 121 | add_header X-XSS-Protection "1; mode=block"; | ||
| 122 | add_header X-Frame-Options "DENY"; | ||
| 123 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; | ||
| 124 | add_header Referrer-Policy "no-referrer"; | ||
| 125 | ... | ||
| 126 | } | ||
| 127 | ``` | ||
| 128 | |||
| 129 | ```sh | ||
| 130 | sudo systemctl restart nginx | ||
| 131 | ``` | ||
| 132 | |||
| 133 | ## Nginx vs. Apache | ||
| 134 | |||
| 135 | As I stated at the beginning, my historical hesitation with trying Nginx was | ||
| 136 | that the differences in configuration formats scared me away from leaving | ||
| 137 | Apache. However, I prefer Nginx to Apache for a few reasons: | ||
| 138 | |||
| 139 | 1. Nginx uses only one config file (`your_domain`) vs. Apache's two-file | ||
| 140 | approach for HTTP vs. HTTPS (`your_domain.conf` and | ||
| 141 | `your_domain-le-ssl.conf`). | ||
| 142 | 2. Symlinking new configurations files and reloading Nginx are way easier than | ||
| 143 | Apache's process of having to enable headers with `a2enmod mod_headers`, | ||
| 144 | enable PHP with `a2enmod php` (plus any other mods you need), and then | ||
| 145 | enabling sites with `a2ensite`, and THEN reloading Apache. | ||
| 146 | 3. The contents of the Nginx config files seem more organized and logical with | ||
| 147 | the curly-bracket approach. This is a minor reason, but everything just felt | ||
| 148 | cleaner while I was installing my sites and that had a big quality of life | ||
| 149 | impact on the installation for me. | ||
| 150 | |||
| 151 | They're both great software packages, but Nginx just seems more organized and | ||
| 152 | easier to use these days. I will certainly be exploring the Nginx docs to see | ||
| 153 | what other fun things I can do with all of this. | ||
| 154 | |||
| 155 | # Gemini Server: Agate | ||
| 156 | |||
| 157 | Finally, I set up the Agate software on this server again to host my Gemini | ||
| 158 | server content, using Rust as I have before. You can read my other post for more | ||
| 159 | information on installing Agate: [Hosting a Gemini | ||
| 160 | Server](https://cleberg.net/blog/hosting-a-gemini-server/). | ||
| 161 | |||
| 162 | All in all, Debian + Nginx is very slick and I prefer it over my old combination | ||
| 163 | of Ubuntu + Apache (although it's really just Nginx > Apache for me, since | ||
| 164 | Debian seems mostly the same as Ubuntu is so far). | ||
content/blog/2022-02-16-debian-and-nginx.org added +167
| @@ -0,0 +1,167 @@ | |||
| 1 | #+date: <2022-02-16> | ||
| 2 | #+title: Migrating to a New Web Server Setup with Debian, Nginx, and Agate | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Server OS: Debian | ||
| 7 | |||
| 8 | I've used various Linux distributions throughout the years, but I've | ||
| 9 | never used anything except Ubuntu for my servers. Why? I really have no | ||
| 10 | idea, mostly just comfort around the commands and software availability. | ||
| 11 | |||
| 12 | However, I have always wanted to try Debian as a server OS after testing | ||
| 13 | it out in a VM a few years ago (side-note: I'd love to try Alpine too, | ||
| 14 | but I always struggle with compatibility). So, I decided to launch a new | ||
| 15 | VPS and use [[https://www.debian.org][Debian]] 11 as the OS. Spoiler | ||
| 16 | alert: it feels identical to Ubuntu for my purposes. | ||
| 17 | |||
| 18 | I did the normal things when first launching the VPS, such as adding a | ||
| 19 | new user, locking down SSH, etc. If you want to see that level of | ||
| 20 | detail, read my other post about | ||
| 21 | [[https://cleberg.net/blog/how-to-set-up-a-vps-web-server/][How to Set | ||
| 22 | Up a VPS Web Server]]. | ||
| 23 | |||
| 24 | All of this has been similar, apart from small things such as the | ||
| 25 | location of users' home folders. No complaints at all from me - Debian | ||
| 26 | seems great. | ||
| 27 | |||
| 28 | * Web Server: Nginx | ||
| 29 | |||
| 30 | Once I had the baseline server configuration set-up for Debian, I moved | ||
| 31 | on to trying out [[https://nginx.org][Nginx]] as my web server software. | ||
| 32 | This required me to install the =nginx= and =ufw= packages, as well as | ||
| 33 | setting up the initial UFW config: | ||
| 34 | |||
| 35 | #+begin_src sh | ||
| 36 | sudo apt install nginx ufw | ||
| 37 | sudo ufw allow 'Nginx Full' | ||
| 38 | sudo ufw allow SSH | ||
| 39 | sudo ufw enable | ||
| 40 | sudo ufw status | ||
| 41 | sudo systemctl status nginx | ||
| 42 | #+end_src | ||
| 43 | |||
| 44 | Once I had the firewall set, I moved on to creating the directories and | ||
| 45 | files for my website. This is very easy and is basically the same as | ||
| 46 | setting up an Apache server, so no struggles here. | ||
| 47 | |||
| 48 | #+begin_src sh | ||
| 49 | sudo mkdir -p /var/www/your_domain/html | ||
| 50 | sudo chown -R $USER:$USER /var/www/your_domain/html | ||
| 51 | sudo chmod -R 755 /var/www/your_domain | ||
| 52 | nano /var/www/your_domain/html/index.html | ||
| 53 | #+end_src | ||
| 54 | |||
| 55 | The next part, creating the Nginx configuration files, is quite a bit | ||
| 56 | different from Apache. First, you need to create the files in the | ||
| 57 | =sites-available= folder and symlink it the =sites-enabled= folder. | ||
| 58 | |||
| 59 | Creating the config file for your domain: | ||
| 60 | |||
| 61 | #+begin_src sh | ||
| 62 | sudo nano /etc/nginx/sites-available/your_domain | ||
| 63 | #+end_src | ||
| 64 | |||
| 65 | Default content for an Nginx config file: | ||
| 66 | |||
| 67 | #+begin_src sh | ||
| 68 | server { | ||
| 69 | listen 80; | ||
| 70 | listen [::]:80; | ||
| 71 | |||
| 72 | root /var/www/your_domain/html; | ||
| 73 | index index.html index.htm index.nginx-debian.html; | ||
| 74 | |||
| 75 | server_name your_domain www.your_domain; | ||
| 76 | |||
| 77 | location / { | ||
| 78 | try_files $uri $uri/ =404; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | Finally, symlink it together: | ||
| 84 | |||
| 85 | #+begin_src sh | ||
| 86 | sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ | ||
| 87 | #+end_src | ||
| 88 | |||
| 89 | This will make your site available to the public (as long as you have | ||
| 90 | =your_domain= DNS records pointed at the server's IP address)! | ||
| 91 | |||
| 92 | Next, I used [[https://certbot.eff.org/][certbot]] to issue an HTTPS | ||
| 93 | certificate for my domains using the following commands: | ||
| 94 | |||
| 95 | #+begin_src sh | ||
| 96 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | ||
| 97 | sudo snap install --classic certbot | ||
| 98 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 99 | sudo certbot --nginx | ||
| 100 | #+end_src | ||
| 101 | |||
| 102 | Now that certbot ran successfully and updated my Nginx config files to | ||
| 103 | include a =443= server block of code, I went back in and edited the | ||
| 104 | config file to include security HTTP headers. This part is optional, but | ||
| 105 | is recommended for security purposes; you can even test a website's HTTP | ||
| 106 | header security at [[https://securityheaders.com/][Security Headers]]. | ||
| 107 | |||
| 108 | The configuration below shows a set-up where you only want your website | ||
| 109 | to serve content from its own domain, except for images and scripts, | ||
| 110 | which may come from =nullitics.com=. All other content would be blocked | ||
| 111 | from loading in a browser. | ||
| 112 | |||
| 113 | #+begin_src sh | ||
| 114 | sudo nano /etc/nginx/sites-available/your_domain | ||
| 115 | #+end_src | ||
| 116 | |||
| 117 | #+begin_src sh | ||
| 118 | server { | ||
| 119 | ... | ||
| 120 | add_header Content-Security-Policy "default-src 'none'; img-src 'self' https://nullitics.com; script-src 'self' https://nullitics.com; style-src 'self'; font-src 'self'"; | ||
| 121 | add_header X-Content-Type-Options "nosniff"; | ||
| 122 | add_header X-XSS-Protection "1; mode=block"; | ||
| 123 | add_header X-Frame-Options "DENY"; | ||
| 124 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; | ||
| 125 | add_header Referrer-Policy "no-referrer"; | ||
| 126 | ... | ||
| 127 | } | ||
| 128 | #+end_src | ||
| 129 | |||
| 130 | #+begin_src sh | ||
| 131 | sudo systemctl restart nginx | ||
| 132 | #+end_src | ||
| 133 | |||
| 134 | ** Nginx vs. Apache | ||
| 135 | |||
| 136 | As I stated at the beginning, my historical hesitation with trying Nginx | ||
| 137 | was that the differences in configuration formats scared me away from | ||
| 138 | leaving Apache. However, I prefer Nginx to Apache for a few reasons: | ||
| 139 | |||
| 140 | 1. Nginx uses only one config file (=your_domain=) vs. Apache's two-file | ||
| 141 | approach for HTTP vs. HTTPS (=your_domain.conf= and | ||
| 142 | =your_domain-le-ssl.conf=). | ||
| 143 | 2. Symlinking new configurations files and reloading Nginx are way | ||
| 144 | easier than Apache's process of having to enable headers with | ||
| 145 | =a2enmod mod_headers=, enable PHP with =a2enmod php= (plus any other | ||
| 146 | mods you need), and then enabling sites with =a2ensite=, and THEN | ||
| 147 | reloading Apache. | ||
| 148 | 3. The contents of the Nginx config files seem more organized and | ||
| 149 | logical with the curly-bracket approach. This is a minor reason, but | ||
| 150 | everything just felt cleaner while I was installing my sites and that | ||
| 151 | had a big quality of life impact on the installation for me. | ||
| 152 | |||
| 153 | They're both great software packages, but Nginx just seems more | ||
| 154 | organized and easier to use these days. I will certainly be exploring | ||
| 155 | the Nginx docs to see what other fun things I can do with all of this. | ||
| 156 | |||
| 157 | * Gemini Server: Agate | ||
| 158 | |||
| 159 | Finally, I set up the Agate software on this server again to host my | ||
| 160 | Gemini server content, using Rust as I have before. You can read my | ||
| 161 | other post for more information on installing Agate: | ||
| 162 | [[https://cleberg.net/blog/hosting-a-gemini-server/][Hosting a Gemini | ||
| 163 | Server]]. | ||
| 164 | |||
| 165 | All in all, Debian + Nginx is very slick and I prefer it over my old | ||
| 166 | combination of Ubuntu + Apache (although it's really just Nginx > Apache | ||
| 167 | for me, since Debian seems mostly the same as Ubuntu is so far). | ||
content/blog/2022-02-17-exiftool.md deleted −59
| @@ -1,59 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-02-17 | ||
| 3 | title = "Stripping Image Metadata with Exiftool" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | ## Why Strip Metadata? | ||
| 9 | |||
| 10 | Okay, so you want to strip metadata from your photos. Perhaps you take pictures | ||
| 11 | of very rare birds, and the location metadata is a gold mine for poachers, or | ||
| 12 | perhaps you're just privacy-oriented like me and prefer to strip metadata from | ||
| 13 | publicly-available images. | ||
| 14 | |||
| 15 | There are various components of image metadata that you may want to delete | ||
| 16 | before releasing a photo to the public. Here's an incomplete list of things I | ||
| 17 | could easily see just by inspecting a photo on my laptop: | ||
| 18 | |||
| 19 | - Location (Latitude & Longitude) | ||
| 20 | - Dimensions | ||
| 21 | - Device Make & Model | ||
| 22 | - Color Space | ||
| 23 | - Color Profile | ||
| 24 | - Focal Length | ||
| 25 | - Alpha Channel | ||
| 26 | - Red Eye | ||
| 27 | - Metering Mode | ||
| 28 | - F Number | ||
| 29 | |||
| 30 | Regardless of your reasoning, I'm going to explain how I used the `exiftool` | ||
| 31 | package in Linux to automatically strip metadata from all images in a directory | ||
| 32 | (+ subdirectories). | ||
| 33 | |||
| 34 | ## Installing `exiftool` | ||
| 35 | |||
| 36 | First things first: we need to install the tool. I'm running Debian 11 on my | ||
| 37 | server (Ubuntu will work the same), so the command is as simple as: | ||
| 38 | |||
| 39 | ```sh | ||
| 40 | sudo apt install exiftool | ||
| 41 | ``` | ||
| 42 | |||
| 43 | There are different tools that can accomplish the same thing across | ||
| 44 | distributions, but I really only care to test out this one package. | ||
| 45 | |||
| 46 | ## Recursively Strip Data | ||
| 47 | |||
| 48 | I actually use this tool extensively to strip any photos uploaded to the website | ||
| 49 | that serves all the images for my blog (`img.cleberg.net`). | ||
| 50 | |||
| 51 | The following command is incredibly useful and can be modified to include any | ||
| 52 | image extensions that `exiftool` supports: | ||
| 53 | |||
| 54 | ```sh | ||
| 55 | exiftool -r -all= -ext jpg -ext png /path/to/directory/ | ||
| 56 | ``` | ||
| 57 | |||
| 58 | The output of the command will let you know how many directories were scanned, | ||
| 59 | how many images were updated, and how many images were unchanged. | ||
content/blog/2022-02-17-exiftool.org added +59
| @@ -0,0 +1,59 @@ | |||
| 1 | #+date: <2022-02-17> | ||
| 2 | #+title: Stripping Image Metadata with Exiftool | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | ** Why Strip Metadata? | ||
| 7 | |||
| 8 | Okay, so you want to strip metadata from your photos. Perhaps you take | ||
| 9 | pictures of very rare birds, and the location metadata is a gold mine | ||
| 10 | for poachers, or perhaps you're just privacy-oriented like me and prefer | ||
| 11 | to strip metadata from publicly-available images. | ||
| 12 | |||
| 13 | There are various components of image metadata that you may want to | ||
| 14 | delete before releasing a photo to the public. Here's an incomplete list | ||
| 15 | of things I could easily see just by inspecting a photo on my laptop: | ||
| 16 | |||
| 17 | - Location (Latitude & Longitude) | ||
| 18 | - Dimensions | ||
| 19 | - Device Make & Model | ||
| 20 | - Color Space | ||
| 21 | - Color Profile | ||
| 22 | - Focal Length | ||
| 23 | - Alpha Channel | ||
| 24 | - Red Eye | ||
| 25 | - Metering Mode | ||
| 26 | - F Number | ||
| 27 | |||
| 28 | Regardless of your reasoning, I'm going to explain how I used the | ||
| 29 | =exiftool= package in Linux to automatically strip metadata from all | ||
| 30 | images in a directory (+ subdirectories). | ||
| 31 | |||
| 32 | ** Installing =exiftool= | ||
| 33 | |||
| 34 | First things first: we need to install the tool. I'm running Debian 11 | ||
| 35 | on my server (Ubuntu will work the same), so the command is as simple | ||
| 36 | as: | ||
| 37 | |||
| 38 | #+begin_src sh | ||
| 39 | sudo apt install exiftool | ||
| 40 | #+end_src | ||
| 41 | |||
| 42 | There are different tools that can accomplish the same thing across | ||
| 43 | distributions, but I really only care to test out this one package. | ||
| 44 | |||
| 45 | ** Recursively Strip Data | ||
| 46 | |||
| 47 | I actually use this tool extensively to strip any photos uploaded to the | ||
| 48 | website that serves all the images for my blog (=img.cleberg.net=). | ||
| 49 | |||
| 50 | The following command is incredibly useful and can be modified to | ||
| 51 | include any image extensions that =exiftool= supports: | ||
| 52 | |||
| 53 | #+begin_src sh | ||
| 54 | exiftool -r -all= -ext jpg -ext png /path/to/directory/ | ||
| 55 | #+end_src | ||
| 56 | |||
| 57 | The output of the command will let you know how many directories were | ||
| 58 | scanned, how many images were updated, and how many images were | ||
| 59 | unchanged. | ||
content/blog/2022-02-20-nginx-caching.md deleted −65
| @@ -1,65 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-02-20 | ||
| 3 | title = "Caching Static Content with Nginx" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Update Your Nginx Config to Cache Static Files | ||
| 9 | |||
| 10 | If you run a website on Nginx that serves static content (i.e., content that is | ||
| 11 | not dynamic and changing with interactions from the user), you would likely | ||
| 12 | benefit from caching that content on the client-side. If you're used to Apache | ||
| 13 | and looking for the Nginx equivalent, this post should help. | ||
| 14 | |||
| 15 | Luckily, setting up the cache is as easy as identifying the file types you want | ||
| 16 | to cache and determining the expiration length. To include more file types, | ||
| 17 | simply use the bar separator (`|`) and type the new file extension you want to | ||
| 18 | include. | ||
| 19 | |||
| 20 | ```config | ||
| 21 | server { | ||
| 22 | ... | ||
| 23 | |||
| 24 | location ~** .(css|js|jpg|jpeg|gif|png|ico)$ { | ||
| 25 | expires 30d; | ||
| 26 | } | ||
| 27 | |||
| 28 | ... | ||
| 29 | } | ||
| 30 | ``` | ||
| 31 | |||
| 32 | I have seen some people who prefer to set `expires` as `365d` or even `max`, but | ||
| 33 | that is only for stable, infrequently changing websites. As my site often | ||
| 34 | changes (i.e., I'm never content with my website), I need to know that my | ||
| 35 | readers are seeing the new content without waiting too long. | ||
| 36 | |||
| 37 | So, I went ahead and set the expiration date at `30d`, which is short enough to | ||
| 38 | refresh for readers but long enough that clients/browsers won't be re-requesting | ||
| 39 | the static files too often, hopefully resulting in faster loading times, as | ||
| 40 | images should be the only thing slowing down my site. | ||
| 41 | |||
| 42 | # Testing Results | ||
| 43 | |||
| 44 | To test my changes to the Nginx configuration, I used the [HTTP Header | ||
| 45 | Live](https://addons.mozilla.org/en-US/firefox/addon/http-header-live/) | ||
| 46 | extension on my Gecko browser and used the sidebar to inspect the headers of a | ||
| 47 | recent image from my blog. | ||
| 48 | |||
| 49 | In the image below, you can see that the `Cache-Control` header is now present | ||
| 50 | and set to 2592000, which is 30 days represented in seconds (30 days \_ 24 | ||
| 51 | hours/day \_ 60 minutes/hour \*\* 60 seconds/minute = 2,592,000 seconds). | ||
| 52 | |||
| 53 | The `Expires` field is now showing 22 March 2022, which is 30 days from the day | ||
| 54 | of this post, 20 February 2022. | ||
| 55 | |||
| 56 | # Caveats | ||
| 57 | |||
| 58 | Remember that this caching system is **client-side**, which means that content | ||
| 59 | is only cached for as long as a client allows it. For example, my browser purges | ||
| 60 | all caches, data, etc. upon exit, so this caching policy will only work as long | ||
| 61 | as my browser remains open and running. | ||
| 62 | |||
| 63 | If you need to test updates to your site, you'll need to clear the cache to see | ||
| 64 | updates for any file extension you configured. This can often be done with the | ||
| 65 | `Shift + F5` or `Ctrl + F5` key combinations in most browsers. | ||
content/blog/2022-02-20-nginx-caching.org added +68
| @@ -0,0 +1,68 @@ | |||
| 1 | #+date: <2022-02-20> | ||
| 2 | #+title: Caching Static Content with Nginx | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Update Your Nginx Config to Cache Static Files | ||
| 7 | |||
| 8 | If you run a website on Nginx that serves static content (i.e., content | ||
| 9 | that is not dynamic and changing with interactions from the user), you | ||
| 10 | would likely benefit from caching that content on the client-side. If | ||
| 11 | you're used to Apache and looking for the Nginx equivalent, this post | ||
| 12 | should help. | ||
| 13 | |||
| 14 | Luckily, setting up the cache is as easy as identifying the file types | ||
| 15 | you want to cache and determining the expiration length. To include more | ||
| 16 | file types, simply use the bar separator (=|=) and type the new file | ||
| 17 | extension you want to include. | ||
| 18 | |||
| 19 | #+begin_src config | ||
| 20 | server { | ||
| 21 | ... | ||
| 22 | |||
| 23 | location ~** .(css|js|jpg|jpeg|gif|png|ico)$ { | ||
| 24 | expires 30d; | ||
| 25 | } | ||
| 26 | |||
| 27 | ... | ||
| 28 | } | ||
| 29 | #+end_src | ||
| 30 | |||
| 31 | I have seen some people who prefer to set =expires= as =365d= or even | ||
| 32 | =max=, but that is only for stable, infrequently changing websites. As | ||
| 33 | my site often changes (i.e., I'm never content with my website), I need | ||
| 34 | to know that my readers are seeing the new content without waiting too | ||
| 35 | long. | ||
| 36 | |||
| 37 | So, I went ahead and set the expiration date at =30d=, which is short | ||
| 38 | enough to refresh for readers but long enough that clients/browsers | ||
| 39 | won't be re-requesting the static files too often, hopefully resulting | ||
| 40 | in faster loading times, as images should be the only thing slowing down | ||
| 41 | my site. | ||
| 42 | |||
| 43 | * Testing Results | ||
| 44 | |||
| 45 | To test my changes to the Nginx configuration, I used the | ||
| 46 | [[https://addons.mozilla.org/en-US/firefox/addon/http-header-live/][HTTP | ||
| 47 | Header Live]] extension on my Gecko browser and used the sidebar to | ||
| 48 | inspect the headers of a recent image from my blog. | ||
| 49 | |||
| 50 | In the image below, you can see that the =Cache-Control= header is now | ||
| 51 | present and set to 2592000, which is 30 days represented in seconds (30 | ||
| 52 | days _ 24 hours/day _ 60 minutes/hour ** 60 seconds/minute = 2,592,000 | ||
| 53 | seconds). | ||
| 54 | |||
| 55 | The =Expires= field is now showing 22 March 2022, which is 30 days from | ||
| 56 | the day of this post, 20 February 2022. | ||
| 57 | |||
| 58 | * Caveats | ||
| 59 | |||
| 60 | Remember that this caching system is *client-side*, which means that | ||
| 61 | content is only cached for as long as a client allows it. For example, | ||
| 62 | my browser purges all caches, data, etc. upon exit, so this caching | ||
| 63 | policy will only work as long as my browser remains open and running. | ||
| 64 | |||
| 65 | If you need to test updates to your site, you'll need to clear the cache | ||
| 66 | to see updates for any file extension you configured. This can often be | ||
| 67 | done with the =Shift + F5= or =Ctrl + F5= key combinations in most | ||
| 68 | browsers. | ||
content/blog/2022-02-22-tuesday.md deleted −35
| @@ -1,35 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-02-22 | ||
| 3 | title = "Tuesday" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Tuesday, Twosday | ||
| 9 | |||
| 10 | I'm taking a break from my usual technology-related posts and writing about | ||
| 11 | something a little more enjoyable today. | ||
| 12 | |||
| 13 | Today is Tuesday, February 22nd, 2022. Today is 02-22-2022. Today is Twosday. | ||
| 14 | |||
| 15 | Let's look at everything that fell in place today: | ||
| 16 | |||
| 17 | 1. Written in the `m-dd-yy` or `dd-m-yy` formats, today is 2-22-22 or 22-2-22, | ||
| 18 | which is a neat little palindrome in either format. (The last ubiquitous | ||
| 19 | six-digit palindrome was 1-11-11.) | ||
| 20 | 2. Today is Tuesday, which is why everyone is using the nickname Twosday to call | ||
| 21 | out these similarities. | ||
| 22 | 3. Falling on Tuesday means today is the 2nd day of the week (for most cultures. | ||
| 23 | For the US, it's the 3rd day of the week since we start on Sunday). | ||
| 24 | 4. The only culture I could find with a connection to a `2` is that some Slavic | ||
| 25 | languages derived their version of Tuesday from the Old Church Slavonic word | ||
| 26 | `въторъ`, meaning "the second." | ||
| 27 | 5. Written in the classic monospaced, digital font (think of digital clocks from | ||
| 28 | the 80s/90s), there is nice symmetry to the numbers ([view the image | ||
| 29 | here](https://img.cleberg.net/blog/20220222-tuesday/digital_font.png)!). | ||
| 30 | 6. This one isn't naturally-occurring, but it seems people around the world are | ||
| 31 | celebrating the day. For example, a group is putting together [a wedding of | ||
| 32 | 222 couples at the California State Capitol in | ||
| 33 | Sacramento](https://www.eventbrite.com/e/2-22-22-a-collective-wedding-ceremony-at-the-state-capitol-tickets-211434605597), | ||
| 34 | concluding at exactly 2:22 PM. These couples will record their marriage dates | ||
| 35 | as 2-22-22 2:22 PM. Tickets were on sale for $222.22. | ||
content/blog/2022-02-22-tuesday.org added +38
| @@ -0,0 +1,38 @@ | |||
| 1 | #+date: <2022-02-22> | ||
| 2 | #+title: Tuesday | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Tuesday, Twosday | ||
| 7 | |||
| 8 | I'm taking a break from my usual technology-related posts and writing | ||
| 9 | about something a little more enjoyable today. | ||
| 10 | |||
| 11 | Today is Tuesday, February 22nd, 2022. Today is 02-22-2022. Today is | ||
| 12 | Twosday. | ||
| 13 | |||
| 14 | Let's look at everything that fell in place today: | ||
| 15 | |||
| 16 | 1. Written in the =m-dd-yy= or =dd-m-yy= formats, today is 2-22-22 or | ||
| 17 | 22-2-22, which is a neat little palindrome in either format. (The | ||
| 18 | last ubiquitous six-digit palindrome was 1-11-11.) | ||
| 19 | 2. Today is Tuesday, which is why everyone is using the nickname Twosday | ||
| 20 | to call out these similarities. | ||
| 21 | 3. Falling on Tuesday means today is the 2nd day of the week (for most | ||
| 22 | cultures. For the US, it's the 3rd day of the week since we start on | ||
| 23 | Sunday). | ||
| 24 | 4. The only culture I could find with a connection to a =2= is that some | ||
| 25 | Slavic languages derived their version of Tuesday from the Old Church | ||
| 26 | Slavonic word =въторъ=, meaning "the second." | ||
| 27 | 5. Written in the classic monospaced, digital font (think of digital | ||
| 28 | clocks from the 80s/90s), there is nice symmetry to the numbers | ||
| 29 | ([[https://img.cleberg.net/blog/20220222-tuesday/digital_font.png][view | ||
| 30 | the image here]]!). | ||
| 31 | 6. This one isn't naturally-occurring, but it seems people around the | ||
| 32 | world are celebrating the day. For example, a group is putting | ||
| 33 | together | ||
| 34 | [[https://www.eventbrite.com/e/2-22-22-a-collective-wedding-ceremony-at-the-state-capitol-tickets-211434605597][a | ||
| 35 | wedding of 222 couples at the California State Capitol in | ||
| 36 | Sacramento]], concluding at exactly 2:22 PM. These couples will | ||
| 37 | record their marriage dates as 2-22-22 2:22 PM. Tickets were on sale | ||
| 38 | for $222.22. | ||
content/blog/2022-03-02-reliable-notes.md deleted −127
| @@ -1,127 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-03-02 | ||
| 3 | title = "Easy, Reliable Note-Taking" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Choosing Durable File Formats | ||
| 9 | |||
| 10 | > TL;DR: Write in a format that can be easily rendered and read in plain-text | ||
| 11 | > mode (e.g., `.txt`, `.md`, etc.). | ||
| 12 | |||
| 13 | As I've written more and more over the years, I've found that my love of | ||
| 14 | note-taking is always growing. Everything I learn or need to remember can be | ||
| 15 | written down in a note and saved digitally, with no cost to myself. Unlike paper | ||
| 16 | copies that need physical storage space, digital files simply need space on your | ||
| 17 | local disk or cloud storage, which is fairly abundant these days. | ||
| 18 | |||
| 19 | However, I had a historical struggle with migration of notes between different | ||
| 20 | apps that require different formats and applied proprietary styling. This meant | ||
| 21 | that I had to go through each note during migration and edit the file to look | ||
| 22 | presentable again. | ||
| 23 | |||
| 24 | For the last year or two, I have written everything exclusively in | ||
| 25 | [Markdown](https://en.wikipedia.org/wiki/Markdown) format. Small notes, | ||
| 26 | long-form writings, and even these blog posts are all written in Markdown. | ||
| 27 | |||
| 28 | Why Markdown? While I do appreciate the simplicity of plain-text files without | ||
| 29 | any formatting, I often need visual cues such as heading and code blocks to keep | ||
| 30 | my thoughts straight. Markdown provides a minimal set of styling indicators for | ||
| 31 | me to style my notes without adding any proprietary, embedded data into the | ||
| 32 | files. If I want a top-level heading, I simply add a hash (`#`) before the line. | ||
| 33 | An added bonus is that even if a system doesn't understand Markdown, it will | ||
| 34 | render it as plain-text and I can read it just as easily. | ||
| 35 | |||
| 36 | For example, here's how TextEdit on macOS will open and display a Markdown file | ||
| 37 | in plain-text, since it does contain any features to preview Markdown as HTML: | ||
| 38 | |||
| 39 |  | ||
| 41 | |||
| 42 | ## Saving & Syncing Files | ||
| 43 | |||
| 44 | In order to read and edit my notes across platforms, I use my personal cloud | ||
| 45 | storage through Tresorit due to its native integration with macOS and iOS file | ||
| 46 | managers. In addition, Tresorit works well on Debian-based Linux distros, which | ||
| 47 | I used before macOS (and will likely switch back to in a few years). | ||
| 48 | |||
| 49 | You can use whatever sync software you want - syncing plain-text or markdown | ||
| 50 | files is incredibly easy and fast, since the files are generally tiny. | ||
| 51 | |||
| 52 | Since the cloud storage syncs files automatically, there is no need for me to | ||
| 53 | sync anything manually or kick-off a sync job to update my files. This means | ||
| 54 | that I can edit on mobile, and it takes about 5-10 seconds to see the changes on | ||
| 55 | desktop. | ||
| 56 | |||
| 57 | ### Version Control with Git | ||
| 58 | |||
| 59 | A different approach I've contemplated is storing my notes and attachments is | ||
| 60 | using a hosted Git repository to track changes to the files. However, I don't | ||
| 61 | want to rely on an external service that could potentially see into my data, | ||
| 62 | even if the repository is private. | ||
| 63 | |||
| 64 | I might just do `git init` locally and then commit my changes each time I write | ||
| 65 | or update a note, but that seems to be a lot of work just for tracking changes - | ||
| 66 | which I don't necessarily care to know. | ||
| 67 | |||
| 68 | ### Backups! | ||
| 69 | |||
| 70 | One small addition to the storage/sync conversation is the idea of backups. | ||
| 71 | Personally, I manually create periodic backups of my entire cloud storage, | ||
| 72 | compress it into an archive, and store it on my home server. | ||
| 73 | |||
| 74 | To improve my workflow, I am going to be exploring options to automatically | ||
| 75 | compress the mounted cloud directory and send it over to my server on a set | ||
| 76 | schedule. | ||
| 77 | |||
| 78 | ## Writing on Desktop | ||
| 79 | |||
| 80 | > **Update (06.14.22)**: Since writing this post, I have reverted to simply | ||
| 81 | > keeping my `notes` folder open and opening notes individually in TextEdit for | ||
| 82 | > a more minimal and relaxing writing experience on the desktop. | ||
| 83 | |||
| 84 | The bulk of my writing occurs in a desktop environment, with a full keyboard | ||
| 85 | layout and wide screen. I don't illustrate with a smart pen, I rarely use | ||
| 86 | embedded images, and I love being able to see all of my notes/directories in a | ||
| 87 | sidebar. | ||
| 88 | |||
| 89 | With this simple set of requirements, I chose [Obsidian](https://obsidian.md) as | ||
| 90 | my desktop text editor. Obsidian has some in-depth tools like a graph view, | ||
| 91 | command palette, mentions, etc., but I've found that using it as a simple | ||
| 92 | Markdown editor is incredibly easy and straightforward. | ||
| 93 | |||
| 94 | Here's an example of how my Markdown notes look when opened in plain-text mode: | ||
| 95 | |||
| 96 |  | ||
| 98 | |||
| 99 | Here's the "live preview" version, where the Markdown is rendered into its HTML | ||
| 100 | format: | ||
| 101 | |||
| 102 |  | ||
| 104 | |||
| 105 | ### Programming on Desktop | ||
| 106 | |||
| 107 | While I was writing this, I realized I should specify that I don't use the same | ||
| 108 | editor for writing notes and for writing code. For programming purposes, I use | ||
| 109 | [VSCodium](https://vscodium.com) as my development IDE. | ||
| 110 | |||
| 111 | ## Writing on Mobile | ||
| 112 | |||
| 113 | Personally, I write very little on mobile, except when needing to take important | ||
| 114 | notes on-the-go. Any long-form writing, journals, etc. are done at home, where I | ||
| 115 | always have my laptop available. | ||
| 116 | |||
| 117 | I wanted a simple and foolproof editor for iOS, preferably open-source. After a | ||
| 118 | long journey of testing the few (& terrible) open-source iOS note-taking apps, I | ||
| 119 | finally found a phenomenal one: | ||
| 120 | [Runestone](https://github.com/simonbs/runestone). This app is fantastic for | ||
| 121 | note-taking, has plenty of optional features, and integrates natively with the | ||
| 122 | iOS file manager. | ||
| 123 | |||
| 124 | This app opens the iOS file manager and allows you to click any file you want, | ||
| 125 | opens it up in an editor, and lets me save and close out of that note. | ||
| 126 | |||
| 127 | Quite simple but effective. | ||
content/blog/2022-03-02-reliable-notes.org added +156
| @@ -0,0 +1,156 @@ | |||
| 1 | #+date: <2022-03-02> | ||
| 2 | #+title: Easy, Reliable Note-Taking | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | * Choosing Durable File Formats | ||
| 6 | :PROPERTIES: | ||
| 7 | :CUSTOM_ID: choosing-durable-file-formats | ||
| 8 | :END: | ||
| 9 | |||
| 10 | #+begin_quote | ||
| 11 | TL;DR: Write in a format that can be easily rendered and read in | ||
| 12 | plain-text mode (e.g., =.txt=, =.md=, etc.). | ||
| 13 | #+end_quote | ||
| 14 | |||
| 15 | As I've written more and more over the years, I've found that my love of | ||
| 16 | note-taking is always growing. Everything I learn or need to remember | ||
| 17 | can be written down in a note and saved digitally, with no cost to | ||
| 18 | myself. Unlike paper copies that need physical storage space, digital | ||
| 19 | files simply need space on your local disk or cloud storage, which is | ||
| 20 | fairly abundant these days. | ||
| 21 | |||
| 22 | However, I had a historical struggle with migration of notes between | ||
| 23 | different apps that require different formats and applied proprietary | ||
| 24 | styling. This meant that I had to go through each note during migration | ||
| 25 | and edit the file to look presentable again. | ||
| 26 | |||
| 27 | For the last year or two, I have written everything exclusively in | ||
| 28 | [[https://en.wikipedia.org/wiki/Markdown][Markdown]] format. Small | ||
| 29 | notes, long-form writings, and even these blog posts are all written in | ||
| 30 | Markdown. | ||
| 31 | |||
| 32 | Why Markdown? While I do appreciate the simplicity of plain-text files | ||
| 33 | without any formatting, I often need visual cues such as heading and | ||
| 34 | code blocks to keep my thoughts straight. Markdown provides a minimal | ||
| 35 | set of styling indicators for me to style my notes without adding any | ||
| 36 | proprietary, embedded data into the files. If I want a top-level | ||
| 37 | heading, I simply add a hash (=#=) before the line. An added bonus is | ||
| 38 | that even if a system doesn't understand Markdown, it will render it as | ||
| 39 | plain-text and I can read it just as easily. | ||
| 40 | |||
| 41 | For example, here's how TextEdit on macOS will open and display a | ||
| 42 | Markdown file in plain-text, since it does contain any features to | ||
| 43 | preview Markdown as HTML: | ||
| 44 | |||
| 45 | #+caption: Plain Text Markdown | ||
| 46 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/plain_markdown.png]] | ||
| 47 | |||
| 48 | ** Saving & Syncing Files | ||
| 49 | :PROPERTIES: | ||
| 50 | :CUSTOM_ID: saving-syncing-files | ||
| 51 | :END: | ||
| 52 | In order to read and edit my notes across platforms, I use my personal | ||
| 53 | cloud storage through Tresorit due to its native integration with macOS | ||
| 54 | and iOS file managers. In addition, Tresorit works well on Debian-based | ||
| 55 | Linux distros, which I used before macOS (and will likely switch back to | ||
| 56 | in a few years). | ||
| 57 | |||
| 58 | You can use whatever sync software you want - syncing plain-text or | ||
| 59 | markdown files is incredibly easy and fast, since the files are | ||
| 60 | generally tiny. | ||
| 61 | |||
| 62 | Since the cloud storage syncs files automatically, there is no need for | ||
| 63 | me to sync anything manually or kick-off a sync job to update my files. | ||
| 64 | This means that I can edit on mobile, and it takes about 5-10 seconds to | ||
| 65 | see the changes on desktop. | ||
| 66 | |||
| 67 | *** Version Control with Git | ||
| 68 | :PROPERTIES: | ||
| 69 | :CUSTOM_ID: version-control-with-git | ||
| 70 | :END: | ||
| 71 | A different approach I've contemplated is storing my notes and | ||
| 72 | attachments is using a hosted Git repository to track changes to the | ||
| 73 | files. However, I don't want to rely on an external service that could | ||
| 74 | potentially see into my data, even if the repository is private. | ||
| 75 | |||
| 76 | I might just do =git init= locally and then commit my changes each time | ||
| 77 | I write or update a note, but that seems to be a lot of work just for | ||
| 78 | tracking changes - which I don't necessarily care to know. | ||
| 79 | |||
| 80 | *** Backups! | ||
| 81 | :PROPERTIES: | ||
| 82 | :CUSTOM_ID: backups | ||
| 83 | :END: | ||
| 84 | One small addition to the storage/sync conversation is the idea of | ||
| 85 | backups. Personally, I manually create periodic backups of my entire | ||
| 86 | cloud storage, compress it into an archive, and store it on my home | ||
| 87 | server. | ||
| 88 | |||
| 89 | To improve my workflow, I am going to be exploring options to | ||
| 90 | automatically compress the mounted cloud directory and send it over to | ||
| 91 | my server on a set schedule. | ||
| 92 | |||
| 93 | ** Writing on Desktop | ||
| 94 | :PROPERTIES: | ||
| 95 | :CUSTOM_ID: writing-on-desktop | ||
| 96 | :END: | ||
| 97 | |||
| 98 | #+begin_quote | ||
| 99 | *Update (06.14.22)*: Since writing this post, I have reverted to simply | ||
| 100 | keeping my =notes= folder open and opening notes individually in | ||
| 101 | TextEdit for a more minimal and relaxing writing experience on the | ||
| 102 | desktop. | ||
| 103 | #+end_quote | ||
| 104 | |||
| 105 | The bulk of my writing occurs in a desktop environment, with a full | ||
| 106 | keyboard layout and wide screen. I don't illustrate with a smart pen, I | ||
| 107 | rarely use embedded images, and I love being able to see all of my | ||
| 108 | notes/directories in a sidebar. | ||
| 109 | |||
| 110 | With this simple set of requirements, I chose | ||
| 111 | [[https://obsidian.md][Obsidian]] as my desktop text editor. Obsidian | ||
| 112 | has some in-depth tools like a graph view, command palette, mentions, | ||
| 113 | etc., but I've found that using it as a simple Markdown editor is | ||
| 114 | incredibly easy and straightforward. | ||
| 115 | |||
| 116 | Here's an example of how my Markdown notes look when opened in | ||
| 117 | plain-text mode: | ||
| 118 | |||
| 119 | #+caption: Obsidian Markdown Source Mode | ||
| 120 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_source_mode.png]] | ||
| 121 | |||
| 122 | Here's the "live preview" version, where the Markdown is rendered into | ||
| 123 | its HTML format: | ||
| 124 | |||
| 125 | #+caption: Obsidian Markdown Live Preview | ||
| 126 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_live_preview.png]] | ||
| 127 | |||
| 128 | *** Programming on Desktop | ||
| 129 | :PROPERTIES: | ||
| 130 | :CUSTOM_ID: programming-on-desktop | ||
| 131 | :END: | ||
| 132 | While I was writing this, I realized I should specify that I don't use | ||
| 133 | the same editor for writing notes and for writing code. For programming | ||
| 134 | purposes, I use [[https://vscodium.com][VSCodium]] as my development | ||
| 135 | IDE. | ||
| 136 | |||
| 137 | ** Writing on Mobile | ||
| 138 | :PROPERTIES: | ||
| 139 | :CUSTOM_ID: writing-on-mobile | ||
| 140 | :END: | ||
| 141 | Personally, I write very little on mobile, except when needing to take | ||
| 142 | important notes on-the-go. Any long-form writing, journals, etc. are | ||
| 143 | done at home, where I always have my laptop available. | ||
| 144 | |||
| 145 | I wanted a simple and foolproof editor for iOS, preferably open-source. | ||
| 146 | After a long journey of testing the few (& terrible) open-source iOS | ||
| 147 | note-taking apps, I finally found a phenomenal one: | ||
| 148 | [[https://github.com/simonbs/runestone][Runestone]]. This app is | ||
| 149 | fantastic for note-taking, has plenty of optional features, and | ||
| 150 | integrates natively with the iOS file manager. | ||
| 151 | |||
| 152 | This app opens the iOS file manager and allows you to click any file you | ||
| 153 | want, opens it up in an editor, and lets me save and close out of that | ||
| 154 | note. | ||
| 155 | |||
| 156 | Quite simple but effective. | ||
content/blog/2022-03-03-financial-database.md → content/blog/2022-03-03-financial-database.org renamed +132 −113
| @@ -1,66 +1,75 @@ | |||
| 1 | +++ | 1 | #+date: <2022-03-03> |
| 2 | date = 2022-03-03 | 2 | #+title: Maintaining a Personal Financial Database |
| 3 | title = "Maintaining a Personal Financial Database" | 3 | #+description: |
| 4 | description = "" | 4 | |
| 5 | draft = false | 5 | |
| 6 | +++ | 6 | * Personal Financial Tracking |
| 7 | 7 | :PROPERTIES: | |
| 8 | # Personal Financial Tracking | 8 | :CUSTOM_ID: personal-financial-tracking |
| 9 | 9 | :END: | |
| 10 | For the last 6-ish years, I've tracked my finances in a spreadsheet. This is | 10 | For the last 6-ish years, I've tracked my finances in a spreadsheet. |
| 11 | common practice in the business world, but any good dev will cringe at the | 11 | This is common practice in the business world, but any good dev will |
| 12 | thought of storing long-term data in a spreadsheet. A spreadsheet is not for | 12 | cringe at the thought of storing long-term data in a spreadsheet. A |
| 13 | long-term storage or as a source of data to pull data/reports. | 13 | spreadsheet is not for long-term storage or as a source of data to pull |
| 14 | 14 | data/reports. | |
| 15 | As I wanted to expand the functionality of my financial data (e.g., adding more | 15 | |
| 16 | reports), I decided to migrate the data into a database. To run reports, I would | 16 | As I wanted to expand the functionality of my financial data (e.g., |
| 17 | query the database and use a language like Python or Javascript to process the | 17 | adding more reports), I decided to migrate the data into a database. To |
| 18 | data, perform calculations, and visualize the data. | 18 | run reports, I would query the database and use a language like Python |
| 19 | 19 | or Javascript to process the data, perform calculations, and visualize | |
| 20 | # SQLite | 20 | the data. |
| 21 | 21 | ||
| 22 | When choosing the type of database I wanted to use for this project, I was split | 22 | * SQLite |
| 23 | between three options: | 23 | :PROPERTIES: |
| 24 | 24 | :CUSTOM_ID: sqlite | |
| 25 | 1. MySQL: The database I have the most experience with and have used for years. | 25 | :END: |
| 26 | 2. PostgreSQL: A database I'm new to, but want to learn. | 26 | When choosing the type of database I wanted to use for this project, I |
| 27 | 3. SQLite: A database that I've used for a couple projects and have moderate | 27 | was split between three options: |
| 28 | experience. | 28 | |
| 29 | 29 | 1. MySQL: The database I have the most experience with and have used for | |
| 30 | I ended up choosing SQLite since it can be maintained within a single `.sqlite` | 30 | years. |
| 31 | file, which allows me more flexibility for storage and backup. I keep this file | 31 | 2. PostgreSQL: A database I'm new to, but want to learn. |
| 32 | in my cloud storage and pull it up whenever needed. | 32 | 3. SQLite: A database that I've used for a couple projects and have |
| 33 | 33 | moderate experience. | |
| 34 | ## GUI Editing | 34 | |
| 35 | 35 | I ended up choosing SQLite since it can be maintained within a single | |
| 36 | Since I didn't want to try and import 1000--1500 records into my new database | 36 | =.sqlite= file, which allows me more flexibility for storage and backup. |
| 37 | via the command line, I opted to use [DB Browser for SQLite | 37 | I keep this file in my cloud storage and pull it up whenever needed. |
| 38 | (DB4S)](https://sqlitebrowser.org/) as a GUI tool. This application is | 38 | |
| 39 | excellent, and I don't see myself going back to the CLI when working in this | 39 | ** GUI Editing |
| 40 | database. | 40 | :PROPERTIES: |
| 41 | :CUSTOM_ID: gui-editing | ||
| 42 | :END: | ||
| 43 | Since I didn't want to try and import 1000--1500 records into my new | ||
| 44 | database via the command line, I opted to use | ||
| 45 | [[https://sqlitebrowser.org/][DB Browser for SQLite (DB4S)]] as a GUI | ||
| 46 | tool. This application is excellent, and I don't see myself going back | ||
| 47 | to the CLI when working in this database. | ||
| 41 | 48 | ||
| 42 | DB4S allows you to copy a range of cells from a spreadsheet and paste it | 49 | DB4S allows you to copy a range of cells from a spreadsheet and paste it |
| 43 | straight into the SQL table. I used this process for all 36 accounts, 1290 | 50 | straight into the SQL table. I used this process for all 36 accounts, |
| 44 | account statements, and 126 pay statements. Overall, I'm guessing this took | 51 | 1290 account statements, and 126 pay statements. Overall, I'm guessing |
| 45 | anywhere between 4--8 hours. In comparison, it probably took me 2-3 days to | 52 | this took anywhere between 4--8 hours. In comparison, it probably took |
| 46 | initially create the spreadsheet. | 53 | me 2-3 days to initially create the spreadsheet. |
| 47 | |||
| 48 | ## Schema | ||
| 49 | 54 | ||
| 50 | The schema for this database is actually extremely simple and involves only | 55 | ** Schema |
| 51 | three tables (for now): | 56 | :PROPERTIES: |
| 57 | :CUSTOM_ID: schema | ||
| 58 | :END: | ||
| 59 | The schema for this database is actually extremely simple and involves | ||
| 60 | only three tables (for now): | ||
| 52 | 61 | ||
| 53 | 1. Accounts | 62 | 1. Accounts |
| 54 | 2. Statements | 63 | 2. Statements |
| 55 | 3. Payroll | 64 | 3. Payroll |
| 56 | 65 | ||
| 57 | **Accounts** | 66 | *Accounts* |
| 58 | 67 | ||
| 59 | The Accounts table contains summary information about an account, such as a car | 68 | The Accounts table contains summary information about an account, such |
| 60 | loan or a credit card. By viewing this table, you can find high-level data, such | 69 | as a car loan or a credit card. By viewing this table, you can find |
| 61 | as interest rate, credit line, or owner. | 70 | high-level data, such as interest rate, credit line, or owner. |
| 62 | 71 | ||
| 63 | ```sql | 72 | #+begin_src sql |
| 64 | CREATE TABLE "Accounts" ( | 73 | CREATE TABLE "Accounts" ( |
| 65 | "AccountID" INTEGER NOT NULL UNIQUE, | 74 | "AccountID" INTEGER NOT NULL UNIQUE, |
| 66 | "AccountType" TEXT, | 75 | "AccountType" TEXT, |
| @@ -72,16 +81,16 @@ CREATE TABLE "Accounts" ( | |||
| 72 | "Co-Owner" TEXT, | 81 | "Co-Owner" TEXT, |
| 73 | PRIMARY KEY("AccountID" AUTOINCREMENT) | 82 | PRIMARY KEY("AccountID" AUTOINCREMENT) |
| 74 | ) | 83 | ) |
| 75 | ``` | 84 | #+end_src |
| 76 | 85 | ||
| 77 | **Statements** | 86 | *Statements* |
| 78 | 87 | ||
| 79 | The Statements table uses the same unique identifier as the Accounts table, | 88 | The Statements table uses the same unique identifier as the Accounts |
| 80 | meaning you can join the tables to find a monthly statement for any of the | 89 | table, meaning you can join the tables to find a monthly statement for |
| 81 | accounts listed in the Accounts table. Each statement has an account ID, | 90 | any of the accounts listed in the Accounts table. Each statement has an |
| 82 | statement date, and total balance. | 91 | account ID, statement date, and total balance. |
| 83 | 92 | ||
| 84 | ```sql | 93 | #+begin_src sql |
| 85 | CREATE TABLE "Statements" ( | 94 | CREATE TABLE "Statements" ( |
| 86 | "StatementID" INTEGER NOT NULL UNIQUE, | 95 | "StatementID" INTEGER NOT NULL UNIQUE, |
| 87 | "AccountID" INTEGER, | 96 | "AccountID" INTEGER, |
| @@ -90,16 +99,16 @@ CREATE TABLE "Statements" ( | |||
| 90 | PRIMARY KEY("StatementID" AUTOINCREMENT), | 99 | PRIMARY KEY("StatementID" AUTOINCREMENT), |
| 91 | FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID") | 100 | FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID") |
| 92 | ) | 101 | ) |
| 93 | ``` | 102 | #+end_src |
| 94 | 103 | ||
| 95 | **Payroll** | 104 | *Payroll* |
| 96 | 105 | ||
| 97 | The Payroll table is a separate entity, unrelated to the Accounts or Statements | 106 | The Payroll table is a separate entity, unrelated to the Accounts or |
| 98 | tables. This table contains all information you would find on a pay statement | 107 | Statements tables. This table contains all information you would find on |
| 99 | from an employer. As you change employers or obtain new perks/benefits, just add | 108 | a pay statement from an employer. As you change employers or obtain new |
| 100 | new columns to adapt to the new data. | 109 | perks/benefits, just add new columns to adapt to the new data. |
| 101 | 110 | ||
| 102 | ```sql | 111 | #+begin_src sql |
| 103 | CREATE TABLE "Payroll" ( | 112 | CREATE TABLE "Payroll" ( |
| 104 | "PaycheckID" INTEGER NOT NULL UNIQUE, | 113 | "PaycheckID" INTEGER NOT NULL UNIQUE, |
| 105 | "PayDate" TEXT, | 114 | "PayDate" TEXT, |
| @@ -129,23 +138,28 @@ CREATE TABLE "Payroll" ( | |||
| 129 | "TaxState" NUMERIC, | 138 | "TaxState" NUMERIC, |
| 130 | PRIMARY KEY("PaycheckID" AUTOINCREMENT) | 139 | PRIMARY KEY("PaycheckID" AUTOINCREMENT) |
| 131 | ) | 140 | ) |
| 132 | ``` | 141 | #+end_src |
| 133 | 142 | ||
| 134 | ## Python Reporting | 143 | ** Python Reporting |
| 135 | 144 | :PROPERTIES: | |
| 136 | Once I created the database tables and imported all my data, the only step left | 145 | :CUSTOM_ID: python-reporting |
| 137 | was to create a process to report and visualize on various aspects of the data. | 146 | :END: |
| 138 | 147 | Once I created the database tables and imported all my data, the only | |
| 139 | In order to explore and create the reports I'm interested in, I utilized a | 148 | step left was to create a process to report and visualize on various |
| 140 | two-part process involving Jupyter Notebooks and Python scripts. | 149 | aspects of the data. |
| 141 | 150 | ||
| 142 | ### Step 1: Jupyter Notebooks | 151 | In order to explore and create the reports I'm interested in, I utilized |
| 143 | 152 | a two-part process involving Jupyter Notebooks and Python scripts. | |
| 153 | |||
| 154 | *** Step 1: Jupyter Notebooks | ||
| 155 | :PROPERTIES: | ||
| 156 | :CUSTOM_ID: step-1-jupyter-notebooks | ||
| 157 | :END: | ||
| 144 | When I need to explore data, try different things, and re-run my code | 158 | When I need to explore data, try different things, and re-run my code |
| 145 | cell-by-cell, I use Jupyter Notebooks. For example, I explored the `Accounts` | 159 | cell-by-cell, I use Jupyter Notebooks. For example, I explored the |
| 146 | table until I found the following useful information: | 160 | =Accounts= table until I found the following useful information: |
| 147 | 161 | ||
| 148 | ```python | 162 | #+begin_src python |
| 149 | import sqlite3 | 163 | import sqlite3 |
| 150 | import pandas as pd | 164 | import pandas as pd |
| 151 | import matplotlib | 165 | import matplotlib |
| @@ -165,14 +179,17 @@ matplotlib.rcParams['legend.labelcolor'] = 'black' | |||
| 165 | 179 | ||
| 166 | # Display graph | 180 | # Display graph |
| 167 | df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%') | 181 | df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%') |
| 168 | ``` | 182 | #+end_src |
| 169 | |||
| 170 | ### Step 2: Python Scripts | ||
| 171 | 183 | ||
| 172 | Once I explored enough through the notebooks and had a list of reports I wanted, | 184 | *** Step 2: Python Scripts |
| 173 | I moved on to create a Python project with the following structure: | 185 | :PROPERTIES: |
| 186 | :CUSTOM_ID: step-2-python-scripts | ||
| 187 | :END: | ||
| 188 | Once I explored enough through the notebooks and had a list of reports I | ||
| 189 | wanted, I moved on to create a Python project with the following | ||
| 190 | structure: | ||
| 174 | 191 | ||
| 175 | ```txt | 192 | #+begin_src txt |
| 176 | finance/ | 193 | finance/ |
| 177 | ├── notebooks/ | 194 | ├── notebooks/ |
| 178 | │ │ ├── account_summary.ipynb | 195 | │ │ ├── account_summary.ipynb |
| @@ -189,23 +206,24 @@ finance/ | |||
| 189 | ├── process.py | 206 | ├── process.py |
| 190 | ├── requirements.txt | 207 | ├── requirements.txt |
| 191 | └── README.md | 208 | └── README.md |
| 192 | ``` | 209 | #+end_src |
| 193 | 210 | ||
| 194 | This structure allows me to: | 211 | This structure allows me to: |
| 195 | 212 | ||
| 196 | 1. Compile all required python packages into `requirements.txt` for easy | 213 | 1. Compile all required python packages into =requirements.txt= for easy |
| 197 | installation if I move to a new machine. | 214 | installation if I move to a new machine. |
| 198 | 2. Activate a virtual environment in `venv/` so I don't need to maintain a | 215 | 2. Activate a virtual environment in =venv/= so I don't need to maintain |
| 199 | system-wide Python environment just for this project. | 216 | a system-wide Python environment just for this project. |
| 200 | 3. Keep my `notebooks/` folder to continuously explore the data as I see fit. | 217 | 3. Keep my =notebooks/= folder to continuously explore the data as I see |
| 201 | 4. Maintain a local copy of the database in `src/` for easy access. | 218 | fit. |
| 202 | 5. Export reports, images, HTML files, etc. to `public/`. | 219 | 4. Maintain a local copy of the database in =src/= for easy access. |
| 220 | 5. Export reports, images, HTML files, etc. to =public/=. | ||
| 203 | 221 | ||
| 204 | Now, onto the differences between the code in a Jupyter Notebook and the actual | 222 | Now, onto the differences between the code in a Jupyter Notebook and the |
| 205 | Python files. To create the report in the Notebook snippet above, I created the | 223 | actual Python files. To create the report in the Notebook snippet above, |
| 206 | following function inside `process.py`: | 224 | I created the following function inside =process.py=: |
| 207 | 225 | ||
| 208 | ```python | 226 | #+begin_src python |
| 209 | # Create summary pie chart | 227 | # Create summary pie chart |
| 210 | def summary_data(accounts: pandas.DataFrame) -> None: | 228 | def summary_data(accounts: pandas.DataFrame) -> None: |
| 211 | accounts_01 = accounts[accounts["Owner"] == "Person01"] | 229 | accounts_01 = accounts[accounts["Owner"] == "Person01"] |
| @@ -231,19 +249,20 @@ def summary_data(accounts: pandas.DataFrame) -> None: | |||
| 231 | ) | 249 | ) |
| 232 | ) | 250 | ) |
| 233 | summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200) | 251 | summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200) |
| 234 | ``` | 252 | #+end_src |
| 235 | 253 | ||
| 236 | The result? A high-quality pie chart that is read directly by the | 254 | The result? A high-quality pie chart that is read directly by the |
| 237 | `public/index.html` template I use. | 255 | =public/index.html= template I use. |
| 238 | 256 | ||
| 239 | Other charts generated by this project include: | 257 | Other charts generated by this project include: |
| 240 | 258 | ||
| 241 | - Charts of account balances over time. | 259 | - Charts of account balances over time. |
| 242 | - Line chart of effective tax rate (taxes divided by taxable income). | 260 | - Line chart of effective tax rate (taxes divided by taxable income). |
| 243 | - Salary projections and error limits using past income and inflation rates. | 261 | - Salary projections and error limits using past income and inflation |
| 244 | - Multi-line chart of gross income, taxable income, and net income. | 262 | rates. |
| 263 | - Multi-line chart of gross income, taxable income, and net income. | ||
| 245 | 264 | ||
| 246 | The best thing about this project? I can improve it at any given time, shaping | 265 | The best thing about this project? I can improve it at any given time, |
| 247 | it into whatever helps me the most for that time. I imagine that I will be | 266 | shaping it into whatever helps me the most for that time. I imagine that |
| 248 | introducing an asset tracking table soon to track the depreciating value of | 267 | I will be introducing an asset tracking table soon to track the |
| 249 | cars, houses, etc. Who knows what's next? | 268 | depreciating value of cars, houses, etc. Who knows what's next? |
content/blog/2022-03-08-plex-migration.md deleted −218
| @@ -1,218 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-03-08 | ||
| 3 | title = "Migrating Plex to a New Server with Nvidia GPU Transcoding" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Migration Phases | ||
| 9 | |||
| 10 | I recently decided to migrate my server from an old OptiPlex desktop machine to | ||
| 11 | a custom-built tower with better hardware in every category. In order to do | ||
| 12 | this, I would need to properly migrate a full Plex installation. | ||
| 13 | |||
| 14 | The second part of this migration is that the new server uses an Nvidia GPU and | ||
| 15 | does not have any integrated graphics, which requires extra work for | ||
| 16 | installation, but provides much better hardware transcoding options for Plex. | ||
| 17 | |||
| 18 | Therefore, I have broken this migration down into three phases: | ||
| 19 | |||
| 20 | 1. Configure the New Server | ||
| 21 | 2. Migrate Plex Data & Devices | ||
| 22 | 3. Configure GPU Transcoding | ||
| 23 | |||
| 24 | # Phase 1: Configure the New Server | ||
| 25 | |||
| 26 | ## Choosing an OS | ||
| 27 | |||
| 28 | In order to migrate Plex to my new server, I first needed to choose an | ||
| 29 | appropriate operating system (OS) and install it on the machine. Given that I | ||
| 30 | have encountered numerous issues installing other Linux distributions properly | ||
| 31 | with Nvidia graphics, I chose [Ubuntu | ||
| 32 | Server](https://ubuntu.com/download/server). | ||
| 33 | |||
| 34 | The first step is to create a bootable USB with Ubuntu Server. This is easy with | ||
| 35 | [Etcher](https://www.balena.io/etcher/), an app that runs on many different | ||
| 36 | platforms. Just download the Ubuntu Server `.iso` image, launch Etcher, and | ||
| 37 | install the `.iso` on the USB. | ||
| 38 | |||
| 39 | Once the USB is created, insert it into my server, reboot, and click `Esc` (or | ||
| 40 | any of the `F1-12` keys) until the BIOS menu appears. Finally, launch the USB | ||
| 41 | boot drive. | ||
| 42 | |||
| 43 | ## Booting with Nvidia | ||
| 44 | |||
| 45 | In order to install Ubuntu Server with an Nvidia Graphics card (and no | ||
| 46 | integrated graphics on this device for some reason), you'll have to configure | ||
| 47 | the boot menu to allow different graphics drivers to be loaded. | ||
| 48 | |||
| 49 | When booting from the USB, the machine will launch the initial installation | ||
| 50 | menu. From this menu, type `e` to view the default command options that come | ||
| 51 | with the device - it's a good idea to take a photo of this screen, so you can | ||
| 52 | enter these commands on the next screen (along with adding support for Nvidia). | ||
| 53 | |||
| 54 | Finally, type `Ctrl + C` to enter the command line. From this command line, | ||
| 55 | enter the commands found on the `e` screen. _Remember to add `nomodeset` to the | ||
| 56 | `linux ...` line so that your Nvidia device will display the installation | ||
| 57 | screens properly!_ | ||
| 58 | |||
| 59 | Here's an example of the commands I pulled from the `e` screen and entered on | ||
| 60 | the command line. | ||
| 61 | |||
| 62 | ```sh | ||
| 63 | setparams 'Install Ubuntu Server' | ||
| 64 | setgfxpayload=keep | ||
| 65 | linux /casper/vmlinuz quiet nomodeset --- | ||
| 66 | initrd /casper/initrd | ||
| 67 | boot | ||
| 68 | ``` | ||
| 69 | |||
| 70 | Once the machine is rebooted, enter the `e` screen again and add `nomodeset` to | ||
| 71 | the `linux ...` line again and press `Ctrl + X` to save the boot options. | ||
| 72 | |||
| 73 | The machine is now fully installed and can properly display on an external | ||
| 74 | display using the Nvidia GPU. | ||
| 75 | |||
| 76 | Always remember to update and upgrade on a new installation: | ||
| 77 | |||
| 78 | ```sh | ||
| 79 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | ||
| 80 | ``` | ||
| 81 | |||
| 82 | # Phase 2: Migrate Plex Data & Devices | ||
| 83 | |||
| 84 | This phase uses the great Plex article on migrations ([Move an Installation to | ||
| 85 | Another | ||
| 86 | System](https://support.plex.tv/articles/201370363-move-an-install-to-another-system/)) | ||
| 87 | and adds a bit more information to help with commands and context. | ||
| 88 | |||
| 89 | ## Terminology | ||
| 90 | |||
| 91 | **Source:** The original server that is being replaced.\ | ||
| 92 | **Destination:** The new server.\ | ||
| 93 | **Client:** Any application that can be used to modify settings for both | ||
| 94 | source/destination. | ||
| 95 | |||
| 96 | ## Step 01: [Client] Update Settings | ||
| 97 | |||
| 98 | Open up a Plex app and _disable_ the `Account` > `Library` > `Empty trash | ||
| 99 | automatically after every scan` preference for the source server. | ||
| 100 | |||
| 101 | ## Step 02: [Destination] Install Plex | ||
| 102 | |||
| 103 | Open up the [Plex Media Server download | ||
| 104 | page](https://www.plex.tv/media-server-downloads/) and copy the link for the | ||
| 105 | appropriate platform. | ||
| 106 | |||
| 107 | Execute the following commands on the destination server to install Plex: | ||
| 108 | |||
| 109 | ```sh | ||
| 110 | wget <url> | ||
| 111 | sudo dpkg -i <filename> | ||
| 112 | sudo systemctl stop plexmediaserver.service | ||
| 113 | ``` | ||
| 114 | |||
| 115 | ## Step 03: [Source] Stop Plex & Migrate Data | ||
| 116 | |||
| 117 | First, stop the Plex service so that no data is created or modified during the | ||
| 118 | migration. | ||
| 119 | |||
| 120 | ```sh | ||
| 121 | sudo systemctl stop plexmediaserver.service | ||
| 122 | ``` | ||
| 123 | |||
| 124 | Next, copy the data to the new server. To find where the Plex data directory is | ||
| 125 | located, Plex has another excellent article available: [Where is the Plex Media | ||
| 126 | Server data directory | ||
| 127 | located?](https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/). | ||
| 128 | |||
| 129 | There are many ways to copy the data to the new server and will largely depend | ||
| 130 | on the size of the folder being copied. Personally, my data folder was ~23GB and | ||
| 131 | I opted to simply use the `scp` command to copy the files over SSH. | ||
| 132 | |||
| 133 | This process was throttled by the old server's slow HDD and ports and took | ||
| 134 | approximately 90 minutes to complete. In comparison, moving the data from the | ||
| 135 | new server's `home/user/` directory to the `/var/.../Plex Media Server` | ||
| 136 | directory took 2-3 minutes. | ||
| 137 | |||
| 138 | ```sh | ||
| 139 | scp -r "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" your_user@xxx.xxx.xxx.xxx:"'/path/to/destination/'" | ||
| 140 | ``` | ||
| 141 | |||
| 142 | ## Step 04: [Destination] Update File Permissions | ||
| 143 | |||
| 144 | In case you move the data directory to a common area on the new server, it will | ||
| 145 | have to be moved to the proper location before Plex can function properly: | ||
| 146 | |||
| 147 | ```sh | ||
| 148 | mv "Plex Media Server" /var/lib/plexmediaserver/Library/Application Support/ | ||
| 149 | ``` | ||
| 150 | |||
| 151 | To ensure permissions were retained properly, the server will need to show that | ||
| 152 | all files and folders in the data directory are owned by `plex:plex` (or | ||
| 153 | whichever user is running the Plex application). | ||
| 154 | |||
| 155 | ```sh | ||
| 156 | sudo chown -R plex:plex "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" | ||
| 157 | ``` | ||
| 158 | |||
| 159 | Finally, start the service and check the status. | ||
| 160 | |||
| 161 | ```sh | ||
| 162 | sudo systemctl start plexmediaserver.service | ||
| 163 | sudo systemctl status plexmediaserver.service | ||
| 164 | ``` | ||
| 165 | |||
| 166 | ## Step 05: [Client] Update Libraries & Metadata | ||
| 167 | |||
| 168 | The first step - now that the new server is up and running - is to sign out of | ||
| 169 | the client and sign back in. Once this is done, update any library locations, if | ||
| 170 | necessary. This was unnecessary in my case since I simply moved my storage | ||
| 171 | drives from the source server to the destination server. | ||
| 172 | |||
| 173 | Next, perform the following actions in the client: | ||
| 174 | |||
| 175 | 1. On the left sidebar, click `More` > Three-Dot Menu > `Scan Library Files` | ||
| 176 | 2. _Enable_ the `Account` > `Library` > `Empty trash automatically after every | ||
| 177 | scan` preference for the source server. | ||
| 178 | 3. On the left sidebar, click `More` > Three-Dot Menu > `Manage Server` > `Empty | ||
| 179 | Trash` | ||
| 180 | 4. On the left sidebar, click `More` > Three-Dot Menu > `Manage Server` > `Clean | ||
| 181 | Bundles` | ||
| 182 | 5. On the left sidebar, click `More` > Three-Dot Menu > `Manage Server` > | ||
| 183 | `Optimize Database` | ||
| 184 | |||
| 185 | Finally, double-check the Remote Access settings to make sure no changes have | ||
| 186 | caused issues with accessing the server from outside the network. | ||
| 187 | |||
| 188 | In my case, I use a single port forwarding rule in my router and needed to | ||
| 189 | update the Local LAN IP Address to the new server IP address. | ||
| 190 | |||
| 191 | # Phase 3: Configure GPU Transcoding | ||
| 192 | |||
| 193 | The final piece to the migration is enabling hardware transcoding so that Plex | ||
| 194 | can fully utilize the new Nvidia GPU available in the server. The first step is | ||
| 195 | to install Nvidia graphics drivers. This process may take a few minutes, but the | ||
| 196 | commands are pretty simple: | ||
| 197 | |||
| 198 | ```sh | ||
| 199 | sudo add-apt-repository ppa:graphics-drivers/ppa | ||
| 200 | sudo apt update | ||
| 201 | sudo apt-get install ubuntu-drivers-common | ||
| 202 | sudo ubuntu-drivers autoinstall | ||
| 203 | ``` | ||
| 204 | |||
| 205 | Finally, reboot so that the changes are loaded: | ||
| 206 | |||
| 207 | ```sh | ||
| 208 | sudo reboot now | ||
| 209 | ``` | ||
| 210 | |||
| 211 | To ensure that the Nvidia graphics drivers are working properly, run the | ||
| 212 | following command to view the available GPUs, statistics, and processes: | ||
| 213 | |||
| 214 | ```sh | ||
| 215 | sudo nvidia-smi | ||
| 216 | ``` | ||
| 217 | |||
| 218 | Finally, enable hardware transcoding settings in the Plex application to finish the process. | ||
content/blog/2022-03-08-plex-migration.org added +252
| @@ -0,0 +1,252 @@ | |||
| 1 | #+date: <2022-03-08> | ||
| 2 | #+title: Migrating Plex to a New Server with Nvidia GPU Transcoding | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Migration Phases | ||
| 7 | :PROPERTIES: | ||
| 8 | :CUSTOM_ID: migration-phases | ||
| 9 | :END: | ||
| 10 | I recently decided to migrate my server from an old OptiPlex desktop | ||
| 11 | machine to a custom-built tower with better hardware in every category. | ||
| 12 | In order to do this, I would need to properly migrate a full Plex | ||
| 13 | installation. | ||
| 14 | |||
| 15 | The second part of this migration is that the new server uses an Nvidia | ||
| 16 | GPU and does not have any integrated graphics, which requires extra work | ||
| 17 | for installation, but provides much better hardware transcoding options | ||
| 18 | for Plex. | ||
| 19 | |||
| 20 | Therefore, I have broken this migration down into three phases: | ||
| 21 | |||
| 22 | 1. Configure the New Server | ||
| 23 | 2. Migrate Plex Data & Devices | ||
| 24 | 3. Configure GPU Transcoding | ||
| 25 | |||
| 26 | * Phase 1: Configure the New Server | ||
| 27 | :PROPERTIES: | ||
| 28 | :CUSTOM_ID: phase-1-configure-the-new-server | ||
| 29 | :END: | ||
| 30 | ** Choosing an OS | ||
| 31 | :PROPERTIES: | ||
| 32 | :CUSTOM_ID: choosing-an-os | ||
| 33 | :END: | ||
| 34 | In order to migrate Plex to my new server, I first needed to choose an | ||
| 35 | appropriate operating system (OS) and install it on the machine. Given | ||
| 36 | that I have encountered numerous issues installing other Linux | ||
| 37 | distributions properly with Nvidia graphics, I chose | ||
| 38 | [[https://ubuntu.com/download/server][Ubuntu Server]]. | ||
| 39 | |||
| 40 | The first step is to create a bootable USB with Ubuntu Server. This is | ||
| 41 | easy with [[https://www.balena.io/etcher/][Etcher]], an app that runs on | ||
| 42 | many different platforms. Just download the Ubuntu Server =.iso= image, | ||
| 43 | launch Etcher, and install the =.iso= on the USB. | ||
| 44 | |||
| 45 | Once the USB is created, insert it into my server, reboot, and click | ||
| 46 | =Esc= (or any of the =F1-12= keys) until the BIOS menu appears. Finally, | ||
| 47 | launch the USB boot drive. | ||
| 48 | |||
| 49 | ** Booting with Nvidia | ||
| 50 | :PROPERTIES: | ||
| 51 | :CUSTOM_ID: booting-with-nvidia | ||
| 52 | :END: | ||
| 53 | In order to install Ubuntu Server with an Nvidia Graphics card (and no | ||
| 54 | integrated graphics on this device for some reason), you'll have to | ||
| 55 | configure the boot menu to allow different graphics drivers to be | ||
| 56 | loaded. | ||
| 57 | |||
| 58 | When booting from the USB, the machine will launch the initial | ||
| 59 | installation menu. From this menu, type =e= to view the default command | ||
| 60 | options that come with the device - it's a good idea to take a photo of | ||
| 61 | this screen, so you can enter these commands on the next screen (along | ||
| 62 | with adding support for Nvidia). | ||
| 63 | |||
| 64 | Finally, type =Ctrl + C= to enter the command line. From this command | ||
| 65 | line, enter the commands found on the =e= screen. /Remember to add | ||
| 66 | =nomodeset= to the =linux ...= line so that your Nvidia device will | ||
| 67 | display the installation screens properly!/ | ||
| 68 | |||
| 69 | Here's an example of the commands I pulled from the =e= screen and | ||
| 70 | entered on the command line. | ||
| 71 | |||
| 72 | #+begin_src sh | ||
| 73 | setparams 'Install Ubuntu Server' | ||
| 74 | setgfxpayload=keep | ||
| 75 | linux /casper/vmlinuz quiet nomodeset --- | ||
| 76 | initrd /casper/initrd | ||
| 77 | boot | ||
| 78 | #+end_src | ||
| 79 | |||
| 80 | Once the machine is rebooted, enter the =e= screen again and add | ||
| 81 | =nomodeset= to the =linux ...= line again and press =Ctrl + X= to save | ||
| 82 | the boot options. | ||
| 83 | |||
| 84 | The machine is now fully installed and can properly display on an | ||
| 85 | external display using the Nvidia GPU. | ||
| 86 | |||
| 87 | Always remember to update and upgrade on a new installation: | ||
| 88 | |||
| 89 | #+begin_src sh | ||
| 90 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | ||
| 91 | #+end_src | ||
| 92 | |||
| 93 | * Phase 2: Migrate Plex Data & Devices | ||
| 94 | :PROPERTIES: | ||
| 95 | :CUSTOM_ID: phase-2-migrate-plex-data-devices | ||
| 96 | :END: | ||
| 97 | This phase uses the great Plex article on migrations | ||
| 98 | ([[https://support.plex.tv/articles/201370363-move-an-install-to-another-system/][Move | ||
| 99 | an Installation to Another System]]) and adds a bit more information to | ||
| 100 | help with commands and context. | ||
| 101 | |||
| 102 | ** Terminology | ||
| 103 | :PROPERTIES: | ||
| 104 | :CUSTOM_ID: terminology | ||
| 105 | :END: | ||
| 106 | *Source:* The original server that is being replaced.\\ | ||
| 107 | *Destination:* The new server.\\ | ||
| 108 | *Client:* Any application that can be used to modify settings for both | ||
| 109 | source/destination. | ||
| 110 | |||
| 111 | ** Step 01: [Client] Update Settings | ||
| 112 | :PROPERTIES: | ||
| 113 | :CUSTOM_ID: step-01-client-update-settings | ||
| 114 | :END: | ||
| 115 | Open up a Plex app and /disable/ the =Account= > =Library= > | ||
| 116 | =Empty trash automatically after every scan= preference for the source | ||
| 117 | server. | ||
| 118 | |||
| 119 | ** Step 02: [Destination] Install Plex | ||
| 120 | :PROPERTIES: | ||
| 121 | :CUSTOM_ID: step-02-destination-install-plex | ||
| 122 | :END: | ||
| 123 | Open up the [[https://www.plex.tv/media-server-downloads/][Plex Media | ||
| 124 | Server download page]] and copy the link for the appropriate platform. | ||
| 125 | |||
| 126 | Execute the following commands on the destination server to install | ||
| 127 | Plex: | ||
| 128 | |||
| 129 | #+begin_src sh | ||
| 130 | wget <url> | ||
| 131 | sudo dpkg -i <filename> | ||
| 132 | sudo systemctl stop plexmediaserver.service | ||
| 133 | #+end_src | ||
| 134 | |||
| 135 | ** Step 03: [Source] Stop Plex & Migrate Data | ||
| 136 | :PROPERTIES: | ||
| 137 | :CUSTOM_ID: step-03-source-stop-plex-migrate-data | ||
| 138 | :END: | ||
| 139 | First, stop the Plex service so that no data is created or modified | ||
| 140 | during the migration. | ||
| 141 | |||
| 142 | #+begin_src sh | ||
| 143 | sudo systemctl stop plexmediaserver.service | ||
| 144 | #+end_src | ||
| 145 | |||
| 146 | Next, copy the data to the new server. To find where the Plex data | ||
| 147 | directory is located, Plex has another excellent article available: | ||
| 148 | [[https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/][Where | ||
| 149 | is the Plex Media Server data directory located?]]. | ||
| 150 | |||
| 151 | There are many ways to copy the data to the new server and will largely | ||
| 152 | depend on the size of the folder being copied. Personally, my data | ||
| 153 | folder was ~23GB and I opted to simply use the =scp= command to copy the | ||
| 154 | files over SSH. | ||
| 155 | |||
| 156 | This process was throttled by the old server's slow HDD and ports and | ||
| 157 | took approximately 90 minutes to complete. In comparison, moving the | ||
| 158 | data from the new server's =home/user/= directory to the | ||
| 159 | =/var/.../Plex Media Server= directory took 2-3 minutes. | ||
| 160 | |||
| 161 | #+begin_src sh | ||
| 162 | scp -r "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" your_user@xxx.xxx.xxx.xxx:"'/path/to/destination/'" | ||
| 163 | #+end_src | ||
| 164 | |||
| 165 | ** Step 04: [Destination] Update File Permissions | ||
| 166 | :PROPERTIES: | ||
| 167 | :CUSTOM_ID: step-04-destination-update-file-permissions | ||
| 168 | :END: | ||
| 169 | In case you move the data directory to a common area on the new server, | ||
| 170 | it will have to be moved to the proper location before Plex can function | ||
| 171 | properly: | ||
| 172 | |||
| 173 | #+begin_src sh | ||
| 174 | mv "Plex Media Server" /var/lib/plexmediaserver/Library/Application Support/ | ||
| 175 | #+end_src | ||
| 176 | |||
| 177 | To ensure permissions were retained properly, the server will need to | ||
| 178 | show that all files and folders in the data directory are owned by | ||
| 179 | =plex:plex= (or whichever user is running the Plex application). | ||
| 180 | |||
| 181 | #+begin_src sh | ||
| 182 | sudo chown -R plex:plex "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" | ||
| 183 | #+end_src | ||
| 184 | |||
| 185 | Finally, start the service and check the status. | ||
| 186 | |||
| 187 | #+begin_src sh | ||
| 188 | sudo systemctl start plexmediaserver.service | ||
| 189 | sudo systemctl status plexmediaserver.service | ||
| 190 | #+end_src | ||
| 191 | |||
| 192 | ** Step 05: [Client] Update Libraries & Metadata | ||
| 193 | :PROPERTIES: | ||
| 194 | :CUSTOM_ID: step-05-client-update-libraries-metadata | ||
| 195 | :END: | ||
| 196 | The first step - now that the new server is up and running - is to sign | ||
| 197 | out of the client and sign back in. Once this is done, update any | ||
| 198 | library locations, if necessary. This was unnecessary in my case since I | ||
| 199 | simply moved my storage drives from the source server to the destination | ||
| 200 | server. | ||
| 201 | |||
| 202 | Next, perform the following actions in the client: | ||
| 203 | |||
| 204 | 1. On the left sidebar, click =More= > Three-Dot Menu > | ||
| 205 | =Scan Library Files= | ||
| 206 | 2. /Enable/ the =Account= > =Library= > | ||
| 207 | =Empty trash automatically after every scan= preference for the | ||
| 208 | source server. | ||
| 209 | 3. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | ||
| 210 | > =Empty Trash= | ||
| 211 | 4. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | ||
| 212 | > =Clean Bundles= | ||
| 213 | 5. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | ||
| 214 | > =Optimize Database= | ||
| 215 | |||
| 216 | Finally, double-check the Remote Access settings to make sure no changes | ||
| 217 | have caused issues with accessing the server from outside the network. | ||
| 218 | |||
| 219 | In my case, I use a single port forwarding rule in my router and needed | ||
| 220 | to update the Local LAN IP Address to the new server IP address. | ||
| 221 | |||
| 222 | * Phase 3: Configure GPU Transcoding | ||
| 223 | :PROPERTIES: | ||
| 224 | :CUSTOM_ID: phase-3-configure-gpu-transcoding | ||
| 225 | :END: | ||
| 226 | The final piece to the migration is enabling hardware transcoding so | ||
| 227 | that Plex can fully utilize the new Nvidia GPU available in the server. | ||
| 228 | The first step is to install Nvidia graphics drivers. This process may | ||
| 229 | take a few minutes, but the commands are pretty simple: | ||
| 230 | |||
| 231 | #+begin_src sh | ||
| 232 | sudo add-apt-repository ppa:graphics-drivers/ppa | ||
| 233 | sudo apt update | ||
| 234 | sudo apt-get install ubuntu-drivers-common | ||
| 235 | sudo ubuntu-drivers autoinstall | ||
| 236 | #+end_src | ||
| 237 | |||
| 238 | Finally, reboot so that the changes are loaded: | ||
| 239 | |||
| 240 | #+begin_src sh | ||
| 241 | sudo reboot now | ||
| 242 | #+end_src | ||
| 243 | |||
| 244 | To ensure that the Nvidia graphics drivers are working properly, run the | ||
| 245 | following command to view the available GPUs, statistics, and processes: | ||
| 246 | |||
| 247 | #+begin_src sh | ||
| 248 | sudo nvidia-smi | ||
| 249 | #+end_src | ||
| 250 | |||
| 251 | Finally, enable hardware transcoding settings in the Plex application to | ||
| 252 | finish the process. | ||
content/blog/2022-03-23-cloudflare-dns-api.md → content/blog/2022-03-23-cloudflare-dns-api.org renamed +48 −45
| @@ -1,39 +1,42 @@ | |||
| 1 | +++ | 1 | #+date: <2022-03-23> |
| 2 | date = 2022-03-23 | 2 | #+title: Dynamic DNS with Cloudflare API |
| 3 | title = "Dynamic DNS with Cloudflare API" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # DDNS: Dynamic DNS | ||
| 9 | 5 | ||
| 10 | If you're hosting a service from a location with dynamic DNS (where your IP may | 6 | * DDNS: Dynamic DNS |
| 11 | change at any time), you must have a solution to update the DNS so that you can | 7 | :PROPERTIES: |
| 12 | access your service even when the IP of the server changes. | 8 | :CUSTOM_ID: ddns-dynamic-dns |
| 9 | :END: | ||
| 10 | If you're hosting a service from a location with dynamic DNS (where your | ||
| 11 | IP may change at any time), you must have a solution to update the DNS | ||
| 12 | so that you can access your service even when the IP of the server | ||
| 13 | changes. | ||
| 13 | 14 | ||
| 14 | The process below uses the [Cloudflare API](https://api.cloudflare.com/) to | 15 | The process below uses the [[https://api.cloudflare.com/][Cloudflare |
| 15 | update DNS `A` and `AAAA` records with the server's current IP. If you use | 16 | API]] to update DNS =A= and =AAAA= records with the server's current IP. |
| 16 | another DNS provider, you will have to find a way to update your DNS (or find a | 17 | If you use another DNS provider, you will have to find a way to update |
| 17 | way to get a static IP). | 18 | your DNS (or find a way to get a static IP). |
| 18 | 19 | ||
| 19 | First, install `jq` since we will use it in the next script: | 20 | First, install =jq= since we will use it in the next script: |
| 20 | 21 | ||
| 21 | ```sh | 22 | #+begin_src sh |
| 22 | sudo apt install jq | 23 | sudo apt install jq |
| 23 | ``` | 24 | #+end_src |
| 24 | 25 | ||
| 25 | Next, create a location for your DDNS update scripts and open the first script: | 26 | Next, create a location for your DDNS update scripts and open the first |
| 27 | script: | ||
| 26 | 28 | ||
| 27 | ```sh | 29 | #+begin_src sh |
| 28 | mkdir ~/ddns | 30 | mkdir ~/ddns |
| 29 | nano ~/ddns/update.sh | 31 | nano ~/ddns/update.sh |
| 30 | ``` | 32 | #+end_src |
| 31 | 33 | ||
| 32 | The following `update.sh` script will take all of your domains and subdomains | 34 | The following =update.sh= script will take all of your domains and |
| 33 | and check Cloudflare to see if the current `A` and `AAAA` records match your | 35 | subdomains and check Cloudflare to see if the current =A= and =AAAA= |
| 34 | server's IP address. If not, it will update the records. | 36 | records match your server's IP address. If not, it will update the |
| 37 | records. | ||
| 35 | 38 | ||
| 36 | ```sh | 39 | #+begin_src sh |
| 37 | # file: update.sh | 40 | # file: update.sh |
| 38 | #!/bin/bash | 41 | #!/bin/bash |
| 39 | 42 | ||
| @@ -55,20 +58,20 @@ do | |||
| 55 | echo -e "\nUpdating $subdomain..." | 58 | echo -e "\nUpdating $subdomain..." |
| 56 | zone_name=$domain dns_record=$subdomain /home/<your-username>/ddns/ddns.sh | 59 | zone_name=$domain dns_record=$subdomain /home/<your-username>/ddns/ddns.sh |
| 57 | done | 60 | done |
| 58 | ``` | 61 | #+end_src |
| 59 | 62 | ||
| 60 | Next, open up the `ddns.sh` script. Paste the following into the script and | 63 | Next, open up the =ddns.sh= script. Paste the following into the script |
| 61 | update the `api_token` and `email` variables. | 64 | and update the =api_token= and =email= variables. |
| 62 | 65 | ||
| 63 | ```sh | 66 | #+begin_src sh |
| 64 | nano ~/ddns/ddns.sh | 67 | nano ~/ddns/ddns.sh |
| 65 | ``` | 68 | #+end_src |
| 66 | 69 | ||
| 67 | **Note**: If you want your DNS records to be proxied through Cloudflare, find | 70 | *Note*: If you want your DNS records to be proxied through Cloudflare, |
| 68 | and update the following snippet: `"proxied":false}"` to say `true` instead of | 71 | find and update the following snippet: ="proxied":false}"= to say =true= |
| 69 | `false`. | 72 | instead of =false=. |
| 70 | 73 | ||
| 71 | ```sh | 74 | #+begin_src sh |
| 72 | # file: ddns.sh | 75 | # file: ddns.sh |
| 73 | #!/bin/bash | 76 | #!/bin/bash |
| 74 | # based on https://gist.github.com/Tras2/cba88201b17d765ec065ccbedfb16d9a | 77 | # based on https://gist.github.com/Tras2/cba88201b17d765ec065ccbedfb16d9a |
| @@ -161,30 +164,30 @@ then | |||
| 161 | else | 164 | else |
| 162 | echo "There is a problem with either the email or the password" | 165 | echo "There is a problem with either the email or the password" |
| 163 | fi | 166 | fi |
| 164 | ``` | 167 | #+end_src |
| 165 | 168 | ||
| 166 | Once the script is saved and closed, make the scripts executable: | 169 | Once the script is saved and closed, make the scripts executable: |
| 167 | 170 | ||
| 168 | ```sh | 171 | #+begin_src sh |
| 169 | chmod +x ~/ddns/ddns.sh | 172 | chmod +x ~/ddns/ddns.sh |
| 170 | chmod +x ~/ddns/update.sh | 173 | chmod +x ~/ddns/update.sh |
| 171 | ``` | 174 | #+end_src |
| 172 | 175 | ||
| 173 | You can test the script by running it manually: | 176 | You can test the script by running it manually: |
| 174 | 177 | ||
| 175 | ```sh | 178 | #+begin_src sh |
| 176 | ./update.sh | 179 | ./update.sh |
| 177 | ``` | 180 | #+end_src |
| 178 | 181 | ||
| 179 | To make sure the scripts run automatically, add it to the `cron` file so that it | 182 | To make sure the scripts run automatically, add it to the =cron= file so |
| 180 | will run on a schedule. To do this, open the cron file: | 183 | that it will run on a schedule. To do this, open the cron file: |
| 181 | 184 | ||
| 182 | ```sh | 185 | #+begin_src sh |
| 183 | crontab -e | 186 | crontab -e |
| 184 | ``` | 187 | #+end_src |
| 185 | 188 | ||
| 186 | In the cron file, paste the following at the bottom of the editor: | 189 | In the cron file, paste the following at the bottom of the editor: |
| 187 | 190 | ||
| 188 | ```sh | 191 | #+begin_src sh |
| 189 | */5 ** ** ** ** bash /home/<your_username>/ddns/update.sh | 192 | ,*/5 ** ** ** ** bash /home/<your_username>/ddns/update.sh |
| 190 | ``` | 193 | #+end_src |
content/blog/2022-03-23-nextcloud-on-ubuntu.md → content/blog/2022-03-23-nextcloud-on-ubuntu.org renamed +93 −73
| @@ -1,80 +1,92 @@ | |||
| 1 | +++ | 1 | #+date: <2022-03-23> |
| 2 | date = 2022-03-23 | 2 | #+title: Self-Hosting Nextcloud |
| 3 | title = "Self-Hosting Nextcloud" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # What is Nextcloud? | ||
| 9 | 5 | ||
| 10 | [Nextcloud](https://nextcloud.com/) is a self-hosted solution for storage, | 6 | * What is Nextcloud? |
| 11 | communications, editing, calendar, contacts, and more. | 7 | :PROPERTIES: |
| 8 | :CUSTOM_ID: what-is-nextcloud | ||
| 9 | :END: | ||
| 10 | [[https://nextcloud.com/][Nextcloud]] is a self-hosted solution for | ||
| 11 | storage, communications, editing, calendar, contacts, and more. | ||
| 12 | 12 | ||
| 13 | This tutorial assumes that you have an Ubuntu server and a domain name | 13 | This tutorial assumes that you have an Ubuntu server and a domain name |
| 14 | configured to point toward the server. | 14 | configured to point toward the server. |
| 15 | 15 | ||
| 16 | # Install Dependencies | 16 | * Install Dependencies |
| 17 | 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: install-dependencies | ||
| 19 | :END: | ||
| 18 | To start, you will need to install the packages that Nextcloud requires: | 20 | To start, you will need to install the packages that Nextcloud requires: |
| 19 | 21 | ||
| 20 | ```sh | 22 | #+begin_src sh |
| 21 | sudo apt install apache2 mariadb-server libapache2-mod-php7.4 | 23 | sudo apt install apache2 mariadb-server libapache2-mod-php7.4 |
| 22 | sudo apt install php7.4-gd php7.4-mysql php7.4-curl php7.4-mbstring php7.4-intl | 24 | sudo apt install php7.4-gd php7.4-mysql php7.4-curl php7.4-mbstring php7.4-intl |
| 23 | sudo apt install php7.4-gmp php7.4-bcmath php-imagick php7.4-xml php7.4-zip | 25 | sudo apt install php7.4-gmp php7.4-bcmath php-imagick php7.4-xml php7.4-zip |
| 24 | ``` | 26 | #+end_src |
| 25 | |||
| 26 | # Set Up MySQL | ||
| 27 | 27 | ||
| 28 | Next, you will need to log in to MySQL as the `root` user of the machine. | 28 | * Set Up MySQL |
| 29 | :PROPERTIES: | ||
| 30 | :CUSTOM_ID: set-up-mysql | ||
| 31 | :END: | ||
| 32 | Next, you will need to log in to MySQL as the =root= user of the | ||
| 33 | machine. | ||
| 29 | 34 | ||
| 30 | ```sh | 35 | #+begin_src sh |
| 31 | sudo mysql -uroot -p | 36 | sudo mysql -uroot -p |
| 32 | ``` | 37 | #+end_src |
| 33 | 38 | ||
| 34 | Once you've logged in, you must create a new user so that Nextcloud can manage | 39 | Once you've logged in, you must create a new user so that Nextcloud can |
| 35 | the database. You will also create a `nextcloud` database and assign privileges: | 40 | manage the database. You will also create a =nextcloud= database and |
| 41 | assign privileges: | ||
| 36 | 42 | ||
| 37 | ```sql | 43 | #+begin_src sql |
| 38 | CREATE USER 'username'@'localhost' IDENTIFIED BY 'password'; | 44 | CREATE USER 'username'@'localhost' IDENTIFIED BY 'password'; |
| 39 | CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; | 45 | CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; |
| 40 | GRANT ALL PRIVILEGES ON nextcloud.** TO 'username'@'localhost'; | 46 | GRANT ALL PRIVILEGES ON nextcloud.** TO 'username'@'localhost'; |
| 41 | FLUSH PRIVILEGES; | 47 | FLUSH PRIVILEGES; |
| 42 | quit; | 48 | quit; |
| 43 | ``` | 49 | #+end_src |
| 44 | 50 | ||
| 45 | # Download & Install Nextcloud | 51 | * Download & Install Nextcloud |
| 46 | 52 | :PROPERTIES: | |
| 47 | To download Nextcloud, go the [Nextcloud downloads | 53 | :CUSTOM_ID: download-install-nextcloud |
| 48 | page](https://nextcloud.com/install/#instructions-server), click on `Archive | 54 | :END: |
| 49 | File` and right-click the big blue button to copy the link. | 55 | To download Nextcloud, go the |
| 50 | 56 | [[https://nextcloud.com/install/#instructions-server][Nextcloud | |
| 51 | Then, go to your server and enter the following commands to download, unzip, and | 57 | downloads page]], click on =Archive File= and right-click the big blue |
| 52 | move the files to your destination directory. This example uses `example.com` as | 58 | button to copy the link. |
| 53 | the destination, but you can put it wherever you want to server your files from. | 59 | |
| 54 | 60 | Then, go to your server and enter the following commands to download, | |
| 55 | ```sh | 61 | unzip, and move the files to your destination directory. This example |
| 62 | uses =example.com= as the destination, but you can put it wherever you | ||
| 63 | want to server your files from. | ||
| 64 | |||
| 65 | #+begin_src sh | ||
| 56 | wget https://download.nextcloud.com/server/releases/nextcloud-23.0.3.zip | 66 | wget https://download.nextcloud.com/server/releases/nextcloud-23.0.3.zip |
| 57 | sudo apt install unzip | 67 | sudo apt install unzip |
| 58 | unzip nextcloud-23.0.3.zip | 68 | unzip nextcloud-23.0.3.zip |
| 59 | sudo cp -r nextcloud /var/www/example.com | 69 | sudo cp -r nextcloud /var/www/example.com |
| 60 | ``` | 70 | #+end_src |
| 61 | 71 | ||
| 62 | # Configure the Apache Web Server | 72 | * Configure the Apache Web Server |
| 63 | 73 | :PROPERTIES: | |
| 64 | Now that the database is set up and Nextcloud is installed, you need to set up | 74 | :CUSTOM_ID: configure-the-apache-web-server |
| 65 | the Apache configuration files to tell the server how to handle requests for | 75 | :END: |
| 66 | `example.com/nextcloud`. | 76 | Now that the database is set up and Nextcloud is installed, you need to |
| 77 | set up the Apache configuration files to tell the server how to handle | ||
| 78 | requests for =example.com/nextcloud=. | ||
| 67 | 79 | ||
| 68 | First, open the following file in the editor: | 80 | First, open the following file in the editor: |
| 69 | 81 | ||
| 70 | ```sh | 82 | #+begin_src sh |
| 71 | sudo nano /etc/apache2/sites-available/nextcloud.conf | 83 | sudo nano /etc/apache2/sites-available/nextcloud.conf |
| 72 | ``` | 84 | #+end_src |
| 73 | 85 | ||
| 74 | Once the editor is open, paste the following information in. Then, save and | 86 | Once the editor is open, paste the following information in. Then, save |
| 75 | close the file. | 87 | and close the file. |
| 76 | 88 | ||
| 77 | ```config | 89 | #+begin_src config |
| 78 | <VirtualHost *:80> | 90 | <VirtualHost *:80> |
| 79 | DocumentRoot /var/www/example.com | 91 | DocumentRoot /var/www/example.com |
| 80 | ServerName example.com | 92 | ServerName example.com |
| @@ -93,57 +105,65 @@ close the file. | |||
| 93 | </IfModule> | 105 | </IfModule> |
| 94 | </Directory> | 106 | </Directory> |
| 95 | </VirtualHost> | 107 | </VirtualHost> |
| 96 | ``` | 108 | #+end_src |
| 97 | 109 | ||
| 98 | Once the file is saved, enable it with Apache: | 110 | Once the file is saved, enable it with Apache: |
| 99 | 111 | ||
| 100 | ```sh | 112 | #+begin_src sh |
| 101 | sudo a2ensite nextcloud.conf | 113 | sudo a2ensite nextcloud.conf |
| 102 | ``` | 114 | #+end_src |
| 103 | 115 | ||
| 104 | Next, enable the Apache mods required by Nextcloud: | 116 | Next, enable the Apache mods required by Nextcloud: |
| 105 | 117 | ||
| 106 | ```sh | 118 | #+begin_src sh |
| 107 | sudo a2enmod rewrite headers env dir mime | 119 | sudo a2enmod rewrite headers env dir mime |
| 108 | ``` | 120 | #+end_src |
| 109 | 121 | ||
| 110 | Finally, restart Apache. If any errors arise, you must solve those before | 122 | Finally, restart Apache. If any errors arise, you must solve those |
| 111 | continuing. | 123 | before continuing. |
| 112 | 124 | ||
| 113 | ```sh | 125 | #+begin_src sh |
| 114 | sudo systemctl restart apache2 | 126 | sudo systemctl restart apache2 |
| 115 | ``` | 127 | #+end_src |
| 116 | 128 | ||
| 117 | For the app to work, you must have the correct file permissions on your | 129 | For the app to work, you must have the correct file permissions on your |
| 118 | `nextcloud` directory. Set the owner to be `www-data`: | 130 | =nextcloud= directory. Set the owner to be =www-data=: |
| 119 | 131 | ||
| 120 | ```sh | 132 | #+begin_src sh |
| 121 | sudo chown -R www-data:www-data /var/www/example.com/nextcloud/ | 133 | sudo chown -R www-data:www-data /var/www/example.com/nextcloud/ |
| 122 | ``` | 134 | #+end_src |
| 123 | |||
| 124 | # DNS | ||
| 125 | 135 | ||
| 136 | * DNS | ||
| 137 | :PROPERTIES: | ||
| 138 | :CUSTOM_ID: dns | ||
| 139 | :END: | ||
| 126 | If you do not have a static IP address, you will need to update your DNS | 140 | If you do not have a static IP address, you will need to update your DNS |
| 127 | settings (at your DNS provider) whenever your dynamic IP address changes. | 141 | settings (at your DNS provider) whenever your dynamic IP address |
| 128 | 142 | changes. | |
| 129 | For an example on how I do that with Cloudflare, see my other post: [Updating | ||
| 130 | Dynamic DNS with Cloudflare API](../updating-dynamic-dns-with-cloudflare-api/) | ||
| 131 | 143 | ||
| 132 | # Certbot | 144 | For an example on how I do that with Cloudflare, see my other post: |
| 145 | [[../updating-dynamic-dns-with-cloudflare-api/][Updating Dynamic DNS | ||
| 146 | with Cloudflare API]] | ||
| 133 | 147 | ||
| 134 | If you want to serve Nextcloud from HTTPS rather than plain HTTP, use the | 148 | * Certbot |
| 135 | following commands to issue Let's Encrypt SSL certificates: | 149 | :PROPERTIES: |
| 150 | :CUSTOM_ID: certbot | ||
| 151 | :END: | ||
| 152 | If you want to serve Nextcloud from HTTPS rather than plain HTTP, use | ||
| 153 | the following commands to issue Let's Encrypt SSL certificates: | ||
| 136 | 154 | ||
| 137 | ```sh | 155 | #+begin_src sh |
| 138 | sudo apt install snapd | 156 | sudo apt install snapd |
| 139 | sudo snap install core | 157 | sudo snap install core |
| 140 | sudo snap refresh core | 158 | sudo snap refresh core |
| 141 | sudo snap install --classic certbot | 159 | sudo snap install --classic certbot |
| 142 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | 160 | sudo ln -s /snap/bin/certbot /usr/bin/certbot |
| 143 | sudo certbot --apache | 161 | sudo certbot --apache |
| 144 | ``` | 162 | #+end_src |
| 145 | 163 | ||
| 146 | # Results | 164 | * Results |
| 147 | 165 | :PROPERTIES: | |
| 148 | Voilà! You're all done and should be able to access Nextcloud from your domain | 166 | :CUSTOM_ID: results |
| 149 | or IP address. | 167 | :END: |
| 168 | Voilà! You're all done and should be able to access Nextcloud from your | ||
| 169 | domain or IP address. | ||
content/blog/2022-03-24-server-hardening.md deleted −345
| @@ -1,345 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-03-24 | ||
| 3 | title = "Hardening a Public-Facing Home Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Post Updates | ||
| 9 | |||
| 10 | > After reviewing this post today (2022-10-04), I noticed quite a few gaps in my | ||
| 11 | > write-up and wanted to add a few things, even though this blog is really just | ||
| 12 | > a retrospective and knowledge dump for myself. I left things intact and simply | ||
| 13 | > crossed them out (~~like this~~) for posterity. | ||
| 14 | |||
| 15 | # Planning Data Flows & Security | ||
| 16 | |||
| 17 | ## My Personal Data Flow | ||
| 18 | |||
| 19 | ```txt | ||
| 20 | ┌───────┐ ┌─────────────────┐ | ||
| 21 | ┌──► VLAN1 ├───► Private Devices │ | ||
| 22 | │ └───────┘ └─────────────────┘ | ||
| 23 | ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ | ||
| 24 | │ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤ | ||
| 25 | └──────────┘ └────────┘ └──────────┘ └────────┘ │ | ||
| 26 | │ ┌───────┐ ┌───────────────┐ | ||
| 27 | └──► VLAN2 ├───► Public Server │ | ||
| 28 | └───────┘ └───────────────┘ | ||
| 29 | ``` | ||
| 30 | |||
| 31 | ## Thought Process | ||
| 32 | |||
| 33 | To serve content from your home server and harden your security posture, you | ||
| 34 | have to think about the transport of data from `server` to `client`. | ||
| 35 | |||
| 36 | Let's start with the actual server itself. Think about the following: | ||
| 37 | |||
| 38 | - Do I have a firewall enabled? Do I need to update this to allow new ports or | ||
| 39 | IPs? | ||
| 40 | - Do I have an IPS/IDS that may prevent outside traffic? | ||
| 41 | - Do I have any other security software installed? | ||
| 42 | - Are the services hosted inside Docker containers, behind a reverse proxy, or | ||
| 43 | virtualized? If so, are they configured to allow outside traffic? | ||
| 44 | |||
| 45 | Once the data leaves the server, where does it go? In my case, it goes to a | ||
| 46 | managed switch. In this case, I asked the following: | ||
| 47 | |||
| 48 | - What configurations is the switch using? | ||
| 49 | - Am I using VLANs? | ||
| 50 | - Yes, I am using 802.1Q VLANs. | ||
| 51 | - Are the VLANs configured properly? | ||
| 52 | - Yes, as shown in the Switch section below, I have a separate VLAN to | ||
| 53 | allow outside traffic to and from the server alone. No other devices, | ||
| 54 | except for a service port, and in that VLAN. | ||
| 55 | |||
| 56 | At this point, the data has been processed through the switch. Where does it go | ||
| 57 | next? In my case, it's pretty simple: it goes to the router/modem device. | ||
| 58 | |||
| 59 | - Does my ISP block any ports that I need? | ||
| 60 | - This is an important step that a lot of people run into when | ||
| 61 | self-hosting at home. Use an online port-checker tool for your IP or | ||
| 62 | call your ISP if you think ports are blocked. | ||
| 63 | - Is there a router firewall? | ||
| 64 | - Yes, I checked that it's configured to allow the ports I need to run my | ||
| 65 | services publicly. Common web servers and reverse proxies require ports | ||
| 66 | 80 and 443, but other services like media servers or games can require | ||
| 67 | unique ports, so be sure to check the documentation for your service(s). | ||
| 68 | - Are there any other settings affecting inbound/outbound traffic? | ||
| 69 | - Schedules or access blocks | ||
| 70 | - Static Routing | ||
| 71 | - QoS | ||
| 72 | - Port Forwarding | ||
| 73 | - DMZ Hosting | ||
| 74 | - Remote Management (this can sometimes mess with services that also | ||
| 75 | require the use of ports 80 and 443) | ||
| 76 | |||
| 77 | Once the data leaves my router, it goes to the upstream ISP and can be accessed | ||
| 78 | publicly. | ||
| 79 | |||
| 80 | ### Server | ||
| 81 | |||
| 82 | +The services I run on my server are installed straight into the OS, without any | ||
| 83 | use of Docker or VMs, so I don't need any extra application configuration to | ||
| 84 | make them accessible to the outside world.+ | ||
| 85 | |||
| 86 | As of 2022-10-04, the paragraph above is no longer true as I now run a reverse | ||
| 87 | proxy with Nginx and host many services inside Docker. However, it doesn't | ||
| 88 | change anything regarding this post as I still just need to open ports 80 & 443 | ||
| 89 | and create the necessary website configuration files. | ||
| 90 | |||
| 91 | When creating new services - either installed directly on bare metal or within | ||
| 92 | something like Docker - I ensure that I read through the documentation | ||
| 93 | thoroughly to understand a few key things: - What network activities should this | ||
| 94 | app perform (if any)? Using which ports and protocols? - Does this app require | ||
| 95 | any commands/services to be run as `root`? - Does this app log errors, | ||
| 96 | authentication failures/successes, or anything else that would be useful for an | ||
| 97 | investigation? | ||
| 98 | |||
| 99 | For extra security, I use limit all incoming connections to SSH connections | ||
| 100 | through my server firewall (`ufw`) and disable common SSH settings. After all of | ||
| 101 | that, I use `fail2ban` as a preventative measure against brute-force login | ||
| 102 | attempts. | ||
| 103 | |||
| 104 | As another piece of security, you can randomize your SSH port to ensure that | ||
| 105 | random scanners or attackers can't easily try to force their way into your | ||
| 106 | network. For example, you can edit the port rules in your server to block all | ||
| 107 | connection requests to port `22` but forward all remote connections from port | ||
| 108 | `12345` to your server's port `22`. Then you just need to SSH to your network | ||
| 109 | via your randomized port. | ||
| 110 | |||
| 111 | ## `ufw` | ||
| 112 | |||
| 113 | To see how to configure `ufw`, see my other post: [Secure Your Network with the | ||
| 114 | Uncomplicated | ||
| 115 | Firewall](../secure-your-network-with-the-uncomplicated-firewall/). | ||
| 116 | |||
| 117 | The general notion with an on-device firewall is that you want to deny all | ||
| 118 | incoming connections by default and then selectively open certain ports for | ||
| 119 | services or users that you know need access. | ||
| 120 | |||
| 121 | If you know that you will only be logging into this server from a certain set or | ||
| 122 | list of IPs, you can always set the firewall to only allow connections to port | ||
| 123 | 22 from those IPs. | ||
| 124 | |||
| 125 | For a quick start to only allow SSH connections to the server, use this: | ||
| 126 | |||
| 127 | ```sh | ||
| 128 | sudo ufw default deny incoming | ||
| 129 | sudo ufw default allow outgoing | ||
| 130 | sudo ufw allow 22 | ||
| 131 | sudo ufw enable | ||
| 132 | ``` | ||
| 133 | |||
| 134 | ## `ssh` | ||
| 135 | |||
| 136 | 1. Using SSH Keys | ||
| 137 | |||
| 138 | First, make sure you have an SSH keypair generated on the device(s) that | ||
| 139 | you'll be using to log in to the server. If you don't have an SSH key, run | ||
| 140 | this command: | ||
| 141 | |||
| 142 | ```sh | ||
| 143 | ssh-keygen | ||
| 144 | ``` | ||
| 145 | |||
| 146 | Now that we have an SSH key, copy it to the server with the following | ||
| 147 | command, which will ask for the user's password before accepting the key: | ||
| 148 | |||
| 149 | ```sh | ||
| 150 | ssh-copy-id my_user@my_server | ||
| 151 | ``` | ||
| 152 | |||
| 153 | If you have multiple keys, you'll need to specify which to use. After it's | ||
| 154 | complete, `ssh` back into the server as that user and make sure it doesn't | ||
| 155 | ask for a password. | ||
| 156 | |||
| 157 | 2. Disable Password & Root Authentication | ||
| 158 | |||
| 159 | Now that we can access the server without a password, we will disable | ||
| 160 | password authentication and disable anyone from using `ssh` to login as | ||
| 161 | `root`. | ||
| 162 | |||
| 163 | To do this, open the `sshd_config` file: | ||
| 164 | |||
| 165 | ```sh | ||
| 166 | sudo nano /etc/ssh/sshd_config | ||
| 167 | ``` | ||
| 168 | |||
| 169 | You'll need to update the parameters to the values below. If one of these | ||
| 170 | rules is commented-out or doesn't exist, create the rule at the bottom of | ||
| 171 | the file. | ||
| 172 | |||
| 173 | ```config | ||
| 174 | PermitRootLogin no | ||
| 175 | PasswordAuthentication no | ||
| 176 | PubkeyAuthentication yes | ||
| 177 | ``` | ||
| 178 | |||
| 179 | Finally, restart the `ssh` service: | ||
| 180 | |||
| 181 | ```sh | ||
| 182 | sudo systemctl restart sshd.service | ||
| 183 | ``` | ||
| 184 | |||
| 185 | To test that everything's working so far, open ANOTHER terminal and try | ||
| 186 | logging in as `root` over SSH. It is very important that you keep your | ||
| 187 | current SSH session open and test with an additional session, or you will | ||
| 188 | lock yourself out at some point and will need to use a recovery method | ||
| 189 | (e.g., hooking monitor up to home server) to get yourself back in. | ||
| 190 | |||
| 191 | 3. Enable MFA for `ssh` | ||
| 192 | |||
| 193 | This part is optional, but I highly recommend it. So far, we've ensured that | ||
| 194 | no one can log into our user on the server without using our secret key, and | ||
| 195 | we've ensured that no one can log in remotely as `root`. Next, you can | ||
| 196 | enable MFA authentication for `ssh` connections. | ||
| 197 | |||
| 198 | This process involves editing a couple files and installing an MFA package, | ||
| 199 | so I will not include all the details in this post. To see how to configure | ||
| 200 | MFA for `ssh`, see my other post: [Enabling MFA for | ||
| 201 | SSH](../enable-totp-mfa-for-ssh/). | ||
| 202 | |||
| 203 | ## `fail2ban` | ||
| 204 | |||
| 205 | I haven't written a post on how I use `fail2ban`, but it's quite simple. I use | ||
| 206 | the default `sshd` jail, but you can always create new jails for respective | ||
| 207 | applications or ports. For example, if you use Nginx as your web server, you can | ||
| 208 | use the `nginx-http-auth` jail. | ||
| 209 | |||
| 210 | In order to get it up and running, use the following commands: | ||
| 211 | |||
| 212 | ```sh | ||
| 213 | sudo apt install fail2ban | ||
| 214 | sudo fail2ban-client start sshd | ||
| 215 | sudo fail2ban-client status sshd | ||
| 216 | ``` | ||
| 217 | |||
| 218 | This should be used as a last-resort defense and shouldn't be a replacement for | ||
| 219 | the security measures mentioned above. | ||
| 220 | |||
| 221 | # Switch | ||
| 222 | |||
| 223 | Between the router and any local devices is my managed switch, which is used to | ||
| 224 | create VLANs. The example below shows how I would isolate the VLANs if I were | ||
| 225 | starting to host a single service at home. | ||
| 226 | |||
| 227 | ## 802.1Q VLAN Configuration | ||
| 228 | |||
| 229 | In this configuration, port 8 is the public server that needs to be accessed | ||
| 230 | from the outside. Port 23 is my 'dedicated service port' for this server. In | ||
| 231 | order to SSH to this server, I need to plug my laptop into port 23 or else I | ||
| 232 | cannot SSH. Otherwise, I'd need to hook up a monitor and keyboard directly to | ||
| 233 | the server to manage it. | ||
| 234 | |||
| 235 | | | ||
| 236 | |||
| 237 | | VLAN ID | VLAN Name | Member Ports | Tagged Ports | Untagged Ports | | ||
| 238 | | ------- | --------- | ------------ | ------------ | -------------- | | ||
| 239 | | 1 | Default | 1-24 | 1-24 | | | ||
| 240 | | 2 | Server | 1,8,23 | 1,8,23 | | | ||
| 241 | |||
| 242 | ## 802.1Q VLAN PVID Setting | ||
| 243 | |||
| 244 | Once the VLAN is created, I simply add the `VLAN ID` of `2` as the `PVID` for | ||
| 245 | any related ports (in this case, see that ports `8` and `23` have a PVID of | ||
| 246 | `2`). | ||
| 247 | |||
| 248 | | Port | PVID | | ||
| 249 | | ---- | ---- | | ||
| 250 | | 1 | 1 | | ||
| 251 | | 2 | 1 | | ||
| 252 | | 3 | 1 | | ||
| 253 | | 4 | 1 | | ||
| 254 | | 5 | 1 | | ||
| 255 | | 6 | 1 | | ||
| 256 | | 7 | 1 | | ||
| 257 | | 8 | 2 | | ||
| 258 | | 9 | 1 | | ||
| 259 | | 10 | 1 | | ||
| 260 | | 11 | 1 | | ||
| 261 | | 12 | 1 | | ||
| 262 | | 13 | 1 | | ||
| 263 | | 14 | 1 | | ||
| 264 | | 15 | 1 | | ||
| 265 | | 16 | 1 | | ||
| 266 | | 17 | 1 | | ||
| 267 | | 18 | 1 | | ||
| 268 | | 19 | 1 | | ||
| 269 | | 20 | 1 | | ||
| 270 | | 21 | 1 | | ||
| 271 | | 22 | 1 | | ||
| 272 | | 23 | 2 | | ||
| 273 | | 24 | 1 | | ||
| 274 | |||
| 275 | # Router | ||
| 276 | |||
| 277 | On my router, the configuration was as easy as opening the firewall settings and | ||
| 278 | unblocking the ports I needed for my services (e.g., HTTP/S, Plex, SSH, MySQL, | ||
| 279 | etc.). | ||
| 280 | |||
| 281 | _Since I'm relying on an ISP-provided modem/router combo for now (not by | ||
| 282 | choice), I do not use any other advanced settings on my router that would | ||
| 283 | inhibit any valid traffic to these services._ | ||
| 284 | |||
| 285 | The paragraph above regarding the ISP-owned router is no longer accurate as I | ||
| 286 | now use the Ubiquiti Unifi Dream Machine Pro as my router. Within this router, I | ||
| 287 | enabled port forwarding/firewall rules, segregate the network based on the | ||
| 288 | device, and enable traffic restrictions (e.g., silently drop traffic from | ||
| 289 | certain countries and threat categories). | ||
| 290 | |||
| 291 | If you have the option with your ISP, I recommend using a personal router with | ||
| 292 | software that you are familiar with so that you can explore all the options | ||
| 293 | available to you. | ||
| 294 | |||
| 295 | # Physical Security | ||
| 296 | |||
| 297 | One large piece of self-hosting that people generally don't discuss online is | ||
| 298 | physical security. However, physical security is very important for everyone who | ||
| 299 | hosts a server like this. Exactly _how_ important it is depends on the server | ||
| 300 | use/purpose. | ||
| 301 | |||
| 302 | If you self-host customer applications that hold protected data (HIPAA, GDPR, | ||
| 303 | COPPA, etc.), then physical security is extremely important and cannot be | ||
| 304 | ignored. If you simply host a blog and some hobby sites, then it's a relatively | ||
| 305 | minor consideration, but one you still need to think about. | ||
| 306 | |||
| 307 | ## Location | ||
| 308 | |||
| 309 | The first consideration is quite simple: location. | ||
| 310 | |||
| 311 | - Is the server within a property you own or housed on someone else's | ||
| 312 | property? | ||
| 313 | - Is it nearby (in your house, in your work office, in your neighbor's garage, | ||
| 314 | in a storage unit, etc.)? | ||
| 315 | - Do you have 24/7 access to the server? | ||
| 316 | - Are there climate considerations, such as humidity, fires, tornadoes, | ||
| 317 | monsoons? | ||
| 318 | - Do you have emergency equipment nearby in case of emergency? | ||
| 319 | |||
| 320 | ## Hardware Ownership | ||
| 321 | |||
| 322 | Secondly, consider the hardware itself: | ||
| 323 | |||
| 324 | - Do you own the server in its entirety? | ||
| 325 | - Are any other users able to access the server, even if your data/space is | ||
| 326 | segregated? | ||
| 327 | - If you're utilizing a third party, do they have any documentation to show | ||
| 328 | responsibility? This could be a SOC 1/2/3 report, ISO compliance report, | ||
| 329 | internal security/safety documentation. | ||
| 330 | |||
| 331 | ## Physical Controls | ||
| 332 | |||
| 333 | Regardless of who owns the hardware, ensure that there are adequate safeguards | ||
| 334 | in place, if necessary. These usually don't apply to small home servers and are | ||
| 335 | usually covered already if you're utilizing a third party. | ||
| 336 | |||
| 337 | These can include: | ||
| 338 | |||
| 339 | - Server bezel locks | ||
| 340 | - Server room locks - physical, digital, or biometric authentication | ||
| 341 | - Security cameras | ||
| 342 | - Raised floors/lowered ceilings with proper guards/gates in-place within the | ||
| 343 | floors or ceilings | ||
| 344 | - Security personnel | ||
| 345 | - Log sheets and/or guest badges | ||
content/blog/2022-03-24-server-hardening.org added +341
| @@ -0,0 +1,341 @@ | |||
| 1 | #+date: <2022-03-24> | ||
| 2 | #+title: Hardening a Public-Facing Home Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Post Updates | ||
| 7 | |||
| 8 | #+begin_quote | ||
| 9 | After reviewing this post today (2022-10-04), I noticed quite a few gaps in my | ||
| 10 | write-up and wanted to add a few things, even though this blog is really just a | ||
| 11 | retrospective and knowledge dump for myself. I left things intact and simply | ||
| 12 | crossed them out (+like this+) for posterity. | ||
| 13 | #+end_quote | ||
| 14 | |||
| 15 | * Planning Data Flows & Security | ||
| 16 | |||
| 17 | ** My Personal Data Flow | ||
| 18 | |||
| 19 | #+begin_src | ||
| 20 | ┌───────┐ ┌─────────────────┐ | ||
| 21 | ┌──► VLAN1 ├───► Private Devices │ | ||
| 22 | │ └───────┘ └─────────────────┘ | ||
| 23 | ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ | ||
| 24 | │ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤ | ||
| 25 | └──────────┘ └────────┘ └──────────┘ └────────┘ │ | ||
| 26 | │ ┌───────┐ ┌───────────────┐ | ||
| 27 | └──► VLAN2 ├───► Public Server │ | ||
| 28 | └───────┘ └───────────────┘ | ||
| 29 | #+end_src | ||
| 30 | |||
| 31 | ** Thought Process | ||
| 32 | |||
| 33 | To serve content from your home server and harden your security posture, you | ||
| 34 | have to think about the transport of data from =server= to =client=. | ||
| 35 | |||
| 36 | Let's start with the actual server itself. Think about the following: | ||
| 37 | |||
| 38 | - Do I have a firewall enabled? Do I need to update this to allow new ports or | ||
| 39 | IPs? | ||
| 40 | - Do I have an IPS/IDS that may prevent outside traffic? | ||
| 41 | - Do I have any other security software installed? | ||
| 42 | - Are the services hosted inside Docker containers, behind a reverse proxy, or | ||
| 43 | virtualized? If so, are they configured to allow outside traffic? | ||
| 44 | |||
| 45 | Once the data leaves the server, where does it go? In my case, it goes to a | ||
| 46 | managed switch. In this case, I asked the following: | ||
| 47 | |||
| 48 | - What configurations is the switch using? | ||
| 49 | - Am I using VLANs? | ||
| 50 | - Yes, I am using 802.1Q VLANs. | ||
| 51 | - Are the VLANs configured properly? | ||
| 52 | - Yes, as shown in the Switch section below, I have a separate VLAN to allow | ||
| 53 | outside traffic to and from the server alone. No other devices, except for a | ||
| 54 | service port, and in that VLAN. | ||
| 55 | |||
| 56 | At this point, the data has been processed through the switch. Where does it go | ||
| 57 | next? In my case, it's pretty simple: it goes to the router/modem device. | ||
| 58 | |||
| 59 | - Does my ISP block any ports that I need? | ||
| 60 | - This is an important step that a lot of people run into when self-hosting at | ||
| 61 | home. Use an online port-checker tool for your IP or call your ISP if you | ||
| 62 | think ports are blocked. | ||
| 63 | - Is there a router firewall? | ||
| 64 | - Yes, I checked that it's configured to allow the ports I need to run my | ||
| 65 | services publicly. Common web servers and reverse proxies require ports 80 | ||
| 66 | and 443, but other services like media servers or games can require unique | ||
| 67 | ports, so be sure to check the documentation for your service(s). | ||
| 68 | - Are there any other settings affecting inbound/outbound traffic? | ||
| 69 | - Schedules or access blocks | ||
| 70 | - Static Routing | ||
| 71 | - QoS | ||
| 72 | - Port Forwarding | ||
| 73 | - DMZ Hosting | ||
| 74 | - Remote Management (this can sometimes mess with services that also require | ||
| 75 | the use of ports 80 and 443) | ||
| 76 | |||
| 77 | Once the data leaves my router, it goes to the upstream ISP and can be accessed | ||
| 78 | publicly. | ||
| 79 | |||
| 80 | *** Server | ||
| 81 | |||
| 82 | The services I run on my server are installed straight into the OS, without any | ||
| 83 | use of Docker or VMs, so I don't need any extra application configuration to | ||
| 84 | make them accessible to the outside world.+ | ||
| 85 | |||
| 86 | As of 2022-10-04, the paragraph above is no longer true as I now run a reverse | ||
| 87 | proxy with Nginx and host many services inside Docker. However, it doesn't | ||
| 88 | change anything regarding this post as I still just need to open ports 80 & 443 | ||
| 89 | and create the necessary website configuration files. | ||
| 90 | |||
| 91 | When creating new services - either installed directly on bare metal or within | ||
| 92 | something like Docker - I ensure that I read through the documentation | ||
| 93 | thoroughly to understand a few key things: - What network activities should this | ||
| 94 | app perform (if any)? Using which ports and protocols? - Does this app require | ||
| 95 | any commands/services to be run as =root=? - Does this app log errors, | ||
| 96 | authentication failures/successes, or anything else that would be useful for an | ||
| 97 | investigation? | ||
| 98 | |||
| 99 | For extra security, I use limit all incoming connections to SSH connections | ||
| 100 | through my server firewall (=ufw=) and disable common SSH settings. After all of | ||
| 101 | that, I use =fail2ban= as a preventative measure against brute-force login | ||
| 102 | attempts. | ||
| 103 | |||
| 104 | As another piece of security, you can randomize your SSH port to ensure that | ||
| 105 | random scanners or attackers can't easily try to force their way into your | ||
| 106 | network. For example, you can edit the port rules in your server to block all | ||
| 107 | connection requests to port =22= but forward all remote connections from port | ||
| 108 | =12345= to your server's port =22=. Then you just need to SSH to your network | ||
| 109 | via your randomized port. | ||
| 110 | |||
| 111 | ** =ufw= | ||
| 112 | |||
| 113 | To see how to configure =ufw=, see my other post: [[/blog/ufw.html][Secure Your | ||
| 114 | Network with the Uncomplicated Firewall]]. | ||
| 115 | |||
| 116 | The general notion with an on-device firewall is that you want to deny all | ||
| 117 | incoming connections by default and then selectively open certain ports for | ||
| 118 | services or users that you know need access. | ||
| 119 | |||
| 120 | If you know that you will only be logging into this server from a certain set or | ||
| 121 | list of IPs, you can always set the firewall to only allow connections to port | ||
| 122 | 22 from those IPs. | ||
| 123 | |||
| 124 | For a quick start to only allow SSH connections to the server, use this: | ||
| 125 | |||
| 126 | #+begin_src sh | ||
| 127 | sudo ufw default deny incoming | ||
| 128 | sudo ufw default allow outgoing | ||
| 129 | sudo ufw allow 22 | ||
| 130 | sudo ufw enable | ||
| 131 | #+end_src | ||
| 132 | |||
| 133 | ** =ssh= | ||
| 134 | |||
| 135 | 1. Using SSH Keys | ||
| 136 | |||
| 137 | First, make sure you have an SSH keypair generated on the device(s) that | ||
| 138 | you'll be using to log in to the server. If you don't have an SSH key, run | ||
| 139 | this command: | ||
| 140 | |||
| 141 | #+begin_src sh | ||
| 142 | ssh-keygen | ||
| 143 | #+end_src | ||
| 144 | |||
| 145 | Now that we have an SSH key, copy it to the server with the following | ||
| 146 | command, which will ask for the user's password before accepting the key: | ||
| 147 | |||
| 148 | #+begin_src sh | ||
| 149 | ssh-copy-id my_user@my_server | ||
| 150 | #+end_src | ||
| 151 | |||
| 152 | If you have multiple keys, you'll need to specify which to use. After it's | ||
| 153 | complete, =ssh= back into the server as that user and make sure it doesn't | ||
| 154 | ask for a password. | ||
| 155 | |||
| 156 | 2. Disable Password & Root Authentication | ||
| 157 | |||
| 158 | Now that we can access the server without a password, we will disable | ||
| 159 | password authentication and disable anyone from using =ssh= to login as | ||
| 160 | =root=. | ||
| 161 | |||
| 162 | To do this, open the =sshd_config= file: | ||
| 163 | |||
| 164 | #+begin_src sh | ||
| 165 | sudo nano /etc/ssh/sshd_config | ||
| 166 | #+end_src | ||
| 167 | |||
| 168 | You'll need to update the parameters to the values below. If one of these | ||
| 169 | rules is commented-out or doesn't exist, create the rule at the bottom of the | ||
| 170 | file. | ||
| 171 | |||
| 172 | #+begin_src config | ||
| 173 | PermitRootLogin no | ||
| 174 | PasswordAuthentication no | ||
| 175 | PubkeyAuthentication yes | ||
| 176 | #+end_src | ||
| 177 | |||
| 178 | Finally, restart the =ssh= service: | ||
| 179 | |||
| 180 | #+begin_src sh | ||
| 181 | sudo systemctl restart sshd.service | ||
| 182 | #+end_src | ||
| 183 | |||
| 184 | To test that everything's working so far, open ANOTHER terminal and try | ||
| 185 | logging in as =root= over SSH. It is very important that you keep your | ||
| 186 | current SSH session open and test with an additional session, or you will | ||
| 187 | lock yourself out at some point and will need to use a recovery method (e.g., | ||
| 188 | hooking monitor up to home server) to get yourself back in. | ||
| 189 | |||
| 190 | 3. Enable MFA for =ssh= | ||
| 191 | |||
| 192 | This part is optional, but I highly recommend it. So far, we've ensured that | ||
| 193 | no one can log into our user on the server without using our secret key, and | ||
| 194 | we've ensured that no one can log in remotely as =root=. Next, you can enable | ||
| 195 | MFA authentication for =ssh= connections. | ||
| 196 | |||
| 197 | This process involves editing a couple files and installing an MFA package, | ||
| 198 | so I will not include all the details in this post. To see how to configure | ||
| 199 | MFA for =ssh=, see my other post: [[/blog/ssh-mfa.html][Enabling MFA for | ||
| 200 | SSH]]. | ||
| 201 | |||
| 202 | ** =fail2ban= | ||
| 203 | |||
| 204 | I haven't written a post on how I use =fail2ban=, but it's quite simple. I use | ||
| 205 | the default =sshd= jail, but you can always create new jails for respective | ||
| 206 | applications or ports. For example, if you use Nginx as your web server, you can | ||
| 207 | use the =nginx-http-auth= jail. | ||
| 208 | |||
| 209 | In order to get it up and running, use the following commands: | ||
| 210 | |||
| 211 | #+begin_src sh | ||
| 212 | sudo apt install fail2ban | ||
| 213 | sudo fail2ban-client start sshd | ||
| 214 | sudo fail2ban-client status sshd | ||
| 215 | #+end_src | ||
| 216 | |||
| 217 | This should be used as a last-resort defense and shouldn't be a replacement for | ||
| 218 | the security measures mentioned above. | ||
| 219 | |||
| 220 | * Switch | ||
| 221 | |||
| 222 | Between the router and any local devices is my managed switch, which is used to | ||
| 223 | create VLANs. The example below shows how I would isolate the VLANs if I were | ||
| 224 | starting to host a single service at home. | ||
| 225 | |||
| 226 | ** 802.1Q VLAN Configuration | ||
| 227 | |||
| 228 | In this configuration, port 8 is the public server that needs to be accessed | ||
| 229 | from the outside. Port 23 is my 'dedicated service port' for this server. In | ||
| 230 | order to SSH to this server, I need to plug my laptop into port 23 or else I | ||
| 231 | cannot SSH. Otherwise, I'd need to hook up a monitor and keyboard directly to | ||
| 232 | the server to manage it. | ||
| 233 | |||
| 234 | | VLAN ID | VLAN Name | Member Ports | Tagged Ports | Untagged Ports | | ||
| 235 | |---------+-----------+--------------+--------------+----------------| | ||
| 236 | | 1 | Default | 1-24 | 1-24 | | | ||
| 237 | | 2 | Server | 1,8,23 | 1,8,23 | | | ||
| 238 | |||
| 239 | ** 802.1Q VLAN PVID Setting | ||
| 240 | |||
| 241 | Once the VLAN is created, I simply add the =VLAN ID= of =2= as the =PVID= for | ||
| 242 | any related ports (in this case, see that ports =8= and =23= have a PVID of | ||
| 243 | =2=). | ||
| 244 | |||
| 245 | | Port | PVID | | ||
| 246 | |------+------| | ||
| 247 | | 1 | 1 | | ||
| 248 | | 2 | 1 | | ||
| 249 | | 3 | 1 | | ||
| 250 | | 4 | 1 | | ||
| 251 | | 5 | 1 | | ||
| 252 | | 6 | 1 | | ||
| 253 | | 7 | 1 | | ||
| 254 | | 8 | 2 | | ||
| 255 | | 9 | 1 | | ||
| 256 | | 10 | 1 | | ||
| 257 | | 11 | 1 | | ||
| 258 | | 12 | 1 | | ||
| 259 | | 13 | 1 | | ||
| 260 | | 14 | 1 | | ||
| 261 | | 15 | 1 | | ||
| 262 | | 16 | 1 | | ||
| 263 | | 17 | 1 | | ||
| 264 | | 18 | 1 | | ||
| 265 | | 19 | 1 | | ||
| 266 | | 20 | 1 | | ||
| 267 | | 21 | 1 | | ||
| 268 | | 22 | 1 | | ||
| 269 | | 23 | 2 | | ||
| 270 | | 24 | 1 | | ||
| 271 | |||
| 272 | * Router | ||
| 273 | |||
| 274 | On my router, the configuration was as easy as opening the firewall settings and | ||
| 275 | unblocking the ports I needed for my services (e.g., HTTP/S, Plex, SSH, MySQL, | ||
| 276 | etc.). | ||
| 277 | |||
| 278 | Since I'm relying on an ISP-provided modem/router combo for now (not by | ||
| 279 | choice), I do not use any other advanced settings on my router that would | ||
| 280 | inhibit any valid traffic to these services. | ||
| 281 | |||
| 282 | The paragraph above regarding the ISP-owned router is no longer accurate as I | ||
| 283 | now use the Ubiquiti Unifi Dream Machine Pro as my router. Within this router, I | ||
| 284 | enabled port forwarding/firewall rules, segregate the network based on the | ||
| 285 | device, and enable traffic restrictions (e.g., silently drop traffic from | ||
| 286 | certain countries and threat categories). | ||
| 287 | |||
| 288 | If you have the option with your ISP, I recommend using a personal router with | ||
| 289 | software that you are familiar with so that you can explore all the options | ||
| 290 | available to you. | ||
| 291 | |||
| 292 | * Physical Security | ||
| 293 | |||
| 294 | One large piece of self-hosting that people generally don't discuss online is | ||
| 295 | physical security. However, physical security is very important for everyone who | ||
| 296 | hosts a server like this. Exactly /how/ important it is depends on the server | ||
| 297 | use/purpose. | ||
| 298 | |||
| 299 | If you self-host customer applications that hold protected data (HIPAA, GDPR, | ||
| 300 | COPPA, etc.), then physical security is extremely important and cannot be | ||
| 301 | ignored. If you simply host a blog and some hobby sites, then it's a relatively | ||
| 302 | minor consideration, but one you still need to think about. | ||
| 303 | |||
| 304 | ** Location | ||
| 305 | |||
| 306 | The first consideration is quite simple: location. | ||
| 307 | |||
| 308 | - Is the server within a property you own or housed on someone else's property? | ||
| 309 | - Is it nearby (in your house, in your work office, in your neighbor's garage, | ||
| 310 | in a storage unit, etc.)? | ||
| 311 | - Do you have 24/7 access to the server? | ||
| 312 | - Are there climate considerations, such as humidity, fires, tornadoes, | ||
| 313 | monsoons? | ||
| 314 | - Do you have emergency equipment nearby in case of emergency? | ||
| 315 | |||
| 316 | ** Hardware Ownership | ||
| 317 | |||
| 318 | Secondly, consider the hardware itself: | ||
| 319 | |||
| 320 | - Do you own the server in its entirety? | ||
| 321 | - Are any other users able to access the server, even if your data/space is | ||
| 322 | segregated? | ||
| 323 | - If you're utilizing a third party, do they have any documentation to show | ||
| 324 | responsibility? This could be a SOC 1/2/3 report, ISO compliance report, | ||
| 325 | internal security/safety documentation. | ||
| 326 | |||
| 327 | ** Physical Controls | ||
| 328 | |||
| 329 | Regardless of who owns the hardware, ensure that there are adequate safeguards | ||
| 330 | in place, if necessary. These usually don't apply to small home servers and are | ||
| 331 | usually covered already if you're utilizing a third party. | ||
| 332 | |||
| 333 | These can include: | ||
| 334 | |||
| 335 | - Server bezel locks | ||
| 336 | - Server room locks - physical, digital, or biometric authentication | ||
| 337 | - Security cameras | ||
| 338 | - Raised floors/lowered ceilings with proper guards/gates in-place within the | ||
| 339 | floors or ceilings | ||
| 340 | - Security personnel | ||
| 341 | - Log sheets and/or guest badges | ||
content/blog/2022-03-26-ssh-mfa.md → content/blog/2022-03-26-ssh-mfa.org renamed +89 −89
| @@ -1,82 +1,82 @@ | |||
| 1 | +++ | 1 | #+date: <2022-03-26> |
| 2 | date = 2022-03-26 | 2 | #+title: Enable TOTP MFA for SSH |
| 3 | title = "Enable TOTP MFA for SSH" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Why Do I Need MFA for SSH? | ||
| 9 | 5 | ||
| 10 | If you are a sysadmin of a server anywhere (that includes at home!), you may | 6 | * Why Do I Need MFA for SSH? |
| 11 | want an added layer of protection against intruders. This is not a replacement | ||
| 12 | for other security measures, such as: | ||
| 13 | 7 | ||
| 14 | - Disable root SSH | 8 | If you are a sysadmin of a server anywhere (that includes at home!), you |
| 15 | - Disable SSH password authentication | 9 | may want an added layer of protection against intruders. This is not a |
| 16 | - Allow only certain users to login via SSH | 10 | replacement for other security measures, such as: |
| 17 | - Allow SSH only from certain IPs | ||
| 18 | 11 | ||
| 19 | However, MFA can be added as an additional security measure to ensure that your | 12 | - Disable root SSH |
| 20 | server is protected. This is especially important if you need to allow password | 13 | - Disable SSH password authentication |
| 21 | authentication for SSH. | 14 | - Allow only certain users to login via SSH |
| 15 | - Allow SSH only from certain IPs | ||
| 22 | 16 | ||
| 23 | For more guidance on server security measures, see my other post: [Hardening a | 17 | However, MFA can be added as an additional security measure to ensure |
| 24 | Public-Facing Home Server](../hardening-a-public-facing-home-server/). | 18 | that your server is protected. This is especially important if you need |
| 19 | to allow password authentication for SSH. | ||
| 25 | 20 | ||
| 26 | # Install MFA PAM Module | 21 | For more guidance on server security measures, see my other post: |
| 22 | [[../hardening-a-public-facing-home-server/][Hardening a Public-Facing | ||
| 23 | Home Server]]. | ||
| 27 | 24 | ||
| 28 | PAM, which stands for Pluggable Authentication Module, is an authentication | 25 | * Install MFA PAM Module |
| 29 | infrastructure used on Linux systems to authenticate a user. In order to use | ||
| 30 | this technology, let's install the `libpam-google-authenticator` package: | ||
| 31 | 26 | ||
| 32 | ```sh | 27 | PAM, which stands for Pluggable Authentication Module, is an |
| 28 | authentication infrastructure used on Linux systems to authenticate a | ||
| 29 | user. In order to use this technology, let's install the | ||
| 30 | =libpam-google-authenticator= package: | ||
| 31 | |||
| 32 | #+begin_src sh | ||
| 33 | sudo apt-get update | 33 | sudo apt-get update |
| 34 | ``` | 34 | #+end_src |
| 35 | 35 | ||
| 36 | ```sh | 36 | #+begin_src sh |
| 37 | sudo apt-get install libpam-google-authenticator | 37 | sudo apt-get install libpam-google-authenticator |
| 38 | ``` | 38 | #+end_src |
| 39 | 39 | ||
| 40 | # Initialize the PAM Module | 40 | * Initialize the PAM Module |
| 41 | 41 | ||
| 42 | ## Interactive Method | 42 | ** Interactive Method |
| 43 | 43 | ||
| 44 | Once the package is installed, initialize it and following the interactive | 44 | Once the package is installed, initialize it and following the |
| 45 | prompts to generate your OTP or TOTP: | 45 | interactive prompts to generate your OTP or TOTP: |
| 46 | 46 | ||
| 47 | ```sh | 47 | #+begin_src sh |
| 48 | google-authenticator | 48 | google-authenticator |
| 49 | ``` | 49 | #+end_src |
| 50 | 50 | ||
| 51 | If you are not sure how to answer, read the prompts carefully and think about | 51 | If you are not sure how to answer, read the prompts carefully and think |
| 52 | having to how each situation would affect your normal login attempts. If you are | 52 | about having to how each situation would affect your normal login |
| 53 | still not sure, use my default responses below. | 53 | attempts. If you are still not sure, use my default responses below. |
| 54 | 54 | ||
| 55 | ```txt | 55 | #+begin_src txt |
| 56 | OUTPUT | 56 | OUTPUT |
| 57 | 57 | ||
| 58 | Do you want authentication tokens to be time-based (y/n) y | 58 | Do you want authentication tokens to be time-based (y/n) y |
| 59 | ``` | 59 | #+end_src |
| 60 | 60 | ||
| 61 | At this point, use an authenticator app somewhere one of your devices to scan | 61 | At this point, use an authenticator app somewhere one of your devices to |
| 62 | the QR code. Any future login attempts after our upcoming configuration changes | 62 | scan the QR code. Any future login attempts after our upcoming |
| 63 | will require that TOTP. | 63 | configuration changes will require that TOTP. |
| 64 | 64 | ||
| 65 | ```txt | 65 | #+begin_src txt |
| 66 | OUTPUT | 66 | OUTPUT |
| 67 | 67 | ||
| 68 | Do you want me to update your "/home/user/.google_authenticator" file? (y/n) y | 68 | Do you want me to update your "/home/user/.google_authenticator" file? (y/n) y |
| 69 | ``` | 69 | #+end_src |
| 70 | 70 | ||
| 71 | ```txt | 71 | #+begin_src txt |
| 72 | OUTPUT | 72 | OUTPUT |
| 73 | 73 | ||
| 74 | Do you want to disallow multiple uses of the same authentication | 74 | Do you want to disallow multiple uses of the same authentication |
| 75 | token? This restricts you to one login about every 30s, but it increases | 75 | token? This restricts you to one login about every 30s, but it increases |
| 76 | your chances to notice or even prevent man-in-the-middle attacks (y/n) y | 76 | your chances to notice or even prevent man-in-the-middle attacks (y/n) y |
| 77 | ``` | 77 | #+end_src |
| 78 | 78 | ||
| 79 | ```txt | 79 | #+begin_src txt |
| 80 | OUTPUT | 80 | OUTPUT |
| 81 | 81 | ||
| 82 | By default, a new token is generated every 30 seconds by the mobile app. | 82 | By default, a new token is generated every 30 seconds by the mobile app. |
| @@ -89,30 +89,30 @@ code, the next code) to 17 permitted codes (the 8 previous codes, the current | |||
| 89 | code, and the 8 next codes). This will permit for a time skew of up to 4 minutes | 89 | code, and the 8 next codes). This will permit for a time skew of up to 4 minutes |
| 90 | between client and server. | 90 | between client and server. |
| 91 | Do you want to do so? (y/n) n | 91 | Do you want to do so? (y/n) n |
| 92 | ``` | 92 | #+end_src |
| 93 | 93 | ||
| 94 | ```txt | 94 | #+begin_src txt |
| 95 | OUTPUT | 95 | OUTPUT |
| 96 | 96 | ||
| 97 | If the computer that you are logging into isn't hardened against brute-force | 97 | If the computer that you are logging into isn't hardened against brute-force |
| 98 | login attempts, you can enable rate-limiting for the authentication module. | 98 | login attempts, you can enable rate-limiting for the authentication module. |
| 99 | By default, this limits attackers to no more than 3 login attempts every 30s. | 99 | By default, this limits attackers to no more than 3 login attempts every 30s. |
| 100 | Do you want to enable rate-limiting? (y/n) y | 100 | Do you want to enable rate-limiting? (y/n) y |
| 101 | ``` | 101 | #+end_src |
| 102 | 102 | ||
| 103 | ## Non-Interactive Method | 103 | ** Non-Interactive Method |
| 104 | 104 | ||
| 105 | If you need to do this quickly, know your responses to the prompts, or are | 105 | If you need to do this quickly, know your responses to the prompts, or |
| 106 | setting this up for numerous users, the non-interactive method can be much | 106 | are setting this up for numerous users, the non-interactive method can |
| 107 | faster: | 107 | be much faster: |
| 108 | 108 | ||
| 109 | ```sh | 109 | #+begin_src sh |
| 110 | google-authenticator -t -d -f -r 3 -R 30 -w 3 | 110 | google-authenticator -t -d -f -r 3 -R 30 -w 3 |
| 111 | ``` | 111 | #+end_src |
| 112 | 112 | ||
| 113 | The options referenced above are as follows: | 113 | The options referenced above are as follows: |
| 114 | 114 | ||
| 115 | ```txt | 115 | #+begin_src txt |
| 116 | google-authenticator [<options>] | 116 | google-authenticator [<options>] |
| 117 | -h, --help Print this message | 117 | -h, --help Print this message |
| 118 | -c, --counter-based Set up counter-based (HOTP) verification | 118 | -c, --counter-based Set up counter-based (HOTP) verification |
| @@ -132,65 +132,65 @@ google-authenticator [<options>] | |||
| 132 | -w, --window-size=W Set window of concurrently valid codes | 132 | -w, --window-size=W Set window of concurrently valid codes |
| 133 | -W, --minimal-window Disable window of concurrently valid codes | 133 | -W, --minimal-window Disable window of concurrently valid codes |
| 134 | -e, --emergency-codes=N Number of emergency codes to generate | 134 | -e, --emergency-codes=N Number of emergency codes to generate |
| 135 | ``` | 135 | #+end_src |
| 136 | 136 | ||
| 137 | This fully configures the authenticator, saves it to a file, and then outputs | 137 | This fully configures the authenticator, saves it to a file, and then |
| 138 | the secret key, QR code, and recovery codes. (If you add the flag `-q`, then | 138 | outputs the secret key, QR code, and recovery codes. (If you add the |
| 139 | there won't be any output). If you use this command in an automated fashion, | 139 | flag =-q=, then there won't be any output). If you use this command in |
| 140 | make sure your script captures the secret key and/or recovery codes and makes | 140 | an automated fashion, make sure your script captures the secret key |
| 141 | them available to the user. | 141 | and/or recovery codes and makes them available to the user. |
| 142 | 142 | ||
| 143 | # PAM Configuration Settings | 143 | * PAM Configuration Settings |
| 144 | 144 | ||
| 145 | Once you've enabled the T/OTP and have it saved to an MFA app on your phone or | 145 | Once you've enabled the T/OTP and have it saved to an MFA app on your |
| 146 | other device, open the PAM `sshd` file: | 146 | phone or other device, open the PAM =sshd= file: |
| 147 | 147 | ||
| 148 | ```sh | 148 | #+begin_src sh |
| 149 | sudo nano /etc/pam.d/sshd | 149 | sudo nano /etc/pam.d/sshd |
| 150 | ``` | 150 | #+end_src |
| 151 | 151 | ||
| 152 | You need to do two things in this file. First, add the following lines to the | 152 | You need to do two things in this file. First, add the following lines |
| 153 | bottom of the file: | 153 | to the bottom of the file: |
| 154 | 154 | ||
| 155 | ```config | 155 | #+begin_src config |
| 156 | auth required pam_google_authenticator.so nullok | 156 | auth required pam_google_authenticator.so nullok |
| 157 | auth required pam_permit.so | 157 | auth required pam_permit.so |
| 158 | ``` | 158 | #+end_src |
| 159 | 159 | ||
| 160 | Second, comment-out the following line near the top of the file. | 160 | Second, comment-out the following line near the top of the file. |
| 161 | 161 | ||
| 162 | If you leave this line uncommented, every SSH login attempt will ask for the | 162 | If you leave this line uncommented, every SSH login attempt will ask for |
| 163 | following three authentication factors: | 163 | the following three authentication factors: |
| 164 | 164 | ||
| 165 | 1. Publickey | 165 | 1. Publickey |
| 166 | 2. Password | 166 | 2. Password |
| 167 | 3. T/OTP code | 167 | 3. T/OTP code |
| 168 | 168 | ||
| 169 | ```config | 169 | #+begin_src config |
| 170 | #@include common-auth | 170 | #@include common-auth |
| 171 | ``` | 171 | #+end_src |
| 172 | 172 | ||
| 173 | # SSH Configuration Settings | 173 | * SSH Configuration Settings |
| 174 | 174 | ||
| 175 | Finally, edit the `sshd_config` file again: | 175 | Finally, edit the =sshd_config= file again: |
| 176 | 176 | ||
| 177 | ```sh | 177 | #+begin_src sh |
| 178 | sudo nano /etc/ssh/sshd_config | 178 | sudo nano /etc/ssh/sshd_config |
| 179 | ``` | 179 | #+end_src |
| 180 | 180 | ||
| 181 | You'll need to change `ChallengeResponseAuthentication` to yes and add the | 181 | You'll need to change =ChallengeResponseAuthentication= to yes and add |
| 182 | `AuthenticationMethods` line to the bottom of the file. | 182 | the =AuthenticationMethods= line to the bottom of the file. |
| 183 | 183 | ||
| 184 | ```config | 184 | #+begin_src config |
| 185 | ChallengeResponseAuthentication yes | 185 | ChallengeResponseAuthentication yes |
| 186 | AuthenticationMethods publickey,password publickey,keyboard-interactive | 186 | AuthenticationMethods publickey,password publickey,keyboard-interactive |
| 187 | ``` | 187 | #+end_src |
| 188 | 188 | ||
| 189 | Finally, restart the `ssh` service: | 189 | Finally, restart the =ssh= service: |
| 190 | 190 | ||
| 191 | ```sh | 191 | #+begin_src sh |
| 192 | sudo systemctl restart sshd.service | 192 | sudo systemctl restart sshd.service |
| 193 | ``` | 193 | #+end_src |
| 194 | 194 | ||
| 195 | The next time you log in, you should be greeted with a verification code | 195 | The next time you log in, you should be greeted with a verification code |
| 196 | request! | 196 | request! |
content/blog/2022-04-02-nginx-reverse-proxy.md deleted −226
| @@ -1,226 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-04-02 | ||
| 3 | title = "Set-Up a Reverse Proxy with Nginx" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is a Reverse Proxy? | ||
| 9 | |||
| 10 | A reverse proxy is a server that is placed between local servers or services and | ||
| 11 | clients/users (e.g., the internet). The reverse proxy intercepts all requests | ||
| 12 | from clients at the network edge and uses its configuration files to determine | ||
| 13 | where each request should be sent. | ||
| 14 | |||
| 15 | ## A Brief Example | ||
| 16 | |||
| 17 | For example, let's say that I run three servers in my home: | ||
| 18 | |||
| 19 | - Server01 (`example.com`) | ||
| 20 | - Server02 (`service01.example.com`) | ||
| 21 | - Server03 (`service02.example.com`) | ||
| 22 | |||
| 23 | I also run a reverse proxy in my home that intercepts all public traffic: | ||
| 24 | |||
| 25 | - Reverse Proxy | ||
| 26 | |||
| 27 | Assume that I have a domain name (`example.com`) that allows clients to request | ||
| 28 | websites or services from my home servers. | ||
| 29 | |||
| 30 | In this case, the reverse proxy will intercept all traffic from `example.com` | ||
| 31 | that enters my network and determine if the client is requesting valid data, | ||
| 32 | based on my configuration. | ||
| 33 | |||
| 34 | If the user is requesting `example.com` and my configuration files say that | ||
| 35 | Server~01~ holds that data, Nginx will send the user to Server~01~. If I were to | ||
| 36 | change the configuration so that `example.com` is routed to Server~02~, that | ||
| 37 | same user would be sent to Server~02~ instead. | ||
| 38 | |||
| 39 | ```txt | ||
| 40 | ┌──────┐ ┌───────────┐ | ||
| 41 | │ User │─┐ ┌──► Server_01 │ | ||
| 42 | └──────┘ │ │ └───────────┘ | ||
| 43 | │ ┌──────────┐ ┌───────────────┐ │ ┌───────────┐ | ||
| 44 | ├────► Internet ├───► Reverse Proxy ├─────├──► Server_02 │ | ||
| 45 | │ └──────────┘ └───────────────┘ │ └───────────┘ | ||
| 46 | ┌──────┐ │ │ ┌───────────┐ | ||
| 47 | │ User │─┘ └──► Server_03 │ | ||
| 48 | └──────┘ └───────────┘ | ||
| 49 | ``` | ||
| 50 | |||
| 51 | # Reverse Proxy Options | ||
| 52 | |||
| 53 | There are a lot of options when it comes to reverse proxy servers, so I'm just | ||
| 54 | going to list a few of the options I've heard recommended over the last few | ||
| 55 | years: | ||
| 56 | |||
| 57 | - [Nginx](https://nginx.com) | ||
| 58 | - [Caddy](https://caddyserver.com) | ||
| 59 | - [Traefik](https://traefik.io/) | ||
| 60 | - [HAProxy](https://www.haproxy.org/) | ||
| 61 | - [Squid](https://ubuntu.com/server/docs/proxy-servers-squid) | ||
| 62 | |||
| 63 | In this post, we will be using Nginx as our reverse proxy, running on Ubuntu | ||
| 64 | Server 20.04.4 LTS. | ||
| 65 | |||
| 66 | # Nginx Reverse Proxy Example | ||
| 67 | |||
| 68 | ## Local Applications | ||
| 69 | |||
| 70 | You may be like me and have a lot of applications running on your local network | ||
| 71 | that you'd like to expose publicly with a domain. | ||
| 72 | |||
| 73 | In my case, I have services running in multiple Docker containers within a | ||
| 74 | single server and want a way to visit those services from anywhere with a URL. | ||
| 75 | For example, on my local network, [Dashy](https://dashy.to) runs through port | ||
| 76 | 4000 (`localhost:4000`) and [Uptime | ||
| 77 | Kuma](https://github.com/louislam/uptime-kuma) runs through port 3001 | ||
| 78 | (`localhost:3001`). | ||
| 79 | |||
| 80 | In order to expose these services to the public, I will need to do the | ||
| 81 | following: | ||
| 82 | |||
| 83 | 1. Set up DNS records for a domain or subdomain (one per service) to point | ||
| 84 | toward the IP address of the server. | ||
| 85 | 2. Open up the server network's HTTP and HTTPS ports (80 & 443) so that the | ||
| 86 | reverse proxy can accept traffic and determine where to send it. | ||
| 87 | 3. Install the reverse proxy software. | ||
| 88 | 4. Configure the reverse proxy to recognize which service should get traffic | ||
| 89 | from any of the domains or subdomains. | ||
| 90 | |||
| 91 | ## Step 1: DNS Configuration | ||
| 92 | |||
| 93 | To start, update your DNS configuration so that you have an `A` record for each | ||
| 94 | domain or subdomain. | ||
| 95 | |||
| 96 | The `A` records should point toward the public IP address of the server. If you | ||
| 97 | don't know the public IP address, log in to the server and run the following | ||
| 98 | command: | ||
| 99 | |||
| 100 | ```sh | ||
| 101 | curl ifconfig.co | ||
| 102 | ``` | ||
| 103 | |||
| 104 | In the DNS example below, `xxx.xxx.xxx.xxx` is the public IP address of the | ||
| 105 | server. | ||
| 106 | |||
| 107 | ```conf | ||
| 108 | example.com A xxx.xxx.xxx.xxx | ||
| 109 | uptime.example.com A xxx.xxx.xxx.xxx | ||
| 110 | dashy.example.com A xxx.xxx.xxx.xxx | ||
| 111 | www CNAME example.com | ||
| 112 | ``` | ||
| 113 | |||
| 114 | Finally, ensure the DNS has propagated correctly with [DNS | ||
| 115 | Checker](https://dnschecker.org) by entering your domains or subdomains in the | ||
| 116 | search box and ensuring the results are showing the correct IP address. | ||
| 117 | |||
| 118 | ## Step 2: Open Network Ports | ||
| 119 | |||
| 120 | This step will be different depending on which router you have in your home. If | ||
| 121 | you're not sure, try to visit [192.168.1.1](http://192.168.1.1) in your browser. | ||
| 122 | Login credentials are usually written on a sticker somewhere on your | ||
| 123 | modem/router. | ||
| 124 | |||
| 125 | Once you're able to log in to your router, find the Port Forwarding settings. | ||
| 126 | You will need to forward ports `80` and `443` to whichever machine is running | ||
| 127 | the reverse proxy. | ||
| 128 | |||
| 129 | In my case, the table below shows the port-forwarding rules I've created. In | ||
| 130 | this table, `xxx.xxx.xxx.xxx` is the local device IP of the reverse proxy | ||
| 131 | server, it will probably be an IP between `192.168.1.1` and `192.168.1.255`. | ||
| 132 | |||
| 133 | | NAME | FROM PORT | DEST PORT/IP | ENABLED | | ||
| 134 | | ----- | --------- | --------------- | ------- | | ||
| 135 | | HTTP | 80 | xxx.xxx.xxx.xxx | TRUE | | ||
| 136 | | HTTPS | 443 | xxx.xxx.xxx.xxx | TRUE | | ||
| 137 | |||
| 138 | Once configured, these rules will direct all web traffic to your reverse proxy. | ||
| 139 | |||
| 140 | ## Step 3: Nginx Installation | ||
| 141 | |||
| 142 | To install Nginx, simply run the following command: | ||
| 143 | |||
| 144 | ```sh | ||
| 145 | sudo apt install nginx | ||
| 146 | ``` | ||
| 147 | |||
| 148 | If you have a firewall enabled, open up ports `80` and `443` on your server so | ||
| 149 | that Nginx can accept web traffic from the router. | ||
| 150 | |||
| 151 | For example, if you want to use `ufw` for web traffic and SSH, run the following | ||
| 152 | commands: | ||
| 153 | |||
| 154 | ```sh | ||
| 155 | sudo ufw allow 'Nginx Full' | ||
| 156 | sudo ufw allow SSH | ||
| 157 | sudo ufw enable | ||
| 158 | ``` | ||
| 159 | |||
| 160 | ## Step 4: Nginx Configuration | ||
| 161 | |||
| 162 | Now that we have domains pointing toward the server, the only step left is to | ||
| 163 | configure the reverse proxy to direct traffic from domains to local services. | ||
| 164 | |||
| 165 | To start, you'll need to create a configuration file for each domain in | ||
| 166 | `/etc/nginx/sites-available/`. They will look identical except for the | ||
| 167 | `server_name` variable and the `proxy_pass` port. | ||
| 168 | |||
| 169 | Dashy: | ||
| 170 | |||
| 171 | ```sh | ||
| 172 | nano /etc/nginx/sites-available/dashy.example.com | ||
| 173 | ``` | ||
| 174 | |||
| 175 | ```config | ||
| 176 | server { | ||
| 177 | listen 80; | ||
| 178 | server_name dashy.example.com; | ||
| 179 | |||
| 180 | location / { | ||
| 181 | proxy_pass http://localhost:4000; | ||
| 182 | } | ||
| 183 | } | ||
| 184 | ``` | ||
| 185 | |||
| 186 | Uptime: | ||
| 187 | |||
| 188 | ```sh | ||
| 189 | nano /etc/nginx/sites-available/uptime.example.com | ||
| 190 | ``` | ||
| 191 | |||
| 192 | ```config | ||
| 193 | server { | ||
| 194 | listen 80; | ||
| 195 | server_name uptime.example.com; | ||
| 196 | |||
| 197 | location / { | ||
| 198 | proxy_pass http://localhost:3001; | ||
| 199 | } | ||
| 200 | } | ||
| 201 | ``` | ||
| 202 | |||
| 203 | Once the configuration files are created, you will need to enable them with the | ||
| 204 | `symlink` command: | ||
| 205 | |||
| 206 | ```sh | ||
| 207 | sudo ln -s /etc/nginx/sites-available/dashy.example.com /etc/nginx/sites-enabled/ | ||
| 208 | ``` | ||
| 209 | |||
| 210 | Voilà! Your local services should now be available through their URLs. | ||
| 211 | |||
| 212 | # HTTPS with Certbot | ||
| 213 | |||
| 214 | If you've followed along, you'll notice that your services are only available | ||
| 215 | via HTTP (not HTTPS). | ||
| 216 | |||
| 217 | If you want to enable HTTPS for your new domains, you will need to generate | ||
| 218 | SSL/TLS certificates for them. The easiest way to generate certificates on Nginx | ||
| 219 | is [Certbot](https://certbot.eff.org): | ||
| 220 | |||
| 221 | ```sh | ||
| 222 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | ||
| 223 | sudo snap install --classic certbot | ||
| 224 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 225 | sudo certbot --nginx | ||
| 226 | ``` | ||
content/blog/2022-04-02-nginx-reverse-proxy.org added +230
| @@ -0,0 +1,230 @@ | |||
| 1 | #+date: <2022-04-02> | ||
| 2 | #+title: Set-Up a Reverse Proxy with Nginx | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is a Reverse Proxy? | ||
| 7 | |||
| 8 | A reverse proxy is a server that is placed between local servers or | ||
| 9 | services and clients/users (e.g., the internet). The reverse proxy | ||
| 10 | intercepts all requests from clients at the network edge and uses its | ||
| 11 | configuration files to determine where each request should be sent. | ||
| 12 | |||
| 13 | ** A Brief Example | ||
| 14 | |||
| 15 | For example, let's say that I run three servers in my home: | ||
| 16 | |||
| 17 | - Server01 (=example.com=) | ||
| 18 | - Server02 (=service01.example.com=) | ||
| 19 | - Server03 (=service02.example.com=) | ||
| 20 | |||
| 21 | I also run a reverse proxy in my home that intercepts all public | ||
| 22 | traffic: | ||
| 23 | |||
| 24 | - Reverse Proxy | ||
| 25 | |||
| 26 | Assume that I have a domain name (=example.com=) that allows clients to | ||
| 27 | request websites or services from my home servers. | ||
| 28 | |||
| 29 | In this case, the reverse proxy will intercept all traffic from | ||
| 30 | =example.com= that enters my network and determine if the client is | ||
| 31 | requesting valid data, based on my configuration. | ||
| 32 | |||
| 33 | If the user is requesting =example.com= and my configuration files say | ||
| 34 | that Server_{01} holds that data, Nginx will send the user to | ||
| 35 | Server_{01}. If I were to change the configuration so that =example.com= | ||
| 36 | is routed to Server_{02}, that same user would be sent to Server_{02} | ||
| 37 | instead. | ||
| 38 | |||
| 39 | #+begin_src txt | ||
| 40 | ┌──────┐ ┌───────────┐ | ||
| 41 | │ User │─┐ ┌──► Server_01 │ | ||
| 42 | └──────┘ │ │ └───────────┘ | ||
| 43 | │ ┌──────────┐ ┌───────────────┐ │ ┌───────────┐ | ||
| 44 | ├────► Internet ├───► Reverse Proxy ├─────├──► Server_02 │ | ||
| 45 | │ └──────────┘ └───────────────┘ │ └───────────┘ | ||
| 46 | ┌──────┐ │ │ ┌───────────┐ | ||
| 47 | │ User │─┘ └──► Server_03 │ | ||
| 48 | └──────┘ └───────────┘ | ||
| 49 | #+end_src | ||
| 50 | |||
| 51 | * Reverse Proxy Options | ||
| 52 | |||
| 53 | There are a lot of options when it comes to reverse proxy servers, so | ||
| 54 | I'm just going to list a few of the options I've heard recommended over | ||
| 55 | the last few years: | ||
| 56 | |||
| 57 | - [[https://nginx.com][Nginx]] | ||
| 58 | - [[https://caddyserver.com][Caddy]] | ||
| 59 | - [[https://traefik.io/][Traefik]] | ||
| 60 | - [[https://www.haproxy.org/][HAProxy]] | ||
| 61 | - [[https://ubuntu.com/server/docs/proxy-servers-squid][Squid]] | ||
| 62 | |||
| 63 | In this post, we will be using Nginx as our reverse proxy, running on | ||
| 64 | Ubuntu Server 20.04.4 LTS. | ||
| 65 | |||
| 66 | * Nginx Reverse Proxy Example | ||
| 67 | |||
| 68 | ** Local Applications | ||
| 69 | |||
| 70 | You may be like me and have a lot of applications running on your local | ||
| 71 | network that you'd like to expose publicly with a domain. | ||
| 72 | |||
| 73 | In my case, I have services running in multiple Docker containers within | ||
| 74 | a single server and want a way to visit those services from anywhere | ||
| 75 | with a URL. For example, on my local network, | ||
| 76 | [[https://dashy.to][Dashy]] runs through port 4000 (=localhost:4000=) | ||
| 77 | and [[https://github.com/louislam/uptime-kuma][Uptime Kuma]] runs | ||
| 78 | through port 3001 (=localhost:3001=). | ||
| 79 | |||
| 80 | In order to expose these services to the public, I will need to do the | ||
| 81 | following: | ||
| 82 | |||
| 83 | 1. Set up DNS records for a domain or subdomain (one per service) to | ||
| 84 | point toward the IP address of the server. | ||
| 85 | 2. Open up the server network's HTTP and HTTPS ports (80 & 443) so that | ||
| 86 | the reverse proxy can accept traffic and determine where to send it. | ||
| 87 | 3. Install the reverse proxy software. | ||
| 88 | 4. Configure the reverse proxy to recognize which service should get | ||
| 89 | traffic from any of the domains or subdomains. | ||
| 90 | |||
| 91 | ** Step 1: DNS Configuration | ||
| 92 | |||
| 93 | To start, update your DNS configuration so that you have an =A= record | ||
| 94 | for each domain or subdomain. | ||
| 95 | |||
| 96 | The =A= records should point toward the public IP address of the server. | ||
| 97 | If you don't know the public IP address, log in to the server and run | ||
| 98 | the following command: | ||
| 99 | |||
| 100 | #+begin_src sh | ||
| 101 | curl ifconfig.co | ||
| 102 | #+end_src | ||
| 103 | |||
| 104 | In the DNS example below, =xxx.xxx.xxx.xxx= is the public IP address of | ||
| 105 | the server. | ||
| 106 | |||
| 107 | #+begin_src conf | ||
| 108 | example.com A xxx.xxx.xxx.xxx | ||
| 109 | uptime.example.com A xxx.xxx.xxx.xxx | ||
| 110 | dashy.example.com A xxx.xxx.xxx.xxx | ||
| 111 | www CNAME example.com | ||
| 112 | #+end_src | ||
| 113 | |||
| 114 | Finally, ensure the DNS has propagated correctly with | ||
| 115 | [[https://dnschecker.org][DNS Checker]] by entering your domains or | ||
| 116 | subdomains in the search box and ensuring the results are showing the | ||
| 117 | correct IP address. | ||
| 118 | |||
| 119 | ** Step 2: Open Network Ports | ||
| 120 | |||
| 121 | This step will be different depending on which router you have in your | ||
| 122 | home. If you're not sure, try to visit | ||
| 123 | [[http://192.168.1.1][192.168.1.1]] in your browser. Login credentials | ||
| 124 | are usually written on a sticker somewhere on your modem/router. | ||
| 125 | |||
| 126 | Once you're able to log in to your router, find the Port Forwarding | ||
| 127 | settings. You will need to forward ports =80= and =443= to whichever | ||
| 128 | machine is running the reverse proxy. | ||
| 129 | |||
| 130 | In my case, the table below shows the port-forwarding rules I've | ||
| 131 | created. In this table, =xxx.xxx.xxx.xxx= is the local device IP of the | ||
| 132 | reverse proxy server, it will probably be an IP between =192.168.1.1= | ||
| 133 | and =192.168.1.255=. | ||
| 134 | |||
| 135 | | NAME | FROM PORT | DEST PORT/IP | ENABLED | | ||
| 136 | |-------+-----------+-----------------+---------| | ||
| 137 | | HTTP | 80 | xxx.xxx.xxx.xxx | TRUE | | ||
| 138 | | HTTPS | 443 | xxx.xxx.xxx.xxx | TRUE | | ||
| 139 | |||
| 140 | Once configured, these rules will direct all web traffic to your reverse | ||
| 141 | proxy. | ||
| 142 | |||
| 143 | ** Step 3: Nginx Installation | ||
| 144 | |||
| 145 | To install Nginx, simply run the following command: | ||
| 146 | |||
| 147 | #+begin_src sh | ||
| 148 | sudo apt install nginx | ||
| 149 | #+end_src | ||
| 150 | |||
| 151 | If you have a firewall enabled, open up ports =80= and =443= on your | ||
| 152 | server so that Nginx can accept web traffic from the router. | ||
| 153 | |||
| 154 | For example, if you want to use =ufw= for web traffic and SSH, run the | ||
| 155 | following commands: | ||
| 156 | |||
| 157 | #+begin_src sh | ||
| 158 | sudo ufw allow 'Nginx Full' | ||
| 159 | sudo ufw allow SSH | ||
| 160 | sudo ufw enable | ||
| 161 | #+end_src | ||
| 162 | |||
| 163 | ** Step 4: Nginx Configuration | ||
| 164 | |||
| 165 | Now that we have domains pointing toward the server, the only step left | ||
| 166 | is to configure the reverse proxy to direct traffic from domains to | ||
| 167 | local services. | ||
| 168 | |||
| 169 | To start, you'll need to create a configuration file for each domain in | ||
| 170 | =/etc/nginx/sites-available/=. They will look identical except for the | ||
| 171 | =server_name= variable and the =proxy_pass= port. | ||
| 172 | |||
| 173 | Dashy: | ||
| 174 | |||
| 175 | #+begin_src sh | ||
| 176 | nano /etc/nginx/sites-available/dashy.example.com | ||
| 177 | #+end_src | ||
| 178 | |||
| 179 | #+begin_src config | ||
| 180 | server { | ||
| 181 | listen 80; | ||
| 182 | server_name dashy.example.com; | ||
| 183 | |||
| 184 | location / { | ||
| 185 | proxy_pass http://localhost:4000; | ||
| 186 | } | ||
| 187 | } | ||
| 188 | #+end_src | ||
| 189 | |||
| 190 | Uptime: | ||
| 191 | |||
| 192 | #+begin_src sh | ||
| 193 | nano /etc/nginx/sites-available/uptime.example.com | ||
| 194 | #+end_src | ||
| 195 | |||
| 196 | #+begin_src config | ||
| 197 | server { | ||
| 198 | listen 80; | ||
| 199 | server_name uptime.example.com; | ||
| 200 | |||
| 201 | location / { | ||
| 202 | proxy_pass http://localhost:3001; | ||
| 203 | } | ||
| 204 | } | ||
| 205 | #+end_src | ||
| 206 | |||
| 207 | Once the configuration files are created, you will need to enable them | ||
| 208 | with the =symlink= command: | ||
| 209 | |||
| 210 | #+begin_src sh | ||
| 211 | sudo ln -s /etc/nginx/sites-available/dashy.example.com /etc/nginx/sites-enabled/ | ||
| 212 | #+end_src | ||
| 213 | |||
| 214 | Voilà! Your local services should now be available through their URLs. | ||
| 215 | |||
| 216 | * HTTPS with Certbot | ||
| 217 | |||
| 218 | If you've followed along, you'll notice that your services are only | ||
| 219 | available via HTTP (not HTTPS). | ||
| 220 | |||
| 221 | If you want to enable HTTPS for your new domains, you will need to | ||
| 222 | generate SSL/TLS certificates for them. The easiest way to generate | ||
| 223 | certificates on Nginx is [[https://certbot.eff.org][Certbot]]: | ||
| 224 | |||
| 225 | #+begin_src sh | ||
| 226 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | ||
| 227 | sudo snap install --classic certbot | ||
| 228 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | ||
| 229 | sudo certbot --nginx | ||
| 230 | #+end_src | ||
content/blog/2022-04-09-pinetime.md deleted −134
| @@ -1,134 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-04-09 | ||
| 3 | title = "PineTime: An Open-Source Smart Watch" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # PineTime Product Information | ||
| 9 | |||
| 10 | ## Overview | ||
| 11 | |||
| 12 | The [PineTime](https://www.pine64.org/pinetime/) is an open-source smartwatch, | ||
| 13 | created by [PINE64](https://www.pine64.org). Originally announced in September | ||
| 14 | 2019, this ARM-based watch is a fantastic option for users who want the benefits | ||
| 15 | of a modern smartwatch with the backing of open-source components and software. | ||
| 16 | |||
| 17 | ## Product Specifications | ||
| 18 | |||
| 19 | I won't dive into too many details that you can find on [the product | ||
| 20 | page](https://www.pine64.org/pinetime/), but I wanted to point out the prices | ||
| 21 | for each watch and the primary functions: | ||
| 22 | |||
| 23 | 1. Price: | ||
| 24 | |||
| 25 | - $26.99 (Sealed) | ||
| 26 | - $24.99 (Dev Kit) | ||
| 27 | - $51.98 (One Sealed + One Dev Kit) | ||
| 28 | |||
| 29 | 2. Primary Functionality: | ||
| 30 | |||
| 31 | - Clock (+ Smartphone Sync) | ||
| 32 | - Pedometer | ||
| 33 | - Heart Rate Monitor | ||
| 34 | - Sleep Monitor | ||
| 35 | - Calories burned | ||
| 36 | - Messaging | ||
| 37 | - Smartphone Notifications | ||
| 38 | - Media Controls | ||
| 39 | |||
| 40 | # Unboxing | ||
| 41 | |||
| 42 | Now, my PineTime was ordered on 2022-02-17, shipped on 2022-02-22, and was | ||
| 43 | delivered on 2022-03-23. With the current delays on shipping times around the | ||
| 44 | world (and the semiconductor shortage), a month for delivery from China seems | ||
| 45 | reasonable to me. | ||
| 46 | |||
| 47 | The packaging is simple, and the watch comes with instructions, technical | ||
| 48 | information, the watch, and a charger (it does not include a USB wall adapter). | ||
| 49 | |||
| 50 | The watch itself was able to turn on immediately when I pulled it out of the | ||
| 51 | box, but the battery was depleted and required charging right away. | ||
| 52 | |||
| 53 | # Software | ||
| 54 | |||
| 55 | ## Watch OS: InfiniTime | ||
| 56 | |||
| 57 | While turning on the watch for the first time, some of the main design choices | ||
| 58 | you can see in the watch OS, | ||
| 59 | [InfiniTime](https://wiki.pine64.org/wiki/InfiniTime), are: | ||
| 60 | |||
| 61 | - A square bezel, not too thin against the sides of the watch. | ||
| 62 | - A simple, rubber band. | ||
| 63 | - Basic font and screen pixel design. | ||
| 64 | - Swipe gestures to access other screens. | ||
| 65 | |||
| 66 | The OS itself is fantastic in terms of functionality for me. It does exactly | ||
| 67 | what a smartwatch should do - track time, steps, heart rates, and connect to | ||
| 68 | another smart device, without being overly burdensome to the user. | ||
| 69 | |||
| 70 | My only gripe so far is that it's _really_ difficult to swipe to different | ||
| 71 | screens, such as pulling down the notification tray. I'm not sure if this is an | ||
| 72 | OS or hardware issue, but it makes it quite hard to quickly move around the | ||
| 73 | screens. | ||
| 74 | |||
| 75 | However, my absolute favorite design choice is that the button the side turns | ||
| 76 | the screen on and off and tilting/waving my wrist doesn't accidentally turn on | ||
| 77 | the screen. With other watches, I absolutely hated not being able to turn off | ||
| 78 | the raise-to-wake or wave features (i.e., blinding myself while wearing a watch | ||
| 79 | at night because I moved my arm). | ||
| 80 | |||
| 81 | ## iOS App: InfiniLink | ||
| 82 | |||
| 83 | Since I am using iOS as my primary mobile device OS, I am using the | ||
| 84 | [InfiniLink](https://github.com/xan-m/InfiniLink) app to connect my watch. | ||
| 85 | |||
| 86 | This app provides the following for PineTime owners: | ||
| 87 | |||
| 88 | - Firmware updates | ||
| 89 | - Steps | ||
| 90 | - Charts | ||
| 91 | - Notifications | ||
| 92 | |||
| 93 | Another big feature of InfiniLink is the ability to track pedometer steps in a | ||
| 94 | collection of beautiful graphs, with the option to change your step goal and add | ||
| 95 | in manual steps. | ||
| 96 | |||
| 97 | Finally, there are charts to display the battery percentage and heart rates over | ||
| 98 | time. This area also comes with an option to clear data. | ||
| 99 | |||
| 100 | # Final Thoughts | ||
| 101 | |||
| 102 | ## Pros | ||
| 103 | |||
| 104 | After wearing my watch for a few weeks, I have mostly positive thoughts about | ||
| 105 | the watch so far. In the past, I have owned smartwatches by FitBit, Fossil, | ||
| 106 | Apple, etc. - **but I prefer the PineTime over all of those watches**. | ||
| 107 | |||
| 108 | The PineTime strips out all the unnecessary features and performs the functions | ||
| 109 | that it provides effectively and efficiently. | ||
| 110 | |||
| 111 | The battery life is amazing on this device. By default, the watch seems to last | ||
| 112 | anywhere from a few days to a week before dying. | ||
| 113 | |||
| 114 | And of course, it's open source and backed by some of the most dedicated | ||
| 115 | enthusiasts and developers I've seen. Watching the Matrix channel, forums, and | ||
| 116 | website have been exciting to see. | ||
| 117 | |||
| 118 | ## Cons | ||
| 119 | |||
| 120 | If I had to complain about anything, it would simply be the small bugs in some | ||
| 121 | features that can be contributed to the companion apps more than the watch | ||
| 122 | itself. | ||
| 123 | |||
| 124 | A regular user would want native notification support out-of-the-box, which is | ||
| 125 | the biggest item not working for me at the moment. | ||
| 126 | |||
| 127 | My only other complaint is that the battery indicator on the watch doesn't seem | ||
| 128 | accurate when it's nearing depletion - it seems that there's a bit of battery | ||
| 129 | life left and then my watch is dead very suddenly after. This could just be me | ||
| 130 | misinterpreting the battery level icons, but it has fooled me a few times into | ||
| 131 | thinking I had more battery left than I actually did. | ||
| 132 | |||
| 133 | Other than those small items, I really do love this watch and am glad I replaced | ||
| 134 | my Apple Watch with the PineTime. | ||
content/blog/2022-04-09-pinetime.org added +138
| @@ -0,0 +1,138 @@ | |||
| 1 | #+date: <2022-04-09> | ||
| 2 | #+title: PineTime: An Open-Source Smart Watch | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * PineTime Product Information | ||
| 7 | |||
| 8 | ** Overview | ||
| 9 | |||
| 10 | The [[https://www.pine64.org/pinetime/][PineTime]] is an open-source | ||
| 11 | smartwatch, created by [[https://www.pine64.org][PINE64]]. Originally | ||
| 12 | announced in September 2019, this ARM-based watch is a fantastic option | ||
| 13 | for users who want the benefits of a modern smartwatch with the backing | ||
| 14 | of open-source components and software. | ||
| 15 | |||
| 16 | ** Product Specifications | ||
| 17 | |||
| 18 | I won't dive into too many details that you can find on | ||
| 19 | [[https://www.pine64.org/pinetime/][the product page]], but I wanted to | ||
| 20 | point out the prices for each watch and the primary functions: | ||
| 21 | |||
| 22 | 1. Price: | ||
| 23 | |||
| 24 | - $26.99 (Sealed) | ||
| 25 | - $24.99 (Dev Kit) | ||
| 26 | - $51.98 (One Sealed + One Dev Kit) | ||
| 27 | |||
| 28 | 2. Primary Functionality: | ||
| 29 | |||
| 30 | - Clock (+ Smartphone Sync) | ||
| 31 | - Pedometer | ||
| 32 | - Heart Rate Monitor | ||
| 33 | - Sleep Monitor | ||
| 34 | - Calories burned | ||
| 35 | - Messaging | ||
| 36 | - Smartphone Notifications | ||
| 37 | - Media Controls | ||
| 38 | |||
| 39 | * Unboxing | ||
| 40 | |||
| 41 | Now, my PineTime was ordered on 2022-02-17, shipped on 2022-02-22, and | ||
| 42 | was delivered on 2022-03-23. With the current delays on shipping times | ||
| 43 | around the world (and the semiconductor shortage), a month for delivery | ||
| 44 | from China seems reasonable to me. | ||
| 45 | |||
| 46 | The packaging is simple, and the watch comes with instructions, | ||
| 47 | technical information, the watch, and a charger (it does not include a | ||
| 48 | USB wall adapter). | ||
| 49 | |||
| 50 | The watch itself was able to turn on immediately when I pulled it out of | ||
| 51 | the box, but the battery was depleted and required charging right away. | ||
| 52 | |||
| 53 | * Software | ||
| 54 | |||
| 55 | ** Watch OS: InfiniTime | ||
| 56 | |||
| 57 | While turning on the watch for the first time, some of the main design | ||
| 58 | choices you can see in the watch OS, | ||
| 59 | [[https://wiki.pine64.org/wiki/InfiniTime][InfiniTime]], are: | ||
| 60 | |||
| 61 | - A square bezel, not too thin against the sides of the watch. | ||
| 62 | - A simple, rubber band. | ||
| 63 | - Basic font and screen pixel design. | ||
| 64 | - Swipe gestures to access other screens. | ||
| 65 | |||
| 66 | The OS itself is fantastic in terms of functionality for me. It does | ||
| 67 | exactly what a smartwatch should do - track time, steps, heart rates, | ||
| 68 | and connect to another smart device, without being overly burdensome to | ||
| 69 | the user. | ||
| 70 | |||
| 71 | My only gripe so far is that it's /really/ difficult to swipe to | ||
| 72 | different screens, such as pulling down the notification tray. I'm not | ||
| 73 | sure if this is an OS or hardware issue, but it makes it quite hard to | ||
| 74 | quickly move around the screens. | ||
| 75 | |||
| 76 | However, my absolute favorite design choice is that the button the side | ||
| 77 | turns the screen on and off and tilting/waving my wrist doesn't | ||
| 78 | accidentally turn on the screen. With other watches, I absolutely hated | ||
| 79 | not being able to turn off the raise-to-wake or wave features (i.e., | ||
| 80 | blinding myself while wearing a watch at night because I moved my arm). | ||
| 81 | |||
| 82 | ** iOS App: InfiniLink | ||
| 83 | |||
| 84 | Since I am using iOS as my primary mobile device OS, I am using the | ||
| 85 | [[https://github.com/xan-m/InfiniLink][InfiniLink]] app to connect my | ||
| 86 | watch. | ||
| 87 | |||
| 88 | This app provides the following for PineTime owners: | ||
| 89 | |||
| 90 | - Firmware updates | ||
| 91 | - Steps | ||
| 92 | - Charts | ||
| 93 | - Notifications | ||
| 94 | |||
| 95 | Another big feature of InfiniLink is the ability to track pedometer | ||
| 96 | steps in a collection of beautiful graphs, with the option to change | ||
| 97 | your step goal and add in manual steps. | ||
| 98 | |||
| 99 | Finally, there are charts to display the battery percentage and heart | ||
| 100 | rates over time. This area also comes with an option to clear data. | ||
| 101 | |||
| 102 | * Final Thoughts | ||
| 103 | |||
| 104 | ** Pros | ||
| 105 | |||
| 106 | After wearing my watch for a few weeks, I have mostly positive thoughts | ||
| 107 | about the watch so far. In the past, I have owned smartwatches by | ||
| 108 | FitBit, Fossil, Apple, etc. - *but I prefer the PineTime over all of | ||
| 109 | those watches*. | ||
| 110 | |||
| 111 | The PineTime strips out all the unnecessary features and performs the | ||
| 112 | functions that it provides effectively and efficiently. | ||
| 113 | |||
| 114 | The battery life is amazing on this device. By default, the watch seems | ||
| 115 | to last anywhere from a few days to a week before dying. | ||
| 116 | |||
| 117 | And of course, it's open source and backed by some of the most dedicated | ||
| 118 | enthusiasts and developers I've seen. Watching the Matrix channel, | ||
| 119 | forums, and website have been exciting to see. | ||
| 120 | |||
| 121 | ** Cons | ||
| 122 | |||
| 123 | If I had to complain about anything, it would simply be the small bugs | ||
| 124 | in some features that can be contributed to the companion apps more than | ||
| 125 | the watch itself. | ||
| 126 | |||
| 127 | A regular user would want native notification support out-of-the-box, | ||
| 128 | which is the biggest item not working for me at the moment. | ||
| 129 | |||
| 130 | My only other complaint is that the battery indicator on the watch | ||
| 131 | doesn't seem accurate when it's nearing depletion - it seems that | ||
| 132 | there's a bit of battery life left and then my watch is dead very | ||
| 133 | suddenly after. This could just be me misinterpreting the battery level | ||
| 134 | icons, but it has fooled me a few times into thinking I had more battery | ||
| 135 | left than I actually did. | ||
| 136 | |||
| 137 | Other than those small items, I really do love this watch and am glad I | ||
| 138 | replaced my Apple Watch with the PineTime. | ||
content/blog/2022-06-01-ditching-cloudflare.md deleted −89
| @@ -1,89 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-06-01 | ||
| 3 | title = "Ditching Cloudflare for Njalla" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Registrar | ||
| 9 | |||
| 10 | After spending a year or so using Cloudflare for DNS only - no proxying or | ||
| 11 | applications - I spent the last few months using Cloudflare Tunnels and | ||
| 12 | Cloudflare Access to protect my self-hosted websites and applications via their | ||
| 13 | proxy traffic model. | ||
| 14 | |||
| 15 | However, I have never liked using Cloudflare due to their increasingly large | ||
| 16 | share of control over web traffic, as well as their business model of being a | ||
| 17 | MITM for all of your traffic. | ||
| 18 | |||
| 19 | So, as of today, I have switched over to [Njalla](https://njal.la) as my | ||
| 20 | registrar and DNS manager. I was able to easily transfer my domains over | ||
| 21 | rapidly, with only one domain taking more than 15-30 minutes to propagate. | ||
| 22 | |||
| 23 | +I do still have two domains sitting at Cloudflare for the moment while I decide | ||
| 24 | if they're worth the higher rates (one domain is 30€ and the other is 45€).+ | ||
| 25 | |||
| 26 | > **Update (2022.06.03)**: I ended up transferring my final two domains over to | ||
| 27 | > Njalla, clearing my Cloudflare account of personal data, and deleting the | ||
| 28 | > Cloudflare account entirely. _I actually feel relieved to have moved on to a | ||
| 29 | > provider I trust._ | ||
| 30 | |||
| 31 | # DNS | ||
| 32 | |||
| 33 | As noted above, I'm using Njalla exclusively for DNS configurations on my | ||
| 34 | domains. | ||
| 35 | |||
| 36 | However, the transfer process was not ideal. As soon as the domains transferred | ||
| 37 | over, I switched the nameservers from Cloudflare to Njalla and lost most of the | ||
| 38 | associated DNS records. So, the majority of the time spent during the migration | ||
| 39 | was simply re-typing all the DNS records back in one-by-one. | ||
| 40 | |||
| 41 | This would be much simpler if I were able to edit the plain-text format of the | ||
| 42 | DNS configuration. I was able to do that at a past registrar (perhaps it was | ||
| 43 | [Gandi.net](https://gandi.net/)?) and it made life a lot easier. | ||
| 44 | |||
| 45 | ## Dynamic DNS Updates | ||
| 46 | |||
| 47 | I have built an easy Python script to run (or set-up in `cron` to run | ||
| 48 | automatically) that will check my server's IPv4 and IPv6, compare it to Njalla, | ||
| 49 | and update the DNS records if they don't match. You can see the full script and | ||
| 50 | process in my other post: [Updating Dynamic DNS with Njalla | ||
| 51 | API](../njalla-dns-api/). | ||
| 52 | |||
| 53 | I haven't used this other method, but I do know that you can create `Dynamic` | ||
| 54 | DNS records with Njalla that [work for updating dynamic | ||
| 55 | subdomains](https://njal.la/docs/ddns/). | ||
| 56 | |||
| 57 | ## Njalla's DNS Tool | ||
| 58 | |||
| 59 | One neat upside to Njalla is that they have a [DNS lookup | ||
| 60 | tool](https://check.njal.la/dns/) that provides a lot of great information for | ||
| 61 | those of you (AKA: me) who hate using the `dig` command. | ||
| 62 | |||
| 63 | This was very useful for monitoring a couple of my transferred domains to see | ||
| 64 | when the changes in nameservers, records, and DNSSEC went into effect. | ||
| 65 | |||
| 66 | # Tunnel | ||
| 67 | |||
| 68 | Cloudflare Tunnel is a service that acts as a reverse-proxy (hosted on | ||
| 69 | Cloudflare's servers) and allowed me to mask the private IP address of the | ||
| 70 | server hosting my various websites and apps. | ||
| 71 | |||
| 72 | However, as I was moving away from Cloudflare, I was not able to find a suitable | ||
| 73 | replacement that was both inexpensive and simple. So, I simply went back to | ||
| 74 | hosting [my own reverse proxy with | ||
| 75 | Nginx](file:///blog/set-up-nginx-reverse-proxy/). With the recent additions of | ||
| 76 | Unifi hardware in my server/network rack, I am much more protected against spam | ||
| 77 | and malicious attacks at the network edge than I was before I switched to | ||
| 78 | Cloudflare. | ||
| 79 | |||
| 80 | # Access | ||
| 81 | |||
| 82 | Cloudflare Access, another app I used in combination with Cloudflare Tunnel, | ||
| 83 | provided an authentication screen that required you to enter valid credentials | ||
| 84 | before Cloudflare would forward you to the actual website or app (if the | ||
| 85 | website/app has their own authentication, you'd then have to authenticate a | ||
| 86 | second time). | ||
| 87 | |||
| 88 | I did not replace this service with anything since I only host a handful of | ||
| 89 | non-sensitive apps that don't require duplicate authentication. | ||
content/blog/2022-06-01-ditching-cloudflare.org added +95
| @@ -0,0 +1,95 @@ | |||
| 1 | #+date: <2022-06-01> | ||
| 2 | #+title: Ditching Cloudflare for Njalla | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Registrar | ||
| 7 | |||
| 8 | After spending a year or so using Cloudflare for DNS only - no proxying | ||
| 9 | or applications - I spent the last few months using Cloudflare Tunnels | ||
| 10 | and Cloudflare Access to protect my self-hosted websites and | ||
| 11 | applications via their proxy traffic model. | ||
| 12 | |||
| 13 | However, I have never liked using Cloudflare due to their increasingly | ||
| 14 | large share of control over web traffic, as well as their business model | ||
| 15 | of being a MITM for all of your traffic. | ||
| 16 | |||
| 17 | So, as of today, I have switched over to [[https://njal.la][Njalla]] as | ||
| 18 | my registrar and DNS manager. I was able to easily transfer my domains | ||
| 19 | over rapidly, with only one domain taking more than 15-30 minutes to | ||
| 20 | propagate. | ||
| 21 | |||
| 22 | +I do still have two domains sitting at Cloudflare for the moment while | ||
| 23 | I decide if they're worth the higher rates (one domain is 30€ and the | ||
| 24 | other is 45€).+ | ||
| 25 | |||
| 26 | #+begin_quote | ||
| 27 | *Update (2022.06.03)*: I ended up transferring my final two domains over | ||
| 28 | to Njalla, clearing my Cloudflare account of personal data, and deleting | ||
| 29 | the Cloudflare account entirely. /I actually feel relieved to have moved | ||
| 30 | on to a provider I trust./ | ||
| 31 | #+end_quote | ||
| 32 | |||
| 33 | * DNS | ||
| 34 | |||
| 35 | As noted above, I'm using Njalla exclusively for DNS configurations on | ||
| 36 | my domains. | ||
| 37 | |||
| 38 | However, the transfer process was not ideal. As soon as the domains | ||
| 39 | transferred over, I switched the nameservers from Cloudflare to Njalla | ||
| 40 | and lost most of the associated DNS records. So, the majority of the | ||
| 41 | time spent during the migration was simply re-typing all the DNS records | ||
| 42 | back in one-by-one. | ||
| 43 | |||
| 44 | This would be much simpler if I were able to edit the plain-text format | ||
| 45 | of the DNS configuration. I was able to do that at a past registrar | ||
| 46 | (perhaps it was [[https://gandi.net/][Gandi.net]]?) and it made life a | ||
| 47 | lot easier. | ||
| 48 | |||
| 49 | ** Dynamic DNS Updates | ||
| 50 | |||
| 51 | I have built an easy Python script to run (or set-up in =cron= to run | ||
| 52 | automatically) that will check my server's IPv4 and IPv6, compare it to | ||
| 53 | Njalla, and update the DNS records if they don't match. You can see the | ||
| 54 | full script and process in my other post: [[../njalla-dns-api/][Updating | ||
| 55 | Dynamic DNS with Njalla API]]. | ||
| 56 | |||
| 57 | I haven't used this other method, but I do know that you can create | ||
| 58 | =Dynamic= DNS records with Njalla that | ||
| 59 | [[https://njal.la/docs/ddns/][work for updating dynamic subdomains]]. | ||
| 60 | |||
| 61 | ** Njalla's DNS Tool | ||
| 62 | |||
| 63 | One neat upside to Njalla is that they have a | ||
| 64 | [[https://check.njal.la/dns/][DNS lookup tool]] that provides a lot of | ||
| 65 | great information for those of you (AKA: me) who hate using the =dig= | ||
| 66 | command. | ||
| 67 | |||
| 68 | This was very useful for monitoring a couple of my transferred domains | ||
| 69 | to see when the changes in nameservers, records, and DNSSEC went into | ||
| 70 | effect. | ||
| 71 | |||
| 72 | * Tunnel | ||
| 73 | |||
| 74 | Cloudflare Tunnel is a service that acts as a reverse-proxy (hosted on | ||
| 75 | Cloudflare's servers) and allowed me to mask the private IP address of | ||
| 76 | the server hosting my various websites and apps. | ||
| 77 | |||
| 78 | However, as I was moving away from Cloudflare, I was not able to find a | ||
| 79 | suitable replacement that was both inexpensive and simple. So, I simply | ||
| 80 | went back to hosting [[file:///blog/set-up-nginx-reverse-proxy/][my own | ||
| 81 | reverse proxy with Nginx]]. With the recent additions of Unifi hardware | ||
| 82 | in my server/network rack, I am much more protected against spam and | ||
| 83 | malicious attacks at the network edge than I was before I switched to | ||
| 84 | Cloudflare. | ||
| 85 | |||
| 86 | * Access | ||
| 87 | |||
| 88 | Cloudflare Access, another app I used in combination with Cloudflare | ||
| 89 | Tunnel, provided an authentication screen that required you to enter | ||
| 90 | valid credentials before Cloudflare would forward you to the actual | ||
| 91 | website or app (if the website/app has their own authentication, you'd | ||
| 92 | then have to authenticate a second time). | ||
| 93 | |||
| 94 | I did not replace this service with anything since I only host a handful | ||
| 95 | of non-sensitive apps that don't require duplicate authentication. | ||
content/blog/2022-06-07-self-hosting-freshrss.md deleted −238
| @@ -1,238 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-06-07 | ||
| 3 | title = "Self-Hosting FreshRSS" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Why RSS? | ||
| 9 | |||
| 10 | After noticing that I have collected 50+ blogs as bookmarks, I decided to | ||
| 11 | migrate back to using RSS feeds to stay up-to-date with my favorite websites. | ||
| 12 | Using RSS allows me to read all of these posts in a single app (on both mobile & | ||
| 13 | desktop) and allows me to be notified when new posts are available. | ||
| 14 | |||
| 15 | However, I ran into one issue: syncing subscriptions and read/unread posts | ||
| 16 | across devices. Since I want to be able to easily read on both mobile and | ||
| 17 | desktop, I decided to look for a self-hosted RSS solution. | ||
| 18 | |||
| 19 | Thus, I found [FreshRSS](https://www.freshrss.org/) and was able to successfully | ||
| 20 | install it on my server in about 30 minutes. | ||
| 21 | |||
| 22 | # Documentation | ||
| 23 | |||
| 24 | While it's certainly not robust, the [FreshRSS | ||
| 25 | documentation](https://freshrss.github.io/FreshRSS/) is helpful for figuring out | ||
| 26 | basic information about the service. | ||
| 27 | |||
| 28 | However, I wanted to install this service as a Docker container and stumbled | ||
| 29 | across the [Docker | ||
| 30 | README](https://github.com/FreshRSS/FreshRSS/tree/edge/Docker) within the GitHub | ||
| 31 | repository. | ||
| 32 | |||
| 33 | This README was the documentation I actually needed. However, as you'll see | ||
| 34 | below, I still had to manually edit one file (`config.php`) to access the API | ||
| 35 | externally via my RSS apps. | ||
| 36 | |||
| 37 | # Installation | ||
| 38 | |||
| 39 | ## DNS | ||
| 40 | |||
| 41 | The first step, as required by any external web service, was assigning a domain | ||
| 42 | name to use. I chose to use a subdomain, like `rss.example.com`. | ||
| 43 | |||
| 44 | To assign this, I created an `A` record in my DNS settings with the IPv4 address | ||
| 45 | of the server and an `AAAA` record with the IPv6 address of the server. Note: | ||
| 46 | assigning an IPv6 (`AAAA`) record is optional, but I like to enable IPV6 for my | ||
| 47 | services. | ||
| 48 | |||
| 49 | ```config | ||
| 50 | rss.example.com A xxx.xxx.xxx.xxx | ||
| 51 | rss.example.com AAAA xxxx:xxxx: ... :xxxx | ||
| 52 | ``` | ||
| 53 | |||
| 54 | ## Docker | ||
| 55 | |||
| 56 | I initially tried to set up a `docker-compose.yml` file with a `.env` file | ||
| 57 | because I prefer to have a file I can look back at later to see how I initially | ||
| 58 | started the container, but it simply wouldn't work for me. I'm not sure why, but | ||
| 59 | I assume I wasn't telling `docker-compose` where the `.env` file was. | ||
| 60 | |||
| 61 | Regardless, I chose to simply run the service with `docker run`. See the | ||
| 62 | following command for my `docker run` configuration: | ||
| 63 | |||
| 64 | ```sh | ||
| 65 | sudo docker run -d --restart unless-stopped --log-opt max-size=10m \ | ||
| 66 | -p 8080:80 \ | ||
| 67 | -e TZ=America/Chicago \ | ||
| 68 | -e 'CRON_MIN=1,31' \ | ||
| 69 | -v freshrss_data:/var/www/FreshRSS/data \ | ||
| 70 | -v freshrss_extensions:/var/www/FreshRSS/extensions \ | ||
| 71 | --name freshrss \ | ||
| 72 | freshrss/freshrss | ||
| 73 | ``` | ||
| 74 | |||
| 75 | This started the container successfully and allowed me to visit the FreshRSS | ||
| 76 | instance at `localhost:8080`. | ||
| 77 | |||
| 78 | ## Fresh RSS Set-Up | ||
| 79 | |||
| 80 | I **HIGHLY** suggest that you set up your user account prior to exposing this | ||
| 81 | service to the public. It's unlikely that someone is trying to access the exact | ||
| 82 | domain or IP/port you're assigning here, but as soon as you expose this service, | ||
| 83 | the first person to open the URL will be able to create the admin user. | ||
| 84 | |||
| 85 | In order to set up your FreshRSS service, open the `localhost:8080` URL in your | ||
| 86 | browser (you may need to use a local IP instead of `localhost` if you're | ||
| 87 | accessing the page from a different machine on the network - e.g., | ||
| 88 | `192.168.1.20:8080`). | ||
| 89 | |||
| 90 | Once the page loads, set up your default user with a strong username and | ||
| 91 | password. You may also choose to configure other settings prior to exposing this | ||
| 92 | service. | ||
| 93 | |||
| 94 | ## Nginx Reverse-Proxy | ||
| 95 | |||
| 96 | In order to access this service outside my home, I needed to set up a | ||
| 97 | reverse-proxy to connect `localhost:8080` to `rss.example.com`. | ||
| 98 | |||
| 99 | First, I created a new Nginx configuration file: | ||
| 100 | |||
| 101 | ```sh | ||
| 102 | sudo nano /etc/nginx/sites-available/rss.example.com | ||
| 103 | ``` | ||
| 104 | |||
| 105 | Within the config file, I pasted the following code: | ||
| 106 | |||
| 107 | ```config | ||
| 108 | upstream freshrss { | ||
| 109 | server 127.0.0.1:8080; | ||
| 110 | keepalive 64; | ||
| 111 | } | ||
| 112 | |||
| 113 | server { | ||
| 114 | server_name rss.example.com; | ||
| 115 | listen 80; | ||
| 116 | |||
| 117 | location / { | ||
| 118 | # The final `/` is important. | ||
| 119 | proxy_pass http://localhost:8080/; | ||
| 120 | add_header X-Frame-Options SAMEORIGIN; | ||
| 121 | add_header X-XSS-Protection "1; mode=block"; | ||
| 122 | proxy_redirect off; | ||
| 123 | proxy_buffering off; | ||
| 124 | proxy_set_header Host $host; | ||
| 125 | proxy_set_header X-Real-IP $remote_addr; | ||
| 126 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 127 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 128 | proxy_set_header X-Forwarded-Port $server_port; | ||
| 129 | proxy_read_timeout 90; | ||
| 130 | |||
| 131 | # Forward the Authorization header for the Google Reader API. | ||
| 132 | proxy_set_header Authorization $http_authorization; | ||
| 133 | proxy_pass_header Authorization; | ||
| 134 | } | ||
| 135 | } | ||
| 136 | ``` | ||
| 137 | |||
| 138 | Finally, restart Nginx and you will be able to access your service via HTTP: | ||
| 139 | |||
| 140 | ```sh | ||
| 141 | sudo systemctl restart nginx.service | ||
| 142 | ``` | ||
| 143 | |||
| 144 | ## HTTPS | ||
| 145 | |||
| 146 | However, I don't want to access my RSS feeds via HTTP. I want it available only | ||
| 147 | via HTTPS. In order to do this, I ran the [certbot](https://certbot.eff.org/) | ||
| 148 | program to generate SSL certificates for me: | ||
| 149 | |||
| 150 | ```sh | ||
| 151 | sudo certbot --nginx | ||
| 152 | ``` | ||
| 153 | |||
| 154 | This process will automatically generate an SSL certificate for you and modify | ||
| 155 | the Nginx configuration file to include a redirect from HTTP to HTTPS. | ||
| 156 | |||
| 157 | # Post-Installation Fixes | ||
| 158 | |||
| 159 | At this point, we have a functional FreshRSS website, available from anywhere | ||
| 160 | and secured with HTTPS. However, attempting to connect this service to an RSS | ||
| 161 | app resulted in many errors regarding unavailable URLs and incorrect | ||
| 162 | credentials. | ||
| 163 | |||
| 164 | ## API Set-Up | ||
| 165 | |||
| 166 | First, you need to open your user profile in FreshRSS (`Settings` > `Profile`) | ||
| 167 | and set an API password in the field at the bottom. This is the password you | ||
| 168 | will need to provide to your RSS apps. | ||
| 169 | |||
| 170 | Once that is set and saved, click the link below the API password field to open | ||
| 171 | the API check tool. It should look something like `https://localhost:8080/api/` | ||
| 172 | or `https://rss.example.com/api/`. | ||
| 173 | |||
| 174 | Within this page, you _should_ see your correct external URL and "PASS" at the | ||
| 175 | bottom of each API type. This would mean everything is set up correctly, and you | ||
| 176 | can now move on and login to any RSS apps that support self-hosted options. | ||
| 177 | |||
| 178 | In my case, the URL showed an internal URL and I had a warning that the | ||
| 179 | `base_url` variable may be misconfigured. If this is the case, see the next | ||
| 180 | section for a fix. | ||
| 181 | |||
| 182 | ## Base URL Fix | ||
| 183 | |||
| 184 | In order to fix the `base_url` for the API, I opened up my docker container with | ||
| 185 | the following command: | ||
| 186 | |||
| 187 | ```sh | ||
| 188 | sudo docker exec -it freshrss bash | ||
| 189 | ``` | ||
| 190 | |||
| 191 | Within this container, update the packages and install an editor: | ||
| 192 | |||
| 193 | ```sh | ||
| 194 | apt-get update | ||
| 195 | apt-get install nano | ||
| 196 | ``` | ||
| 197 | |||
| 198 | Finally, open up `config.php` in the `data` directory: | ||
| 199 | |||
| 200 | ```sh | ||
| 201 | nano data/config.php | ||
| 202 | ``` | ||
| 203 | |||
| 204 | Within `config.php`, you will need to update the `base_url` variable and update | ||
| 205 | it to match your external URL. In my case, I simply commented-out the incorrect | ||
| 206 | URL with `//` and added the correct one on a new line: | ||
| 207 | |||
| 208 | ```php | ||
| 209 | <?php | ||
| 210 | return array ( | ||
| 211 | ... | ||
| 212 | // 'base_url' => 'http://localhost:8080', | ||
| 213 | 'base_url' => 'https://rss.example.com', | ||
| 214 | ... | ||
| 215 | ) | ||
| 216 | > | ||
| 217 | ``` | ||
| 218 | |||
| 219 | You can now exit the file with `Ctrl + x`, press `y` to save the file, and then | ||
| 220 | click `Enter` to keep the same file name. | ||
| 221 | |||
| 222 | Finally, just exit out of the docker container: | ||
| 223 | |||
| 224 | ```sh | ||
| 225 | exit | ||
| 226 | ``` | ||
| 227 | |||
| 228 | Next, just restart the container: | ||
| 229 | |||
| 230 | ```sh | ||
| 231 | sudo docker restart freshrss | ||
| 232 | ``` | ||
| 233 | |||
| 234 | Voilà! Your API check should now "PASS" and you should be able to use one of the | ||
| 235 | API URLs in your RSS apps. | ||
| 236 | |||
| 237 | In my case, I use [NetNewsWire](https://netnewswire.com) on my desktop and | ||
| 238 | phone. | ||
content/blog/2022-06-07-self-hosting-freshrss.org added +243
| @@ -0,0 +1,243 @@ | |||
| 1 | #+date: <2022-06-07> | ||
| 2 | #+title: Self-Hosting FreshRSS | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Why RSS? | ||
| 7 | |||
| 8 | After noticing that I have collected 50+ blogs as bookmarks, I decided | ||
| 9 | to migrate back to using RSS feeds to stay up-to-date with my favorite | ||
| 10 | websites. Using RSS allows me to read all of these posts in a single app | ||
| 11 | (on both mobile & desktop) and allows me to be notified when new posts | ||
| 12 | are available. | ||
| 13 | |||
| 14 | However, I ran into one issue: syncing subscriptions and read/unread | ||
| 15 | posts across devices. Since I want to be able to easily read on both | ||
| 16 | mobile and desktop, I decided to look for a self-hosted RSS solution. | ||
| 17 | |||
| 18 | Thus, I found [[https://www.freshrss.org/][FreshRSS]] and was able to | ||
| 19 | successfully install it on my server in about 30 minutes. | ||
| 20 | |||
| 21 | * Documentation | ||
| 22 | |||
| 23 | While it's certainly not robust, the | ||
| 24 | [[https://freshrss.github.io/FreshRSS/][FreshRSS documentation]] is | ||
| 25 | helpful for figuring out basic information about the service. | ||
| 26 | |||
| 27 | However, I wanted to install this service as a Docker container and | ||
| 28 | stumbled across the | ||
| 29 | [[https://github.com/FreshRSS/FreshRSS/tree/edge/Docker][Docker README]] | ||
| 30 | within the GitHub repository. | ||
| 31 | |||
| 32 | This README was the documentation I actually needed. However, as you'll | ||
| 33 | see below, I still had to manually edit one file (=config.php=) to | ||
| 34 | access the API externally via my RSS apps. | ||
| 35 | |||
| 36 | * Installation | ||
| 37 | |||
| 38 | ** DNS | ||
| 39 | |||
| 40 | The first step, as required by any external web service, was assigning a | ||
| 41 | domain name to use. I chose to use a subdomain, like =rss.example.com=. | ||
| 42 | |||
| 43 | To assign this, I created an =A= record in my DNS settings with the IPv4 | ||
| 44 | address of the server and an =AAAA= record with the IPv6 address of the | ||
| 45 | server. Note: assigning an IPv6 (=AAAA=) record is optional, but I like | ||
| 46 | to enable IPV6 for my services. | ||
| 47 | |||
| 48 | #+begin_src config | ||
| 49 | rss.example.com A xxx.xxx.xxx.xxx | ||
| 50 | rss.example.com AAAA xxxx:xxxx: ... :xxxx | ||
| 51 | #+end_src | ||
| 52 | |||
| 53 | ** Docker | ||
| 54 | |||
| 55 | I initially tried to set up a =docker-compose.yml= file with a =.env= | ||
| 56 | file because I prefer to have a file I can look back at later to see how | ||
| 57 | I initially started the container, but it simply wouldn't work for me. | ||
| 58 | I'm not sure why, but I assume I wasn't telling =docker-compose= where | ||
| 59 | the =.env= file was. | ||
| 60 | |||
| 61 | Regardless, I chose to simply run the service with =docker run=. See the | ||
| 62 | following command for my =docker run= configuration: | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | sudo docker run -d --restart unless-stopped --log-opt max-size=10m \ | ||
| 66 | -p 8080:80 \ | ||
| 67 | -e TZ=America/Chicago \ | ||
| 68 | -e 'CRON_MIN=1,31' \ | ||
| 69 | -v freshrss_data:/var/www/FreshRSS/data \ | ||
| 70 | -v freshrss_extensions:/var/www/FreshRSS/extensions \ | ||
| 71 | --name freshrss \ | ||
| 72 | freshrss/freshrss | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | This started the container successfully and allowed me to visit the | ||
| 76 | FreshRSS instance at =localhost:8080=. | ||
| 77 | |||
| 78 | ** Fresh RSS Set-Up | ||
| 79 | |||
| 80 | I *HIGHLY* suggest that you set up your user account prior to exposing | ||
| 81 | this service to the public. It's unlikely that someone is trying to | ||
| 82 | access the exact domain or IP/port you're assigning here, but as soon as | ||
| 83 | you expose this service, the first person to open the URL will be able | ||
| 84 | to create the admin user. | ||
| 85 | |||
| 86 | In order to set up your FreshRSS service, open the =localhost:8080= URL | ||
| 87 | in your browser (you may need to use a local IP instead of =localhost= | ||
| 88 | if you're accessing the page from a different machine on the network - | ||
| 89 | e.g., =192.168.1.20:8080=). | ||
| 90 | |||
| 91 | Once the page loads, set up your default user with a strong username and | ||
| 92 | password. You may also choose to configure other settings prior to | ||
| 93 | exposing this service. | ||
| 94 | |||
| 95 | ** Nginx Reverse-Proxy | ||
| 96 | |||
| 97 | In order to access this service outside my home, I needed to set up a | ||
| 98 | reverse-proxy to connect =localhost:8080= to =rss.example.com=. | ||
| 99 | |||
| 100 | First, I created a new Nginx configuration file: | ||
| 101 | |||
| 102 | #+begin_src sh | ||
| 103 | sudo nano /etc/nginx/sites-available/rss.example.com | ||
| 104 | #+end_src | ||
| 105 | |||
| 106 | Within the config file, I pasted the following code: | ||
| 107 | |||
| 108 | #+begin_src config | ||
| 109 | upstream freshrss { | ||
| 110 | server 127.0.0.1:8080; | ||
| 111 | keepalive 64; | ||
| 112 | } | ||
| 113 | |||
| 114 | server { | ||
| 115 | server_name rss.example.com; | ||
| 116 | listen 80; | ||
| 117 | |||
| 118 | location / { | ||
| 119 | # The final `/` is important. | ||
| 120 | proxy_pass http://localhost:8080/; | ||
| 121 | add_header X-Frame-Options SAMEORIGIN; | ||
| 122 | add_header X-XSS-Protection "1; mode=block"; | ||
| 123 | proxy_redirect off; | ||
| 124 | proxy_buffering off; | ||
| 125 | proxy_set_header Host $host; | ||
| 126 | proxy_set_header X-Real-IP $remote_addr; | ||
| 127 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 128 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 129 | proxy_set_header X-Forwarded-Port $server_port; | ||
| 130 | proxy_read_timeout 90; | ||
| 131 | |||
| 132 | # Forward the Authorization header for the Google Reader API. | ||
| 133 | proxy_set_header Authorization $http_authorization; | ||
| 134 | proxy_pass_header Authorization; | ||
| 135 | } | ||
| 136 | } | ||
| 137 | #+end_src | ||
| 138 | |||
| 139 | Finally, restart Nginx and you will be able to access your service via | ||
| 140 | HTTP: | ||
| 141 | |||
| 142 | #+begin_src sh | ||
| 143 | sudo systemctl restart nginx.service | ||
| 144 | #+end_src | ||
| 145 | |||
| 146 | ** HTTPS | ||
| 147 | |||
| 148 | However, I don't want to access my RSS feeds via HTTP. I want it | ||
| 149 | available only via HTTPS. In order to do this, I ran the | ||
| 150 | [[https://certbot.eff.org/][certbot]] program to generate SSL | ||
| 151 | certificates for me: | ||
| 152 | |||
| 153 | #+begin_src sh | ||
| 154 | sudo certbot --nginx | ||
| 155 | #+end_src | ||
| 156 | |||
| 157 | This process will automatically generate an SSL certificate for you and | ||
| 158 | modify the Nginx configuration file to include a redirect from HTTP to | ||
| 159 | HTTPS. | ||
| 160 | |||
| 161 | * Post-Installation Fixes | ||
| 162 | |||
| 163 | At this point, we have a functional FreshRSS website, available from | ||
| 164 | anywhere and secured with HTTPS. However, attempting to connect this | ||
| 165 | service to an RSS app resulted in many errors regarding unavailable URLs | ||
| 166 | and incorrect credentials. | ||
| 167 | |||
| 168 | ** API Set-Up | ||
| 169 | |||
| 170 | First, you need to open your user profile in FreshRSS (=Settings= > | ||
| 171 | =Profile=) and set an API password in the field at the bottom. This is | ||
| 172 | the password you will need to provide to your RSS apps. | ||
| 173 | |||
| 174 | Once that is set and saved, click the link below the API password field | ||
| 175 | to open the API check tool. It should look something like | ||
| 176 | =https://localhost:8080/api/= or =https://rss.example.com/api/=. | ||
| 177 | |||
| 178 | Within this page, you /should/ see your correct external URL and "PASS" | ||
| 179 | at the bottom of each API type. This would mean everything is set up | ||
| 180 | correctly, and you can now move on and login to any RSS apps that | ||
| 181 | support self-hosted options. | ||
| 182 | |||
| 183 | In my case, the URL showed an internal URL and I had a warning that the | ||
| 184 | =base_url= variable may be misconfigured. If this is the case, see the | ||
| 185 | next section for a fix. | ||
| 186 | |||
| 187 | ** Base URL Fix | ||
| 188 | |||
| 189 | In order to fix the =base_url= for the API, I opened up my docker | ||
| 190 | container with the following command: | ||
| 191 | |||
| 192 | #+begin_src sh | ||
| 193 | sudo docker exec -it freshrss bash | ||
| 194 | #+end_src | ||
| 195 | |||
| 196 | Within this container, update the packages and install an editor: | ||
| 197 | |||
| 198 | #+begin_src sh | ||
| 199 | apt-get update | ||
| 200 | apt-get install nano | ||
| 201 | #+end_src | ||
| 202 | |||
| 203 | Finally, open up =config.php= in the =data= directory: | ||
| 204 | |||
| 205 | #+begin_src sh | ||
| 206 | nano data/config.php | ||
| 207 | #+end_src | ||
| 208 | |||
| 209 | Within =config.php=, you will need to update the =base_url= variable and | ||
| 210 | update it to match your external URL. In my case, I simply commented-out | ||
| 211 | the incorrect URL with =//= and added the correct one on a new line: | ||
| 212 | |||
| 213 | #+begin_src php | ||
| 214 | <?php | ||
| 215 | return array ( | ||
| 216 | ... | ||
| 217 | // 'base_url' => 'http://localhost:8080', | ||
| 218 | 'base_url' => 'https://rss.example.com', | ||
| 219 | ... | ||
| 220 | ) | ||
| 221 | > | ||
| 222 | #+end_src | ||
| 223 | |||
| 224 | You can now exit the file with =Ctrl + x=, press =y= to save the file, | ||
| 225 | and then click =Enter= to keep the same file name. | ||
| 226 | |||
| 227 | Finally, just exit out of the docker container: | ||
| 228 | |||
| 229 | #+begin_src sh | ||
| 230 | exit | ||
| 231 | #+end_src | ||
| 232 | |||
| 233 | Next, just restart the container: | ||
| 234 | |||
| 235 | #+begin_src sh | ||
| 236 | sudo docker restart freshrss | ||
| 237 | #+end_src | ||
| 238 | |||
| 239 | Voilà! Your API check should now "PASS" and you should be able to use | ||
| 240 | one of the API URLs in your RSS apps. | ||
| 241 | |||
| 242 | In my case, I use [[https://netnewswire.com][NetNewsWire]] on my desktop | ||
| 243 | and phone. | ||
content/blog/2022-06-16-terminal-lifestyle.md deleted −189
| @@ -1,189 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-06-16 | ||
| 3 | title = "A Terminal Lifestyle" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Text-Based Simplicity | ||
| 9 | |||
| 10 | I've detailed my views on web-based minimalism and related topics in other posts | ||
| 11 | throughout the years; e.g., JavaScript/CSS bloat slowing down websites that are | ||
| 12 | essentially a text document. However, I have never really expanded beyond | ||
| 13 | talking about the web and describing how I focus on minimizing distractions in | ||
| 14 | other digital environments. | ||
| 15 | |||
| 16 | This post is going to set the baseline for how I _try_ to live my digital life. | ||
| 17 | It does not necessarily get into my physical life, which is often harder to | ||
| 18 | control and contain all the noise in our modern world. | ||
| 19 | |||
| 20 | While there are new things to do every day in our digital world, I find that | ||
| 21 | keeping a core set of values and interests can ground you and keep you mindful | ||
| 22 | of _why_ you are participating in the digital world. For example, if - at your | ||
| 23 | core - you have no interest in what strangers think about random topics, it | ||
| 24 | would be unwise to start participating in social media. However, I am someone | ||
| 25 | who has been dragged in by effective advertising to participate in communities | ||
| 26 | that I realize I do not care for. | ||
| 27 | |||
| 28 | I won't dive much further into explaining the philosophy of all this, but I will | ||
| 29 | link a few helpful articles that may pique your interest if you're in search of | ||
| 30 | more meaningful experiences: | ||
| 31 | |||
| 32 | - [Mindfulness](https://en.wikipedia.org/wiki/Mindfulness) | ||
| 33 | - [Minimalism](https://en.wikipedia.org/wiki/Minimalism) | ||
| 34 | - [Stoicism](https://en.wikipedia.org/wiki/Stoicism) | ||
| 35 | |||
| 36 | # Living Life in the Terminal | ||
| 37 | |||
| 38 | My personal approach to reducing digital distractions and increasing my focus on | ||
| 39 | the task at hand is to use a terminal for as much as I possibly can. | ||
| 40 | |||
| 41 | Most days, this means that I have a few tabs open constantly in my terminal: | ||
| 42 | |||
| 43 | 1. A web browser | ||
| 44 | 2. A chat client | ||
| 45 | 3. An email client | ||
| 46 | 4. An RSS feed reader | ||
| 47 | 5. A local shell for navigating my computer's files | ||
| 48 | 6. A remote shell for managing servers and other machines | ||
| 49 | |||
| 50 | Beyond this, I rarely open other tabs or GUI applications, unless absolutely | ||
| 51 | necessary. If you look, you may be surprised what can be accomplished in the | ||
| 52 | terminal. | ||
| 53 | |||
| 54 | For example, I have moved my music and entertainment downloads to the terminal, | ||
| 55 | along with my device VPN connections. I am exploring options for moving my RSS | ||
| 56 | subscriptions to something like [Newsboat](https://newsboat.org/), so that I can | ||
| 57 | read my daily articles without all the fuss. | ||
| 58 | |||
| 59 | Now that we have some examples out of the way, let's dive into the specifics. | ||
| 60 | |||
| 61 | ## Browsing the Web | ||
| 62 | |||
| 63 | I'm going to start off with a hard topic for those who prefer to live in the | ||
| 64 | terminal: web browsing. This task is made hard mostly by websites and web apps | ||
| 65 | that require JavaScript to run. The other difficult part is that if you're using | ||
| 66 | a text-based browser, that means images won't load (hopefully that's obvious). | ||
| 67 | |||
| 68 | I am using [Lynx](https://lynx.invisible-island.net), a text-based browser that | ||
| 69 | runs quickly and easily in the terminal. Lynx allows me to browser most websites | ||
| 70 | by simply typing `g` and then typing in the URL I want. | ||
| 71 | |||
| 72 | If you need a search engine while in Lynx, I recommend [DuckDuckGo | ||
| 73 | (Lite)](https://lite.duckduckgo.com/lite/), which allows you to search the web | ||
| 74 | using their text-only interface. | ||
| 75 | |||
| 76 | Eventually, you will run into websites that don't work (or are just too ugly and | ||
| 77 | messy) in a text-only mode, and you'll be forced to switch over to a GUI browser | ||
| 78 | to look at that site. Personally, I don't mind this as it doesn't happen as | ||
| 79 | often as I thought it would. | ||
| 80 | |||
| 81 | The only time I need to do this is when I want to browse an image/video-focused | ||
| 82 | webpage or if I need to log in to a site, and it doesn't support a text-only | ||
| 83 | login page. For example, I am able to easily log in to | ||
| 84 | [Sourcehut](https://sr.ht) in lynx. | ||
| 85 | |||
| 86 | ## Chatting with Friends | ||
| 87 | |||
| 88 | After web browsing activities, my main form of terminal communication is Matrix. | ||
| 89 | I use the [gomuks](https://docs.mau.fi/gomuks/) client currently. | ||
| 90 | |||
| 91 | This was incredibly easy to install on macOS (but I will need to see if it'll be | ||
| 92 | just as easy on Linux when my new laptop arrives): | ||
| 93 | |||
| 94 | ```sh | ||
| 95 | brew install gomuks | ||
| 96 | ``` | ||
| 97 | |||
| 98 | Once you launch gomuks, it will sync and require your username and password to | ||
| 99 | login. After doing so, the only problem I ran into was verifying my gomuks | ||
| 100 | client so that I could participate in rooms with E2EE. | ||
| 101 | |||
| 102 | Finally, I was able to verify the session by opening the Element desktop app (I | ||
| 103 | assume you can do this in the browser and mobile app too, but I'm not sure) and | ||
| 104 | manually verifying myself with this process: | ||
| 105 | |||
| 106 | 1. Open the Element desktop app | ||
| 107 | 2. Open a room I was a member of | ||
| 108 | 3. Open the `Room Info` pane | ||
| 109 | 4. Open the `People` menu and search for myself | ||
| 110 | 5. Click on my profile name | ||
| 111 | 6. Click on the session link under the `Security` section and follow the prompts | ||
| 112 | to manually verify the session | ||
| 113 | |||
| 114 | Overall, I like gomuks and am able to enjoy all the features I was using in | ||
| 115 | Element. The only hiccup I have occurred is manually downloading images to view | ||
| 116 | them, which can be annoying. | ||
| 117 | |||
| 118 | |||
| 119 | |||
| 120 | Moving email to the terminal has been the hardest of the tasks for me. Unlike | ||
| 121 | web browsing, where I can simply decide to not look at a website that does not | ||
| 122 | work in the terminal, I cannot simply ignore emails sent to me. | ||
| 123 | |||
| 124 | Personally, I am experimenting with [neomutt](https://neomutt.org/) as a | ||
| 125 | potential email client. | ||
| 126 | |||
| 127 | However, this requires a **TON** of configuration and tweaking to get right. | ||
| 128 | Even when I was able to set up neomutt, configure my email account, and | ||
| 129 | customize a few personal preferences, a lot of emails still do not display | ||
| 130 | correctly (mostly due to HTML and images). | ||
| 131 | |||
| 132 | I won't get into the details of configuring `neomutt`; I mostly followed this | ||
| 133 | blog post: [Email in the Terminal: Configuring | ||
| 134 | Neomutt](https://gideonwolfe.com/posts/workflow/neomutt/intro/). | ||
| 135 | |||
| 136 | Finally, I have yet to figure out how to connect my GPG keys to `neomutt`, but | ||
| 137 | that's a problem for another day. | ||
| 138 | |||
| 139 | ## RSS Feed Reader | ||
| 140 | |||
| 141 | I have just started using [Newsboat](https://newsboat.org/) to read articles in | ||
| 142 | my terminal and have found quick success with it. | ||
| 143 | |||
| 144 | The configuration was super easy for this app; I simply installed the app, | ||
| 145 | created a file for URLs, and imported my OPML subscriptions that I had exported | ||
| 146 | out of my old feed reader: | ||
| 147 | |||
| 148 | ```sh | ||
| 149 | brew install newsboat | ||
| 150 | ``` | ||
| 151 | |||
| 152 | ```sh | ||
| 153 | touch ~/.newsboat/urls | ||
| 154 | ``` | ||
| 155 | |||
| 156 | ```sh | ||
| 157 | newsboat -i=my_subscriptions.opml | ||
| 158 | ``` | ||
| 159 | |||
| 160 | ## Writing & Programming | ||
| 161 | |||
| 162 | Unfortunately, the weak link in my terminal-based environment right now is my | ||
| 163 | grasp of the possibilities of editing files within a shell. | ||
| 164 | |||
| 165 | I am used to the easy extensions found in VSCodium and Kate, so I am slowly | ||
| 166 | learning how to mold the default editing tools to my needs. Currently, this | ||
| 167 | means I am using `nano` with the following configuration: | ||
| 168 | |||
| 169 | ```config | ||
| 170 | set breaklonglines | ||
| 171 | set autoindent | ||
| 172 | set linenumbers | ||
| 173 | set tabstospaces | ||
| 174 | set tabsize 2 | ||
| 175 | set fill 80 | ||
| 176 | ``` | ||
| 177 | |||
| 178 | This configuration allows nano to automatically hard-wrap lines at 80 | ||
| 179 | characters, autoindent the wrapped lines (if the previous line was indented), | ||
| 180 | use 2 spaces per tab, and display line numbers within each file I open. | ||
| 181 | |||
| 182 | I am currently looking to see if `vim` or `emacs` would be more useful for my | ||
| 183 | current needs, but I'm not in any rush, so I don't expect to find an answer | ||
| 184 | anytime soon. | ||
| 185 | |||
| 186 | With my current life demands, I am not programming at the moment and have not | ||
| 187 | explored the best terminal set-up for programming. However, I have seen many | ||
| 188 | peers find success configuring `vim` and `emacs`, so that's where I will start | ||
| 189 | when I pick my projects back up. | ||
content/blog/2022-06-16-terminal-lifestyle.org added +201
| @@ -0,0 +1,201 @@ | |||
| 1 | #+date: <2022-06-16> | ||
| 2 | #+title: A Terminal Lifestyle | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Text-Based Simplicity | ||
| 7 | |||
| 8 | I've detailed my views on web-based minimalism and related topics in | ||
| 9 | other posts throughout the years; e.g., JavaScript/CSS bloat slowing | ||
| 10 | down websites that are essentially a text document. However, I have | ||
| 11 | never really expanded beyond talking about the web and describing how I | ||
| 12 | focus on minimizing distractions in other digital environments. | ||
| 13 | |||
| 14 | This post is going to set the baseline for how I /try/ to live my | ||
| 15 | digital life. It does not necessarily get into my physical life, which | ||
| 16 | is often harder to control and contain all the noise in our modern | ||
| 17 | world. | ||
| 18 | |||
| 19 | While there are new things to do every day in our digital world, I find | ||
| 20 | that keeping a core set of values and interests can ground you and keep | ||
| 21 | you mindful of /why/ you are participating in the digital world. For | ||
| 22 | example, if - at your core - you have no interest in what strangers | ||
| 23 | think about random topics, it would be unwise to start participating in | ||
| 24 | social media. However, I am someone who has been dragged in by effective | ||
| 25 | advertising to participate in communities that I realize I do not care | ||
| 26 | for. | ||
| 27 | |||
| 28 | I won't dive much further into explaining the philosophy of all this, | ||
| 29 | but I will link a few helpful articles that may pique your interest if | ||
| 30 | you're in search of more meaningful experiences: | ||
| 31 | |||
| 32 | - [[https://en.wikipedia.org/wiki/Mindfulness][Mindfulness]] | ||
| 33 | - [[https://en.wikipedia.org/wiki/Minimalism][Minimalism]] | ||
| 34 | - [[https://en.wikipedia.org/wiki/Stoicism][Stoicism]] | ||
| 35 | |||
| 36 | * Living Life in the Terminal | ||
| 37 | |||
| 38 | My personal approach to reducing digital distractions and increasing my | ||
| 39 | focus on the task at hand is to use a terminal for as much as I possibly | ||
| 40 | can. | ||
| 41 | |||
| 42 | Most days, this means that I have a few tabs open constantly in my | ||
| 43 | terminal: | ||
| 44 | |||
| 45 | 1. A web browser | ||
| 46 | 2. A chat client | ||
| 47 | 3. An email client | ||
| 48 | 4. An RSS feed reader | ||
| 49 | 5. A local shell for navigating my computer's files | ||
| 50 | 6. A remote shell for managing servers and other machines | ||
| 51 | |||
| 52 | Beyond this, I rarely open other tabs or GUI applications, unless | ||
| 53 | absolutely necessary. If you look, you may be surprised what can be | ||
| 54 | accomplished in the terminal. | ||
| 55 | |||
| 56 | For example, I have moved my music and entertainment downloads to the | ||
| 57 | terminal, along with my device VPN connections. I am exploring options | ||
| 58 | for moving my RSS subscriptions to something like | ||
| 59 | [[https://newsboat.org/][Newsboat]], so that I can read my daily | ||
| 60 | articles without all the fuss. | ||
| 61 | |||
| 62 | Now that we have some examples out of the way, let's dive into the | ||
| 63 | specifics. | ||
| 64 | |||
| 65 | ** Browsing the Web | ||
| 66 | |||
| 67 | I'm going to start off with a hard topic for those who prefer to live in | ||
| 68 | the terminal: web browsing. This task is made hard mostly by websites | ||
| 69 | and web apps that require JavaScript to run. The other difficult part is | ||
| 70 | that if you're using a text-based browser, that means images won't load | ||
| 71 | (hopefully that's obvious). | ||
| 72 | |||
| 73 | I am using [[https://lynx.invisible-island.net][Lynx]], a text-based | ||
| 74 | browser that runs quickly and easily in the terminal. Lynx allows me to | ||
| 75 | browser most websites by simply typing =g= and then typing in the URL I | ||
| 76 | want. | ||
| 77 | |||
| 78 | If you need a search engine while in Lynx, I recommend | ||
| 79 | [[https://lite.duckduckgo.com/lite/][DuckDuckGo (Lite)]], which allows | ||
| 80 | you to search the web using their text-only interface. | ||
| 81 | |||
| 82 | Eventually, you will run into websites that don't work (or are just too | ||
| 83 | ugly and messy) in a text-only mode, and you'll be forced to switch over | ||
| 84 | to a GUI browser to look at that site. Personally, I don't mind this as | ||
| 85 | it doesn't happen as often as I thought it would. | ||
| 86 | |||
| 87 | The only time I need to do this is when I want to browse an | ||
| 88 | image/video-focused webpage or if I need to log in to a site, and it | ||
| 89 | doesn't support a text-only login page. For example, I am able to easily | ||
| 90 | log in to [[https://sr.ht][Sourcehut]] in lynx. | ||
| 91 | |||
| 92 | ** Chatting with Friends | ||
| 93 | |||
| 94 | After web browsing activities, my main form of terminal communication is | ||
| 95 | Matrix. I use the [[https://docs.mau.fi/gomuks/][gomuks]] client | ||
| 96 | currently. | ||
| 97 | |||
| 98 | This was incredibly easy to install on macOS (but I will need to see if | ||
| 99 | it'll be just as easy on Linux when my new laptop arrives): | ||
| 100 | |||
| 101 | #+begin_src sh | ||
| 102 | brew install gomuks | ||
| 103 | #+end_src | ||
| 104 | |||
| 105 | Once you launch gomuks, it will sync and require your username and | ||
| 106 | password to login. After doing so, the only problem I ran into was | ||
| 107 | verifying my gomuks client so that I could participate in rooms with | ||
| 108 | E2EE. | ||
| 109 | |||
| 110 | Finally, I was able to verify the session by opening the Element desktop | ||
| 111 | app (I assume you can do this in the browser and mobile app too, but I'm | ||
| 112 | not sure) and manually verifying myself with this process: | ||
| 113 | |||
| 114 | 1. Open the Element desktop app | ||
| 115 | 2. Open a room I was a member of | ||
| 116 | 3. Open the =Room Info= pane | ||
| 117 | 4. Open the =People= menu and search for myself | ||
| 118 | 5. Click on my profile name | ||
| 119 | 6. Click on the session link under the =Security= section and follow the | ||
| 120 | prompts to manually verify the session | ||
| 121 | |||
| 122 | Overall, I like gomuks and am able to enjoy all the features I was using | ||
| 123 | in Element. The only hiccup I have occurred is manually downloading | ||
| 124 | images to view them, which can be annoying. | ||
| 125 | |||
| 126 | |||
| 127 | |||
| 128 | Moving email to the terminal has been the hardest of the tasks for me. | ||
| 129 | Unlike web browsing, where I can simply decide to not look at a website | ||
| 130 | that does not work in the terminal, I cannot simply ignore emails sent | ||
| 131 | to me. | ||
| 132 | |||
| 133 | Personally, I am experimenting with [[https://neomutt.org/][neomutt]] as | ||
| 134 | a potential email client. | ||
| 135 | |||
| 136 | However, this requires a *TON* of configuration and tweaking to get | ||
| 137 | right. Even when I was able to set up neomutt, configure my email | ||
| 138 | account, and customize a few personal preferences, a lot of emails still | ||
| 139 | do not display correctly (mostly due to HTML and images). | ||
| 140 | |||
| 141 | I won't get into the details of configuring =neomutt=; I mostly followed | ||
| 142 | this blog post: | ||
| 143 | [[https://gideonwolfe.com/posts/workflow/neomutt/intro/][Email in the | ||
| 144 | Terminal: Configuring Neomutt]]. | ||
| 145 | |||
| 146 | Finally, I have yet to figure out how to connect my GPG keys to | ||
| 147 | =neomutt=, but that's a problem for another day. | ||
| 148 | |||
| 149 | ** RSS Feed Reader | ||
| 150 | |||
| 151 | I have just started using [[https://newsboat.org/][Newsboat]] to read | ||
| 152 | articles in my terminal and have found quick success with it. | ||
| 153 | |||
| 154 | The configuration was super easy for this app; I simply installed the | ||
| 155 | app, created a file for URLs, and imported my OPML subscriptions that I | ||
| 156 | had exported out of my old feed reader: | ||
| 157 | |||
| 158 | #+begin_src sh | ||
| 159 | brew install newsboat | ||
| 160 | #+end_src | ||
| 161 | |||
| 162 | #+begin_src sh | ||
| 163 | touch ~/.newsboat/urls | ||
| 164 | #+end_src | ||
| 165 | |||
| 166 | #+begin_src sh | ||
| 167 | newsboat -i=my_subscriptions.opml | ||
| 168 | #+end_src | ||
| 169 | |||
| 170 | ** Writing & Programming | ||
| 171 | |||
| 172 | Unfortunately, the weak link in my terminal-based environment right now | ||
| 173 | is my grasp of the possibilities of editing files within a shell. | ||
| 174 | |||
| 175 | I am used to the easy extensions found in VSCodium and Kate, so I am | ||
| 176 | slowly learning how to mold the default editing tools to my needs. | ||
| 177 | Currently, this means I am using =nano= with the following | ||
| 178 | configuration: | ||
| 179 | |||
| 180 | #+begin_src config | ||
| 181 | set breaklonglines | ||
| 182 | set autoindent | ||
| 183 | set linenumbers | ||
| 184 | set tabstospaces | ||
| 185 | set tabsize 2 | ||
| 186 | set fill 80 | ||
| 187 | #+end_src | ||
| 188 | |||
| 189 | This configuration allows nano to automatically hard-wrap lines at 80 | ||
| 190 | characters, autoindent the wrapped lines (if the previous line was | ||
| 191 | indented), use 2 spaces per tab, and display line numbers within each | ||
| 192 | file I open. | ||
| 193 | |||
| 194 | I am currently looking to see if =vim= or =emacs= would be more useful | ||
| 195 | for my current needs, but I'm not in any rush, so I don't expect to find | ||
| 196 | an answer anytime soon. | ||
| 197 | |||
| 198 | With my current life demands, I am not programming at the moment and | ||
| 199 | have not explored the best terminal set-up for programming. However, I | ||
| 200 | have seen many peers find success configuring =vim= and =emacs=, so | ||
| 201 | that's where I will start when I pick my projects back up. | ||
content/blog/2022-06-22-daily-poetry.md deleted −213
| @@ -1,213 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-06-22 | ||
| 3 | title = "Daily Plaintext Poetry via Email" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Source Code | ||
| 9 | |||
| 10 | I don't want to bury the lede here, so if you'd like to see the full source code | ||
| 11 | I use to email myself plaintext poems daily, visit the repository: | ||
| 12 | [daily-poem.git](https://git.sr.ht/~cyborg/daily-poem). | ||
| 13 | |||
| 14 | # My Daily Dose of Poetry | ||
| 15 | |||
| 16 | Most of my programming projects are small, random projects that are made | ||
| 17 | strictly to fix some small problem I have or enhance my quality of life. | ||
| 18 | |||
| 19 | In this case, I was looking for a simply and easy way to get a daily dose of | ||
| 20 | literature or poetry to read in the mornings. | ||
| 21 | |||
| 22 | However, I don't want to sign up for a random mailing list on just any website. | ||
| 23 | I also don't want to have to work to find the reading content each morning, as I | ||
| 24 | know I would simply give up and stop reading daily. | ||
| 25 | |||
| 26 | Thus, I found a way to deliver poetry to myself in plain-text format, on a daily | ||
| 27 | basis, and scheduled to deliver automatically. | ||
| 28 | |||
| 29 | # Prerequisites | ||
| 30 | |||
| 31 | This solution uses Python and email, so the following process requires the | ||
| 32 | following to be installed: | ||
| 33 | |||
| 34 | 1. An SMTP server, which can be as easy as installing `mailutils` if you're on a | ||
| 35 | Debian-based distro. | ||
| 36 | 2. Python (& pip!) | ||
| 37 | 3. The following Python packages: `email`, `smtplib`, `json`, and `requests` | ||
| 38 | |||
| 39 | # Breaking Down the Logic | ||
| 40 | |||
| 41 | I want to break down the logic for this program, as it's quite simple and | ||
| 42 | informational. | ||
| 43 | |||
| 44 | ## Required Packages | ||
| 45 | |||
| 46 | This program starts with a simple import of the required packages, so I wanted | ||
| 47 | to explain why each package is used: | ||
| 48 | |||
| 49 | ```python | ||
| 50 | from email.mime.text import MIMEText # Required for translating MIMEText | ||
| 51 | import smtplib # Required to process the SMTP mail delivery | ||
| 52 | import json # Required to parse the poetry API results | ||
| 53 | import requests # Required to send out a request to the API | ||
| 54 | ``` | ||
| 55 | |||
| 56 | ## Sending the API Request | ||
| 57 | |||
| 58 | Next, we need to actually send the API request. In my case, I'm calling a random | ||
| 59 | poem from the entire API. If you want, you can call specific poems or authors | ||
| 60 | from this API. | ||
| 61 | |||
| 62 | ```python | ||
| 63 | json_data = requests.get('https://poetrydb.org/random').json() | ||
| 64 | ``` | ||
| 65 | |||
| 66 | This gives us the following result in JSON: | ||
| 67 | |||
| 68 | ```json | ||
| 69 | [ | ||
| 70 | { | ||
| 71 | "title": "Sonnet XXII: With Fools and Children", | ||
| 72 | "author": "Michael Drayton", | ||
| 73 | "lines": [ | ||
| 74 | "To Folly", | ||
| 75 | "", | ||
| 76 | "With fools and children, good discretion bears;", | ||
| 77 | "Then, honest people, bear with Love and me,", | ||
| 78 | "Nor older yet, nor wiser made by years,", | ||
| 79 | "Amongst the rest of fools and children be;", | ||
| 80 | "Love, still a baby, plays with gauds and toys,", | ||
| 81 | "And, like a wanton, sports with every feather,", | ||
| 82 | "And idiots still are running after boys,", | ||
| 83 | "Then fools and children fitt'st to go together.", | ||
| 84 | "He still as young as when he first was born,", | ||
| 85 | "No wiser I than when as young as he;", | ||
| 86 | "You that behold us, laugh us not to scorn;", | ||
| 87 | "Give Nature thanks you are not such as we.", | ||
| 88 | "Yet fools and children sometimes tell in play", | ||
| 89 | "Some, wise in show, more fools indeed than they." | ||
| 90 | ], | ||
| 91 | "linecount": "15" | ||
| 92 | } | ||
| 93 | ] | ||
| 94 | ``` | ||
| 95 | |||
| 96 | ## Parsing the API Results | ||
| 97 | |||
| 98 | In order to parse this into a readable format, we need to use the `json` package | ||
| 99 | and extract the fields we want. In the example below, I am grabbing every field | ||
| 100 | presented by the API. | ||
| 101 | |||
| 102 | For the actual poem content, we need to loop over each line in the `lines` | ||
| 103 | variable since each line is a separate string by default. | ||
| 104 | |||
| 105 | > You _could_ also extract the title or author and make another call out to the | ||
| 106 | > API to avoid having to build the plaintext poem with a loop, but it just | ||
| 107 | > doesn't make sense to me to send multiple requests when we can create a simple | ||
| 108 | > loop on our local machine to work with the data we already have. | ||
| 109 | > | ||
| 110 | > For | ||
| 111 | > [example](https://poetrydb.org/title/Sonnet%20XXII:%20With%20Fools%20and%20Children/lines.text), | ||
| 112 | > look at the raw data response of this link to see the poem's lines returned in | ||
| 113 | > plaintext. | ||
| 114 | |||
| 115 | ```python | ||
| 116 | title = json_data[0]['title'] | ||
| 117 | author = json_data[0]['author'] | ||
| 118 | line_count = json_data[0]['linecount'] | ||
| 119 | lines = '' | ||
| 120 | for line in json_data[0]['lines']: | ||
| 121 | lines = lines + line + "\n" | ||
| 122 | ``` | ||
| 123 | |||
| 124 | ## Composing the Email | ||
| 125 | |||
| 126 | Now that I have all the data I need, I just need to compose it into a message | ||
| 127 | and prepare the message metadata. | ||
| 128 | |||
| 129 | For my daily email, I want to see the title of the poem first, followed by the | ||
| 130 | author, then a blank line, and finally the full poem. This code snippet combines | ||
| 131 | that data and packages it into a MIMEText container, ready to be emailed. | ||
| 132 | |||
| 133 | ```python | ||
| 134 | msg_body = title + "\n" + author + "\n\n" + lines | ||
| 135 | msg = MIMEText(msg_body) | ||
| 136 | ``` | ||
| 137 | |||
| 138 | Before we send the email, we need to prepare the metadata (subject, from, to, | ||
| 139 | etc.): | ||
| 140 | |||
| 141 | ```python | ||
| 142 | sender_email = 'example@server.local' | ||
| 143 | recipient_emails = ['user@example.com'] | ||
| 144 | msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' | ||
| 145 | msg['From'] = sender_email | ||
| 146 | msg['To'] = recipient_email | ||
| 147 | ``` | ||
| 148 | |||
| 149 | ## Sending the Email | ||
| 150 | |||
| 151 | Now that I have everything ready to be emailed, the last step is to simply | ||
| 152 | connect to an SMTP server and send the email out to the recipients. In my case, | ||
| 153 | I installed `mailutils` on Ubuntu and let my SMTP server be `localhost`. | ||
| 154 | |||
| 155 | ```python | ||
| 156 | smtp_server = 'localhost' | ||
| 157 | s = smtplib.SMTP(smtp_server) | ||
| 158 | s.sendmail(sender_email, recipient_emails, msg.as_string()) | ||
| 159 | s.quit() | ||
| 160 | ``` | ||
| 161 | |||
| 162 | # The Result! | ||
| 163 | |||
| 164 | Instead of including a screenshot, I've copied the contents of the email that | ||
| 165 | was delivered to my inbox below since I set this process up in plaintext format. | ||
| 166 | |||
| 167 | ```txt | ||
| 168 | Date: Wed, 22 Jun 2022 14:37:19 +0000 (UTC) | ||
| 169 | From: REDACTED | ||
| 170 | To: REDACTED | ||
| 171 | Subject: Your Daily Poem (36 lines) | ||
| 172 | MIME-Version: 1.0 | ||
| 173 | Content-Transfer-Encoding: 8bit | ||
| 174 | Content-Type: text/plain; charset=utf-8 | ||
| 175 | |||
| 176 | Sonnet XXII: With Fools and Children | ||
| 177 | Michael Drayton | ||
| 178 | |||
| 179 | With fools and children, good discretion bears; | ||
| 180 | Then, honest people, bear with Love and me, | ||
| 181 | Nor older yet, nor wiser made by years, | ||
| 182 | Amongst the rest of fools and children be; | ||
| 183 | Love, still a baby, plays with gauds and toys, | ||
| 184 | And, like a wanton, sports with every feather, | ||
| 185 | And idiots still are running after boys, | ||
| 186 | Then fools and children fitt'st to go together. | ||
| 187 | He still as young as when he first was born, | ||
| 188 | No wiser I than when as young as he; | ||
| 189 | You that behold us, laugh us not to scorn; | ||
| 190 | Give Nature thanks you are not such as we. | ||
| 191 | Yet fools and children sometimes tell in play | ||
| 192 | Some, wise in show, more fools indeed than they. | ||
| 193 | ``` | ||
| 194 | |||
| 195 | # Scheduling the Daily Email | ||
| 196 | |||
| 197 | Last, but not least, is scheduling this Python script with `crontab`. To | ||
| 198 | schedule a script to run daily, you can add it to the `crontab` file. To do | ||
| 199 | this, open `crontab` in editing mode: | ||
| 200 | |||
| 201 | ```sh | ||
| 202 | crontab -e | ||
| 203 | ``` | ||
| 204 | |||
| 205 | In the file, simply paste the following snippet at the bottom of the file and | ||
| 206 | ensure that the file path is correctly pointing to wherever you saved your | ||
| 207 | Python script: | ||
| 208 | |||
| 209 | ```config | ||
| 210 | 0 8 ** ** ** python3 /home/<your_user>/dailypoem/main.py | ||
| 211 | ``` | ||
| 212 | |||
| 213 | We have now set up the script and scheduled it to run daily at 08:00! | ||
content/blog/2022-06-22-daily-poetry.org added +218
| @@ -0,0 +1,218 @@ | |||
| 1 | #+date: <2022-06-22> | ||
| 2 | #+title: Daily Plaintext Poetry via Email | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Source Code | ||
| 7 | |||
| 8 | I don't want to bury the lede here, so if you'd like to see the full | ||
| 9 | source code I use to email myself plaintext poems daily, visit the | ||
| 10 | repository: [[https://git.sr.ht/~cyborg/daily-poem][daily-poem.git]]. | ||
| 11 | |||
| 12 | * My Daily Dose of Poetry | ||
| 13 | |||
| 14 | Most of my programming projects are small, random projects that are made | ||
| 15 | strictly to fix some small problem I have or enhance my quality of life. | ||
| 16 | |||
| 17 | In this case, I was looking for a simply and easy way to get a daily | ||
| 18 | dose of literature or poetry to read in the mornings. | ||
| 19 | |||
| 20 | However, I don't want to sign up for a random mailing list on just any | ||
| 21 | website. I also don't want to have to work to find the reading content | ||
| 22 | each morning, as I know I would simply give up and stop reading daily. | ||
| 23 | |||
| 24 | Thus, I found a way to deliver poetry to myself in plain-text format, on | ||
| 25 | a daily basis, and scheduled to deliver automatically. | ||
| 26 | |||
| 27 | * Prerequisites | ||
| 28 | |||
| 29 | This solution uses Python and email, so the following process requires | ||
| 30 | the following to be installed: | ||
| 31 | |||
| 32 | 1. An SMTP server, which can be as easy as installing =mailutils= if | ||
| 33 | you're on a Debian-based distro. | ||
| 34 | 2. Python (& pip!) | ||
| 35 | 3. The following Python packages: =email=, =smtplib=, =json=, and | ||
| 36 | =requests= | ||
| 37 | |||
| 38 | * Breaking Down the Logic | ||
| 39 | |||
| 40 | I want to break down the logic for this program, as it's quite simple | ||
| 41 | and informational. | ||
| 42 | |||
| 43 | ** Required Packages | ||
| 44 | |||
| 45 | This program starts with a simple import of the required packages, so I | ||
| 46 | wanted to explain why each package is used: | ||
| 47 | |||
| 48 | #+begin_src python | ||
| 49 | from email.mime.text import MIMEText # Required for translating MIMEText | ||
| 50 | import smtplib # Required to process the SMTP mail delivery | ||
| 51 | import json # Required to parse the poetry API results | ||
| 52 | import requests # Required to send out a request to the API | ||
| 53 | #+end_src | ||
| 54 | |||
| 55 | ** Sending the API Request | ||
| 56 | |||
| 57 | Next, we need to actually send the API request. In my case, I'm calling | ||
| 58 | a random poem from the entire API. If you want, you can call specific | ||
| 59 | poems or authors from this API. | ||
| 60 | |||
| 61 | #+begin_src python | ||
| 62 | json_data = requests.get('https://poetrydb.org/random').json() | ||
| 63 | #+end_src | ||
| 64 | |||
| 65 | This gives us the following result in JSON: | ||
| 66 | |||
| 67 | #+begin_src json | ||
| 68 | [ | ||
| 69 | { | ||
| 70 | "title": "Sonnet XXII: With Fools and Children", | ||
| 71 | "author": "Michael Drayton", | ||
| 72 | "lines": [ | ||
| 73 | "To Folly", | ||
| 74 | "", | ||
| 75 | "With fools and children, good discretion bears;", | ||
| 76 | "Then, honest people, bear with Love and me,", | ||
| 77 | "Nor older yet, nor wiser made by years,", | ||
| 78 | "Amongst the rest of fools and children be;", | ||
| 79 | "Love, still a baby, plays with gauds and toys,", | ||
| 80 | "And, like a wanton, sports with every feather,", | ||
| 81 | "And idiots still are running after boys,", | ||
| 82 | "Then fools and children fitt'st to go together.", | ||
| 83 | "He still as young as when he first was born,", | ||
| 84 | "No wiser I than when as young as he;", | ||
| 85 | "You that behold us, laugh us not to scorn;", | ||
| 86 | "Give Nature thanks you are not such as we.", | ||
| 87 | "Yet fools and children sometimes tell in play", | ||
| 88 | "Some, wise in show, more fools indeed than they." | ||
| 89 | ], | ||
| 90 | "linecount": "15" | ||
| 91 | } | ||
| 92 | ] | ||
| 93 | #+end_src | ||
| 94 | |||
| 95 | ** Parsing the API Results | ||
| 96 | |||
| 97 | In order to parse this into a readable format, we need to use the =json= | ||
| 98 | package and extract the fields we want. In the example below, I am | ||
| 99 | grabbing every field presented by the API. | ||
| 100 | |||
| 101 | For the actual poem content, we need to loop over each line in the | ||
| 102 | =lines= variable since each line is a separate string by default. | ||
| 103 | |||
| 104 | #+begin_quote | ||
| 105 | You /could/ also extract the title or author and make another call out | ||
| 106 | to the API to avoid having to build the plaintext poem with a loop, but | ||
| 107 | it just doesn't make sense to me to send multiple requests when we can | ||
| 108 | create a simple loop on our local machine to work with the data we | ||
| 109 | already have. | ||
| 110 | |||
| 111 | For | ||
| 112 | [[https://poetrydb.org/title/Sonnet%20XXII:%20With%20Fools%20and%20Children/lines.text][example]], | ||
| 113 | look at the raw data response of this link to see the poem's lines | ||
| 114 | returned in plaintext. | ||
| 115 | #+end_quote | ||
| 116 | |||
| 117 | #+begin_src python | ||
| 118 | title = json_data[0]['title'] | ||
| 119 | author = json_data[0]['author'] | ||
| 120 | line_count = json_data[0]['linecount'] | ||
| 121 | lines = '' | ||
| 122 | for line in json_data[0]['lines']: | ||
| 123 | lines = lines + line + "\n" | ||
| 124 | #+end_src | ||
| 125 | |||
| 126 | ** Composing the Email | ||
| 127 | |||
| 128 | Now that I have all the data I need, I just need to compose it into a | ||
| 129 | message and prepare the message metadata. | ||
| 130 | |||
| 131 | For my daily email, I want to see the title of the poem first, followed | ||
| 132 | by the author, then a blank line, and finally the full poem. This code | ||
| 133 | snippet combines that data and packages it into a MIMEText container, | ||
| 134 | ready to be emailed. | ||
| 135 | |||
| 136 | #+begin_src python | ||
| 137 | msg_body = title + "\n" + author + "\n\n" + lines | ||
| 138 | msg = MIMEText(msg_body) | ||
| 139 | #+end_src | ||
| 140 | |||
| 141 | Before we send the email, we need to prepare the metadata (subject, | ||
| 142 | from, to, etc.): | ||
| 143 | |||
| 144 | #+begin_src python | ||
| 145 | sender_email = 'example@server.local' | ||
| 146 | recipient_emails = ['user@example.com'] | ||
| 147 | msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' | ||
| 148 | msg['From'] = sender_email | ||
| 149 | msg['To'] = recipient_email | ||
| 150 | #+end_src | ||
| 151 | |||
| 152 | ** Sending the Email | ||
| 153 | |||
| 154 | Now that I have everything ready to be emailed, the last step is to | ||
| 155 | simply connect to an SMTP server and send the email out to the | ||
| 156 | recipients. In my case, I installed =mailutils= on Ubuntu and let my | ||
| 157 | SMTP server be =localhost=. | ||
| 158 | |||
| 159 | #+begin_src python | ||
| 160 | smtp_server = 'localhost' | ||
| 161 | s = smtplib.SMTP(smtp_server) | ||
| 162 | s.sendmail(sender_email, recipient_emails, msg.as_string()) | ||
| 163 | s.quit() | ||
| 164 | #+end_src | ||
| 165 | |||
| 166 | * The Result! | ||
| 167 | |||
| 168 | Instead of including a screenshot, I've copied the contents of the email | ||
| 169 | that was delivered to my inbox below since I set this process up in | ||
| 170 | plaintext format. | ||
| 171 | |||
| 172 | #+begin_src txt | ||
| 173 | Date: Wed, 22 Jun 2022 14:37:19 +0000 (UTC) | ||
| 174 | From: REDACTED | ||
| 175 | To: REDACTED | ||
| 176 | Subject: Your Daily Poem (36 lines) | ||
| 177 | MIME-Version: 1.0 | ||
| 178 | Content-Transfer-Encoding: 8bit | ||
| 179 | Content-Type: text/plain; charset=utf-8 | ||
| 180 | |||
| 181 | Sonnet XXII: With Fools and Children | ||
| 182 | Michael Drayton | ||
| 183 | |||
| 184 | With fools and children, good discretion bears; | ||
| 185 | Then, honest people, bear with Love and me, | ||
| 186 | Nor older yet, nor wiser made by years, | ||
| 187 | Amongst the rest of fools and children be; | ||
| 188 | Love, still a baby, plays with gauds and toys, | ||
| 189 | And, like a wanton, sports with every feather, | ||
| 190 | And idiots still are running after boys, | ||
| 191 | Then fools and children fitt'st to go together. | ||
| 192 | He still as young as when he first was born, | ||
| 193 | No wiser I than when as young as he; | ||
| 194 | You that behold us, laugh us not to scorn; | ||
| 195 | Give Nature thanks you are not such as we. | ||
| 196 | Yet fools and children sometimes tell in play | ||
| 197 | Some, wise in show, more fools indeed than they. | ||
| 198 | #+end_src | ||
| 199 | |||
| 200 | * Scheduling the Daily Email | ||
| 201 | |||
| 202 | Last, but not least, is scheduling this Python script with =crontab=. To | ||
| 203 | schedule a script to run daily, you can add it to the =crontab= file. To | ||
| 204 | do this, open =crontab= in editing mode: | ||
| 205 | |||
| 206 | #+begin_src sh | ||
| 207 | crontab -e | ||
| 208 | #+end_src | ||
| 209 | |||
| 210 | In the file, simply paste the following snippet at the bottom of the | ||
| 211 | file and ensure that the file path is correctly pointing to wherever you | ||
| 212 | saved your Python script: | ||
| 213 | |||
| 214 | #+begin_src config | ||
| 215 | 0 8 ** ** ** python3 /home/<your_user>/dailypoem/main.py | ||
| 216 | #+end_src | ||
| 217 | |||
| 218 | We have now set up the script and scheduled it to run daily at 08:00! | ||
content/blog/2022-06-24-fedora-i3.md deleted −153
| @@ -1,153 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-06-24 | ||
| 3 | title = "Rebooting My Love Affair with Linux" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Leaving macOS | ||
| 9 | |||
| 10 | As I noted [in a recent post](../foss-macos-apps), I have been planning on | ||
| 11 | migrating from macOS back to a Linux-based OS. I am happy to say that I have | ||
| 12 | finally completed my migration and am now stuck in the wonderful world of Linux | ||
| 13 | again. | ||
| 14 | |||
| 15 | My decision to leave macOS really came down to just a few important things: | ||
| 16 | |||
| 17 | - Apple Security (Gatekeeper) restricting me from running any software I want. | ||
| 18 | Even if you disable Gatekeeper and allow software to bypass the rest of the | ||
| 19 | device installation security, you still have to repeat that process every | ||
| 20 | time the allowed software is updated. | ||
| 21 | - macOS sends out nearly constant connections, pings, telemetry, etc. to a | ||
| 22 | myriad of mysterious Apple services. I'm not even going to dive into how | ||
| 23 | many macOS apps have constant telemetry on, as well. | ||
| 24 | - Lastly, I just _really_ missed the customization and freedom that comes with | ||
| 25 | Linux. Being able to switch to entirely new kernel, OS, or desktop within | ||
| 26 | minutes is a freedom I took for granted when I switched to macOS. | ||
| 27 | |||
| 28 | Now that I've covered macOS, I'm going to move on to more exciting topics: my | ||
| 29 | personal choice of OS, DE, and various customizations I'm using. | ||
| 30 | |||
| 31 | # Fedora | ||
| 32 | |||
| 33 | After trying a ton of distros (I think I booted and tested around 20-25 | ||
| 34 | distros), I finally landed on [Fedora Linux](https://getfedora.org/). I have | ||
| 35 | quite a bit of experience with Fedora and enjoy the `dnf` package manager. | ||
| 36 | Fedora allows me to keep up-to-date with recent software (I'm looking at you, | ||
| 37 | Debian), but still provides a level of stability you don't find in every distro. | ||
| 38 | |||
| 39 | In a very close second place was Arch Linux, as well as its spin-off: Garuda | ||
| 40 | Linux (Garuda w/ sway is _beautiful_). Arch is great for compatibility and the | ||
| 41 | massive community it has, but I have just never had the time to properly sit | ||
| 42 | down and learn the methodology behind their packaging systems. | ||
| 43 | |||
| 44 | Basically, everything else I tested was unacceptable in at least one way or | ||
| 45 | another. Void (`glibc`) was great, but doesn't support all the software I need. | ||
| 46 | Slackware worked well as a tui, but I wasn't skilled enough to get a tiling | ||
| 47 | window manager (WM) working on it. | ||
| 48 | |||
| 49 | ## i3 | ||
| 50 | |||
| 51 | One of the reasons I settled on Fedora is that it comes with an official i3 | ||
| 52 | spin. Being able to use a tiling WM, such as i3 or sway, is one of the biggest | ||
| 53 | things I wanted to do as soon as I adopted Linux again. | ||
| 54 | |||
| 55 | I will probably set up a dotfile repository soon, so that I don't lose any of my | ||
| 56 | configurations, but nothing big has been configured thus far. | ||
| 57 | |||
| 58 | The two main things I have updated in i3wm are natural scrolling and binding my | ||
| 59 | brightness keys to the `brightnessctl` program. | ||
| 60 | |||
| 61 | 1. Natural Scrolling | ||
| 62 | |||
| 63 | You can enable natural scrolling by opening the following file: | ||
| 64 | |||
| 65 | ```sh | ||
| 66 | sudo nano /usr/share/X11/xorg.conf.d/40-libinput.conf | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Within the `40-libinput.conf` file, find the following input sections and | ||
| 70 | enable the natural scrolling option. | ||
| 71 | |||
| 72 | This is the `pointer` section: | ||
| 73 | |||
| 74 | ```conf | ||
| 75 | Section "InputClass" | ||
| 76 | Identifier "libinput pointer catchall" | ||
| 77 | MatchIsPointer "on" | ||
| 78 | MatchDevicePath "/dev/input/event*" | ||
| 79 | Driver "libinput" | ||
| 80 | Option "NaturalScrolling" "True" | ||
| 81 | EndSection | ||
| 82 | ``` | ||
| 83 | |||
| 84 | This is the `touchpad` section: | ||
| 85 | |||
| 86 | ```conf | ||
| 87 | Section "InputClass" | ||
| 88 | Identifier "libinput touchpad catchall" | ||
| 89 | MatchIsTouchpad "on" | ||
| 90 | MatchDevicePath "/dev/input/event*" | ||
| 91 | Driver "libinput" | ||
| 92 | Option "NaturalScrolling" "True" | ||
| 93 | EndSection | ||
| 94 | ``` | ||
| 95 | |||
| 96 | 2. Enabling Brightness Keys | ||
| 97 | |||
| 98 | Likewise, enabling brightness key functionality is as simple as binding the | ||
| 99 | keys to the `brightnessctl` program. | ||
| 100 | |||
| 101 | To do this, open up your i3 config file. Mine is located here: | ||
| 102 | |||
| 103 | ```sh | ||
| 104 | nano /home/<my-user>/.config/i3/config | ||
| 105 | ``` | ||
| 106 | |||
| 107 | ```conf | ||
| 108 | # Use brightnessctl to adjust brightness. | ||
| 109 | bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl --min-val=2 -q set 3%- | ||
| 110 | bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl -q set 3%+ | ||
| 111 | ``` | ||
| 112 | |||
| 113 | 3. `polybar` | ||
| 114 | |||
| 115 | Instead of using the default `i3status` bar, I have opted to use `polybar` | ||
| 116 | instead (as you can also see in the screenshot above). | ||
| 117 | |||
| 118 | My config for this menu bar is basically just the default settings with | ||
| 119 | modified colors and an added battery block to quickly show me the machine's | ||
| 120 | battery info. | ||
| 121 | |||
| 122 | 4. `alacritty` | ||
| 123 | |||
| 124 | Not much to say on this part yet, as I haven't configured it much, but I | ||
| 125 | installed `alacritty` as my default terminal, and I am using `zsh` and the | ||
| 126 | shell. | ||
| 127 | |||
| 128 | # Software Choices | ||
| 129 | |||
| 130 | Again, I'm not going to say much that I haven't said yet in other blog posts, so | ||
| 131 | I'll just do a quick rundown of the apps I installed immediately after I set up | ||
| 132 | the environment. | ||
| 133 | |||
| 134 | Flatpak Apps: | ||
| 135 | |||
| 136 | - Cryptomator | ||
| 137 | - pCloud | ||
| 138 | - Signal | ||
| 139 | |||
| 140 | Fedora Packages: | ||
| 141 | |||
| 142 | - gomuks | ||
| 143 | - neomutt | ||
| 144 | - neofetch | ||
| 145 | - Firefox | ||
| 146 | - uBlock Origin | ||
| 147 | - Bitwarden | ||
| 148 | - Stylus | ||
| 149 | - Privacy Redirect | ||
| 150 | |||
| 151 | Other: | ||
| 152 | |||
| 153 | - exiftool | ||
content/blog/2022-06-24-fedora-i3.org added +156
| @@ -0,0 +1,156 @@ | |||
| 1 | #+date: <2022-06-24> | ||
| 2 | #+title: Rebooting My Love Affair with Linux | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Leaving macOS | ||
| 7 | |||
| 8 | As I noted [[../foss-macos-apps][in a recent post]], I have been | ||
| 9 | planning on migrating from macOS back to a Linux-based OS. I am happy to | ||
| 10 | say that I have finally completed my migration and am now stuck in the | ||
| 11 | wonderful world of Linux again. | ||
| 12 | |||
| 13 | My decision to leave macOS really came down to just a few important | ||
| 14 | things: | ||
| 15 | |||
| 16 | - Apple Security (Gatekeeper) restricting me from running any software I | ||
| 17 | want. Even if you disable Gatekeeper and allow software to bypass the | ||
| 18 | rest of the device installation security, you still have to repeat | ||
| 19 | that process every time the allowed software is updated. | ||
| 20 | - macOS sends out nearly constant connections, pings, telemetry, etc. to | ||
| 21 | a myriad of mysterious Apple services. I'm not even going to dive into | ||
| 22 | how many macOS apps have constant telemetry on, as well. | ||
| 23 | - Lastly, I just /really/ missed the customization and freedom that | ||
| 24 | comes with Linux. Being able to switch to entirely new kernel, OS, or | ||
| 25 | desktop within minutes is a freedom I took for granted when I switched | ||
| 26 | to macOS. | ||
| 27 | |||
| 28 | Now that I've covered macOS, I'm going to move on to more exciting | ||
| 29 | topics: my personal choice of OS, DE, and various customizations I'm | ||
| 30 | using. | ||
| 31 | |||
| 32 | * Fedora | ||
| 33 | |||
| 34 | After trying a ton of distros (I think I booted and tested around 20-25 | ||
| 35 | distros), I finally landed on [[https://getfedora.org/][Fedora Linux]]. | ||
| 36 | I have quite a bit of experience with Fedora and enjoy the =dnf= package | ||
| 37 | manager. Fedora allows me to keep up-to-date with recent software (I'm | ||
| 38 | looking at you, Debian), but still provides a level of stability you | ||
| 39 | don't find in every distro. | ||
| 40 | |||
| 41 | In a very close second place was Arch Linux, as well as its spin-off: | ||
| 42 | Garuda Linux (Garuda w/ sway is /beautiful/). Arch is great for | ||
| 43 | compatibility and the massive community it has, but I have just never | ||
| 44 | had the time to properly sit down and learn the methodology behind their | ||
| 45 | packaging systems. | ||
| 46 | |||
| 47 | Basically, everything else I tested was unacceptable in at least one way | ||
| 48 | or another. Void (=glibc=) was great, but doesn't support all the | ||
| 49 | software I need. Slackware worked well as a tui, but I wasn't skilled | ||
| 50 | enough to get a tiling window manager (WM) working on it. | ||
| 51 | |||
| 52 | ** i3 | ||
| 53 | |||
| 54 | One of the reasons I settled on Fedora is that it comes with an official | ||
| 55 | i3 spin. Being able to use a tiling WM, such as i3 or sway, is one of | ||
| 56 | the biggest things I wanted to do as soon as I adopted Linux again. | ||
| 57 | |||
| 58 | I will probably set up a dotfile repository soon, so that I don't lose | ||
| 59 | any of my configurations, but nothing big has been configured thus far. | ||
| 60 | |||
| 61 | The two main things I have updated in i3wm are natural scrolling and | ||
| 62 | binding my brightness keys to the =brightnessctl= program. | ||
| 63 | |||
| 64 | 1. Natural Scrolling | ||
| 65 | |||
| 66 | You can enable natural scrolling by opening the following file: | ||
| 67 | |||
| 68 | #+begin_src sh | ||
| 69 | sudo nano /usr/share/X11/xorg.conf.d/40-libinput.conf | ||
| 70 | #+end_src | ||
| 71 | |||
| 72 | Within the =40-libinput.conf= file, find the following input sections | ||
| 73 | and enable the natural scrolling option. | ||
| 74 | |||
| 75 | This is the =pointer= section: | ||
| 76 | |||
| 77 | #+begin_src conf | ||
| 78 | Section "InputClass" | ||
| 79 | Identifier "libinput pointer catchall" | ||
| 80 | MatchIsPointer "on" | ||
| 81 | MatchDevicePath "/dev/input/event*" | ||
| 82 | Driver "libinput" | ||
| 83 | Option "NaturalScrolling" "True" | ||
| 84 | EndSection | ||
| 85 | #+end_src | ||
| 86 | |||
| 87 | This is the =touchpad= section: | ||
| 88 | |||
| 89 | #+begin_src conf | ||
| 90 | Section "InputClass" | ||
| 91 | Identifier "libinput touchpad catchall" | ||
| 92 | MatchIsTouchpad "on" | ||
| 93 | MatchDevicePath "/dev/input/event*" | ||
| 94 | Driver "libinput" | ||
| 95 | Option "NaturalScrolling" "True" | ||
| 96 | EndSection | ||
| 97 | #+end_src | ||
| 98 | |||
| 99 | 2. Enabling Brightness Keys | ||
| 100 | |||
| 101 | Likewise, enabling brightness key functionality is as simple as | ||
| 102 | binding the keys to the =brightnessctl= program. | ||
| 103 | |||
| 104 | To do this, open up your i3 config file. Mine is located here: | ||
| 105 | |||
| 106 | #+begin_src sh | ||
| 107 | nano /home/<my-user>/.config/i3/config | ||
| 108 | #+end_src | ||
| 109 | |||
| 110 | #+begin_src conf | ||
| 111 | # Use brightnessctl to adjust brightness. | ||
| 112 | bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl --min-val=2 -q set 3%- | ||
| 113 | bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl -q set 3%+ | ||
| 114 | #+end_src | ||
| 115 | |||
| 116 | 3. =polybar= | ||
| 117 | |||
| 118 | Instead of using the default =i3status= bar, I have opted to use | ||
| 119 | =polybar= instead (as you can also see in the screenshot above). | ||
| 120 | |||
| 121 | My config for this menu bar is basically just the default settings | ||
| 122 | with modified colors and an added battery block to quickly show me | ||
| 123 | the machine's battery info. | ||
| 124 | |||
| 125 | 4. =alacritty= | ||
| 126 | |||
| 127 | Not much to say on this part yet, as I haven't configured it much, | ||
| 128 | but I installed =alacritty= as my default terminal, and I am using | ||
| 129 | =zsh= and the shell. | ||
| 130 | |||
| 131 | * Software Choices | ||
| 132 | |||
| 133 | Again, I'm not going to say much that I haven't said yet in other blog | ||
| 134 | posts, so I'll just do a quick rundown of the apps I installed | ||
| 135 | immediately after I set up the environment. | ||
| 136 | |||
| 137 | Flatpak Apps: | ||
| 138 | |||
| 139 | - Cryptomator | ||
| 140 | - pCloud | ||
| 141 | - Signal | ||
| 142 | |||
| 143 | Fedora Packages: | ||
| 144 | |||
| 145 | - gomuks | ||
| 146 | - neomutt | ||
| 147 | - neofetch | ||
| 148 | - Firefox | ||
| 149 | - uBlock Origin | ||
| 150 | - Bitwarden | ||
| 151 | - Stylus | ||
| 152 | - Privacy Redirect | ||
| 153 | |||
| 154 | Other: | ||
| 155 | |||
| 156 | - exiftool | ||
content/blog/2022-07-01-git-server.md deleted −624
| @@ -1,624 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-07-01 | ||
| 3 | title = "Self-Hosting a Personal Git Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # My Approach to Self-Hosting Git | ||
| 9 | |||
| 10 | I have often tried to self-host my Git repositories, but have always fallen | ||
| 11 | short when I tried to find a suitable web interface to show on the front-end. | ||
| 12 | |||
| 13 | After a few years, I have finally found a combination of methods that allow me | ||
| 14 | to easily self-host my projects, view them on the web, and access them from | ||
| 15 | anywhere. | ||
| 16 | |||
| 17 | Before I dive into the details, I want to state a high-level summary of my | ||
| 18 | self-hosted Git approach: | ||
| 19 | |||
| 20 | - This method uses the `ssh://` (read & write) and `git://` (read-only) | ||
| 21 | protocols for push and pull access. | ||
| 22 | - For the `git://` protocol, I create a `git-daemon-export-ok` file in any | ||
| 23 | repository that I want to be cloneable by anyone. | ||
| 24 | - The web interface I am using (`cgit`) allows simple HTTP cloning by | ||
| 25 | default. I do not disable this setting as I want beginners to be able to | ||
| 26 | clone one of my repositories even if they don't know the proper method. | ||
| 27 | - I am not enabling Smart HTTPS for any repositories. Updates to repositories | ||
| 28 | must be pushed via SSH. | ||
| 29 | - Beyond the actual repository management, I am using `cgit` for the front-end | ||
| 30 | web interface. | ||
| 31 | - If you use the `scan-path=<path>` configuration in the `cgitrc` | ||
| 32 | configuration file to automatically find repositories, you can't exclude | ||
| 33 | a repository from `cgit` if it's stored within the path that `cgit` | ||
| 34 | reads. To host private repositories, you'd need to set up another | ||
| 35 | directory that `cgit` can't read. | ||
| 36 | |||
| 37 | # Assumptions | ||
| 38 | |||
| 39 | For the purposes of this walkthrough, I am assuming you have a URL | ||
| 40 | (`git.example.com`) or IP address (`207.84.26.991`) addressed to the server that | ||
| 41 | you will be using to host your git repositories. | ||
| 42 | |||
| 43 | # Adding a Git User | ||
| 44 | |||
| 45 | In order to use the SSH method associated with git, we will need to add a user | ||
| 46 | named `git`. If you have used the SSH method for other git hosting sites, you | ||
| 47 | are probably used to the following syntax: | ||
| 48 | |||
| 49 | ```sh | ||
| 50 | git clone [user@]server:project.git | ||
| 51 | ``` | ||
| 52 | |||
| 53 | The syntax above is an `scp`-like syntax for using SSH on the `git` user on the | ||
| 54 | server to access your repository. | ||
| 55 | |||
| 56 | Let's delete any remnants of an old `git` user, if any, and create the new user | ||
| 57 | account: | ||
| 58 | |||
| 59 | ```sh | ||
| 60 | sudo deluser --remove-home git | ||
| 61 | sudo adduser git | ||
| 62 | ``` | ||
| 63 | |||
| 64 | ## Import Your SSH Keys to the Git User | ||
| 65 | |||
| 66 | Once the `git` user is created, you will need to copy your public SSH key on | ||
| 67 | your local development machine to the `git` user on the server. | ||
| 68 | |||
| 69 | If you don't have an SSH key yet, create one with this command: | ||
| 70 | |||
| 71 | ```sh | ||
| 72 | ssh-keygen | ||
| 73 | ``` | ||
| 74 | |||
| 75 | Once you create the key pair, the public should be saved to `~/.ssh/id_rsa.pub`. | ||
| 76 | |||
| 77 | If your server still has password-based authentication available, you can copy | ||
| 78 | it over to your user's home directory like this: | ||
| 79 | |||
| 80 | ```sh | ||
| 81 | ssh-copy-id git@server | ||
| 82 | ``` | ||
| 83 | |||
| 84 | Otherwise, copy it over to any user that you can access. | ||
| 85 | |||
| 86 | ```sh | ||
| 87 | scp ~/.ssh/id_rsa.pub your_user@your_server: | ||
| 88 | ``` | ||
| 89 | |||
| 90 | Once on the server, you will need to copy the contents into the `git` user's | ||
| 91 | `authorized_keys` file: | ||
| 92 | |||
| 93 | ```sh | ||
| 94 | cat id_rsa.pub > /home/git/.ssh/authorized_keys | ||
| 95 | ``` | ||
| 96 | |||
| 97 | ## (Optional) Disable Password-Based SSH | ||
| 98 | |||
| 99 | If you want to lock down your server and ensure that no one can authenticate in | ||
| 100 | via SSH with a password, you will need to edit your SSH configuration. | ||
| 101 | |||
| 102 | ```sh | ||
| 103 | sudo nano /etc/ssh/sshd_config | ||
| 104 | ``` | ||
| 105 | |||
| 106 | Within this file, find the following settings and set them to the values I am | ||
| 107 | showing below: | ||
| 108 | |||
| 109 | ```conf | ||
| 110 | PermitRootLogin no | ||
| 111 | PasswordAuthentication no | ||
| 112 | AuthenticationMethods publickey | ||
| 113 | ``` | ||
| 114 | |||
| 115 | You may have other Authentication Methods required in your personal set-up, so | ||
| 116 | the key here is just to ensure that `AuthenticationMethods` does not allow | ||
| 117 | passwords. | ||
| 118 | |||
| 119 | ### Setting up the Base Directory | ||
| 120 | |||
| 121 | Now that we have set up a `git` user to handle all transport methods, we need to | ||
| 122 | set up the directory that we will be using as our base of all repositories. | ||
| 123 | |||
| 124 | In my case, I am using `/git` as my source folder. To create this folder and | ||
| 125 | assign it to the user we created, execute the following commands: | ||
| 126 | |||
| 127 | ```sh | ||
| 128 | sudo mkdir /git | ||
| 129 | sudo chown -R git:git /git | ||
| 130 | ``` | ||
| 131 | |||
| 132 | ### Creating a Test Repository | ||
| 133 | |||
| 134 | On your server, switch over to the `git` user in order to start managing git | ||
| 135 | files. | ||
| 136 | |||
| 137 | ```sh | ||
| 138 | su git | ||
| 139 | ``` | ||
| 140 | |||
| 141 | Once logged-in as the `git` user, go to your base directory and create a test | ||
| 142 | repository. | ||
| 143 | |||
| 144 | ```sh | ||
| 145 | cd /git | ||
| 146 | mkdir test.git && cd test.git | ||
| 147 | git init --bare | ||
| 148 | ``` | ||
| 149 | |||
| 150 | If you want to make this repo viewable/cloneable to the public via the `git://` | ||
| 151 | protocol, you need to create a `git-daemon-export-ok` file inside the | ||
| 152 | repository. | ||
| 153 | |||
| 154 | ```sh | ||
| 155 | touch git-daemon-export-ok | ||
| 156 | ``` | ||
| 157 | |||
| 158 | # Change the Login Shell for `git` | ||
| 159 | |||
| 160 | To make sure that the `git` user is only used for git operations and nothing | ||
| 161 | else, you need to change the user's login shell. To do this, simply use the | ||
| 162 | `chsh` command: | ||
| 163 | |||
| 164 | ```sh | ||
| 165 | sudo chsh git | ||
| 166 | ``` | ||
| 167 | |||
| 168 | The interactive prompt will ask which shell you want the `git` user to use. You | ||
| 169 | must use the following value: | ||
| 170 | |||
| 171 | ```sh | ||
| 172 | /usr/bin/git-shell | ||
| 173 | ``` | ||
| 174 | |||
| 175 | Once done, no one will be able to SSH to the `git` user or execute commands | ||
| 176 | other than the standard git commands. | ||
| 177 | |||
| 178 | # Opening the Firewall | ||
| 179 | |||
| 180 | Don't forget to open up ports on the device firewall and network firewall if you | ||
| 181 | want to access these repositories publicly. If you're using default ports, | ||
| 182 | forward ports `22` (ssh) and `9418` (git) from your router to your server's IP | ||
| 183 | address. | ||
| 184 | |||
| 185 | If your server also has a firewall, ensure that the firewall allows the same | ||
| 186 | ports that are forwarded from the router. For example, if you use `ufw`: | ||
| 187 | |||
| 188 | ```sh | ||
| 189 | sudo ufw allow 22 | ||
| 190 | sudo ufw allow 9418 | ||
| 191 | ``` | ||
| 192 | |||
| 193 | ## Non-Standard SSH Ports | ||
| 194 | |||
| 195 | If you use a non-standard port for SSH, such as `9876`, you will need to create | ||
| 196 | an SSH configuration file on your local development machine in order to connect | ||
| 197 | to your server's git repositories. | ||
| 198 | |||
| 199 | To do this, you'll need to define your custom port on your client machine in | ||
| 200 | your `~/.ssh/config` file: | ||
| 201 | |||
| 202 | ```sh | ||
| 203 | nano ~/.ssh/config | ||
| 204 | ``` | ||
| 205 | |||
| 206 | ```conf | ||
| 207 | Host git.example.com | ||
| 208 | # HostName can be a URL or an IP address | ||
| 209 | HostName git.example.com | ||
| 210 | Port 9876 | ||
| 211 | User git | ||
| 212 | ``` | ||
| 213 | |||
| 214 | ## Testing SSH | ||
| 215 | |||
| 216 | There are two main syntaxes you can use to manage git over SSH: | ||
| 217 | |||
| 218 | - `git clone [user@]server:project.git` | ||
| 219 | - `git clone ssh://[user@]server/project.git` | ||
| 220 | |||
| 221 | I prefer the first, which is an `scp`-like syntax. To test it, try to clone the | ||
| 222 | test repository you set up on the server: | ||
| 223 | |||
| 224 | ```sh | ||
| 225 | git clone git@git.example.com:/git/test.git | ||
| 226 | ``` | ||
| 227 | |||
| 228 | # Enabling Read-Only Access | ||
| 229 | |||
| 230 | If you want people to be able to clone any repository where you've placed a | ||
| 231 | `git-daemon-export-ok` file, you will need to start the git daemon. | ||
| 232 | |||
| 233 | To do this on a system with `systemd`, create a service file: | ||
| 234 | |||
| 235 | ```sh | ||
| 236 | sudo nano /etc/systemd/system/git-daemon.service | ||
| 237 | ``` | ||
| 238 | |||
| 239 | Inside the `git-daemon.service` file, paste the following: | ||
| 240 | |||
| 241 | ```conf | ||
| 242 | [Unit] | ||
| 243 | Description=Start Git Daemon | ||
| 244 | |||
| 245 | [Service] | ||
| 246 | ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/git/ /git/ | ||
| 247 | |||
| 248 | Restart=always | ||
| 249 | RestartSec=500ms | ||
| 250 | |||
| 251 | StandardOutput=syslog | ||
| 252 | StandardError=syslog | ||
| 253 | SyslogIdentifier=git-daemon | ||
| 254 | |||
| 255 | User=git | ||
| 256 | Group=git | ||
| 257 | |||
| 258 | [Install] | ||
| 259 | WantedBy=multi-user.target | ||
| 260 | ``` | ||
| 261 | |||
| 262 | Once created, enable and start the service: | ||
| 263 | |||
| 264 | ```sh | ||
| 265 | sudo systemctl enable git-daemon.service | ||
| 266 | sudo systemctl start git-daemon.service | ||
| 267 | ``` | ||
| 268 | |||
| 269 | To clone read-only via the `git://` protocol, you can use the following syntax: | ||
| 270 | |||
| 271 | ```sh | ||
| 272 | git clone git://git.example.com/test.git | ||
| 273 | ``` | ||
| 274 | |||
| 275 | # Migrating Repositories | ||
| 276 | |||
| 277 | At this point, we have a working git server that works with both SSH and | ||
| 278 | read-only access. | ||
| 279 | |||
| 280 | For each of the repositories I had hosted a different provider, I executed the | ||
| 281 | following commands in order to place a copy on my server as my new source of | ||
| 282 | truth: | ||
| 283 | |||
| 284 | Server: | ||
| 285 | |||
| 286 | ```sh | ||
| 287 | su git | ||
| 288 | mkdir /git/<REPOSITORY_NAME>.git && cd /git/<REPOSITORY_NAME>.git | ||
| 289 | git init --bare | ||
| 290 | |||
| 291 | # If you want to make this repo viewable/cloneable to the public | ||
| 292 | touch git-daemon-export-ok | ||
| 293 | ``` | ||
| 294 | |||
| 295 | Client: | ||
| 296 | |||
| 297 | ```sh | ||
| 298 | git clone git@<PREVIOUS_HOST>:<REPOSITORY_NAME> | ||
| 299 | git remote set-url origin git@git.EXAMPLE.COM:/git/<REPOSITORY_NAME>.git | ||
| 300 | git push | ||
| 301 | ``` | ||
| 302 | |||
| 303 | # Optional Web View: `cgit` | ||
| 304 | |||
| 305 | If you want a web viewer for your repositories, you can use various tools, such | ||
| 306 | as `gitweb`, `cgit`, or `klaus`. I chose `cgit` due to its simple interface and | ||
| 307 | fairly easy set-up (compared to others). Not to mention that the [Linux kernel | ||
| 308 | uses `cgit`](https://git.kernel.org/). | ||
| 309 | |||
| 310 | ## Docker Compose | ||
| 311 | |||
| 312 | Instead of using my previous method of using a `docker run` command, I've | ||
| 313 | updated this section to use `docker-compose` instead for an easier installation | ||
| 314 | and simpler management and configuration. | ||
| 315 | |||
| 316 | In order to use Docker Compose, you will set up a `docker-compose.yml` file to | ||
| 317 | automatically connect resources like the repositories, `cgitrc`, and various | ||
| 318 | files or folders to the `cgit` container you're creating: | ||
| 319 | |||
| 320 | ```sh | ||
| 321 | mkdir ~/cgit && cd ~/cgit | ||
| 322 | nano docker-compose.yml | ||
| 323 | ``` | ||
| 324 | |||
| 325 | ```conf | ||
| 326 | # docker-compose.yml | ||
| 327 | version: '3' | ||
| 328 | |||
| 329 | services: | ||
| 330 | cgit: | ||
| 331 | image: invokr/cgit | ||
| 332 | volumes: | ||
| 333 | - /git:/git | ||
| 334 | - ./cgitrc:/etc/cgitrc | ||
| 335 | - ./logo.png:/var/www/htdocs/cgit/logo.png | ||
| 336 | - ./favicon.png:/var/www/htdocs/cgit/favicon.png | ||
| 337 | - ./filters:/var/www/htdocs/cgit/filters | ||
| 338 | ports: | ||
| 339 | - "8763:80" | ||
| 340 | restart: always | ||
| 341 | ``` | ||
| 342 | |||
| 343 | Then, just start the container: | ||
| 344 | |||
| 345 | ```sh | ||
| 346 | sudo docker-compose up -d | ||
| 347 | ``` | ||
| 348 | |||
| 349 | Once it's finished installing, you can access the site at `<SERVER_IP>:8763` or | ||
| 350 | use a reverse-proxy service to forward `cgit` to a URL, such as | ||
| 351 | `git.example.com`. See the next section for more details on reverse proxying a | ||
| 352 | URL to a local port. | ||
| 353 | |||
| 354 | ## Nginx Reverse Proxy | ||
| 355 | |||
| 356 | I am using Nginx as my reverse proxy so that the `cgit` Docker container can use | ||
| 357 | `git.example.com` as its URL. To do so, I simply created the following | ||
| 358 | configuration file: | ||
| 359 | |||
| 360 | ```sh | ||
| 361 | sudo nano /etc/nginx/sites-available/git.example.com | ||
| 362 | ``` | ||
| 363 | |||
| 364 | ```conf | ||
| 365 | server { | ||
| 366 | listen 80; | ||
| 367 | server_name git.example.com; | ||
| 368 | |||
| 369 | if ($host = git.example.com) { | ||
| 370 | return 301 https://$host$request_uri; | ||
| 371 | } | ||
| 372 | |||
| 373 | return 404; | ||
| 374 | } | ||
| 375 | |||
| 376 | server { | ||
| 377 | server_name git.example.com; | ||
| 378 | listen 443 ssl http2; | ||
| 379 | |||
| 380 | location / { | ||
| 381 | # The final `/` is important. | ||
| 382 | proxy_pass http://localhost:8763/; | ||
| 383 | add_header X-Frame-Options SAMEORIGIN; | ||
| 384 | add_header X-XSS-Protection "1; mode=block"; | ||
| 385 | proxy_redirect off; | ||
| 386 | proxy_buffering off; | ||
| 387 | proxy_set_header Host $host; | ||
| 388 | proxy_set_header X-Real-IP $remote_addr; | ||
| 389 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 390 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 391 | proxy_set_header X-Forwarded-Port $server_port; | ||
| 392 | } | ||
| 393 | |||
| 394 | # INCLUDE ANY SSL CERTS HERE | ||
| 395 | include /etc/letsencrypt/options-ssl-nginx.conf; | ||
| 396 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | ||
| 397 | } | ||
| 398 | ``` | ||
| 399 | |||
| 400 | Once created, symlink it and restart the web server. | ||
| 401 | |||
| 402 | ```sh | ||
| 403 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/ | ||
| 404 | sudo systemctl restart nginx.service | ||
| 405 | ``` | ||
| 406 | |||
| 407 | As we can see below, my site at `git.example.com` is available and running: | ||
| 408 | |||
| 409 | ## Settings Up Git Details | ||
| 410 | |||
| 411 | Once you have `cgit` running, you can add some small details, such as repository | ||
| 412 | owners and descriptions by editing the following files within each repository. | ||
| 413 | |||
| 414 | Alternatively, you can use the `cgitrc` file to edit these details if you only | ||
| 415 | care to edit them for the purpose of seeing them on your website. | ||
| 416 | |||
| 417 | The `description` file within the repository on your server will display the | ||
| 418 | description online. | ||
| 419 | |||
| 420 | ```sh | ||
| 421 | cd /git/example.git | ||
| 422 | nano description | ||
| 423 | ``` | ||
| 424 | |||
| 425 | You can add a `[gitweb]` block to the `config` file in order to display the | ||
| 426 | owner of the repository. | ||
| 427 | |||
| 428 | ```sh | ||
| 429 | cd /git/example.git | ||
| 430 | nano config | ||
| 431 | ``` | ||
| 432 | |||
| 433 | ```conf | ||
| 434 | [gitweb] | ||
| 435 | owner = "YourName" | ||
| 436 | ``` | ||
| 437 | |||
| 438 | Note that you can ignore the configuration within each repository and simply set | ||
| 439 | up this information in the `cgitrc` file, if you want to do it that way. | ||
| 440 | |||
| 441 | ## Editing `cgit` | ||
| 442 | |||
| 443 | In order to edit certain items within `cgit`, you need to edit the `cgitrc` | ||
| 444 | file. | ||
| 445 | |||
| 446 | ```sh | ||
| 447 | nano ~/cgit/cgitrc | ||
| 448 | ``` | ||
| 449 | |||
| 450 | Below is an example configuration for `cgitrc`. You can find all the | ||
| 451 | configuration options within the [configuration manual] | ||
| 452 | (<https://git.zx2c4.com/cgit/plain/cgitrc.5.txt>). | ||
| 453 | |||
| 454 | ```conf | ||
| 455 | css=/cgit.css | ||
| 456 | logo=/logo.png | ||
| 457 | favicon=/favicon.png | ||
| 458 | robots=noindex, nofollow | ||
| 459 | |||
| 460 | enable-index-links=1 | ||
| 461 | enable-commit-graph=1 | ||
| 462 | enable-blame=1 | ||
| 463 | enable-log-filecount=1 | ||
| 464 | enable-log-linecount=1 | ||
| 465 | enable-git-config=1 | ||
| 466 | |||
| 467 | clone-url=git://git.example.com/$CGIT_REPO_URL ssh://git@git.example.com:/git/$CGIT_REPO_URL | ||
| 468 | |||
| 469 | root-title=My Git Website | ||
| 470 | root-desc=My personal git repositories. | ||
| 471 | |||
| 472 | # Allow download of tar.gz, tar.bz2 and zip-files | ||
| 473 | snapshots=tar.gz tar.bz2 zip | ||
| 474 | |||
| 475 | ## | ||
| 476 | ## List of common mimetypes | ||
| 477 | ## | ||
| 478 | mimetype.gif=image/gif | ||
| 479 | mimetype.html=text/html | ||
| 480 | mimetype.jpg=image/jpeg | ||
| 481 | mimetype.jpeg=image/jpeg | ||
| 482 | mimetype.pdf=application/pdf | ||
| 483 | mimetype.png=image/png | ||
| 484 | mimetype.svg=image/svg+xml | ||
| 485 | |||
| 486 | # Highlight source code | ||
| 487 | # source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.sh | ||
| 488 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | ||
| 489 | |||
| 490 | # Format markdown, restructuredtext, manpages, text files, and html files | ||
| 491 | # through the right converters | ||
| 492 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | ||
| 493 | |||
| 494 | ## | ||
| 495 | ## Search for these files in the root of the default branch of repositories | ||
| 496 | ## for coming up with the about page: | ||
| 497 | ## | ||
| 498 | readme=:README.md | ||
| 499 | readme=:readme.md | ||
| 500 | readme=:README.mkd | ||
| 501 | readme=:readme.mkd | ||
| 502 | readme=:README.rst | ||
| 503 | readme=:readme.rst | ||
| 504 | readme=:README.html | ||
| 505 | readme=:readme.html | ||
| 506 | readme=:README.htm | ||
| 507 | readme=:readme.htm | ||
| 508 | readme=:README.txt | ||
| 509 | readme=:readme.txt | ||
| 510 | readme=:README | ||
| 511 | readme=:readme | ||
| 512 | |||
| 513 | # Repositories | ||
| 514 | |||
| 515 | # Uncomment the following line to scan a path instead of adding repositories manually | ||
| 516 | # scan-path=/git | ||
| 517 | |||
| 518 | ## Test Section | ||
| 519 | section=git/test-section | ||
| 520 | |||
| 521 | repo.url=test.git | ||
| 522 | repo.path=/git/test.git | ||
| 523 | repo.readme=:README.md | ||
| 524 | repo.owner=John Doe | ||
| 525 | repo.desc=An example repository! | ||
| 526 | ``` | ||
| 527 | |||
| 528 | ## Final Fixes: Syntax Highlighting & README Rendering | ||
| 529 | |||
| 530 | After completing my initial install and playing around with it for a few days, I | ||
| 531 | noticed two issues: | ||
| 532 | |||
| 533 | 1. Syntax highlighting did not work when viewing the source code within a file. | ||
| 534 | 2. The `about` tab within a repository was not rendered to HTML. | ||
| 535 | |||
| 536 | The following process fixes these issues. To start, let's go to the `cgit` | ||
| 537 | directory where we were editing our configuration file earlier. | ||
| 538 | |||
| 539 | ```sh | ||
| 540 | cd ~/cgit | ||
| 541 | ``` | ||
| 542 | |||
| 543 | In here, create two folders that will hold our syntax files: | ||
| 544 | |||
| 545 | ```sh | ||
| 546 | mkdir filters && mkdir filters/html-converters && cd filters | ||
| 547 | ``` | ||
| 548 | |||
| 549 | Next, download the default filters: | ||
| 550 | |||
| 551 | ```sh | ||
| 552 | curl https://git.zx2c4.com/cgit/plain/filters/about-formatting.sh > about-formatting.sh | ||
| 553 | chmod 755 about-formatting.sh | ||
| 554 | curl https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py > syntax-highlighting.py | ||
| 555 | chmod 755 syntax-highlighting.py | ||
| 556 | ``` | ||
| 557 | |||
| 558 | Finally, download the HTML conversion files you need. The example below | ||
| 559 | downloads the Markdown converter: | ||
| 560 | |||
| 561 | ```sh | ||
| 562 | cd html-converters | ||
| 563 | curl https://git.zx2c4.com/cgit/plain/filters/html-converters/md2html > md2html | ||
| 564 | chmod 755 md2html | ||
| 565 | ``` | ||
| 566 | |||
| 567 | If you need other filters or html-converters found within [the cgit project | ||
| 568 | files](https://git.zx2c4.com/cgit/tree/filters), repeat the `curl` and `chmod` | ||
| 569 | process above for whichever files you need. | ||
| 570 | |||
| 571 | However, formatting will not work quite yet since the Docker cgit container | ||
| 572 | we're using doesn't have the formatting package installed. You can install this | ||
| 573 | easily by install Python 3+ and the `pygments` package: | ||
| 574 | |||
| 575 | ```sh | ||
| 576 | # Enter the container's command line | ||
| 577 | sudo docker exec -it cgit bash | ||
| 578 | ``` | ||
| 579 | |||
| 580 | ```sh | ||
| 581 | # Install the necessary packages and then exit | ||
| 582 | yum update -y && \ | ||
| 583 | yum upgrade -y && \ | ||
| 584 | yum install python3 python3-pip -y && \ | ||
| 585 | pip3 install markdown pygments && \ | ||
| 586 | exit | ||
| 587 | ``` | ||
| 588 | |||
| 589 | **You will need to enter the cgit docker container and re-run these `yum` | ||
| 590 | commands every time you kill and restart the container!** | ||
| 591 | |||
| 592 | If not done already, we need to add the following variables to our `cgitrc` file | ||
| 593 | in order for `cgit` to know where our filtering files are: | ||
| 594 | |||
| 595 | ```conf | ||
| 596 | # Highlight source code with python pygments-based highlighter | ||
| 597 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | ||
| 598 | |||
| 599 | # Format markdown, restructuredtext, manpages, text files, and html files | ||
| 600 | # through the right converters | ||
| 601 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | ||
| 602 | ``` | ||
| 603 | |||
| 604 | Now you should see that syntax highlighting and README rendering to the `about` | ||
| 605 | tab is fixed. | ||
| 606 | |||
| 607 | ## Theming | ||
| 608 | |||
| 609 | I won't go into much detail in this section, but you can fully theme your | ||
| 610 | installation of `cgit` since you have access to the `cgit.css` file in your web | ||
| 611 | root. This is another file you can add as a volume to the `docker-compose.yml` | ||
| 612 | file if you want to edit this without entering the container's command line. | ||
| 613 | |||
| 614 | ### :warning: Remember to Back Up Your Data! | ||
| 615 | |||
| 616 | The last thing to note is that running services on your own equipment means that | ||
| 617 | you're assuming a level of risk that exists regarding data loss, catastrophes, | ||
| 618 | etc. In order to reduce the impact of any such occurrence, I suggest backing up | ||
| 619 | your data regularly. | ||
| 620 | |||
| 621 | Backups can be automated via `cron`, by hooking your base directory up to a | ||
| 622 | cloud provider, or even setting up hooks to push all repository info to git | ||
| 623 | mirrors on other git hosts. Whatever the method, make sure that your data | ||
| 624 | doesn't vanish in the event that your drives or servers fail. | ||
content/blog/2022-07-01-git-server.org added +638
| @@ -0,0 +1,638 @@ | |||
| 1 | #+date: <2022-07-01> | ||
| 2 | #+title: Self-Hosting a Personal Git Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * My Approach to Self-Hosting Git | ||
| 7 | |||
| 8 | I have often tried to self-host my Git repositories, but have always | ||
| 9 | fallen short when I tried to find a suitable web interface to show on | ||
| 10 | the front-end. | ||
| 11 | |||
| 12 | After a few years, I have finally found a combination of methods that | ||
| 13 | allow me to easily self-host my projects, view them on the web, and | ||
| 14 | access them from anywhere. | ||
| 15 | |||
| 16 | Before I dive into the details, I want to state a high-level summary of | ||
| 17 | my self-hosted Git approach: | ||
| 18 | |||
| 19 | - This method uses the =ssh://= (read & write) and =git://= (read-only) | ||
| 20 | protocols for push and pull access. | ||
| 21 | - For the =git://= protocol, I create a =git-daemon-export-ok= file in | ||
| 22 | any repository that I want to be cloneable by anyone. | ||
| 23 | - The web interface I am using (=cgit=) allows simple HTTP cloning by | ||
| 24 | default. I do not disable this setting as I want beginners to be | ||
| 25 | able to clone one of my repositories even if they don't know the | ||
| 26 | proper method. | ||
| 27 | - I am not enabling Smart HTTPS for any repositories. Updates to | ||
| 28 | repositories must be pushed via SSH. | ||
| 29 | - Beyond the actual repository management, I am using =cgit= for the | ||
| 30 | front-end web interface. | ||
| 31 | - If you use the =scan-path=<path>= configuration in the =cgitrc= | ||
| 32 | configuration file to automatically find repositories, you can't | ||
| 33 | exclude a repository from =cgit= if it's stored within the path that | ||
| 34 | =cgit= reads. To host private repositories, you'd need to set up | ||
| 35 | another directory that =cgit= can't read. | ||
| 36 | |||
| 37 | * Assumptions | ||
| 38 | |||
| 39 | For the purposes of this walkthrough, I am assuming you have a URL | ||
| 40 | (=git.example.com=) or IP address (=207.84.26.991=) addressed to the | ||
| 41 | server that you will be using to host your git repositories. | ||
| 42 | |||
| 43 | * Adding a Git User | ||
| 44 | |||
| 45 | In order to use the SSH method associated with git, we will need to add | ||
| 46 | a user named =git=. If you have used the SSH method for other git | ||
| 47 | hosting sites, you are probably used to the following syntax: | ||
| 48 | |||
| 49 | #+begin_src sh | ||
| 50 | git clone [user@]server:project.git | ||
| 51 | #+end_src | ||
| 52 | |||
| 53 | The syntax above is an =scp=-like syntax for using SSH on the =git= user | ||
| 54 | on the server to access your repository. | ||
| 55 | |||
| 56 | Let's delete any remnants of an old =git= user, if any, and create the | ||
| 57 | new user account: | ||
| 58 | |||
| 59 | #+begin_src sh | ||
| 60 | sudo deluser --remove-home git | ||
| 61 | sudo adduser git | ||
| 62 | #+end_src | ||
| 63 | |||
| 64 | ** Import Your SSH Keys to the Git User | ||
| 65 | |||
| 66 | Once the =git= user is created, you will need to copy your public SSH | ||
| 67 | key on your local development machine to the =git= user on the server. | ||
| 68 | |||
| 69 | If you don't have an SSH key yet, create one with this command: | ||
| 70 | |||
| 71 | #+begin_src sh | ||
| 72 | ssh-keygen | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | Once you create the key pair, the public should be saved to | ||
| 76 | =~/.ssh/id_rsa.pub=. | ||
| 77 | |||
| 78 | If your server still has password-based authentication available, you | ||
| 79 | can copy it over to your user's home directory like this: | ||
| 80 | |||
| 81 | #+begin_src sh | ||
| 82 | ssh-copy-id git@server | ||
| 83 | #+end_src | ||
| 84 | |||
| 85 | Otherwise, copy it over to any user that you can access. | ||
| 86 | |||
| 87 | #+begin_src sh | ||
| 88 | scp ~/.ssh/id_rsa.pub your_user@your_server: | ||
| 89 | #+end_src | ||
| 90 | |||
| 91 | Once on the server, you will need to copy the contents into the =git= | ||
| 92 | user's =authorized_keys= file: | ||
| 93 | |||
| 94 | #+begin_src sh | ||
| 95 | cat id_rsa.pub > /home/git/.ssh/authorized_keys | ||
| 96 | #+end_src | ||
| 97 | |||
| 98 | ** (Optional) Disable Password-Based SSH | ||
| 99 | |||
| 100 | If you want to lock down your server and ensure that no one can | ||
| 101 | authenticate in via SSH with a password, you will need to edit your SSH | ||
| 102 | configuration. | ||
| 103 | |||
| 104 | #+begin_src sh | ||
| 105 | sudo nano /etc/ssh/sshd_config | ||
| 106 | #+end_src | ||
| 107 | |||
| 108 | Within this file, find the following settings and set them to the values | ||
| 109 | I am showing below: | ||
| 110 | |||
| 111 | #+begin_src conf | ||
| 112 | PermitRootLogin no | ||
| 113 | PasswordAuthentication no | ||
| 114 | AuthenticationMethods publickey | ||
| 115 | #+end_src | ||
| 116 | |||
| 117 | You may have other Authentication Methods required in your personal | ||
| 118 | set-up, so the key here is just to ensure that =AuthenticationMethods= | ||
| 119 | does not allow passwords. | ||
| 120 | |||
| 121 | *** Setting up the Base Directory | ||
| 122 | |||
| 123 | Now that we have set up a =git= user to handle all transport methods, we | ||
| 124 | need to set up the directory that we will be using as our base of all | ||
| 125 | repositories. | ||
| 126 | |||
| 127 | In my case, I am using =/git= as my source folder. To create this folder | ||
| 128 | and assign it to the user we created, execute the following commands: | ||
| 129 | |||
| 130 | #+begin_src sh | ||
| 131 | sudo mkdir /git | ||
| 132 | sudo chown -R git:git /git | ||
| 133 | #+end_src | ||
| 134 | |||
| 135 | *** Creating a Test Repository | ||
| 136 | |||
| 137 | On your server, switch over to the =git= user in order to start managing | ||
| 138 | git files. | ||
| 139 | |||
| 140 | #+begin_src sh | ||
| 141 | su git | ||
| 142 | #+end_src | ||
| 143 | |||
| 144 | Once logged-in as the =git= user, go to your base directory and create a | ||
| 145 | test repository. | ||
| 146 | |||
| 147 | #+begin_src sh | ||
| 148 | cd /git | ||
| 149 | mkdir test.git && cd test.git | ||
| 150 | git init --bare | ||
| 151 | #+end_src | ||
| 152 | |||
| 153 | If you want to make this repo viewable/cloneable to the public via the | ||
| 154 | =git://= protocol, you need to create a =git-daemon-export-ok= file | ||
| 155 | inside the repository. | ||
| 156 | |||
| 157 | #+begin_src sh | ||
| 158 | touch git-daemon-export-ok | ||
| 159 | #+end_src | ||
| 160 | |||
| 161 | * Change the Login Shell for =git= | ||
| 162 | |||
| 163 | To make sure that the =git= user is only used for git operations and | ||
| 164 | nothing else, you need to change the user's login shell. To do this, | ||
| 165 | simply use the =chsh= command: | ||
| 166 | |||
| 167 | #+begin_src sh | ||
| 168 | sudo chsh git | ||
| 169 | #+end_src | ||
| 170 | |||
| 171 | The interactive prompt will ask which shell you want the =git= user to | ||
| 172 | use. You must use the following value: | ||
| 173 | |||
| 174 | #+begin_src sh | ||
| 175 | /usr/bin/git-shell | ||
| 176 | #+end_src | ||
| 177 | |||
| 178 | Once done, no one will be able to SSH to the =git= user or execute | ||
| 179 | commands other than the standard git commands. | ||
| 180 | |||
| 181 | * Opening the Firewall | ||
| 182 | |||
| 183 | Don't forget to open up ports on the device firewall and network | ||
| 184 | firewall if you want to access these repositories publicly. If you're | ||
| 185 | using default ports, forward ports =22= (ssh) and =9418= (git) from your | ||
| 186 | router to your server's IP address. | ||
| 187 | |||
| 188 | If your server also has a firewall, ensure that the firewall allows the | ||
| 189 | same ports that are forwarded from the router. For example, if you use | ||
| 190 | =ufw=: | ||
| 191 | |||
| 192 | #+begin_src sh | ||
| 193 | sudo ufw allow 22 | ||
| 194 | sudo ufw allow 9418 | ||
| 195 | #+end_src | ||
| 196 | |||
| 197 | ** Non-Standard SSH Ports | ||
| 198 | |||
| 199 | If you use a non-standard port for SSH, such as =9876=, you will need to | ||
| 200 | create an SSH configuration file on your local development machine in | ||
| 201 | order to connect to your server's git repositories. | ||
| 202 | |||
| 203 | To do this, you'll need to define your custom port on your client | ||
| 204 | machine in your =~/.ssh/config= file: | ||
| 205 | |||
| 206 | #+begin_src sh | ||
| 207 | nano ~/.ssh/config | ||
| 208 | #+end_src | ||
| 209 | |||
| 210 | #+begin_src conf | ||
| 211 | Host git.example.com | ||
| 212 | # HostName can be a URL or an IP address | ||
| 213 | HostName git.example.com | ||
| 214 | Port 9876 | ||
| 215 | User git | ||
| 216 | #+end_src | ||
| 217 | |||
| 218 | ** Testing SSH | ||
| 219 | |||
| 220 | There are two main syntaxes you can use to manage git over SSH: | ||
| 221 | |||
| 222 | - =git clone [user@]server:project.git= | ||
| 223 | - =git clone ssh://[user@]server/project.git= | ||
| 224 | |||
| 225 | I prefer the first, which is an =scp=-like syntax. To test it, try to | ||
| 226 | clone the test repository you set up on the server: | ||
| 227 | |||
| 228 | #+begin_src sh | ||
| 229 | git clone git@git.example.com:/git/test.git | ||
| 230 | #+end_src | ||
| 231 | |||
| 232 | * Enabling Read-Only Access | ||
| 233 | |||
| 234 | If you want people to be able to clone any repository where you've | ||
| 235 | placed a =git-daemon-export-ok= file, you will need to start the git | ||
| 236 | daemon. | ||
| 237 | |||
| 238 | To do this on a system with =systemd=, create a service file: | ||
| 239 | |||
| 240 | #+begin_src sh | ||
| 241 | sudo nano /etc/systemd/system/git-daemon.service | ||
| 242 | #+end_src | ||
| 243 | |||
| 244 | Inside the =git-daemon.service= file, paste the following: | ||
| 245 | |||
| 246 | #+begin_src conf | ||
| 247 | [Unit] | ||
| 248 | Description=Start Git Daemon | ||
| 249 | |||
| 250 | [Service] | ||
| 251 | ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/git/ /git/ | ||
| 252 | |||
| 253 | Restart=always | ||
| 254 | RestartSec=500ms | ||
| 255 | |||
| 256 | StandardOutput=syslog | ||
| 257 | StandardError=syslog | ||
| 258 | SyslogIdentifier=git-daemon | ||
| 259 | |||
| 260 | User=git | ||
| 261 | Group=git | ||
| 262 | |||
| 263 | [Install] | ||
| 264 | WantedBy=multi-user.target | ||
| 265 | #+end_src | ||
| 266 | |||
| 267 | Once created, enable and start the service: | ||
| 268 | |||
| 269 | #+begin_src sh | ||
| 270 | sudo systemctl enable git-daemon.service | ||
| 271 | sudo systemctl start git-daemon.service | ||
| 272 | #+end_src | ||
| 273 | |||
| 274 | To clone read-only via the =git://= protocol, you can use the following | ||
| 275 | syntax: | ||
| 276 | |||
| 277 | #+begin_src sh | ||
| 278 | git clone git://git.example.com/test.git | ||
| 279 | #+end_src | ||
| 280 | |||
| 281 | * Migrating Repositories | ||
| 282 | |||
| 283 | At this point, we have a working git server that works with both SSH and | ||
| 284 | read-only access. | ||
| 285 | |||
| 286 | For each of the repositories I had hosted a different provider, I | ||
| 287 | executed the following commands in order to place a copy on my server as | ||
| 288 | my new source of truth: | ||
| 289 | |||
| 290 | Server: | ||
| 291 | |||
| 292 | #+begin_src sh | ||
| 293 | su git | ||
| 294 | mkdir /git/<REPOSITORY_NAME>.git && cd /git/<REPOSITORY_NAME>.git | ||
| 295 | git init --bare | ||
| 296 | |||
| 297 | # If you want to make this repo viewable/cloneable to the public | ||
| 298 | touch git-daemon-export-ok | ||
| 299 | #+end_src | ||
| 300 | |||
| 301 | Client: | ||
| 302 | |||
| 303 | #+begin_src sh | ||
| 304 | git clone git@<PREVIOUS_HOST>:<REPOSITORY_NAME> | ||
| 305 | git remote set-url origin git@git.EXAMPLE.COM:/git/<REPOSITORY_NAME>.git | ||
| 306 | git push | ||
| 307 | #+end_src | ||
| 308 | |||
| 309 | * Optional Web View: =cgit= | ||
| 310 | |||
| 311 | If you want a web viewer for your repositories, you can use various | ||
| 312 | tools, such as =gitweb=, =cgit=, or =klaus=. I chose =cgit= due to its | ||
| 313 | simple interface and fairly easy set-up (compared to others). Not to | ||
| 314 | mention that the [[https://git.kernel.org/][Linux kernel uses =cgit=]]. | ||
| 315 | |||
| 316 | ** Docker Compose | ||
| 317 | |||
| 318 | Instead of using my previous method of using a =docker run= command, | ||
| 319 | I've updated this section to use =docker-compose= instead for an easier | ||
| 320 | installation and simpler management and configuration. | ||
| 321 | |||
| 322 | In order to use Docker Compose, you will set up a =docker-compose.yml= | ||
| 323 | file to automatically connect resources like the repositories, =cgitrc=, | ||
| 324 | and various files or folders to the =cgit= container you're creating: | ||
| 325 | |||
| 326 | #+begin_src sh | ||
| 327 | mkdir ~/cgit && cd ~/cgit | ||
| 328 | nano docker-compose.yml | ||
| 329 | #+end_src | ||
| 330 | |||
| 331 | #+begin_src conf | ||
| 332 | # docker-compose.yml | ||
| 333 | version: '3' | ||
| 334 | |||
| 335 | services: | ||
| 336 | cgit: | ||
| 337 | image: invokr/cgit | ||
| 338 | volumes: | ||
| 339 | - /git:/git | ||
| 340 | - ./cgitrc:/etc/cgitrc | ||
| 341 | - ./logo.png:/var/www/htdocs/cgit/logo.png | ||
| 342 | - ./favicon.png:/var/www/htdocs/cgit/favicon.png | ||
| 343 | - ./filters:/var/www/htdocs/cgit/filters | ||
| 344 | ports: | ||
| 345 | - "8763:80" | ||
| 346 | restart: always | ||
| 347 | #+end_src | ||
| 348 | |||
| 349 | Then, just start the container: | ||
| 350 | |||
| 351 | #+begin_src sh | ||
| 352 | sudo docker-compose up -d | ||
| 353 | #+end_src | ||
| 354 | |||
| 355 | Once it's finished installing, you can access the site at | ||
| 356 | =<SERVER_IP>:8763= or use a reverse-proxy service to forward =cgit= to a | ||
| 357 | URL, such as =git.example.com=. See the next section for more details on | ||
| 358 | reverse proxying a URL to a local port. | ||
| 359 | |||
| 360 | ** Nginx Reverse Proxy | ||
| 361 | |||
| 362 | I am using Nginx as my reverse proxy so that the =cgit= Docker container | ||
| 363 | can use =git.example.com= as its URL. To do so, I simply created the | ||
| 364 | following configuration file: | ||
| 365 | |||
| 366 | #+begin_src sh | ||
| 367 | sudo nano /etc/nginx/sites-available/git.example.com | ||
| 368 | #+end_src | ||
| 369 | |||
| 370 | #+begin_src conf | ||
| 371 | server { | ||
| 372 | listen 80; | ||
| 373 | server_name git.example.com; | ||
| 374 | |||
| 375 | if ($host = git.example.com) { | ||
| 376 | return 301 https://$host$request_uri; | ||
| 377 | } | ||
| 378 | |||
| 379 | return 404; | ||
| 380 | } | ||
| 381 | |||
| 382 | server { | ||
| 383 | server_name git.example.com; | ||
| 384 | listen 443 ssl http2; | ||
| 385 | |||
| 386 | location / { | ||
| 387 | # The final `/` is important. | ||
| 388 | proxy_pass http://localhost:8763/; | ||
| 389 | add_header X-Frame-Options SAMEORIGIN; | ||
| 390 | add_header X-XSS-Protection "1; mode=block"; | ||
| 391 | proxy_redirect off; | ||
| 392 | proxy_buffering off; | ||
| 393 | proxy_set_header Host $host; | ||
| 394 | proxy_set_header X-Real-IP $remote_addr; | ||
| 395 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 396 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 397 | proxy_set_header X-Forwarded-Port $server_port; | ||
| 398 | } | ||
| 399 | |||
| 400 | # INCLUDE ANY SSL CERTS HERE | ||
| 401 | include /etc/letsencrypt/options-ssl-nginx.conf; | ||
| 402 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | ||
| 403 | } | ||
| 404 | #+end_src | ||
| 405 | |||
| 406 | Once created, symlink it and restart the web server. | ||
| 407 | |||
| 408 | #+begin_src sh | ||
| 409 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/ | ||
| 410 | sudo systemctl restart nginx.service | ||
| 411 | #+end_src | ||
| 412 | |||
| 413 | As we can see below, my site at =git.example.com= is available and | ||
| 414 | running: | ||
| 415 | |||
| 416 | ** Settings Up Git Details | ||
| 417 | |||
| 418 | Once you have =cgit= running, you can add some small details, such as | ||
| 419 | repository owners and descriptions by editing the following files within | ||
| 420 | each repository. | ||
| 421 | |||
| 422 | Alternatively, you can use the =cgitrc= file to edit these details if | ||
| 423 | you only care to edit them for the purpose of seeing them on your | ||
| 424 | website. | ||
| 425 | |||
| 426 | The =description= file within the repository on your server will display | ||
| 427 | the description online. | ||
| 428 | |||
| 429 | #+begin_src sh | ||
| 430 | cd /git/example.git | ||
| 431 | nano description | ||
| 432 | #+end_src | ||
| 433 | |||
| 434 | You can add a =[gitweb]= block to the =config= file in order to display | ||
| 435 | the owner of the repository. | ||
| 436 | |||
| 437 | #+begin_src sh | ||
| 438 | cd /git/example.git | ||
| 439 | nano config | ||
| 440 | #+end_src | ||
| 441 | |||
| 442 | #+begin_src conf | ||
| 443 | [gitweb] | ||
| 444 | owner = "YourName" | ||
| 445 | #+end_src | ||
| 446 | |||
| 447 | Note that you can ignore the configuration within each repository and | ||
| 448 | simply set up this information in the =cgitrc= file, if you want to do | ||
| 449 | it that way. | ||
| 450 | |||
| 451 | ** Editing =cgit= | ||
| 452 | |||
| 453 | In order to edit certain items within =cgit=, you need to edit the | ||
| 454 | =cgitrc= file. | ||
| 455 | |||
| 456 | #+begin_src sh | ||
| 457 | nano ~/cgit/cgitrc | ||
| 458 | #+end_src | ||
| 459 | |||
| 460 | Below is an example configuration for =cgitrc=. You can find all the | ||
| 461 | configuration options within the [configuration manual] | ||
| 462 | ([[https://git.zx2c4.com/cgit/plain/cgitrc.5.txt]]). | ||
| 463 | |||
| 464 | #+begin_src conf | ||
| 465 | css=/cgit.css | ||
| 466 | logo=/logo.png | ||
| 467 | favicon=/favicon.png | ||
| 468 | robots=noindex, nofollow | ||
| 469 | |||
| 470 | enable-index-links=1 | ||
| 471 | enable-commit-graph=1 | ||
| 472 | enable-blame=1 | ||
| 473 | enable-log-filecount=1 | ||
| 474 | enable-log-linecount=1 | ||
| 475 | enable-git-config=1 | ||
| 476 | |||
| 477 | clone-url=git://git.example.com/$CGIT_REPO_URL ssh://git@git.example.com:/git/$CGIT_REPO_URL | ||
| 478 | |||
| 479 | root-title=My Git Website | ||
| 480 | root-desc=My personal git repositories. | ||
| 481 | |||
| 482 | # Allow download of tar.gz, tar.bz2 and zip-files | ||
| 483 | snapshots=tar.gz tar.bz2 zip | ||
| 484 | |||
| 485 | ## | ||
| 486 | ## List of common mimetypes | ||
| 487 | ## | ||
| 488 | mimetype.gif=image/gif | ||
| 489 | mimetype.html=text/html | ||
| 490 | mimetype.jpg=image/jpeg | ||
| 491 | mimetype.jpeg=image/jpeg | ||
| 492 | mimetype.pdf=application/pdf | ||
| 493 | mimetype.png=image/png | ||
| 494 | mimetype.svg=image/svg+xml | ||
| 495 | |||
| 496 | # Highlight source code | ||
| 497 | # source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.sh | ||
| 498 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | ||
| 499 | |||
| 500 | # Format markdown, restructuredtext, manpages, text files, and html files | ||
| 501 | # through the right converters | ||
| 502 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | ||
| 503 | |||
| 504 | ## | ||
| 505 | ## Search for these files in the root of the default branch of repositories | ||
| 506 | ## for coming up with the about page: | ||
| 507 | ## | ||
| 508 | readme=:README.md | ||
| 509 | readme=:readme.md | ||
| 510 | readme=:README.mkd | ||
| 511 | readme=:readme.mkd | ||
| 512 | readme=:README.rst | ||
| 513 | readme=:readme.rst | ||
| 514 | readme=:README.html | ||
| 515 | readme=:readme.html | ||
| 516 | readme=:README.htm | ||
| 517 | readme=:readme.htm | ||
| 518 | readme=:README.txt | ||
| 519 | readme=:readme.txt | ||
| 520 | readme=:README | ||
| 521 | readme=:readme | ||
| 522 | |||
| 523 | # Repositories | ||
| 524 | |||
| 525 | # Uncomment the following line to scan a path instead of adding repositories manually | ||
| 526 | # scan-path=/git | ||
| 527 | |||
| 528 | ## Test Section | ||
| 529 | section=git/test-section | ||
| 530 | |||
| 531 | repo.url=test.git | ||
| 532 | repo.path=/git/test.git | ||
| 533 | repo.readme=:README.md | ||
| 534 | repo.owner=John Doe | ||
| 535 | repo.desc=An example repository! | ||
| 536 | #+end_src | ||
| 537 | |||
| 538 | ** Final Fixes: Syntax Highlighting & README Rendering | ||
| 539 | |||
| 540 | After completing my initial install and playing around with it for a few | ||
| 541 | days, I noticed two issues: | ||
| 542 | |||
| 543 | 1. Syntax highlighting did not work when viewing the source code within | ||
| 544 | a file. | ||
| 545 | 2. The =about= tab within a repository was not rendered to HTML. | ||
| 546 | |||
| 547 | The following process fixes these issues. To start, let's go to the | ||
| 548 | =cgit= directory where we were editing our configuration file earlier. | ||
| 549 | |||
| 550 | #+begin_src sh | ||
| 551 | cd ~/cgit | ||
| 552 | #+end_src | ||
| 553 | |||
| 554 | In here, create two folders that will hold our syntax files: | ||
| 555 | |||
| 556 | #+begin_src sh | ||
| 557 | mkdir filters && mkdir filters/html-converters && cd filters | ||
| 558 | #+end_src | ||
| 559 | |||
| 560 | Next, download the default filters: | ||
| 561 | |||
| 562 | #+begin_src sh | ||
| 563 | curl https://git.zx2c4.com/cgit/plain/filters/about-formatting.sh > about-formatting.sh | ||
| 564 | chmod 755 about-formatting.sh | ||
| 565 | curl https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py > syntax-highlighting.py | ||
| 566 | chmod 755 syntax-highlighting.py | ||
| 567 | #+end_src | ||
| 568 | |||
| 569 | Finally, download the HTML conversion files you need. The example below | ||
| 570 | downloads the Markdown converter: | ||
| 571 | |||
| 572 | #+begin_src sh | ||
| 573 | cd html-converters | ||
| 574 | curl https://git.zx2c4.com/cgit/plain/filters/html-converters/md2html > md2html | ||
| 575 | chmod 755 md2html | ||
| 576 | #+end_src | ||
| 577 | |||
| 578 | If you need other filters or html-converters found within | ||
| 579 | [[https://git.zx2c4.com/cgit/tree/filters][the cgit project files]], | ||
| 580 | repeat the =curl= and =chmod= process above for whichever files you | ||
| 581 | need. | ||
| 582 | |||
| 583 | However, formatting will not work quite yet since the Docker cgit | ||
| 584 | container we're using doesn't have the formatting package installed. You | ||
| 585 | can install this easily by install Python 3+ and the =pygments= package: | ||
| 586 | |||
| 587 | #+begin_src sh | ||
| 588 | # Enter the container's command line | ||
| 589 | sudo docker exec -it cgit bash | ||
| 590 | #+end_src | ||
| 591 | |||
| 592 | #+begin_src sh | ||
| 593 | # Install the necessary packages and then exit | ||
| 594 | yum update -y && \ | ||
| 595 | yum upgrade -y && \ | ||
| 596 | yum install python3 python3-pip -y && \ | ||
| 597 | pip3 install markdown pygments && \ | ||
| 598 | exit | ||
| 599 | #+end_src | ||
| 600 | |||
| 601 | *You will need to enter the cgit docker container and re-run these =yum= | ||
| 602 | commands every time you kill and restart the container!* | ||
| 603 | |||
| 604 | If not done already, we need to add the following variables to our | ||
| 605 | =cgitrc= file in order for =cgit= to know where our filtering files are: | ||
| 606 | |||
| 607 | #+begin_src conf | ||
| 608 | # Highlight source code with python pygments-based highlighter | ||
| 609 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | ||
| 610 | |||
| 611 | # Format markdown, restructuredtext, manpages, text files, and html files | ||
| 612 | # through the right converters | ||
| 613 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | ||
| 614 | #+end_src | ||
| 615 | |||
| 616 | Now you should see that syntax highlighting and README rendering to the | ||
| 617 | =about= tab is fixed. | ||
| 618 | |||
| 619 | ** Theming | ||
| 620 | |||
| 621 | I won't go into much detail in this section, but you can fully theme | ||
| 622 | your installation of =cgit= since you have access to the =cgit.css= file | ||
| 623 | in your web root. This is another file you can add as a volume to the | ||
| 624 | =docker-compose.yml= file if you want to edit this without entering the | ||
| 625 | container's command line. | ||
| 626 | |||
| 627 | *** :warning: Remember to Back Up Your Data! | ||
| 628 | |||
| 629 | The last thing to note is that running services on your own equipment | ||
| 630 | means that you're assuming a level of risk that exists regarding data | ||
| 631 | loss, catastrophes, etc. In order to reduce the impact of any such | ||
| 632 | occurrence, I suggest backing up your data regularly. | ||
| 633 | |||
| 634 | Backups can be automated via =cron=, by hooking your base directory up | ||
| 635 | to a cloud provider, or even setting up hooks to push all repository | ||
| 636 | info to git mirrors on other git hosts. Whatever the method, make sure | ||
| 637 | that your data doesn't vanish in the event that your drives or servers | ||
| 638 | fail. | ||
content/blog/2022-07-14-gnupg.md deleted −300
| @@ -1,300 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-07-14 | ||
| 3 | title = "GNU Privacy Guard (GPG)" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The History of GPG | ||
| 9 | |||
| 10 | [GNU Privacy Guard](https://gnupg.org/), also known as GnuPG and GPG, is a free | ||
| 11 | ("free" as in both speech and beer) software that fully implements the OpenPGP | ||
| 12 | Message Format documented in [RFC 4880](https://www.rfc-editor.org/rfc/rfc4880). | ||
| 13 | |||
| 14 | I won't go in-depth on the full history of the software in this post, but it is | ||
| 15 | important to understand that GPG is not the same as PGP (Pretty Good Privacy), | ||
| 16 | which is a different implementation of RFC 4880. However, GPG was designed to | ||
| 17 | interoperate with PGP. | ||
| 18 | |||
| 19 | GPG was originally developed in the late 1990s by [Werner | ||
| 20 | Koch](https://en.wikipedia.org/wiki/Werner_Koch) and has historically been | ||
| 21 | funded generously by the German government. | ||
| 22 | |||
| 23 | Now that we have all the high-level info out of the way, let's dive into the | ||
| 24 | different aspects of GPG and its uses. | ||
| 25 | |||
| 26 | # Encryption Algorithms | ||
| 27 | |||
| 28 | GPG supports a wide range of different encryption algorithms, including | ||
| 29 | public-key, cipher, hash, and compression algorithms. The support for these | ||
| 30 | algorithms has grown since the adoption of the Libgcrypt library in the 2.x | ||
| 31 | versions of GPG. | ||
| 32 | |||
| 33 | As you will be able to see below in an example of a full key generation with the | ||
| 34 | GPG command line tool, GPG recommends the following algorithms to new users: | ||
| 35 | |||
| 36 | ```sh | ||
| 37 | Please select what kind of key you want: | ||
| 38 | (1) RSA and RSA | ||
| 39 | (2) DSA and Elgamal | ||
| 40 | (3) DSA (sign only) | ||
| 41 | (4) RSA (sign only) | ||
| 42 | (9) ECC (sign and encrypt) *default* | ||
| 43 | (10) ECC (sign only) | ||
| 44 | ``` | ||
| 45 | |||
| 46 | I am not doing an in-depth explanation here in order to keep the focus on GPG | ||
| 47 | and not encryption algorithms. If you want a deep dive into cryptography or | ||
| 48 | encryption algorithms, please read my other posts: | ||
| 49 | |||
| 50 | - [AES Encryption](../aes-encryption/) (2018) | ||
| 51 | - [Cryptography Basics](../cryptography-basics/) (2020) | ||
| 52 | |||
| 53 | ## Vulnerabilities | ||
| 54 | |||
| 55 | As of 2022-07-14, there are a few different vulnerabilities associated with GPG | ||
| 56 | or the libraries it uses: | ||
| 57 | |||
| 58 | - GPG versions 1.0.2--1.2.3 contains a bug where "as soon as one | ||
| 59 | (GPG-generated) ElGamal signature of an arbitrary message is released, one | ||
| 60 | can recover the signer's private key in less than a second on a PC." | ||
| 61 | ([Source](https://www.di.ens.fr/~pnguyen/pub_Ng04.htm)) | ||
| 62 | - GPG versions prior to 1.4.2.1 contain a false positive signature | ||
| 63 | verification bug. | ||
| 64 | ([Source](https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000211.html)) | ||
| 65 | - GPG versions prior to 1.4.2.2 cannot detect injection of unsigned data. ( | ||
| 66 | [Source](https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000218.html)) | ||
| 67 | - Libgcrypt, a library used by GPG, contained a bug which enabled full key | ||
| 68 | recovery for RSA-1024 and some RSA-2048 keys. This was resolved in a GPG | ||
| 69 | update in 2017. ([Source](https://lwn.net/Articles/727179/)) | ||
| 70 | - The [ROCA Vulnerability](https://en.wikipedia.org/wiki/ROCA_vulnerability) | ||
| 71 | affects RSA keys generated by YubiKey 4 tokens. | ||
| 72 | ([Source](https://crocs.fi.muni.cz/_media/public/papers/nemec_roca_ccs17_preprint.pdf)) | ||
| 73 | - The [SigSpoof Attack](https://en.wikipedia.org/wiki/SigSpoof) allows an | ||
| 74 | attacker to spoof digital signatures. | ||
| 75 | ([Source](https://arstechnica.com/information-technology/2018/06/decades-old-pgp-bug-allowed-hackers-to-spoof-just-about-anyones-signature/)) | ||
| 76 | - Libgcrypt 1.9.0 contains a severe flaw related to a heap buffer overflow, | ||
| 77 | fixed in Libgcrypt 1.9.1 | ||
| 78 | ([Source](https://web.archive.org/web/20210221012505/https://www.theregister.com/2021/01/29/severe_libgcrypt_bug/)) | ||
| 79 | |||
| 80 | ### Platforms | ||
| 81 | |||
| 82 | Originally developed as a command-line program for \*nix systems, GPG now has a | ||
| 83 | wealth of front-end applications and libraries available for end-users. However, | ||
| 84 | the most recommended programs remain the same: | ||
| 85 | |||
| 86 | - [GnuPG](https://gnupg.org) for Linux (depending on distro) | ||
| 87 | - [Gpg4win](https://gpg4win.org) for Windows | ||
| 88 | - [GPGTools](https://gpgtools.org) for macOS | ||
| 89 | |||
| 90 | # Creating a Key Pair | ||
| 91 | |||
| 92 | In order to create a GPG key pair, a user would first need to install GPG on | ||
| 93 | their system. If we're assuming that the user is on Fedora Linux, they would | ||
| 94 | execute the following: | ||
| 95 | |||
| 96 | ```sh | ||
| 97 | sudo dnf install gpg | ||
| 98 | ``` | ||
| 99 | |||
| 100 | Once installed, a user can create a new key pair with the following command(s): | ||
| 101 | |||
| 102 | ```sh | ||
| 103 | gpg --full-generate-key | ||
| 104 | ``` | ||
| 105 | |||
| 106 | GPG will walk the user through an interactive setup that asks for an algorithm | ||
| 107 | preference, expiration date, name, and email to associate with this key. | ||
| 108 | |||
| 109 | See the following example key set-up for a default key generation using the | ||
| 110 | GnuPG command-line interface: | ||
| 111 | |||
| 112 | ```sh | ||
| 113 | gpg (GnuPG) 2.3.6; Copyright (C) 2021 Free Software Foundation, Inc. | ||
| 114 | This is free software: you are free to change and redistribute it. | ||
| 115 | There is NO WARRANTY, to the extent permitted by law. | ||
| 116 | |||
| 117 | Please select what kind of key you want: | ||
| 118 | (1) RSA and RSA | ||
| 119 | (2) DSA and Elgamal | ||
| 120 | (3) DSA (sign only) | ||
| 121 | (4) RSA (sign only) | ||
| 122 | (9) ECC (sign and encrypt) *default* | ||
| 123 | (10) ECC (sign only) | ||
| 124 | (14) Existing key from card | ||
| 125 | Your selection? 9 | ||
| 126 | Please select which elliptic curve you want: | ||
| 127 | (1) Curve 25519 *default* | ||
| 128 | (4) NIST P-384 | ||
| 129 | Your selection? 1 | ||
| 130 | Please specify how long the key should be valid. | ||
| 131 | 0 = key does not expire | ||
| 132 | <n> = key expires in n days | ||
| 133 | <n>w = key expires in n weeks | ||
| 134 | <n>m = key expires in n months | ||
| 135 | <n>y = key expires in n years | ||
| 136 | Key is valid for? (0) 0 | ||
| 137 | Key does not expire at all | ||
| 138 | Is this correct? (y/N) y | ||
| 139 | |||
| 140 | GnuPG needs to construct a user ID to identify your key. | ||
| 141 | |||
| 142 | Real name: John Doe | ||
| 143 | Email address: johndoe@example.com | ||
| 144 | Comment: test key | ||
| 145 | You selected this USER-ID: | ||
| 146 | "John Doe (test key) <johndoe@example.com>" | ||
| 147 | |||
| 148 | Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O | ||
| 149 | We need to generate a lot of random bytes. It is a good idea to perform | ||
| 150 | some other action (type on the keyboard, move the mouse, utilize the | ||
| 151 | disks) during the prime generation; this gives the random number | ||
| 152 | generator a better chance to gain enough entropy. | ||
| 153 | We need to generate a lot of random bytes. It is a good idea to perform | ||
| 154 | some other action (type on the keyboard, move the mouse, utilize the | ||
| 155 | disks) during the prime generation; this gives the random number | ||
| 156 | generator a better chance to gain enough entropy. | ||
| 157 | gpg: revocation certificate stored as 'example.rev' | ||
| 158 | public and secret key created and signed. | ||
| 159 | |||
| 160 | pub ed25519 2022-07-14 [SC] | ||
| 161 | E955B7700FFC11EF51C2BA1FE096AACDD4C32E9C | ||
| 162 | uid John Doe (test key) <johndoe@example.com> | ||
| 163 | sub cv25519 2022-07-14 [E] | ||
| 164 | ``` | ||
| 165 | |||
| 166 | Please note that GUI apps may differ slightly from the GPG command-line | ||
| 167 | interface. | ||
| 168 | |||
| 169 | # Common Usage | ||
| 170 | |||
| 171 | As noted in RFC 4880, the general functions of OpenPGP are as follows: | ||
| 172 | |||
| 173 | - digital signatures | ||
| 174 | - encryption | ||
| 175 | - compression | ||
| 176 | - Radix-64 conversion | ||
| 177 | - key management and certificate services | ||
| 178 | |||
| 179 | From this, you can probably gather that the main use of GPG is for encrypting | ||
| 180 | data and/or signing the data with a key. The purpose of encrypting data with GPG | ||
| 181 | is to ensure that no one except the intended recipient(s) can access the data. | ||
| 182 | |||
| 183 | Let's explore some specific GPG use-cases. | ||
| 184 | |||
| 185 | |||
| 186 | |||
| 187 | One of the more popular uses of GPG is to sign and/or encrypt emails. With the | ||
| 188 | use of a GPG keypair, you can encrypt a message, its subject, and even the | ||
| 189 | attachments within. | ||
| 190 | |||
| 191 | The first process, regarding the signing of a message without any encryption, is | ||
| 192 | generally used to provide assurance that an email is truly coming from the | ||
| 193 | sender that the message claims. When I send an email, and it's signed with my | ||
| 194 | public key, the recipient(s) of the message can verify that the message was | ||
| 195 | signed with my personal key. | ||
| 196 | |||
| 197 | The second process, regarding the actual encryption of the message and its | ||
| 198 | contents, works by using a combination of the sender's keys and the recipient's | ||
| 199 | keys. This process may vary slightly by implementation, but it most commonly | ||
| 200 | uses asymmetric cryptography, also known as public-key cryptography. In this | ||
| 201 | version of encryption, the sender's private key to sign the message and a | ||
| 202 | combination of the sender's keys and the recipient's public key to encrypt the | ||
| 203 | message. | ||
| 204 | |||
| 205 | If two people each have their own private keys and exchange their public keys, | ||
| 206 | they can send encrypted messages back and forth with GPG. This is also possible | ||
| 207 | with symmetric cryptography, but the process differs since there are no key | ||
| 208 | pairs. | ||
| 209 | |||
| 210 | Implementation of email encryption varies greatly between email clients, so you | ||
| 211 | will need to reference your email client's documentation to ensure you are | ||
| 212 | setting it up correctly for that specific client. | ||
| 213 | |||
| 214 | ## File Encryption | ||
| 215 | |||
| 216 | As noted in the section above regarding emails, GPG enables users to be able to | ||
| 217 | send a message to each other if they are both set-up with GPG keys. In this | ||
| 218 | example, I am going to show how a user could send a file called | ||
| 219 | `example_file.txt` to another user via the recipient's email. | ||
| 220 | |||
| 221 | The sender would find the file they want to send and execute the following | ||
| 222 | command: | ||
| 223 | |||
| 224 | ```sh | ||
| 225 | gpg --encrypt --output example_file.txt.gpg --recipient \ | ||
| 226 | recipient@example.com example_file.txt | ||
| 227 | ``` | ||
| 228 | |||
| 229 | Once received, the recipient can decrypt the file with the following command: | ||
| 230 | |||
| 231 | ```sh | ||
| 232 | gpg --decrypt --output example_file.txt example_file.txt.gpg | ||
| 233 | ``` | ||
| 234 | |||
| 235 | ## Ownership Signatures | ||
| 236 | |||
| 237 | One important aspect of GPG, especially for developers, is the ability to sign | ||
| 238 | data without encrypting it. For example, developers often sign code changes when | ||
| 239 | they commit the changes back to a central repository, in order to display | ||
| 240 | ownership of who made the changes. This allows other users to look at a code | ||
| 241 | change and determine that the change was valid. | ||
| 242 | |||
| 243 | In order to do this using [Git](https://git-scm.com), the developer simply needs | ||
| 244 | to alter the `git commit` command to include the `-S` flag. Here's an example: | ||
| 245 | |||
| 246 | ```sh | ||
| 247 | git commit -S -m "my commit message" | ||
| 248 | ``` | ||
| 249 | |||
| 250 | As an expansion of the example above, Git users can configure their environment | ||
| 251 | with a default key to use by adding their GPG signature: | ||
| 252 | |||
| 253 | ```sh | ||
| 254 | git config --global user.signingkey XXXXXXXXXXXXXXXX | ||
| 255 | ``` | ||
| 256 | |||
| 257 | If you're not sure what your signature is, you can find it titled `sig` in the | ||
| 258 | output of this command: | ||
| 259 | |||
| 260 | ```sh | ||
| 261 | gpg --list-signatures | ||
| 262 | ``` | ||
| 263 | |||
| 264 | ## File Integrity | ||
| 265 | |||
| 266 | When a person generates a signature for data, they are allowing users the | ||
| 267 | ability to verify the signature on that data in the future to ensure the data | ||
| 268 | has not been corrupted. This is most common with software applications hosted on | ||
| 269 | the internet - developers provide signatures so that users can verify a website | ||
| 270 | was not hijacked and download links replaced with dangerous software. | ||
| 271 | |||
| 272 | In order to verify signed data, a user needs to have: | ||
| 273 | |||
| 274 | 1. The signed data | ||
| 275 | 2. A signature file | ||
| 276 | 3. The public GPG key of the signer | ||
| 277 | |||
| 278 | Once the signer's public key is imported on the user's system, and they have the | ||
| 279 | data and signature, they can verify the data with the following commands: | ||
| 280 | |||
| 281 | ```sh | ||
| 282 | # If the signature is attached to the data | ||
| 283 | gpg --verify [signature-file] | ||
| 284 | |||
| 285 | # If the signature is detached as a separate file from the data | ||
| 286 | gpg --verify [signature-file] [original-file] | ||
| 287 | ``` | ||
| 288 | |||
| 289 | ### Finding Public Keys | ||
| 290 | |||
| 291 | In order to use GPG with others, a user needs to know the other user(s) keys. | ||
| 292 | This is easy to do if the user knows the other user(s) in person, but may be | ||
| 293 | hard if the relationship is strictly digital. Luckily, there are a few options. | ||
| 294 | The first option is to look at a user's web page or social pages if they have | ||
| 295 | them. | ||
| 296 | |||
| 297 | Otherwise, the best option is to use a keyserver, such as: | ||
| 298 | |||
| 299 | - [pgp.mit.edu](https://pgp.mit.edu) | ||
| 300 | - [keys.openpgp.org](https://keys.openpgp.org) | ||
content/blog/2022-07-14-gnupg.org added +308
| @@ -0,0 +1,308 @@ | |||
| 1 | #+date: <2022-07-14> | ||
| 2 | #+title: GNU Privacy Guard (GPG) | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The History of GPG | ||
| 7 | |||
| 8 | [[https://gnupg.org/][GNU Privacy Guard]], also known as GnuPG and GPG, | ||
| 9 | is a free ("free" as in both speech and beer) software that fully | ||
| 10 | implements the OpenPGP Message Format documented in | ||
| 11 | [[https://www.rfc-editor.org/rfc/rfc4880][RFC 4880]]. | ||
| 12 | |||
| 13 | I won't go in-depth on the full history of the software in this post, | ||
| 14 | but it is important to understand that GPG is not the same as PGP | ||
| 15 | (Pretty Good Privacy), which is a different implementation of RFC 4880. | ||
| 16 | However, GPG was designed to interoperate with PGP. | ||
| 17 | |||
| 18 | GPG was originally developed in the late 1990s by | ||
| 19 | [[https://en.wikipedia.org/wiki/Werner_Koch][Werner Koch]] and has | ||
| 20 | historically been funded generously by the German government. | ||
| 21 | |||
| 22 | Now that we have all the high-level info out of the way, let's dive into | ||
| 23 | the different aspects of GPG and its uses. | ||
| 24 | |||
| 25 | * Encryption Algorithms | ||
| 26 | |||
| 27 | GPG supports a wide range of different encryption algorithms, including | ||
| 28 | public-key, cipher, hash, and compression algorithms. The support for | ||
| 29 | these algorithms has grown since the adoption of the Libgcrypt library | ||
| 30 | in the 2.x versions of GPG. | ||
| 31 | |||
| 32 | As you will be able to see below in an example of a full key generation | ||
| 33 | with the GPG command line tool, GPG recommends the following algorithms | ||
| 34 | to new users: | ||
| 35 | |||
| 36 | #+begin_src sh | ||
| 37 | Please select what kind of key you want: | ||
| 38 | (1) RSA and RSA | ||
| 39 | (2) DSA and Elgamal | ||
| 40 | (3) DSA (sign only) | ||
| 41 | (4) RSA (sign only) | ||
| 42 | (9) ECC (sign and encrypt) *default* | ||
| 43 | (10) ECC (sign only) | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | I am not doing an in-depth explanation here in order to keep the focus | ||
| 47 | on GPG and not encryption algorithms. If you want a deep dive into | ||
| 48 | cryptography or encryption algorithms, please read my other posts: | ||
| 49 | |||
| 50 | - [[../aes-encryption/][AES Encryption]] (2018) | ||
| 51 | - [[../cryptography-basics/][Cryptography Basics]] (2020) | ||
| 52 | |||
| 53 | ** Vulnerabilities | ||
| 54 | |||
| 55 | As of 2022-07-14, there are a few different vulnerabilities associated | ||
| 56 | with GPG or the libraries it uses: | ||
| 57 | |||
| 58 | - GPG versions 1.0.2--1.2.3 contains a bug where "as soon as one | ||
| 59 | (GPG-generated) ElGamal signature of an arbitrary message is released, | ||
| 60 | one can recover the signer's private key in less than a second on a | ||
| 61 | PC." ([[https://www.di.ens.fr/~pnguyen/pub_Ng04.htm][Source]]) | ||
| 62 | - GPG versions prior to 1.4.2.1 contain a false positive signature | ||
| 63 | verification bug. | ||
| 64 | ([[https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000211.html][Source]]) | ||
| 65 | - GPG versions prior to 1.4.2.2 cannot detect injection of unsigned | ||
| 66 | data. ( | ||
| 67 | [[https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000218.html][Source]]) | ||
| 68 | - Libgcrypt, a library used by GPG, contained a bug which enabled full | ||
| 69 | key recovery for RSA-1024 and some RSA-2048 keys. This was resolved in | ||
| 70 | a GPG update in 2017. ([[https://lwn.net/Articles/727179/][Source]]) | ||
| 71 | - The [[https://en.wikipedia.org/wiki/ROCA_vulnerability][ROCA | ||
| 72 | Vulnerability]] affects RSA keys generated by YubiKey 4 tokens. | ||
| 73 | ([[https://crocs.fi.muni.cz/_media/public/papers/nemec_roca_ccs17_preprint.pdf][Source]]) | ||
| 74 | - The [[https://en.wikipedia.org/wiki/SigSpoof][SigSpoof Attack]] allows | ||
| 75 | an attacker to spoof digital signatures. | ||
| 76 | ([[https://arstechnica.com/information-technology/2018/06/decades-old-pgp-bug-allowed-hackers-to-spoof-just-about-anyones-signature/][Source]]) | ||
| 77 | - Libgcrypt 1.9.0 contains a severe flaw related to a heap buffer | ||
| 78 | overflow, fixed in Libgcrypt 1.9.1 | ||
| 79 | ([[https://web.archive.org/web/20210221012505/https://www.theregister.com/2021/01/29/severe_libgcrypt_bug/][Source]]) | ||
| 80 | |||
| 81 | *** Platforms | ||
| 82 | |||
| 83 | Originally developed as a command-line program for *nix systems, GPG now | ||
| 84 | has a wealth of front-end applications and libraries available for | ||
| 85 | end-users. However, the most recommended programs remain the same: | ||
| 86 | |||
| 87 | - [[https://gnupg.org][GnuPG]] for Linux (depending on distro) | ||
| 88 | - [[https://gpg4win.org][Gpg4win]] for Windows | ||
| 89 | - [[https://gpgtools.org][GPGTools]] for macOS | ||
| 90 | |||
| 91 | * Creating a Key Pair | ||
| 92 | |||
| 93 | In order to create a GPG key pair, a user would first need to install | ||
| 94 | GPG on their system. If we're assuming that the user is on Fedora Linux, | ||
| 95 | they would execute the following: | ||
| 96 | |||
| 97 | #+begin_src sh | ||
| 98 | sudo dnf install gpg | ||
| 99 | #+end_src | ||
| 100 | |||
| 101 | Once installed, a user can create a new key pair with the following | ||
| 102 | command(s): | ||
| 103 | |||
| 104 | #+begin_src sh | ||
| 105 | gpg --full-generate-key | ||
| 106 | #+end_src | ||
| 107 | |||
| 108 | GPG will walk the user through an interactive setup that asks for an | ||
| 109 | algorithm preference, expiration date, name, and email to associate with | ||
| 110 | this key. | ||
| 111 | |||
| 112 | See the following example key set-up for a default key generation using | ||
| 113 | the GnuPG command-line interface: | ||
| 114 | |||
| 115 | #+begin_src sh | ||
| 116 | gpg (GnuPG) 2.3.6; Copyright (C) 2021 Free Software Foundation, Inc. | ||
| 117 | This is free software: you are free to change and redistribute it. | ||
| 118 | There is NO WARRANTY, to the extent permitted by law. | ||
| 119 | |||
| 120 | Please select what kind of key you want: | ||
| 121 | (1) RSA and RSA | ||
| 122 | (2) DSA and Elgamal | ||
| 123 | (3) DSA (sign only) | ||
| 124 | (4) RSA (sign only) | ||
| 125 | (9) ECC (sign and encrypt) *default* | ||
| 126 | (10) ECC (sign only) | ||
| 127 | (14) Existing key from card | ||
| 128 | Your selection? 9 | ||
| 129 | Please select which elliptic curve you want: | ||
| 130 | (1) Curve 25519 *default* | ||
| 131 | (4) NIST P-384 | ||
| 132 | Your selection? 1 | ||
| 133 | Please specify how long the key should be valid. | ||
| 134 | 0 = key does not expire | ||
| 135 | <n> = key expires in n days | ||
| 136 | <n>w = key expires in n weeks | ||
| 137 | <n>m = key expires in n months | ||
| 138 | <n>y = key expires in n years | ||
| 139 | Key is valid for? (0) 0 | ||
| 140 | Key does not expire at all | ||
| 141 | Is this correct? (y/N) y | ||
| 142 | |||
| 143 | GnuPG needs to construct a user ID to identify your key. | ||
| 144 | |||
| 145 | Real name: John Doe | ||
| 146 | Email address: johndoe@example.com | ||
| 147 | Comment: test key | ||
| 148 | You selected this USER-ID: | ||
| 149 | "John Doe (test key) <johndoe@example.com>" | ||
| 150 | |||
| 151 | Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O | ||
| 152 | We need to generate a lot of random bytes. It is a good idea to perform | ||
| 153 | some other action (type on the keyboard, move the mouse, utilize the | ||
| 154 | disks) during the prime generation; this gives the random number | ||
| 155 | generator a better chance to gain enough entropy. | ||
| 156 | We need to generate a lot of random bytes. It is a good idea to perform | ||
| 157 | some other action (type on the keyboard, move the mouse, utilize the | ||
| 158 | disks) during the prime generation; this gives the random number | ||
| 159 | generator a better chance to gain enough entropy. | ||
| 160 | gpg: revocation certificate stored as 'example.rev' | ||
| 161 | public and secret key created and signed. | ||
| 162 | |||
| 163 | pub ed25519 2022-07-14 [SC] | ||
| 164 | E955B7700FFC11EF51C2BA1FE096AACDD4C32E9C | ||
| 165 | uid John Doe (test key) <johndoe@example.com> | ||
| 166 | sub cv25519 2022-07-14 [E] | ||
| 167 | #+end_src | ||
| 168 | |||
| 169 | Please note that GUI apps may differ slightly from the GPG command-line | ||
| 170 | interface. | ||
| 171 | |||
| 172 | * Common Usage | ||
| 173 | |||
| 174 | As noted in RFC 4880, the general functions of OpenPGP are as follows: | ||
| 175 | |||
| 176 | - digital signatures | ||
| 177 | - encryption | ||
| 178 | - compression | ||
| 179 | - Radix-64 conversion | ||
| 180 | - key management and certificate services | ||
| 181 | |||
| 182 | From this, you can probably gather that the main use of GPG is for | ||
| 183 | encrypting data and/or signing the data with a key. The purpose of | ||
| 184 | encrypting data with GPG is to ensure that no one except the intended | ||
| 185 | recipient(s) can access the data. | ||
| 186 | |||
| 187 | Let's explore some specific GPG use-cases. | ||
| 188 | |||
| 189 | |||
| 190 | |||
| 191 | One of the more popular uses of GPG is to sign and/or encrypt emails. | ||
| 192 | With the use of a GPG keypair, you can encrypt a message, its subject, | ||
| 193 | and even the attachments within. | ||
| 194 | |||
| 195 | The first process, regarding the signing of a message without any | ||
| 196 | encryption, is generally used to provide assurance that an email is | ||
| 197 | truly coming from the sender that the message claims. When I send an | ||
| 198 | email, and it's signed with my public key, the recipient(s) of the | ||
| 199 | message can verify that the message was signed with my personal key. | ||
| 200 | |||
| 201 | The second process, regarding the actual encryption of the message and | ||
| 202 | its contents, works by using a combination of the sender's keys and the | ||
| 203 | recipient's keys. This process may vary slightly by implementation, but | ||
| 204 | it most commonly uses asymmetric cryptography, also known as public-key | ||
| 205 | cryptography. In this version of encryption, the sender's private key to | ||
| 206 | sign the message and a combination of the sender's keys and the | ||
| 207 | recipient's public key to encrypt the message. | ||
| 208 | |||
| 209 | If two people each have their own private keys and exchange their public | ||
| 210 | keys, they can send encrypted messages back and forth with GPG. This is | ||
| 211 | also possible with symmetric cryptography, but the process differs since | ||
| 212 | there are no key pairs. | ||
| 213 | |||
| 214 | Implementation of email encryption varies greatly between email clients, | ||
| 215 | so you will need to reference your email client's documentation to | ||
| 216 | ensure you are setting it up correctly for that specific client. | ||
| 217 | |||
| 218 | ** File Encryption | ||
| 219 | |||
| 220 | As noted in the section above regarding emails, GPG enables users to be | ||
| 221 | able to send a message to each other if they are both set-up with GPG | ||
| 222 | keys. In this example, I am going to show how a user could send a file | ||
| 223 | called =example_file.txt= to another user via the recipient's email. | ||
| 224 | |||
| 225 | The sender would find the file they want to send and execute the | ||
| 226 | following command: | ||
| 227 | |||
| 228 | #+begin_src sh | ||
| 229 | gpg --encrypt --output example_file.txt.gpg --recipient \ | ||
| 230 | recipient@example.com example_file.txt | ||
| 231 | #+end_src | ||
| 232 | |||
| 233 | Once received, the recipient can decrypt the file with the following | ||
| 234 | command: | ||
| 235 | |||
| 236 | #+begin_src sh | ||
| 237 | gpg --decrypt --output example_file.txt example_file.txt.gpg | ||
| 238 | #+end_src | ||
| 239 | |||
| 240 | ** Ownership Signatures | ||
| 241 | |||
| 242 | One important aspect of GPG, especially for developers, is the ability | ||
| 243 | to sign data without encrypting it. For example, developers often sign | ||
| 244 | code changes when they commit the changes back to a central repository, | ||
| 245 | in order to display ownership of who made the changes. This allows other | ||
| 246 | users to look at a code change and determine that the change was valid. | ||
| 247 | |||
| 248 | In order to do this using [[https://git-scm.com][Git]], the developer | ||
| 249 | simply needs to alter the =git commit= command to include the =-S= flag. | ||
| 250 | Here's an example: | ||
| 251 | |||
| 252 | #+begin_src sh | ||
| 253 | git commit -S -m "my commit message" | ||
| 254 | #+end_src | ||
| 255 | |||
| 256 | As an expansion of the example above, Git users can configure their | ||
| 257 | environment with a default key to use by adding their GPG signature: | ||
| 258 | |||
| 259 | #+begin_src sh | ||
| 260 | git config --global user.signingkey XXXXXXXXXXXXXXXX | ||
| 261 | #+end_src | ||
| 262 | |||
| 263 | If you're not sure what your signature is, you can find it titled =sig= | ||
| 264 | in the output of this command: | ||
| 265 | |||
| 266 | #+begin_src sh | ||
| 267 | gpg --list-signatures | ||
| 268 | #+end_src | ||
| 269 | |||
| 270 | ** File Integrity | ||
| 271 | |||
| 272 | When a person generates a signature for data, they are allowing users | ||
| 273 | the ability to verify the signature on that data in the future to ensure | ||
| 274 | the data has not been corrupted. This is most common with software | ||
| 275 | applications hosted on the internet - developers provide signatures so | ||
| 276 | that users can verify a website was not hijacked and download links | ||
| 277 | replaced with dangerous software. | ||
| 278 | |||
| 279 | In order to verify signed data, a user needs to have: | ||
| 280 | |||
| 281 | 1. The signed data | ||
| 282 | 2. A signature file | ||
| 283 | 3. The public GPG key of the signer | ||
| 284 | |||
| 285 | Once the signer's public key is imported on the user's system, and they | ||
| 286 | have the data and signature, they can verify the data with the following | ||
| 287 | commands: | ||
| 288 | |||
| 289 | #+begin_src sh | ||
| 290 | # If the signature is attached to the data | ||
| 291 | gpg --verify [signature-file] | ||
| 292 | |||
| 293 | # If the signature is detached as a separate file from the data | ||
| 294 | gpg --verify [signature-file] [original-file] | ||
| 295 | #+end_src | ||
| 296 | |||
| 297 | *** Finding Public Keys | ||
| 298 | |||
| 299 | In order to use GPG with others, a user needs to know the other user(s) | ||
| 300 | keys. This is easy to do if the user knows the other user(s) in person, | ||
| 301 | but may be hard if the relationship is strictly digital. Luckily, there | ||
| 302 | are a few options. The first option is to look at a user's web page or | ||
| 303 | social pages if they have them. | ||
| 304 | |||
| 305 | Otherwise, the best option is to use a keyserver, such as: | ||
| 306 | |||
| 307 | - [[https://pgp.mit.edu][pgp.mit.edu]] | ||
| 308 | - [[https://keys.openpgp.org][keys.openpgp.org]] | ||
content/blog/2022-07-25-curseradio.md deleted −98
| @@ -1,98 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-07-25 | ||
| 3 | title = "CurseRadio: Listening to the Radio on the Command Line" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | While exploring some interesting Linux applications, I stumbled across | ||
| 11 | [curseradio](https://github.com/chronitis/curseradio), a command-line radio | ||
| 12 | player based on Python. | ||
| 13 | |||
| 14 | This application is fantastic and incredibly easy to install, so I wanted to | ||
| 15 | dedicate a post today to this app. Let's look at the features within the app and | ||
| 16 | then walk through the installation process I took to get `curseradio` working. | ||
| 17 | |||
| 18 | # Features | ||
| 19 | |||
| 20 | The radio player itself is quite minimal. As you can see in the screenshot | ||
| 21 | above, it contains a simple plaintext list of all available categories, which | ||
| 22 | can be broken down further and further. In addition, radio shows are available | ||
| 23 | for listening, alongside regular radio stations. | ||
| 24 | |||
| 25 | For example, the `Sports` > `Pro Basketball` > `Shows` category contains a | ||
| 26 | number of specific shows related to Professional Basketball. | ||
| 27 | |||
| 28 | Aside from being able to play any of the listed stations/shows, you can make a | ||
| 29 | channel your favorite by pressing `f`. It will now show up at the top of the | ||
| 30 | radio player in the `Favourites` category. | ||
| 31 | |||
| 32 | ## Commands/Shortcuts | ||
| 33 | |||
| 34 | | Key(s) | Command | | ||
| 35 | | ---------- | ------------------------------- | | ||
| 36 | | ↑, ↓ | navigate | | ||
| 37 | | PgUp, PgDn | navigate quickly | | ||
| 38 | | Home, End | to top/bottom | | ||
| 39 | | Enter | open/close folders, play stream | | ||
| 40 | | k | stop playing stream | | ||
| 41 | | q | quit | | ||
| 42 | | f | toggle favourite | | ||
| 43 | |||
| 44 | # Installation | ||
| 45 | |||
| 46 | ## Dependencies | ||
| 47 | |||
| 48 | Before installing `curseradio`, a handful of system and Python packages are | ||
| 49 | required. To get started, install `python3`, `pip3`, and `mpv` on your system. | ||
| 50 | In this example, I'm using Fedora Linux, which uses the `dnf` package manager. | ||
| 51 | You may need to adjust this if you're using a different system. | ||
| 52 | |||
| 53 | ```sh | ||
| 54 | sudo dnf install python3 pip3 mpv | ||
| 55 | ``` | ||
| 56 | |||
| 57 | Next, use `pip3` to install `requests`, `xdg`, and `lxml`: | ||
| 58 | |||
| 59 | ```sh | ||
| 60 | pip3 install requests xdg lxml | ||
| 61 | ``` | ||
| 62 | |||
| 63 | ## Repository Source Installation | ||
| 64 | |||
| 65 | Once all the dependencies are installed, we can clone the source code and enter | ||
| 66 | that directory: | ||
| 67 | |||
| 68 | ```sh | ||
| 69 | git clone https://github.com/chronitis/curseradio && cd curseradio | ||
| 70 | ``` | ||
| 71 | |||
| 72 | Once you're within the `curseradio` directory, you can install the application | ||
| 73 | with the provided `setup.py` script. | ||
| 74 | |||
| 75 | ```sh | ||
| 76 | sudo python3 setup.py install | ||
| 77 | ``` | ||
| 78 | |||
| 79 | In my case, I ran into a few errors and needed to create the folders that | ||
| 80 | curseradio wanted to use for its installation. If you don't get any errors, you | ||
| 81 | can skip this and run the app. | ||
| 82 | |||
| 83 | ```sh | ||
| 84 | sudo mkdir /usr/local/lib/python3.10/ | ||
| 85 | sudo mkdir /usr/local/lib/python3.10/site-packages/ | ||
| 86 | ``` | ||
| 87 | |||
| 88 | ```sh | ||
| 89 | sudo chown -R $USER:$USER /usr/local/lib/python3.10/ | ||
| 90 | ``` | ||
| 91 | |||
| 92 | # Run the Application | ||
| 93 | |||
| 94 | Once fully installed without errors, you can run the application! | ||
| 95 | |||
| 96 | ```sh | ||
| 97 | python3 /usr/local/bin/curseradio | ||
| 98 | ``` | ||
content/blog/2022-07-25-curseradio.org added +99
| @@ -0,0 +1,99 @@ | |||
| 1 | #+date: <2022-07-25> | ||
| 2 | #+title: CurseRadio: Listening to the Radio on the Command Line | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | While exploring some interesting Linux applications, I stumbled across | ||
| 9 | [[https://github.com/chronitis/curseradio][curseradio]], a command-line | ||
| 10 | radio player based on Python. | ||
| 11 | |||
| 12 | This application is fantastic and incredibly easy to install, so I | ||
| 13 | wanted to dedicate a post today to this app. Let's look at the features | ||
| 14 | within the app and then walk through the installation process I took to | ||
| 15 | get =curseradio= working. | ||
| 16 | |||
| 17 | * Features | ||
| 18 | |||
| 19 | The radio player itself is quite minimal. As you can see in the | ||
| 20 | screenshot above, it contains a simple plaintext list of all available | ||
| 21 | categories, which can be broken down further and further. In addition, | ||
| 22 | radio shows are available for listening, alongside regular radio | ||
| 23 | stations. | ||
| 24 | |||
| 25 | For example, the =Sports= > =Pro Basketball= > =Shows= category contains | ||
| 26 | a number of specific shows related to Professional Basketball. | ||
| 27 | |||
| 28 | Aside from being able to play any of the listed stations/shows, you can | ||
| 29 | make a channel your favorite by pressing =f=. It will now show up at the | ||
| 30 | top of the radio player in the =Favourites= category. | ||
| 31 | |||
| 32 | ** Commands/Shortcuts | ||
| 33 | |||
| 34 | | Key(s) | Command | | ||
| 35 | |------------+---------------------------------| | ||
| 36 | | ↑, ↓ | navigate | | ||
| 37 | | PgUp, PgDn | navigate quickly | | ||
| 38 | | Home, End | to top/bottom | | ||
| 39 | | Enter | open/close folders, play stream | | ||
| 40 | | k | stop playing stream | | ||
| 41 | | q | quit | | ||
| 42 | | f | toggle favourite | | ||
| 43 | |||
| 44 | * Installation | ||
| 45 | |||
| 46 | ** Dependencies | ||
| 47 | |||
| 48 | Before installing =curseradio=, a handful of system and Python packages | ||
| 49 | are required. To get started, install =python3=, =pip3=, and =mpv= on | ||
| 50 | your system. In this example, I'm using Fedora Linux, which uses the | ||
| 51 | =dnf= package manager. You may need to adjust this if you're using a | ||
| 52 | different system. | ||
| 53 | |||
| 54 | #+begin_src sh | ||
| 55 | sudo dnf install python3 pip3 mpv | ||
| 56 | #+end_src | ||
| 57 | |||
| 58 | Next, use =pip3= to install =requests=, =xdg=, and =lxml=: | ||
| 59 | |||
| 60 | #+begin_src sh | ||
| 61 | pip3 install requests xdg lxml | ||
| 62 | #+end_src | ||
| 63 | |||
| 64 | ** Repository Source Installation | ||
| 65 | |||
| 66 | Once all the dependencies are installed, we can clone the source code | ||
| 67 | and enter that directory: | ||
| 68 | |||
| 69 | #+begin_src sh | ||
| 70 | git clone https://github.com/chronitis/curseradio && cd curseradio | ||
| 71 | #+end_src | ||
| 72 | |||
| 73 | Once you're within the =curseradio= directory, you can install the | ||
| 74 | application with the provided =setup.py= script. | ||
| 75 | |||
| 76 | #+begin_src sh | ||
| 77 | sudo python3 setup.py install | ||
| 78 | #+end_src | ||
| 79 | |||
| 80 | In my case, I ran into a few errors and needed to create the folders | ||
| 81 | that curseradio wanted to use for its installation. If you don't get any | ||
| 82 | errors, you can skip this and run the app. | ||
| 83 | |||
| 84 | #+begin_src sh | ||
| 85 | sudo mkdir /usr/local/lib/python3.10/ | ||
| 86 | sudo mkdir /usr/local/lib/python3.10/site-packages/ | ||
| 87 | #+end_src | ||
| 88 | |||
| 89 | #+begin_src sh | ||
| 90 | sudo chown -R $USER:$USER /usr/local/lib/python3.10/ | ||
| 91 | #+end_src | ||
| 92 | |||
| 93 | * Run the Application | ||
| 94 | |||
| 95 | Once fully installed without errors, you can run the application! | ||
| 96 | |||
| 97 | #+begin_src sh | ||
| 98 | python3 /usr/local/bin/curseradio | ||
| 99 | #+end_src | ||
content/blog/2022-07-30-flac-to-opus.md → content/blog/2022-07-30-flac-to-opus.org renamed +68 −68
| @@ -1,68 +1,68 @@ | |||
| 1 | +++ | 1 | #+date: <2022-07-30> |
| 2 | date = 2022-07-30 | 2 | #+title: Recursive Command-Line FLAC to Opus Conversion |
| 3 | title = "Recursive Command-Line FLAC to Opus Conversion" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Converting FLAC to OPUS | 5 | |
| 6 | * Converting FLAC to OPUS | ||
| 9 | 7 | ||
| 10 | I am currently rebuilding my music library from scratch so that I can | 8 | I am currently rebuilding my music library from scratch so that I can |
| 11 | effectively archive all the music I own in the [FLAC file | 9 | effectively archive all the music I own in the |
| 12 | format](https://en.wikipedia.org/wiki/FLAC), a lossless audio codec. | 10 | [[https://en.wikipedia.org/wiki/FLAC][FLAC file format]], a lossless |
| 11 | audio codec. | ||
| 13 | 12 | ||
| 14 | However, streaming FLAC files outside the home can be difficult due to the size | 13 | However, streaming FLAC files outside the home can be difficult due to |
| 15 | of the files, especially if you're using a weak connection. | 14 | the size of the files, especially if you're using a weak connection. |
| 16 | 15 | ||
| 17 | So, in order to archive the music in a lossless format and still be able to | 16 | So, in order to archive the music in a lossless format and still be able |
| 18 | stream it easily, I opted to create a copy of my FLAC files in the [Opus audio | 17 | to stream it easily, I opted to create a copy of my FLAC files in the |
| 19 | codec](<https://en.wikipedia.org/wiki/Opus_(audio_format)>). This allows me to | 18 | [[https://en.wikipedia.org/wiki/Opus_(audio_format)][Opus audio codec]]. |
| 20 | archive a quality, lossless version of the music and then point my streaming | 19 | This allows me to archive a quality, lossless version of the music and |
| 21 | service to the smaller, stream-ready version. | 20 | then point my streaming service to the smaller, stream-ready version. |
| 22 | 21 | ||
| 23 | ## Dependencies | 22 | ** Dependencies |
| 24 | 23 | ||
| 25 | The process I follow utilizes the `opus-tools` package in Ubuntu. Before | 24 | The process I follow utilizes the =opus-tools= package in Ubuntu. Before |
| 26 | proceeding, install the package: | 25 | proceeding, install the package: |
| 27 | 26 | ||
| 28 | ```sh | 27 | #+begin_src sh |
| 29 | sudo apt install opus-tools | 28 | sudo apt install opus-tools |
| 30 | ``` | 29 | #+end_src |
| 31 | 30 | ||
| 32 | If you want to use a different conversion method, such as `ffmpeg` or `avconv`, | 31 | If you want to use a different conversion method, such as =ffmpeg= or |
| 33 | simply install that package instead. | 32 | =avconv=, simply install that package instead. |
| 34 | 33 | ||
| 35 | ## Conversion Process | 34 | ** Conversion Process |
| 36 | 35 | ||
| 37 | The script I'm using is stored in my home directory, but feel free to create it | 36 | The script I'm using is stored in my home directory, but feel free to |
| 38 | wherever you want. It does not need to be in the same directory as your music | 37 | create it wherever you want. It does not need to be in the same |
| 39 | files. | 38 | directory as your music files. |
| 40 | 39 | ||
| 41 | ```sh | 40 | #+begin_src sh |
| 42 | cd ~ && nano transform.sh | 41 | cd ~ && nano transform.sh |
| 43 | ``` | 42 | #+end_src |
| 44 | 43 | ||
| 45 | Once you have your new bash script opened in an editor, go ahead and paste the | 44 | Once you have your new bash script opened in an editor, go ahead and |
| 46 | following logic into the script. | 45 | paste the following logic into the script. |
| 47 | 46 | ||
| 48 | You **MUST** edit the following variables in order for it to work: | 47 | You *MUST* edit the following variables in order for it to work: |
| 49 | 48 | ||
| 50 | - `source`: The source directory where your FLAC files are stored. | 49 | - =source=: The source directory where your FLAC files are stored. |
| 51 | - `dest`: The destination directory where you want the resulting Opus files to | 50 | - =dest=: The destination directory where you want the resulting Opus |
| 52 | be stored. | 51 | files to be stored. |
| 53 | 52 | ||
| 54 | You **MAY** want to edit the following variables to suit your needs: | 53 | You *MAY* want to edit the following variables to suit your needs: |
| 55 | 54 | ||
| 56 | - `filename`: If you are converting to a file format other than Opus, you'll | 55 | - =filename=: If you are converting to a file format other than Opus, |
| 57 | need to edit this so that your resulting files have the correct filename | 56 | you'll need to edit this so that your resulting files have the correct |
| 58 | extension. | 57 | filename extension. |
| 59 | - `reldir`: This variable can be edited to strip out more leading directories | 58 | - =reldir=: This variable can be edited to strip out more leading |
| 60 | in the file path. As you'll see later, I ignore this for now and simply | 59 | directories in the file path. As you'll see later, I ignore this for |
| 61 | clean it up afterward. | 60 | now and simply clean it up afterward. |
| 62 | - `opusenc`: This is the actual conversion process. You may want to edit the | 61 | - =opusenc=: This is the actual conversion process. You may want to edit |
| 63 | bitrate to suit your needs. I set mine at 128 but some prefer 160 or higher. | 62 | the bitrate to suit your needs. I set mine at 128 but some prefer 160 |
| 63 | or higher. | ||
| 64 | 64 | ||
| 65 | ```sh | 65 | #+begin_src sh |
| 66 | #!/bin/bash | 66 | #!/bin/bash |
| 67 | ## - The IFS takes care of spaces in file and dirnames | 67 | ## - The IFS takes care of spaces in file and dirnames |
| 68 | ## - your folders may vary | 68 | ## - your folders may vary |
| @@ -124,51 +124,51 @@ opusenc --vbr --bitrate 128 --date "$DATE" \ | |||
| 124 | ## just for testing | 124 | ## just for testing |
| 125 | # sleep 1 | 125 | # sleep 1 |
| 126 | done | 126 | done |
| 127 | ``` | 127 | #+end_src |
| 128 | 128 | ||
| 129 | Once you're done, simply save the file and exit your editor. Don't forget to | 129 | Once you're done, simply save the file and exit your editor. Don't |
| 130 | enable execution of the script: | 130 | forget to enable execution of the script: |
| 131 | 131 | ||
| 132 | ```sh | 132 | #+begin_src sh |
| 133 | chmod +x transform.sh | 133 | chmod +x transform.sh |
| 134 | ``` | 134 | #+end_src |
| 135 | 135 | ||
| 136 | Finally, you may now run the script: | 136 | Finally, you may now run the script: |
| 137 | 137 | ||
| 138 | ```sh | 138 | #+begin_src sh |
| 139 | ./transform.sh | 139 | ./transform.sh |
| 140 | ``` | 140 | #+end_src |
| 141 | 141 | ||
| 142 | If you used `opusenc`, you'll see the conversions happen within the terminal as | 142 | If you used =opusenc=, you'll see the conversions happen within the |
| 143 | it progresses. You will also see variables printed if you uncommented any of the | 143 | terminal as it progresses. You will also see variables printed if you |
| 144 | bash script's comments. | 144 | uncommented any of the bash script's comments. |
| 145 | 145 | ||
| 146 | ## Cleanup | 146 | ** Cleanup |
| 147 | 147 | ||
| 148 | As I noted above, I didn't customize my `reldir` variable in the script, which | 148 | As I noted above, I didn't customize my =reldir= variable in the script, |
| 149 | caused my output directory to be `/mnt/music/library/archives` instead of | 149 | which caused my output directory to be =/mnt/music/library/archives= |
| 150 | `/mnt/music/library`. So, I moved the output up one level and deleted the | 150 | instead of =/mnt/music/library=. So, I moved the output up one level and |
| 151 | accidental directory. | 151 | deleted the accidental directory. |
| 152 | 152 | ||
| 153 | ```sh | 153 | #+begin_src sh |
| 154 | cd /mnt/music/library | 154 | cd /mnt/music/library |
| 155 | mv archives/** . | 155 | mv archives/** . |
| 156 | rm -rf archives | 156 | rm -rf archives |
| 157 | ``` | 157 | #+end_src |
| 158 | 158 | ||
| 159 | ## Check the Resulting Size | 159 | ** Check the Resulting Size |
| 160 | 160 | ||
| 161 | If you want to see what kind of file size savings you've gained, you can always | 161 | If you want to see what kind of file size savings you've gained, you can |
| 162 | use the `du` command to check: | 162 | always use the =du= command to check: |
| 163 | 163 | ||
| 164 | ```sh | 164 | #+begin_src sh |
| 165 | cd /mnt/music | 165 | cd /mnt/music |
| 166 | du -h --max-depth=1 . | 166 | du -h --max-depth=1 . |
| 167 | ``` | 167 | #+end_src |
| 168 | 168 | ||
| 169 | In my case, my small library went from 78GB to 6.3GB! | 169 | In my case, my small library went from 78GB to 6.3GB! |
| 170 | 170 | ||
| 171 | ```txt | 171 | #+begin_src txt |
| 172 | 78G ./archives | 172 | 78G ./archives |
| 173 | 6.3G ./library | 173 | 6.3G ./library |
| 174 | ``` | 174 | #+end_src |
content/blog/2022-07-31-bash-it.md deleted −237
| @@ -1,237 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-07-31 | ||
| 3 | title = "Upgrade Bash with Bash-It and Ble.sh" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Bash | ||
| 9 | |||
| 10 | For those who are not familiar, | ||
| 11 | [Bash](<https://en.wikipedia.org/wiki/Bash_(Unix_shell)>) is a Unix shell that | ||
| 12 | is used as the default login shell for most Linux distributions. This shell and | ||
| 13 | command processor should be familiar if you've used Linux (or older version of | ||
| 14 | macOS) before. | ||
| 15 | |||
| 16 | However, bash is not the only option. There are numerous other shells that | ||
| 17 | exist. Here are some popular examples: | ||
| 18 | |||
| 19 | - [zsh](https://en.wikipedia.org/wiki/Z_shell) | ||
| 20 | - [fish](<https://en.wikipedia.org/wiki/Fish_(Unix_shell)>) | ||
| 21 | - [oksh](https://github.com/ibara/oksh) | ||
| 22 | - [mksh](https://wiki.gentoo.org/wiki/Mksh) | ||
| 23 | - [dash](https://en.wikipedia.org/wiki/Debian_Almquist_shell) | ||
| 24 | |||
| 25 | While each shell has its differences, bash is POSIX compliant and the default | ||
| 26 | for many Linux users. Because of this, I am going to explore a program called | ||
| 27 | `bash-it` below that helps bash users increase the utility of their shell | ||
| 28 | without installing a completely new shell. | ||
| 29 | |||
| 30 | ## Installation | ||
| 31 | |||
| 32 | First, if bash is not already installed on your system, you can [download bash | ||
| 33 | from GNU](https://www.gnu.org/software/bash/) or use your package manager to | ||
| 34 | install it. | ||
| 35 | |||
| 36 | For example, this is how you can install bash on Fedora Linux: | ||
| 37 | |||
| 38 | ```sh | ||
| 39 | sudo dnf install bash | ||
| 40 | ``` | ||
| 41 | |||
| 42 | If you are not using bash as your default shell, use the `chsh` command to | ||
| 43 | change your shell: | ||
| 44 | |||
| 45 | ```sh | ||
| 46 | chsh | ||
| 47 | ``` | ||
| 48 | |||
| 49 | You should see a prompt like the one below. If the brackets (`[]`) contain | ||
| 50 | `bash` already, you're done, and you can simply continue by hitting the Enter | ||
| 51 | key. | ||
| 52 | |||
| 53 | If the brackets contain another shell path (e.g. `/usr/bin/zsh`), enter the path | ||
| 54 | to the bash program on your system (it's most likely located at | ||
| 55 | `/usr/bin/bash`). | ||
| 56 | |||
| 57 | ```sh | ||
| 58 | Changing shell for <user>. | ||
| 59 | New shell [/usr/bin/bash]: | ||
| 60 | ``` | ||
| 61 | |||
| 62 | You must log out or restart the machine in order for the login shell to be | ||
| 63 | refreshed. You can do it now or wait until you're finished customizing the | ||
| 64 | shell. | ||
| 65 | |||
| 66 | ```sh | ||
| 67 | sudo reboot now | ||
| 68 | ``` | ||
| 69 | |||
| 70 | # Bash-it | ||
| 71 | |||
| 72 | As noted on the [Bash-it](https://github.com/Bash-it/bash-it) repository: | ||
| 73 | |||
| 74 | > Bash-it is a collection of community Bash commands and scripts for Bash 3.2+. | ||
| 75 | > (And a shameless ripoff of oh-my-zsh 😃) | ||
| 76 | |||
| 77 | Bash-it makes it easy to install plugins, set up aliases for common commands, | ||
| 78 | and easily change the visual theme of your shell. | ||
| 79 | |||
| 80 | ## Installation | ||
| 81 | |||
| 82 | To install the framework, simply copy the repository files and use the | ||
| 83 | `install.sh` script provided. If you want, you can (and should!) inspect the | ||
| 84 | contents of the installation script before you run it. | ||
| 85 | |||
| 86 | ```sh | ||
| 87 | git clone --depth=1 https://github.com/Bash-it/bash-it.git ~/.bash_it | ||
| 88 | ~/.bash_it/install.sh | ||
| 89 | ``` | ||
| 90 | |||
| 91 | If you didn't restart your session after making bash the default, and are | ||
| 92 | currently working within another shell, be sure to enter a bash session before | ||
| 93 | using `bash-it`: | ||
| 94 | |||
| 95 | ```sh | ||
| 96 | bash | ||
| 97 | ``` | ||
| 98 | |||
| 99 | ## Aliases | ||
| 100 | |||
| 101 | Bash-it contains a number of aliases for common commands to help improve | ||
| 102 | efficiency in the terminal. To list all available options, use the following | ||
| 103 | command: | ||
| 104 | |||
| 105 | ```sh | ||
| 106 | bash-it show aliases | ||
| 107 | ``` | ||
| 108 | |||
| 109 | This will provide you a list that looks like the following text block. Within | ||
| 110 | this screen, you will be able to see all available options and which ones are | ||
| 111 | currently enabled. | ||
| 112 | |||
| 113 | ```txt | ||
| 114 | Alias Enabled? Description | ||
| 115 | ag [ ] the silver searcher (ag) aliases | ||
| 116 | ansible [ ] ansible abbreviations | ||
| 117 | apt [ ] Apt and dpkg aliases for Ubuntu and Debian distros. | ||
| 118 | atom [ ] Atom.io editor abbreviations | ||
| 119 | bash-it [ ] Aliases for the bash-it command (these aliases are automatically included with the "general" aliases) | ||
| 120 | bolt [ ] puppet bolt aliases | ||
| 121 | bundler [ ] ruby bundler | ||
| 122 | clipboard [ ] xclip shortcuts | ||
| 123 | composer [ ] common composer abbreviations | ||
| 124 | curl [x] Curl aliases for convenience. | ||
| 125 | ... | ||
| 126 | ``` | ||
| 127 | |||
| 128 | To enable an alias, do: | ||
| 129 | |||
| 130 | ```sh | ||
| 131 | bash-it enable alias <alias name> [alias name]... -or- $ bash-it enable alias all | ||
| 132 | ``` | ||
| 133 | |||
| 134 | To disable an alias, do: | ||
| 135 | |||
| 136 | ```sh | ||
| 137 | bash-it disable alias <alias name> [alias name]... -or- $ bash-it disable alias all | ||
| 138 | ``` | ||
| 139 | |||
| 140 | ## Plugins | ||
| 141 | |||
| 142 | Similar to aliases, plugins are available with bash-it. You can find a complete | ||
| 143 | list of plugins in the same way as aliases. Simply execute the following: | ||
| 144 | |||
| 145 | ```sh | ||
| 146 | bash-it show plugins | ||
| 147 | ``` | ||
| 148 | |||
| 149 | You will see the following output showing enabled and disabled plugins: | ||
| 150 | |||
| 151 | ```txt | ||
| 152 | Plugin Enabled? Description | ||
| 153 | alias-completion [ ] | ||
| 154 | autojump [ ] Autojump configuration, see https://github.com/wting/autojump for more details | ||
| 155 | aws [ ] AWS helper functions | ||
| 156 | base [x] miscellaneous tools | ||
| 157 | basher [ ] initializes basher, the shell package manager | ||
| 158 | battery [x] display info about your battery charge level | ||
| 159 | blesh [ ] load ble.sh, the Bash line editor! | ||
| 160 | boot2docker [ ] Helpers to get Docker setup correctly for boot2docker | ||
| 161 | browser [ ] render commandline output in your browser | ||
| 162 | ``` | ||
| 163 | |||
| 164 | To enable a plugin, do: | ||
| 165 | |||
| 166 | ```sh | ||
| 167 | bash-it enable plugin <plugin name> [plugin name]... -or- $ bash-it enable plugin all | ||
| 168 | ``` | ||
| 169 | |||
| 170 | To disable a plugin, do: | ||
| 171 | |||
| 172 | ```sh | ||
| 173 | bash-it disable plugin <plugin name> [plugin name]... -or- $ bash-it disable plugin all | ||
| 174 | ``` | ||
| 175 | |||
| 176 | ## Themes | ||
| 177 | |||
| 178 | There are quite a few pre-defined | ||
| 179 | [themes](https://bash-it.readthedocs.io/en/latest/themes-list/#list-of-themes) | ||
| 180 | available with bash-it. | ||
| 181 | |||
| 182 | To list all themes: | ||
| 183 | |||
| 184 | ```sh | ||
| 185 | ls ~/.bash_it/themes/ | ||
| 186 | ``` | ||
| 187 | |||
| 188 | To use a new theme, you'll need to edit `.bashrc` and alter the `BASH_IT_THEME` | ||
| 189 | variable to your desired theme. For example, I am using the `zork` theme. | ||
| 190 | |||
| 191 | ```sh | ||
| 192 | nano ~/.bashrc | ||
| 193 | ``` | ||
| 194 | |||
| 195 | ```sh | ||
| 196 | export BASH_IT_THEME='zork' | ||
| 197 | ``` | ||
| 198 | |||
| 199 | Once you save your changes, you just need to exit your terminal and create a new | ||
| 200 | one in order to see your changes to the `.bashrc` file. You can also `source` | ||
| 201 | the file to see changes, but I recommend starting a completely new shell | ||
| 202 | instead. | ||
| 203 | |||
| 204 | ### ble.sh | ||
| 205 | |||
| 206 | One big feature I was missing in Bash that both `zsh` and `fish` have is an | ||
| 207 | autosuggestion feature. To explain: as you type, an autosuggestion feature in | ||
| 208 | the shell will offer suggestions in a lighter font color beyond the characters | ||
| 209 | already typed. Once you see the command you want, you can click the right arrow | ||
| 210 | and have the shell auto-complete that line for you. | ||
| 211 | |||
| 212 | Luckily, the [Bash Line Editor](https://github.com/akinomyoga/ble.sh) (ble.sh) | ||
| 213 | exists! This program provides a wonderful autosuggestions feature perfectly, | ||
| 214 | among other features that I haven't tested yet. | ||
| 215 | |||
| 216 | In order to install ble.sh, execute the following: | ||
| 217 | |||
| 218 | ```sh | ||
| 219 | git clone --recursive https://github.com/akinomyoga/ble.sh.git | ||
| 220 | make -C ble.sh install PREFIX=~/.local | ||
| 221 | echo 'source ~/.local/share/blesh/ble.sh' >> ~/.bashrc | ||
| 222 | ``` | ||
| 223 | |||
| 224 | Again, exit the terminal and open a new one in order to see the newly-configured | ||
| 225 | shell. | ||
| 226 | |||
| 227 | # Restart the Session | ||
| 228 | |||
| 229 | Finally, as mentioned above, you'll need to restart the session to ensure that | ||
| 230 | your user is using bash by default. | ||
| 231 | |||
| 232 | You will also need to exit and re-open a shell (e.g., terminal or terminal tab) | ||
| 233 | any time you make changes to the `.bashrc` file. | ||
| 234 | |||
| 235 | ```sh | ||
| 236 | sudo reboot now | ||
| 237 | ``` | ||
content/blog/2022-07-31-bash-it.org added +241
| @@ -0,0 +1,241 @@ | |||
| 1 | #+date: <2022-07-31> | ||
| 2 | #+title: Upgrade Bash with Bash-It and Ble.sh | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Bash | ||
| 7 | |||
| 8 | For those who are not familiar, | ||
| 9 | [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][Bash]] is a Unix | ||
| 10 | shell that is used as the default login shell for most Linux | ||
| 11 | distributions. This shell and command processor should be familiar if | ||
| 12 | you've used Linux (or older version of macOS) before. | ||
| 13 | |||
| 14 | However, bash is not the only option. There are numerous other shells | ||
| 15 | that exist. Here are some popular examples: | ||
| 16 | |||
| 17 | - [[https://en.wikipedia.org/wiki/Z_shell][zsh]] | ||
| 18 | - [[https://en.wikipedia.org/wiki/Fish_(Unix_shell)][fish]] | ||
| 19 | - [[https://github.com/ibara/oksh][oksh]] | ||
| 20 | - [[https://wiki.gentoo.org/wiki/Mksh][mksh]] | ||
| 21 | - [[https://en.wikipedia.org/wiki/Debian_Almquist_shell][dash]] | ||
| 22 | |||
| 23 | While each shell has its differences, bash is POSIX compliant and the | ||
| 24 | default for many Linux users. Because of this, I am going to explore a | ||
| 25 | program called =bash-it= below that helps bash users increase the | ||
| 26 | utility of their shell without installing a completely new shell. | ||
| 27 | |||
| 28 | ** Installation | ||
| 29 | |||
| 30 | First, if bash is not already installed on your system, you can | ||
| 31 | [[https://www.gnu.org/software/bash/][download bash from GNU]] or use | ||
| 32 | your package manager to install it. | ||
| 33 | |||
| 34 | For example, this is how you can install bash on Fedora Linux: | ||
| 35 | |||
| 36 | #+begin_src sh | ||
| 37 | sudo dnf install bash | ||
| 38 | #+end_src | ||
| 39 | |||
| 40 | If you are not using bash as your default shell, use the =chsh= command | ||
| 41 | to change your shell: | ||
| 42 | |||
| 43 | #+begin_src sh | ||
| 44 | chsh | ||
| 45 | #+end_src | ||
| 46 | |||
| 47 | You should see a prompt like the one below. If the brackets (=[]=) | ||
| 48 | contain =bash= already, you're done, and you can simply continue by | ||
| 49 | hitting the Enter key. | ||
| 50 | |||
| 51 | If the brackets contain another shell path (e.g. =/usr/bin/zsh=), enter | ||
| 52 | the path to the bash program on your system (it's most likely located at | ||
| 53 | =/usr/bin/bash=). | ||
| 54 | |||
| 55 | #+begin_src sh | ||
| 56 | Changing shell for <user>. | ||
| 57 | New shell [/usr/bin/bash]: | ||
| 58 | #+end_src | ||
| 59 | |||
| 60 | You must log out or restart the machine in order for the login shell to | ||
| 61 | be refreshed. You can do it now or wait until you're finished | ||
| 62 | customizing the shell. | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | sudo reboot now | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | * Bash-it | ||
| 69 | |||
| 70 | As noted on the [[https://github.com/Bash-it/bash-it][Bash-it]] | ||
| 71 | repository: | ||
| 72 | |||
| 73 | #+begin_quote | ||
| 74 | Bash-it is a collection of community Bash commands and scripts for Bash | ||
| 75 | 3.2+. (And a shameless ripoff of oh-my-zsh 😃) | ||
| 76 | #+end_quote | ||
| 77 | |||
| 78 | Bash-it makes it easy to install plugins, set up aliases for common | ||
| 79 | commands, and easily change the visual theme of your shell. | ||
| 80 | |||
| 81 | ** Installation | ||
| 82 | |||
| 83 | To install the framework, simply copy the repository files and use the | ||
| 84 | =install.sh= script provided. If you want, you can (and should!) inspect | ||
| 85 | the contents of the installation script before you run it. | ||
| 86 | |||
| 87 | #+begin_src sh | ||
| 88 | git clone --depth=1 https://github.com/Bash-it/bash-it.git ~/.bash_it | ||
| 89 | ~/.bash_it/install.sh | ||
| 90 | #+end_src | ||
| 91 | |||
| 92 | If you didn't restart your session after making bash the default, and | ||
| 93 | are currently working within another shell, be sure to enter a bash | ||
| 94 | session before using =bash-it=: | ||
| 95 | |||
| 96 | #+begin_src sh | ||
| 97 | bash | ||
| 98 | #+end_src | ||
| 99 | |||
| 100 | ** Aliases | ||
| 101 | |||
| 102 | Bash-it contains a number of aliases for common commands to help improve | ||
| 103 | efficiency in the terminal. To list all available options, use the | ||
| 104 | following command: | ||
| 105 | |||
| 106 | #+begin_src sh | ||
| 107 | bash-it show aliases | ||
| 108 | #+end_src | ||
| 109 | |||
| 110 | This will provide you a list that looks like the following text block. | ||
| 111 | Within this screen, you will be able to see all available options and | ||
| 112 | which ones are currently enabled. | ||
| 113 | |||
| 114 | #+begin_src txt | ||
| 115 | Alias Enabled? Description | ||
| 116 | ag [ ] the silver searcher (ag) aliases | ||
| 117 | ansible [ ] ansible abbreviations | ||
| 118 | apt [ ] Apt and dpkg aliases for Ubuntu and Debian distros. | ||
| 119 | atom [ ] Atom.io editor abbreviations | ||
| 120 | bash-it [ ] Aliases for the bash-it command (these aliases are automatically included with the "general" aliases) | ||
| 121 | bolt [ ] puppet bolt aliases | ||
| 122 | bundler [ ] ruby bundler | ||
| 123 | clipboard [ ] xclip shortcuts | ||
| 124 | composer [ ] common composer abbreviations | ||
| 125 | curl [x] Curl aliases for convenience. | ||
| 126 | ... | ||
| 127 | #+end_src | ||
| 128 | |||
| 129 | To enable an alias, do: | ||
| 130 | |||
| 131 | #+begin_src sh | ||
| 132 | bash-it enable alias <alias name> [alias name]... -or- $ bash-it enable alias all | ||
| 133 | #+end_src | ||
| 134 | |||
| 135 | To disable an alias, do: | ||
| 136 | |||
| 137 | #+begin_src sh | ||
| 138 | bash-it disable alias <alias name> [alias name]... -or- $ bash-it disable alias all | ||
| 139 | #+end_src | ||
| 140 | |||
| 141 | ** Plugins | ||
| 142 | |||
| 143 | Similar to aliases, plugins are available with bash-it. You can find a | ||
| 144 | complete list of plugins in the same way as aliases. Simply execute the | ||
| 145 | following: | ||
| 146 | |||
| 147 | #+begin_src sh | ||
| 148 | bash-it show plugins | ||
| 149 | #+end_src | ||
| 150 | |||
| 151 | You will see the following output showing enabled and disabled plugins: | ||
| 152 | |||
| 153 | #+begin_src txt | ||
| 154 | Plugin Enabled? Description | ||
| 155 | alias-completion [ ] | ||
| 156 | autojump [ ] Autojump configuration, see https://github.com/wting/autojump for more details | ||
| 157 | aws [ ] AWS helper functions | ||
| 158 | base [x] miscellaneous tools | ||
| 159 | basher [ ] initializes basher, the shell package manager | ||
| 160 | battery [x] display info about your battery charge level | ||
| 161 | blesh [ ] load ble.sh, the Bash line editor! | ||
| 162 | boot2docker [ ] Helpers to get Docker setup correctly for boot2docker | ||
| 163 | browser [ ] render commandline output in your browser | ||
| 164 | #+end_src | ||
| 165 | |||
| 166 | To enable a plugin, do: | ||
| 167 | |||
| 168 | #+begin_src sh | ||
| 169 | bash-it enable plugin <plugin name> [plugin name]... -or- $ bash-it enable plugin all | ||
| 170 | #+end_src | ||
| 171 | |||
| 172 | To disable a plugin, do: | ||
| 173 | |||
| 174 | #+begin_src sh | ||
| 175 | bash-it disable plugin <plugin name> [plugin name]... -or- $ bash-it disable plugin all | ||
| 176 | #+end_src | ||
| 177 | |||
| 178 | ** Themes | ||
| 179 | |||
| 180 | There are quite a few pre-defined | ||
| 181 | [[https://bash-it.readthedocs.io/en/latest/themes-list/#list-of-themes][themes]] | ||
| 182 | available with bash-it. | ||
| 183 | |||
| 184 | To list all themes: | ||
| 185 | |||
| 186 | #+begin_src sh | ||
| 187 | ls ~/.bash_it/themes/ | ||
| 188 | #+end_src | ||
| 189 | |||
| 190 | To use a new theme, you'll need to edit =.bashrc= and alter the | ||
| 191 | =BASH_IT_THEME= variable to your desired theme. For example, I am using | ||
| 192 | the =zork= theme. | ||
| 193 | |||
| 194 | #+begin_src sh | ||
| 195 | nano ~/.bashrc | ||
| 196 | #+end_src | ||
| 197 | |||
| 198 | #+begin_src sh | ||
| 199 | export BASH_IT_THEME='zork' | ||
| 200 | #+end_src | ||
| 201 | |||
| 202 | Once you save your changes, you just need to exit your terminal and | ||
| 203 | create a new one in order to see your changes to the =.bashrc= file. You | ||
| 204 | can also =source= the file to see changes, but I recommend starting a | ||
| 205 | completely new shell instead. | ||
| 206 | |||
| 207 | *** ble.sh | ||
| 208 | |||
| 209 | One big feature I was missing in Bash that both =zsh= and =fish= have is | ||
| 210 | an autosuggestion feature. To explain: as you type, an autosuggestion | ||
| 211 | feature in the shell will offer suggestions in a lighter font color | ||
| 212 | beyond the characters already typed. Once you see the command you want, | ||
| 213 | you can click the right arrow and have the shell auto-complete that line | ||
| 214 | for you. | ||
| 215 | |||
| 216 | Luckily, the [[https://github.com/akinomyoga/ble.sh][Bash Line Editor]] | ||
| 217 | (ble.sh) exists! This program provides a wonderful autosuggestions | ||
| 218 | feature perfectly, among other features that I haven't tested yet. | ||
| 219 | |||
| 220 | In order to install ble.sh, execute the following: | ||
| 221 | |||
| 222 | #+begin_src sh | ||
| 223 | git clone --recursive https://github.com/akinomyoga/ble.sh.git | ||
| 224 | make -C ble.sh install PREFIX=~/.local | ||
| 225 | echo 'source ~/.local/share/blesh/ble.sh' >> ~/.bashrc | ||
| 226 | #+end_src | ||
| 227 | |||
| 228 | Again, exit the terminal and open a new one in order to see the | ||
| 229 | newly-configured shell. | ||
| 230 | |||
| 231 | * Restart the Session | ||
| 232 | |||
| 233 | Finally, as mentioned above, you'll need to restart the session to | ||
| 234 | ensure that your user is using bash by default. | ||
| 235 | |||
| 236 | You will also need to exit and re-open a shell (e.g., terminal or | ||
| 237 | terminal tab) any time you make changes to the =.bashrc= file. | ||
| 238 | |||
| 239 | #+begin_src sh | ||
| 240 | sudo reboot now | ||
| 241 | #+end_src | ||
content/blog/2022-08-31-privacy-com-changes.md deleted −92
| @@ -1,92 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-08-31 | ||
| 3 | title = "Concerning Changes on Privacy.com" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Privacy.com Changes Their Terms | ||
| 9 | |||
| 10 | Recently, Privacy.com reached out to their customers regarding a change in their | ||
| 11 | terms of use. Further, all customers are required to agree to the changes in | ||
| 12 | order to continue using their accounts. | ||
| 13 | |||
| 14 | [You can view the new cardholder agreement | ||
| 15 | here](https://privacy.com/commercial-cardholder-agreement). | ||
| 16 | |||
| 17 | When you log in, you'll be greeted with a pop-up window asking you to review and | ||
| 18 | agree to the new terms of use. You will also not be able to open any new cards | ||
| 19 | until the terms are agreed to. | ||
| 20 | |||
| 21 | ## Changing from a "Prepaid Debit" Model to a "Charge Card" Model | ||
| 22 | |||
| 23 | The actual content of the changes is interesting. While the historical model of | ||
| 24 | using Privacy.com was akin to prepaid debit cards, the new model is very similar | ||
| 25 | to a credit card (they use the term "charge card"). | ||
| 26 | |||
| 27 | I have used Privacy.com for 1-2 years, and the process was always that you would | ||
| 28 | create a single-use or merchant-locked card. This card could be used for any | ||
| 29 | dollar limit you set and would immediately draw the funds from whatever funding | ||
| 30 | source you connected, e.g. PayPal account or a bank account. | ||
| 31 | |||
| 32 | The benefit this service provides with a premium account is masking the merchant | ||
| 33 | names from your funding source. If you have a concern that your bank account | ||
| 34 | uses merchant data from your account, you could direct all charges through | ||
| 35 | Privacy.com and set the merchant as one of their pre-set options, such as | ||
| 36 | "Smiley's Corner Store" or "NSA Gift Shop." | ||
| 37 | |||
| 38 | The new model still works with a bank account as a funding source, but the model | ||
| 39 | is changed so that you get a "line of credit" set according to a 14-day billing | ||
| 40 | cycle. It seems that Privacy.com will now allow charges to be incurred without | ||
| 41 | being immediately paid. | ||
| 42 | |||
| 43 | ## Daily Payments and Available Credit | ||
| 44 | |||
| 45 | Instead of paying as charges are incurred, you must make a "Daily Payment" and | ||
| 46 | your "Available Credit" will be locked until you make that payment. There are | ||
| 47 | also "End of Billing Cycle Payments" that are assigned a due date. | ||
| 48 | |||
| 49 | Further, Privacy.com will decline charges that would cause you to exceed your | ||
| 50 | Available Credit or Credit Limit. | ||
| 51 | |||
| 52 | One particular interesting section states the following: | ||
| 53 | |||
| 54 | > YOUR OBLIGATION TO PAY US BACK FOR ALL CARD TRANSACTIONS AND OTHER OBLIGATIONS | ||
| 55 | > YOU INCUR IS SECURED BY THE SECURED ACCOUNT. IF YOU DO NOT PAY US BACK FOR ANY | ||
| 56 | > AMOUNT YOU OWE US WHEN YOUR PAYMENTS ARE DUE, WE WILL EXERCISE OUR INTEREST | ||
| 57 | > AND DEBIT THE SECURED ACCOUNT, AND YOU WILL LOSE THE MONEY IN THE SECURED | ||
| 58 | > ACCOUNT. SEE SECTION 8: SECURITY AGREEMENT FOR MORE INFORMATION. | ||
| 59 | |||
| 60 | ## Personal Information | ||
| 61 | |||
| 62 | Now that Privacy.com is more of a financial institution, they are obligated to | ||
| 63 | comply with the [know your | ||
| 64 | customer](https://en.wikipedia.org/wiki/Know_your_customer) guidelines/laws. | ||
| 65 | |||
| 66 | I did not proceed with the change to my Privacy.com account, but I have heard | ||
| 67 | from some peers that the changes require more personal information to be | ||
| 68 | submitted, such as SSN. I am not aware of all new personal information required | ||
| 69 | or if the funding source is now required to only be a bank account. | ||
| 70 | |||
| 71 | ## Fees | ||
| 72 | |||
| 73 | Luckily, the fees section did not change much. The subscription fees for a | ||
| 74 | premium account are still the only fees. | ||
| 75 | |||
| 76 | # My Thoughts | ||
| 77 | |||
| 78 | Personally, I wiped my personal information from my account and then permanently | ||
| 79 | deleted it when I heard about these changes. I have no interest in yet another | ||
| 80 | method of credit lending offered by private companies. While I accepted that | ||
| 81 | they would have access to my bank account information for the purposes of paying | ||
| 82 | off my prepaid debit payments, I have no interest in incurring charges that will | ||
| 83 | need to be paid back at a later date. I also have no interest in submitting | ||
| 84 | personal information to Privacy.com. | ||
| 85 | |||
| 86 | This type of change toward a "buy it now, pay us later" model is concerning, and | ||
| 87 | I will be watching Privacy.com to see if they further their interests in the | ||
| 88 | credit model as time goes on. | ||
| 89 | |||
| 90 | Could we see them start charging interest, fees, etc.? I'm not sure, but this | ||
| 91 | change does not inspire confidence in their mission as a privacy-focused | ||
| 92 | company. | ||
content/blog/2022-08-31-privacy-com-changes.org added +99
| @@ -0,0 +1,99 @@ | |||
| 1 | #+date: <2022-08-31> | ||
| 2 | #+title: Concerning Changes on Privacy.com | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Privacy.com Changes Their Terms | ||
| 7 | |||
| 8 | Recently, Privacy.com reached out to their customers regarding a change | ||
| 9 | in their terms of use. Further, all customers are required to agree to | ||
| 10 | the changes in order to continue using their accounts. | ||
| 11 | |||
| 12 | [[https://privacy.com/commercial-cardholder-agreement][You can view the | ||
| 13 | new cardholder agreement here]]. | ||
| 14 | |||
| 15 | When you log in, you'll be greeted with a pop-up window asking you to | ||
| 16 | review and agree to the new terms of use. You will also not be able to | ||
| 17 | open any new cards until the terms are agreed to. | ||
| 18 | |||
| 19 | ** Changing from a "Prepaid Debit" Model to a "Charge Card" Model | ||
| 20 | |||
| 21 | The actual content of the changes is interesting. While the historical | ||
| 22 | model of using Privacy.com was akin to prepaid debit cards, the new | ||
| 23 | model is very similar to a credit card (they use the term "charge | ||
| 24 | card"). | ||
| 25 | |||
| 26 | I have used Privacy.com for 1-2 years, and the process was always that | ||
| 27 | you would create a single-use or merchant-locked card. This card could | ||
| 28 | be used for any dollar limit you set and would immediately draw the | ||
| 29 | funds from whatever funding source you connected, e.g. PayPal account or | ||
| 30 | a bank account. | ||
| 31 | |||
| 32 | The benefit this service provides with a premium account is masking the | ||
| 33 | merchant names from your funding source. If you have a concern that your | ||
| 34 | bank account uses merchant data from your account, you could direct all | ||
| 35 | charges through Privacy.com and set the merchant as one of their pre-set | ||
| 36 | options, such as "Smiley's Corner Store" or "NSA Gift Shop." | ||
| 37 | |||
| 38 | The new model still works with a bank account as a funding source, but | ||
| 39 | the model is changed so that you get a "line of credit" set according to | ||
| 40 | a 14-day billing cycle. It seems that Privacy.com will now allow charges | ||
| 41 | to be incurred without being immediately paid. | ||
| 42 | |||
| 43 | ** Daily Payments and Available Credit | ||
| 44 | |||
| 45 | Instead of paying as charges are incurred, you must make a "Daily | ||
| 46 | Payment" and your "Available Credit" will be locked until you make that | ||
| 47 | payment. There are also "End of Billing Cycle Payments" that are | ||
| 48 | assigned a due date. | ||
| 49 | |||
| 50 | Further, Privacy.com will decline charges that would cause you to exceed | ||
| 51 | your Available Credit or Credit Limit. | ||
| 52 | |||
| 53 | One particular interesting section states the following: | ||
| 54 | |||
| 55 | #+begin_quote | ||
| 56 | YOUR OBLIGATION TO PAY US BACK FOR ALL CARD TRANSACTIONS AND OTHER | ||
| 57 | OBLIGATIONS YOU INCUR IS SECURED BY THE SECURED ACCOUNT. IF YOU DO NOT | ||
| 58 | PAY US BACK FOR ANY AMOUNT YOU OWE US WHEN YOUR PAYMENTS ARE DUE, WE | ||
| 59 | WILL EXERCISE OUR INTEREST AND DEBIT THE SECURED ACCOUNT, AND YOU WILL | ||
| 60 | LOSE THE MONEY IN THE SECURED ACCOUNT. SEE SECTION 8: SECURITY AGREEMENT | ||
| 61 | FOR MORE INFORMATION. | ||
| 62 | #+end_quote | ||
| 63 | |||
| 64 | ** Personal Information | ||
| 65 | |||
| 66 | Now that Privacy.com is more of a financial institution, they are | ||
| 67 | obligated to comply with the | ||
| 68 | [[https://en.wikipedia.org/wiki/Know_your_customer][know your customer]] | ||
| 69 | guidelines/laws. | ||
| 70 | |||
| 71 | I did not proceed with the change to my Privacy.com account, but I have | ||
| 72 | heard from some peers that the changes require more personal information | ||
| 73 | to be submitted, such as SSN. I am not aware of all new personal | ||
| 74 | information required or if the funding source is now required to only be | ||
| 75 | a bank account. | ||
| 76 | |||
| 77 | ** Fees | ||
| 78 | |||
| 79 | Luckily, the fees section did not change much. The subscription fees for | ||
| 80 | a premium account are still the only fees. | ||
| 81 | |||
| 82 | * My Thoughts | ||
| 83 | |||
| 84 | Personally, I wiped my personal information from my account and then | ||
| 85 | permanently deleted it when I heard about these changes. I have no | ||
| 86 | interest in yet another method of credit lending offered by private | ||
| 87 | companies. While I accepted that they would have access to my bank | ||
| 88 | account information for the purposes of paying off my prepaid debit | ||
| 89 | payments, I have no interest in incurring charges that will need to be | ||
| 90 | paid back at a later date. I also have no interest in submitting | ||
| 91 | personal information to Privacy.com. | ||
| 92 | |||
| 93 | This type of change toward a "buy it now, pay us later" model is | ||
| 94 | concerning, and I will be watching Privacy.com to see if they further | ||
| 95 | their interests in the credit model as time goes on. | ||
| 96 | |||
| 97 | Could we see them start charging interest, fees, etc.? I'm not sure, but | ||
| 98 | this change does not inspire confidence in their mission as a | ||
| 99 | privacy-focused company. | ||
content/blog/2022-09-17-serenity-os.md deleted −104
| @@ -1,104 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-09-17 | ||
| 3 | title = "Serenity OS: Testing Out a Unique System" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | [SerenityOS](https://serenityos.org) is a unique operating system (OS) that I | ||
| 11 | have seen pop up in my news feed a few times over the last few years, but I have | ||
| 12 | never had time to test it out until now. | ||
| 13 | |||
| 14 | Testing out this system brought back fond memories of yellowed, | ||
| 15 | modem-screeching, 100-pound computers that brought so many fond memories to my | ||
| 16 | youth. | ||
| 17 | |||
| 18 | Per their website: | ||
| 19 | |||
| 20 | > A graphical Unix-like operating system for desktop computers! | ||
| 21 | > | ||
| 22 | > SerenityOS is a love letter to '90s user interfaces with a custom Unix-like | ||
| 23 | > core. It flatters with sincerity by stealing beautiful ideas from various | ||
| 24 | > other systems. | ||
| 25 | > | ||
| 26 | > Roughly speaking, the goal is a marriage between the aesthetic of late-1990s | ||
| 27 | > productivity software and the power-user accessibility of late-2000s \*nix. | ||
| 28 | > | ||
| 29 | > This is a system by us, for us, based on the things we like. | ||
| 30 | |||
| 31 | # Building | ||
| 32 | |||
| 33 | Your first question may be "Where's the iso?" and the answer is... there are | ||
| 34 | none. SerenityOS does not provide pre-built images for testing. You must build | ||
| 35 | the images yourself. This seems intentionally to limit participation to only | ||
| 36 | those who are truly interested enough to learn how to build the OS. | ||
| 37 | |||
| 38 | ## Clone | ||
| 39 | |||
| 40 | In order to get started, you'll need to clone the source repository: | ||
| 41 | |||
| 42 | ```sh | ||
| 43 | git clone https://github.com/SerenityOS/serenity && cd serenity | ||
| 44 | ``` | ||
| 45 | |||
| 46 | ## Build | ||
| 47 | |||
| 48 | Note that I followed the [Build | ||
| 49 | Instructions](https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md) | ||
| 50 | in the SerenityOS repository as of commit | ||
| 51 | `660d2b53b1206e868d5470eee80b5e62d7e30da7`. Things may have changed since my | ||
| 52 | installation, and you should double-check the instructions first. | ||
| 53 | |||
| 54 | Regardless, I want to repeat my steps here to illustrate any errors or differing | ||
| 55 | commands I needed to run in order to build and run SerenityOS. | ||
| 56 | |||
| 57 | Since I am running Fedora, I needed to install these packages in order to build | ||
| 58 | the OS images: | ||
| 59 | |||
| 60 | ```sh | ||
| 61 | sudo dnf install texinfo binutils-devel curl cmake mpfr-devel libmpc-devel gmp-devel e2fsprogs ninja-build patch ccache rsync @"C Development Tools and Libraries" @Virtualization | ||
| 62 | ``` | ||
| 63 | |||
| 64 | Next, make sure you're inside the `serenity` directory created earlier during | ||
| 65 | the git cloning process and process to build the toolchain: | ||
| 66 | |||
| 67 | ```sh | ||
| 68 | Meta/serenity.sh rebuild-toolchain | ||
| 69 | ``` | ||
| 70 | |||
| 71 | Once the toolchain is built, you can Build and run the OS! | ||
| 72 | |||
| 73 | ```sh | ||
| 74 | Meta/serenity.sh run | ||
| 75 | ``` | ||
| 76 | |||
| 77 | After this process is completed, the image should run automatically and launch. | ||
| 78 | |||
| 79 | # Issues | ||
| 80 | |||
| 81 | I played around in SerenityOS for an hour or two in order to see what I could do | ||
| 82 | and had a lot of fun with it. The only issue I ran into was a lack of working | ||
| 83 | internet. I didn't try very hard, but I could tell that the main network link | ||
| 84 | wasn't connecting to my Fedora host properly. | ||
| 85 | |||
| 86 | # Screenshots | ||
| 87 | |||
| 88 | The initial launch of the image displays the SerenityOS desktop, with a simple | ||
| 89 | terminal already launched. | ||
| 90 | |||
| 91 | There is also a "Fire" application (literally just shows fire burning), a | ||
| 92 | browser with the local Serenity Browser page loaded, and a text editor. | ||
| 93 | |||
| 94 | I also poked around the system utilities and found most tools you'd expect to | ||
| 95 | find within a standard desktop, such as a system monitoring tool. | ||
| 96 | |||
| 97 | Lastly, I noted that the default desktop contains numerous pre-defined themes to | ||
| 98 | choose from. This is a small piece, but it's actually wonderful to see desktop | ||
| 99 | developers consider theming directly out of the box rather than using an | ||
| 100 | addon-based mentality. | ||
| 101 | |||
| 102 | I didn't take a screenshot of the other pre-installed games, but I did spend | ||
| 103 | nearly 30 minutes playing Solitaire before remembering that I was supposed to be | ||
| 104 | writing a post about the OS. | ||
content/blog/2022-09-17-serenity-os.org added +110
| @@ -0,0 +1,110 @@ | |||
| 1 | #+date: <2022-09-17> | ||
| 2 | #+title: Serenity OS: Testing Out a Unique System | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | [[https://serenityos.org][SerenityOS]] is a unique operating system (OS) | ||
| 9 | that I have seen pop up in my news feed a few times over the last few | ||
| 10 | years, but I have never had time to test it out until now. | ||
| 11 | |||
| 12 | Testing out this system brought back fond memories of yellowed, | ||
| 13 | modem-screeching, 100-pound computers that brought so many fond memories | ||
| 14 | to my youth. | ||
| 15 | |||
| 16 | Per their website: | ||
| 17 | |||
| 18 | #+begin_quote | ||
| 19 | A graphical Unix-like operating system for desktop computers! | ||
| 20 | |||
| 21 | SerenityOS is a love letter to '90s user interfaces with a custom | ||
| 22 | Unix-like core. It flatters with sincerity by stealing beautiful ideas | ||
| 23 | from various other systems. | ||
| 24 | |||
| 25 | Roughly speaking, the goal is a marriage between the aesthetic of | ||
| 26 | late-1990s productivity software and the power-user accessibility of | ||
| 27 | late-2000s *nix. | ||
| 28 | |||
| 29 | This is a system by us, for us, based on the things we like. | ||
| 30 | #+end_quote | ||
| 31 | |||
| 32 | * Building | ||
| 33 | |||
| 34 | Your first question may be "Where's the iso?" and the answer is... there | ||
| 35 | are none. SerenityOS does not provide pre-built images for testing. You | ||
| 36 | must build the images yourself. This seems intentionally to limit | ||
| 37 | participation to only those who are truly interested enough to learn how | ||
| 38 | to build the OS. | ||
| 39 | |||
| 40 | ** Clone | ||
| 41 | |||
| 42 | In order to get started, you'll need to clone the source repository: | ||
| 43 | |||
| 44 | #+begin_src sh | ||
| 45 | git clone https://github.com/SerenityOS/serenity && cd serenity | ||
| 46 | #+end_src | ||
| 47 | |||
| 48 | ** Build | ||
| 49 | |||
| 50 | Note that I followed the | ||
| 51 | [[https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md][Build | ||
| 52 | Instructions]] in the SerenityOS repository as of commit | ||
| 53 | =660d2b53b1206e868d5470eee80b5e62d7e30da7=. Things may have changed | ||
| 54 | since my installation, and you should double-check the instructions | ||
| 55 | first. | ||
| 56 | |||
| 57 | Regardless, I want to repeat my steps here to illustrate any errors or | ||
| 58 | differing commands I needed to run in order to build and run SerenityOS. | ||
| 59 | |||
| 60 | Since I am running Fedora, I needed to install these packages in order | ||
| 61 | to build the OS images: | ||
| 62 | |||
| 63 | #+begin_src sh | ||
| 64 | sudo dnf install texinfo binutils-devel curl cmake mpfr-devel libmpc-devel gmp-devel e2fsprogs ninja-build patch ccache rsync @"C Development Tools and Libraries" @Virtualization | ||
| 65 | #+end_src | ||
| 66 | |||
| 67 | Next, make sure you're inside the =serenity= directory created earlier | ||
| 68 | during the git cloning process and process to build the toolchain: | ||
| 69 | |||
| 70 | #+begin_src sh | ||
| 71 | Meta/serenity.sh rebuild-toolchain | ||
| 72 | #+end_src | ||
| 73 | |||
| 74 | Once the toolchain is built, you can Build and run the OS! | ||
| 75 | |||
| 76 | #+begin_src sh | ||
| 77 | Meta/serenity.sh run | ||
| 78 | #+end_src | ||
| 79 | |||
| 80 | After this process is completed, the image should run automatically and | ||
| 81 | launch. | ||
| 82 | |||
| 83 | * Issues | ||
| 84 | |||
| 85 | I played around in SerenityOS for an hour or two in order to see what I | ||
| 86 | could do and had a lot of fun with it. The only issue I ran into was a | ||
| 87 | lack of working internet. I didn't try very hard, but I could tell that | ||
| 88 | the main network link wasn't connecting to my Fedora host properly. | ||
| 89 | |||
| 90 | * Screenshots | ||
| 91 | |||
| 92 | The initial launch of the image displays the SerenityOS desktop, with a | ||
| 93 | simple terminal already launched. | ||
| 94 | |||
| 95 | There is also a "Fire" application (literally just shows fire burning), | ||
| 96 | a browser with the local Serenity Browser page loaded, and a text | ||
| 97 | editor. | ||
| 98 | |||
| 99 | I also poked around the system utilities and found most tools you'd | ||
| 100 | expect to find within a standard desktop, such as a system monitoring | ||
| 101 | tool. | ||
| 102 | |||
| 103 | Lastly, I noted that the default desktop contains numerous pre-defined | ||
| 104 | themes to choose from. This is a small piece, but it's actually | ||
| 105 | wonderful to see desktop developers consider theming directly out of the | ||
| 106 | box rather than using an addon-based mentality. | ||
| 107 | |||
| 108 | I didn't take a screenshot of the other pre-installed games, but I did | ||
| 109 | spend nearly 30 minutes playing Solitaire before remembering that I was | ||
| 110 | supposed to be writing a post about the OS. | ||
content/blog/2022-09-21-graphene-os.md deleted −159
| @@ -1,159 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-09-21 | ||
| 3 | title = "Installing Graphene OS on the Pixel 6 Pro" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Introduction | ||
| 9 | |||
| 10 | After using iOS for a couple of years, I finally took the plunge and purchased a | ||
| 11 | Pixel 6 Pro in order to test and use [GrapheneOS] (<https://grapheneos.org>). | ||
| 12 | |||
| 13 | The installation process was rather quick once you have the tools and files you | ||
| 14 | need. Overall, it can be done in just a few minutes. | ||
| 15 | |||
| 16 | # Gathering Tools & Files | ||
| 17 | |||
| 18 | ## Android Tools | ||
| 19 | |||
| 20 | First, in order to interact with the device, we will need the [Android platform | ||
| 21 | tools](https://developer.android.com/studio/releases/platform-tools.html). Find | ||
| 22 | the Linux download and save the ZIP folder to your preferred location. | ||
| 23 | |||
| 24 | Once we've downloaded the files, we will need to unzip them, enter the | ||
| 25 | directory, and move the necessary executables to a central location, such as | ||
| 26 | `/usr/bin/`. For this installation, we only need the `fastboot` and `adb` | ||
| 27 | executables. | ||
| 28 | |||
| 29 | ```sh | ||
| 30 | cd ~/Downloads | ||
| 31 | ``` | ||
| 32 | |||
| 33 | ```sh | ||
| 34 | unzip platform-tools_r33.0.3-linux.zip | ||
| 35 | cd platform-tools | ||
| 36 | sudo mv fastboot /usr/bin/ | ||
| 37 | sudo mv adb /usr/bin | ||
| 38 | ``` | ||
| 39 | |||
| 40 | ## GrapheneOS Files | ||
| 41 | |||
| 42 | Next, we need the [GrapheneOS files](https://grapheneos.org/releases) for our | ||
| 43 | device and model. For example, the Pixel 6 Pro is codenamed `raven` on the | ||
| 44 | release page. | ||
| 45 | |||
| 46 | Once we have the links, let's download them to our working directory: | ||
| 47 | |||
| 48 | ```sh | ||
| 49 | curl -O https://releases.grapheneos.org/factory.pub | ||
| 50 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip | ||
| 51 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip.sig | ||
| 52 | ``` | ||
| 53 | |||
| 54 | 1. Validate Integrity | ||
| 55 | |||
| 56 | In order to validate the integrity of the downloaded files, we will need the | ||
| 57 | `signify` package and Graphene's `factory.pub` file. | ||
| 58 | |||
| 59 | ```sh | ||
| 60 | sudo dnf install signify | ||
| 61 | ``` | ||
| 62 | |||
| 63 | ```sh | ||
| 64 | curl -O https://releases.grapheneos.org/factory.pub | ||
| 65 | ``` | ||
| 66 | |||
| 67 | Then we can validate the files and ensure that no data was corrupted or | ||
| 68 | modified before it was saved to our device. | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | signify -Cqp factory.pub -x raven-factory-2022091400.zip.sig && echo verified | ||
| 72 | ``` | ||
| 73 | |||
| 74 | 2. Unzip Files | ||
| 75 | |||
| 76 | Once the files are verified, we can unzip the Graphene image and enter the | ||
| 77 | directory: | ||
| 78 | |||
| 79 | ```sh | ||
| 80 | unzip raven-factory-2022091400.zip && cd raven-factory-2022091400 | ||
| 81 | ``` | ||
| 82 | |||
| 83 | # Installation Process | ||
| 84 | |||
| 85 | ## Enable Developer Debugging & OEM Unlock | ||
| 86 | |||
| 87 | Before we can actually flash anything to the phone, we will need to enable OEM | ||
| 88 | Unlocking, as well as either USB Debugging or Wireless Debugging, depending on | ||
| 89 | which method we will be using. | ||
| 90 | |||
| 91 | To start, enable developer mode by going to `Settings` > `About` and tapping | ||
| 92 | `Build Number` seven (7) times. You may need to enter your PIN to enable this | ||
| 93 | mode. | ||
| 94 | |||
| 95 | Once developer mode is enabled, go to `Settings` > `System` > `Devloper Options` | ||
| 96 | and enable OEM Unlocking, as well as USB or Wireless Debugging. In my case, I | ||
| 97 | chose USB Debugging and performed all actions via USB cable. | ||
| 98 | |||
| 99 | Once these options are enabled, plug the phone into the computer and execute the | ||
| 100 | following command: | ||
| 101 | |||
| 102 | ```sh | ||
| 103 | adb devices | ||
| 104 | ``` | ||
| 105 | |||
| 106 | If an unauthorized error occurs, make sure the USB mode on the phone is changed | ||
| 107 | from charging to something like "File Transfer" or "PTP." You can find the USB | ||
| 108 | mode in the notification tray. | ||
| 109 | |||
| 110 | ## Reboot Device | ||
| 111 | |||
| 112 | Once we have found the device via `adb`, we can either boot into the bootloader | ||
| 113 | interface by holding the volume down button while the phone reboots or by | ||
| 114 | executing the following command: | ||
| 115 | |||
| 116 | ```sh | ||
| 117 | adb reboot bootloader | ||
| 118 | ``` | ||
| 119 | |||
| 120 | ## Unlock the Bootloader | ||
| 121 | |||
| 122 | The phone will reboot and load the bootloader screen upon startup. At this | ||
| 123 | point, we are ready to start the actual flashing of GrapheneOS onto the device. | ||
| 124 | |||
| 125 | **NOTE**: In my situation, I needed to use `sudo` with every `fastboot` command, | ||
| 126 | but not with `adb` commands. I am not sure if this is standard or a Fedora | ||
| 127 | quirk, but I'm documenting my commands verbatim in this post. | ||
| 128 | |||
| 129 | First, we start by unlocking the bootloader so that we can load other ROMs: | ||
| 130 | |||
| 131 | ```sh | ||
| 132 | sudo fastboot flashing unlock | ||
| 133 | ``` | ||
| 134 | |||
| 135 | ## Flashing Factory Images | ||
| 136 | |||
| 137 | Once the phone is unlocked, we can flash it with the `flash-all.sh` script found | ||
| 138 | inside the `raven-factory-2022091400` folder we entered earlier: | ||
| 139 | |||
| 140 | ```sh | ||
| 141 | sudo ./flash-all.sh | ||
| 142 | ``` | ||
| 143 | |||
| 144 | This process should take a few minutes and will print informational messages as | ||
| 145 | things progress. Avoid doing anything on the phone while this process is | ||
| 146 | operating. | ||
| 147 | |||
| 148 | ## Lock the Bootloader | ||
| 149 | |||
| 150 | If everything was successful, the phone should reboot a few times and finally | ||
| 151 | land back on the bootloader screen. At this point, we can re-lock the bootloader | ||
| 152 | to enable full verified boot and protect the device from unwanted flashing or | ||
| 153 | erasure of data. | ||
| 154 | |||
| 155 | ```sh | ||
| 156 | sudo fastboot flashing lock | ||
| 157 | ``` | ||
| 158 | |||
| 159 | Once done, the device will be wiped and ready for a fresh set-up! | ||
content/blog/2022-09-21-graphene-os.org added +164
| @@ -0,0 +1,164 @@ | |||
| 1 | #+date: <2022-09-21> | ||
| 2 | #+title: Installing Graphene OS on the Pixel 6 Pro | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Introduction | ||
| 7 | |||
| 8 | After using iOS for a couple of years, I finally took the plunge and | ||
| 9 | purchased a Pixel 6 Pro in order to test and use [GrapheneOS] | ||
| 10 | ([[https://grapheneos.org]]). | ||
| 11 | |||
| 12 | The installation process was rather quick once you have the tools and | ||
| 13 | files you need. Overall, it can be done in just a few minutes. | ||
| 14 | |||
| 15 | * Gathering Tools & Files | ||
| 16 | |||
| 17 | ** Android Tools | ||
| 18 | |||
| 19 | First, in order to interact with the device, we will need the | ||
| 20 | [[https://developer.android.com/studio/releases/platform-tools.html][Android | ||
| 21 | platform tools]]. Find the Linux download and save the ZIP folder to | ||
| 22 | your preferred location. | ||
| 23 | |||
| 24 | Once we've downloaded the files, we will need to unzip them, enter the | ||
| 25 | directory, and move the necessary executables to a central location, | ||
| 26 | such as =/usr/bin/=. For this installation, we only need the =fastboot= | ||
| 27 | and =adb= executables. | ||
| 28 | |||
| 29 | #+begin_src sh | ||
| 30 | cd ~/Downloads | ||
| 31 | #+end_src | ||
| 32 | |||
| 33 | #+begin_src sh | ||
| 34 | unzip platform-tools_r33.0.3-linux.zip | ||
| 35 | cd platform-tools | ||
| 36 | sudo mv fastboot /usr/bin/ | ||
| 37 | sudo mv adb /usr/bin | ||
| 38 | #+end_src | ||
| 39 | |||
| 40 | ** GrapheneOS Files | ||
| 41 | |||
| 42 | Next, we need the [[https://grapheneos.org/releases][GrapheneOS files]] | ||
| 43 | for our device and model. For example, the Pixel 6 Pro is codenamed | ||
| 44 | =raven= on the release page. | ||
| 45 | |||
| 46 | Once we have the links, let's download them to our working directory: | ||
| 47 | |||
| 48 | #+begin_src sh | ||
| 49 | curl -O https://releases.grapheneos.org/factory.pub | ||
| 50 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip | ||
| 51 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip.sig | ||
| 52 | #+end_src | ||
| 53 | |||
| 54 | 1. Validate Integrity | ||
| 55 | |||
| 56 | In order to validate the integrity of the downloaded files, we will | ||
| 57 | need the =signify= package and Graphene's =factory.pub= file. | ||
| 58 | |||
| 59 | #+begin_src sh | ||
| 60 | sudo dnf install signify | ||
| 61 | #+end_src | ||
| 62 | |||
| 63 | #+begin_src sh | ||
| 64 | curl -O https://releases.grapheneos.org/factory.pub | ||
| 65 | #+end_src | ||
| 66 | |||
| 67 | Then we can validate the files and ensure that no data was corrupted | ||
| 68 | or modified before it was saved to our device. | ||
| 69 | |||
| 70 | #+begin_src sh | ||
| 71 | signify -Cqp factory.pub -x raven-factory-2022091400.zip.sig && echo verified | ||
| 72 | #+end_src | ||
| 73 | |||
| 74 | 2. Unzip Files | ||
| 75 | |||
| 76 | Once the files are verified, we can unzip the Graphene image and | ||
| 77 | enter the directory: | ||
| 78 | |||
| 79 | #+begin_src sh | ||
| 80 | unzip raven-factory-2022091400.zip && cd raven-factory-2022091400 | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | * Installation Process | ||
| 84 | |||
| 85 | ** Enable Developer Debugging & OEM Unlock | ||
| 86 | |||
| 87 | Before we can actually flash anything to the phone, we will need to | ||
| 88 | enable OEM Unlocking, as well as either USB Debugging or Wireless | ||
| 89 | Debugging, depending on which method we will be using. | ||
| 90 | |||
| 91 | To start, enable developer mode by going to =Settings= > =About= and | ||
| 92 | tapping =Build Number= seven (7) times. You may need to enter your PIN | ||
| 93 | to enable this mode. | ||
| 94 | |||
| 95 | Once developer mode is enabled, go to =Settings= > =System= > | ||
| 96 | =Devloper Options= and enable OEM Unlocking, as well as USB or Wireless | ||
| 97 | Debugging. In my case, I chose USB Debugging and performed all actions | ||
| 98 | via USB cable. | ||
| 99 | |||
| 100 | Once these options are enabled, plug the phone into the computer and | ||
| 101 | execute the following command: | ||
| 102 | |||
| 103 | #+begin_src sh | ||
| 104 | adb devices | ||
| 105 | #+end_src | ||
| 106 | |||
| 107 | If an unauthorized error occurs, make sure the USB mode on the phone is | ||
| 108 | changed from charging to something like "File Transfer" or "PTP." You | ||
| 109 | can find the USB mode in the notification tray. | ||
| 110 | |||
| 111 | ** Reboot Device | ||
| 112 | |||
| 113 | Once we have found the device via =adb=, we can either boot into the | ||
| 114 | bootloader interface by holding the volume down button while the phone | ||
| 115 | reboots or by executing the following command: | ||
| 116 | |||
| 117 | #+begin_src sh | ||
| 118 | adb reboot bootloader | ||
| 119 | #+end_src | ||
| 120 | |||
| 121 | ** Unlock the Bootloader | ||
| 122 | |||
| 123 | The phone will reboot and load the bootloader screen upon startup. At | ||
| 124 | this point, we are ready to start the actual flashing of GrapheneOS onto | ||
| 125 | the device. | ||
| 126 | |||
| 127 | *NOTE*: In my situation, I needed to use =sudo= with every =fastboot= | ||
| 128 | command, but not with =adb= commands. I am not sure if this is standard | ||
| 129 | or a Fedora quirk, but I'm documenting my commands verbatim in this | ||
| 130 | post. | ||
| 131 | |||
| 132 | First, we start by unlocking the bootloader so that we can load other | ||
| 133 | ROMs: | ||
| 134 | |||
| 135 | #+begin_src sh | ||
| 136 | sudo fastboot flashing unlock | ||
| 137 | #+end_src | ||
| 138 | |||
| 139 | ** Flashing Factory Images | ||
| 140 | |||
| 141 | Once the phone is unlocked, we can flash it with the =flash-all.sh= | ||
| 142 | script found inside the =raven-factory-2022091400= folder we entered | ||
| 143 | earlier: | ||
| 144 | |||
| 145 | #+begin_src sh | ||
| 146 | sudo ./flash-all.sh | ||
| 147 | #+end_src | ||
| 148 | |||
| 149 | This process should take a few minutes and will print informational | ||
| 150 | messages as things progress. Avoid doing anything on the phone while | ||
| 151 | this process is operating. | ||
| 152 | |||
| 153 | ** Lock the Bootloader | ||
| 154 | |||
| 155 | If everything was successful, the phone should reboot a few times and | ||
| 156 | finally land back on the bootloader screen. At this point, we can | ||
| 157 | re-lock the bootloader to enable full verified boot and protect the | ||
| 158 | device from unwanted flashing or erasure of data. | ||
| 159 | |||
| 160 | #+begin_src sh | ||
| 161 | sudo fastboot flashing lock | ||
| 162 | #+end_src | ||
| 163 | |||
| 164 | Once done, the device will be wiped and ready for a fresh set-up! | ||
content/blog/2022-10-04-mtp-linux.md deleted −78
| @@ -1,78 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-10-04 | ||
| 3 | title = "How to Mount an MTP Mobile Device on Fedora Linux" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | I recently ran into trouble attempting to mount my GrapheneOS phone to my laptop | ||
| 9 | running Fedora Linux via the [Media Transfer | ||
| 10 | Protocol](https://en.wikipedia.org/wiki/Media_transfer_protocol) (MTP) and | ||
| 11 | discovered a simple and effective solution. | ||
| 12 | |||
| 13 | # Use a USB 3.0 Port | ||
| 14 | |||
| 15 | First, ensure that the device was plugged in to the laptop through a USB 3.0 | ||
| 16 | port, if possible. From a brief glance online, it seems that USB 2.0 ports may | ||
| 17 | cause issues with dropped connections over MTP. This is purely anecdotal since I | ||
| 18 | don't have any evidence to link showing that USB 2.0 causes issues, but I can | ||
| 19 | confirm that switching to a USB 3.0 port seemed to cut out most of my issues. | ||
| 20 | |||
| 21 | # Switch USB Preferences to MTP | ||
| 22 | |||
| 23 | Secondly, you need to ensure that the phone's USB preferences/mode is changed to | ||
| 24 | MTP or File Transfer once the phone is plugged in. Other modes will not allow | ||
| 25 | you to access the phone's file system. | ||
| 26 | |||
| 27 | # Install `jmtpfs` | ||
| 28 | |||
| 29 | Next, I used the `jmtpfs` package to mount my phone to my laptop. There are | ||
| 30 | other packages that exist, but this one worked perfectly for me. On Fedora | ||
| 31 | Linux, you can install it like this: | ||
| 32 | |||
| 33 | ```sh | ||
| 34 | sudo dnf install jmtpfs -y | ||
| 35 | ``` | ||
| 36 | |||
| 37 | # Create a Mount Point | ||
| 38 | |||
| 39 | Once you have the package installed, you just need to create a folder for the | ||
| 40 | device to use as a mount point. In my case, I used `/mnt/pixel`: | ||
| 41 | |||
| 42 | ```sh | ||
| 43 | sudo mkdir /mnt/pixel | ||
| 44 | sudo chown -R $USER:$USER /mnt/pixel | ||
| 45 | ``` | ||
| 46 | |||
| 47 | # Mount & Access the Phone's File System | ||
| 48 | |||
| 49 | Finally, plug-in and mount the device, and you should be able to see all storage | ||
| 50 | (internal and external) inside your new folder! | ||
| 51 | |||
| 52 | ```sh | ||
| 53 | jmtpfs /mnt/pixel | ||
| 54 | ``` | ||
| 55 | |||
| 56 | The output should look something like this: | ||
| 57 | |||
| 58 | ```sh | ||
| 59 | Device 0 (VID=18d1 and PID=4ee1) is a Google Inc Nexus/Pixel (MTP). | ||
| 60 | Android device detected, assigning default bug flags | ||
| 61 | ``` | ||
| 62 | |||
| 63 | Now you are mounted and can do anything you'd like with the device's files: | ||
| 64 | |||
| 65 | ```sh | ||
| 66 | cd /mnt/pixel | ||
| 67 | ls -lha | ||
| 68 | ``` | ||
| 69 | |||
| 70 | From here, you will be able to see any internal or external storage available on | ||
| 71 | the device: | ||
| 72 | |||
| 73 | ```sh | ||
| 74 | total 0 | ||
| 75 | drwxr-xr-x. 3 user user 0 Jan 1 1970 . | ||
| 76 | drwxr-xr-x. 1 root root 10 Oct 4 13:29 .. | ||
| 77 | drwxr-xr-x. 16 user user 0 Apr 21 4426383 'Internal shared storage' | ||
| 78 | ``` | ||
content/blog/2022-10-04-mtp-linux.org added +78
| @@ -0,0 +1,78 @@ | |||
| 1 | #+date: <2022-10-04> | ||
| 2 | #+title: How to Mount an MTP Mobile Device on Fedora Linux | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | I recently ran into trouble attempting to mount my GrapheneOS phone to | ||
| 7 | my laptop running Fedora Linux via the | ||
| 8 | [[https://en.wikipedia.org/wiki/Media_transfer_protocol][Media Transfer | ||
| 9 | Protocol]] (MTP) and discovered a simple and effective solution. | ||
| 10 | |||
| 11 | * Use a USB 3.0 Port | ||
| 12 | |||
| 13 | First, ensure that the device was plugged in to the laptop through a USB | ||
| 14 | 3.0 port, if possible. From a brief glance online, it seems that USB 2.0 | ||
| 15 | ports may cause issues with dropped connections over MTP. This is purely | ||
| 16 | anecdotal since I don't have any evidence to link showing that USB 2.0 | ||
| 17 | causes issues, but I can confirm that switching to a USB 3.0 port seemed | ||
| 18 | to cut out most of my issues. | ||
| 19 | |||
| 20 | * Switch USB Preferences to MTP | ||
| 21 | |||
| 22 | Secondly, you need to ensure that the phone's USB preferences/mode is | ||
| 23 | changed to MTP or File Transfer once the phone is plugged in. Other | ||
| 24 | modes will not allow you to access the phone's file system. | ||
| 25 | |||
| 26 | * Install =jmtpfs= | ||
| 27 | |||
| 28 | Next, I used the =jmtpfs= package to mount my phone to my laptop. There | ||
| 29 | are other packages that exist, but this one worked perfectly for me. On | ||
| 30 | Fedora Linux, you can install it like this: | ||
| 31 | |||
| 32 | #+begin_src sh | ||
| 33 | sudo dnf install jmtpfs -y | ||
| 34 | #+end_src | ||
| 35 | |||
| 36 | * Create a Mount Point | ||
| 37 | |||
| 38 | Once you have the package installed, you just need to create a folder | ||
| 39 | for the device to use as a mount point. In my case, I used =/mnt/pixel=: | ||
| 40 | |||
| 41 | #+begin_src sh | ||
| 42 | sudo mkdir /mnt/pixel | ||
| 43 | sudo chown -R $USER:$USER /mnt/pixel | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | * Mount & Access the Phone's File System | ||
| 47 | |||
| 48 | Finally, plug-in and mount the device, and you should be able to see all | ||
| 49 | storage (internal and external) inside your new folder! | ||
| 50 | |||
| 51 | #+begin_src sh | ||
| 52 | jmtpfs /mnt/pixel | ||
| 53 | #+end_src | ||
| 54 | |||
| 55 | The output should look something like this: | ||
| 56 | |||
| 57 | #+begin_src sh | ||
| 58 | Device 0 (VID=18d1 and PID=4ee1) is a Google Inc Nexus/Pixel (MTP). | ||
| 59 | Android device detected, assigning default bug flags | ||
| 60 | #+end_src | ||
| 61 | |||
| 62 | Now you are mounted and can do anything you'd like with the device's | ||
| 63 | files: | ||
| 64 | |||
| 65 | #+begin_src sh | ||
| 66 | cd /mnt/pixel | ||
| 67 | ls -lha | ||
| 68 | #+end_src | ||
| 69 | |||
| 70 | From here, you will be able to see any internal or external storage | ||
| 71 | available on the device: | ||
| 72 | |||
| 73 | #+begin_src sh | ||
| 74 | total 0 | ||
| 75 | drwxr-xr-x. 3 user user 0 Jan 1 1970 . | ||
| 76 | drwxr-xr-x. 1 root root 10 Oct 4 13:29 .. | ||
| 77 | drwxr-xr-x. 16 user user 0 Apr 21 4426383 'Internal shared storage' | ||
| 78 | #+end_src | ||
content/blog/2022-10-04-syncthing.md deleted −169
| @@ -1,169 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-10-04 | ||
| 3 | title = "Syncthing: A Minimal Self-Hosted Cloud Storage Solution" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # An Overview of Syncthing | ||
| 9 | |||
| 10 | If you've been looking around the self-hosted cloud storage space for a while, | ||
| 11 | you've undoubtedly run into someone suggesting | ||
| 12 | [Syncthing](https://syncthing.net) as an option. However, it is an unusual | ||
| 13 | alternative for those users out there who are used to having a centralized cloud | ||
| 14 | server that serves as the "controller" of the data and interacts with clients on | ||
| 15 | devices to fetch files. | ||
| 16 | |||
| 17 | This post is a walkthrough of the Syncthing software, how I set up my personal | ||
| 18 | storage, and some pros and cons of using the software. | ||
| 19 | |||
| 20 | # Installing Syncthing | ||
| 21 | |||
| 22 | To install Syncthing, visit the [Downloads](https://syncthing.net/downloads/) | ||
| 23 | page or install via your device's package manager. | ||
| 24 | |||
| 25 | ## Server & Desktop | ||
| 26 | |||
| 27 | You can install Syncthing on servers and desktops via the Downloads page linked | ||
| 28 | above or via the command-line. | ||
| 29 | |||
| 30 | For Debian-based distros: | ||
| 31 | |||
| 32 | ```sh | ||
| 33 | sudo apt install syncthing | ||
| 34 | ``` | ||
| 35 | |||
| 36 | For Fedora-based distros: | ||
| 37 | |||
| 38 | ```sh | ||
| 39 | sudo dnf install syncthing | ||
| 40 | ``` | ||
| 41 | |||
| 42 | ## Mobile | ||
| 43 | |||
| 44 | Syncthing for Android is available on | ||
| 45 | [F-Droid](https://f-droid.org/packages/com.nutomic.syncthingandroid/) and | ||
| 46 | |||
| 47 | Play](https://play.google.com/store/apps/details?id=com.nutomic.syncthingandroid). | ||
| 48 | Syncthing does not have an official iOS client, but there is a third-party | ||
| 49 | client called [Möbius | ||
| 50 | Sync](https://apps.apple.com/us/app/m%C3%B6bius-sync/id1539203216). | ||
| 51 | |||
| 52 | # How Does Syncthing Work? | ||
| 53 | |||
| 54 | To start, I wanted to include the main marketing blurb from their website: | ||
| 55 | |||
| 56 | > Syncthing is a continuous file synchronization program. It synchronizes files | ||
| 57 | > between two or more computers in real time, safely protected from prying eyes. | ||
| 58 | > Your data is your data alone, and you deserve to choose where it is stored, | ||
| 59 | > whether it is shared with some third party, and how it's transmitted over the | ||
| 60 | > internet. | ||
| 61 | |||
| 62 | Let's break this apart and add in some other details to help explain what | ||
| 63 | exactly Syncthing does in order to sync files between devices. | ||
| 64 | |||
| 65 | ## Local Syncthing Server(s) | ||
| 66 | |||
| 67 | Syncthing syncs files between multiple devices by creating a local server on | ||
| 68 | each device. These local servers handle a few different things, such as watching | ||
| 69 | files and directories for changes, hosting an administrative GUI website, and | ||
| 70 | authenticating with connected devices. | ||
| 71 | |||
| 72 | You can also start, stop, and restart the Syncthing server via the command-line | ||
| 73 | or web dashboard. If you're running Syncthing on a device with `systemd`, you | ||
| 74 | can use the following commands: | ||
| 75 | |||
| 76 | ```sh | ||
| 77 | sudo systemctl start syncthing@username.service | ||
| 78 | sudo systemctl restart syncthing@username.service | ||
| 79 | sudo systemctl stop syncthing@username.service | ||
| 80 | ``` | ||
| 81 | |||
| 82 | ## Syncthing Dashboard | ||
| 83 | |||
| 84 | This biggest part of Syncthing is the admin GUI website that runs on each device | ||
| 85 | (note that mobile devices will use the Syncthing app rather than the web GUI). | ||
| 86 | The admin GUI is available through the web browser on the local device that is | ||
| 87 | running Syncthing - simply go to `http://localhost:8384` or | ||
| 88 | `http://127.0.0.1:8384`. This web page is the place where you will change | ||
| 89 | settings, add/modify synced files, and add/modify connected devices. | ||
| 90 | |||
| 91 | Here's an example web GUI dashboard: | ||
| 92 | |||
| 93 |  | ||
| 95 | |||
| 96 | ## Remote Devices | ||
| 97 | |||
| 98 | A cloud storage solution wouldn't be very useful if you aren't able to share | ||
| 99 | data among various devices. Syncthing does this by sharing Device IDs to connect | ||
| 100 | servers, and then by manually sharing Folders with devices that have been | ||
| 101 | connected. | ||
| 102 | |||
| 103 | For instance, if you have a laptop running Syncthing and then install the | ||
| 104 | Syncthing mobile app on a phone, you could scan the laptop's QR code for Device | ||
| 105 | ID and then accept the authentication on the laptop's dashboard. Next, you can | ||
| 106 | use either device to select a folder for sharing and dictating which device | ||
| 107 | should send, receive, or both. | ||
| 108 | |||
| 109 | When you connect devices, you can set one device as an "Introducer," which can | ||
| 110 | add devices from the introducer to the device list, for mutually shared folders. | ||
| 111 | You can also configure Auto Accept, compression, rate limits, and more settings | ||
| 112 | per device. | ||
| 113 | |||
| 114 | # My Personal Cloud Storage Set-up | ||
| 115 | |||
| 116 | Personally, I use a model similar to a traditional cloud storage service. I have | ||
| 117 | a "centralized" server running 24/7 that acts as an Introducer for my Syncthing | ||
| 118 | network. I think of this as my main storage and all other devices as tertiary | ||
| 119 | client devices. I will likely add additional servers as backups as time goes on | ||
| 120 | so that I don't have to rely on my laptop or phone as the only backups. | ||
| 121 | |||
| 122 | Currently, I have one desktop and one mobile device connected to the network, | ||
| 123 | both running intermittently as they are not powered-on 24/7. | ||
| 124 | |||
| 125 | The initial set-up of the software was easy enough, but data transfer rates were | ||
| 126 | incredibly slow for me due to the Wi-Fi. Instead, I plugged my laptop into the | ||
| 127 | ethernet network that my server is on and manually copied my folders over to the | ||
| 128 | server with `scp`. Once complete, Syncthing validated that all files were there | ||
| 129 | and not missing, and it did not need to transfer any data through the WAN. | ||
| 130 | |||
| 131 | As slow as the transfer was going, this probably saved me a few days of waiting | ||
| 132 | for my ~100GB sync. | ||
| 133 | |||
| 134 | # Pros & Cons | ||
| 135 | |||
| 136 | I've put together a short list of pros and cons for Syncthing. I thought about | ||
| 137 | my experiences with Nextcloud, WebDAV, proprietary services (Google Drive, | ||
| 138 | iCloud, etc.), and privacy-focused cloud solutions (pCloud, Tresorit, etc.). | ||
| 139 | |||
| 140 | **Pros:** | ||
| 141 | |||
| 142 | - I've faced no data loss at all through my two-month trial run. | ||
| 143 | - No third-parties store your data on their servers. | ||
| 144 | - You have full control over your data and can take your data and leave at any | ||
| 145 | time. | ||
| 146 | - It's possible to encrypt client-side easily with software like Cryptomator. | ||
| 147 | - No proprietary clients or mounted volumes, just plain files and folders. | ||
| 148 | |||
| 149 | **Cons:** | ||
| 150 | |||
| 151 | - The learning curve is steeper than traditional cloud services and is focused | ||
| 152 | on a technical audience. | ||
| 153 | - If a device needs to modify files in a Folder, the devices will need to sync | ||
| 154 | ALL files from the folder, which may be large. To avoid size restraints, | ||
| 155 | split large folders into smaller folders for syncing. | ||
| 156 | - Syncing can be slow due to the clients/servers initially connecting or | ||
| 157 | re-connecting after sleeping. | ||
| 158 | - Multiple personal devices are required and require the user to own or rent | ||
| 159 | them as no third-party servers are involved in the storage of data. | ||
| 160 | |||
| 161 | Overall, I've had a great experience with Syncthing so far. I've had no data | ||
| 162 | loss, syncing has been quick and easy when changes are made to files, device | ||
| 163 | connections are reliable, and I love the freedom of controlling the clients and | ||
| 164 | servers as I choose. | ||
| 165 | |||
| 166 | Not to mention that I appreciate that I - or someone else - could pull the | ||
| 167 | Syncthing [source code](https://github.com/syncthing) and continue | ||
| 168 | development/support if the Syncthing Foundation decides to stop developing the | ||
| 169 | software or sells the business. | ||
content/blog/2022-10-04-syncthing.org added +179
| @@ -0,0 +1,179 @@ | |||
| 1 | #+date: <2022-10-04> | ||
| 2 | #+title: Syncthing: A Minimal Self-Hosted Cloud Storage Solution | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * An Overview of Syncthing | ||
| 7 | |||
| 8 | If you've been looking around the self-hosted cloud storage space for a | ||
| 9 | while, you've undoubtedly run into someone suggesting | ||
| 10 | [[https://syncthing.net][Syncthing]] as an option. However, it is an | ||
| 11 | unusual alternative for those users out there who are used to having a | ||
| 12 | centralized cloud server that serves as the "controller" of the data and | ||
| 13 | interacts with clients on devices to fetch files. | ||
| 14 | |||
| 15 | This post is a walkthrough of the Syncthing software, how I set up my | ||
| 16 | personal storage, and some pros and cons of using the software. | ||
| 17 | |||
| 18 | * Installing Syncthing | ||
| 19 | |||
| 20 | To install Syncthing, visit the | ||
| 21 | [[https://syncthing.net/downloads/][Downloads]] page or install via your | ||
| 22 | device's package manager. | ||
| 23 | |||
| 24 | ** Server & Desktop | ||
| 25 | |||
| 26 | You can install Syncthing on servers and desktops via the Downloads page | ||
| 27 | linked above or via the command-line. | ||
| 28 | |||
| 29 | For Debian-based distros: | ||
| 30 | |||
| 31 | #+begin_src sh | ||
| 32 | sudo apt install syncthing | ||
| 33 | #+end_src | ||
| 34 | |||
| 35 | For Fedora-based distros: | ||
| 36 | |||
| 37 | #+begin_src sh | ||
| 38 | sudo dnf install syncthing | ||
| 39 | #+end_src | ||
| 40 | |||
| 41 | ** Mobile | ||
| 42 | |||
| 43 | Syncthing for Android is available on | ||
| 44 | [[https://f-droid.org/packages/com.nutomic.syncthingandroid/][F-Droid]] | ||
| 45 | and | ||
| 46 | [[https://play.google.com/store/apps/details?id=com.nutomic.syncthingandroid][Google | ||
| 47 | Play]]. Syncthing does not have an official iOS client, but there is a | ||
| 48 | third-party client called | ||
| 49 | [[https://apps.apple.com/us/app/m%C3%B6bius-sync/id1539203216][Möbius | ||
| 50 | Sync]]. | ||
| 51 | |||
| 52 | * How Does Syncthing Work? | ||
| 53 | |||
| 54 | To start, I wanted to include the main marketing blurb from their | ||
| 55 | website: | ||
| 56 | |||
| 57 | #+begin_quote | ||
| 58 | Syncthing is a continuous file synchronization program. It synchronizes | ||
| 59 | files between two or more computers in real time, safely protected from | ||
| 60 | prying eyes. Your data is your data alone, and you deserve to choose | ||
| 61 | where it is stored, whether it is shared with some third party, and how | ||
| 62 | it's transmitted over the internet. | ||
| 63 | #+end_quote | ||
| 64 | |||
| 65 | Let's break this apart and add in some other details to help explain | ||
| 66 | what exactly Syncthing does in order to sync files between devices. | ||
| 67 | |||
| 68 | ** Local Syncthing Server(s) | ||
| 69 | |||
| 70 | Syncthing syncs files between multiple devices by creating a local | ||
| 71 | server on each device. These local servers handle a few different | ||
| 72 | things, such as watching files and directories for changes, hosting an | ||
| 73 | administrative GUI website, and authenticating with connected devices. | ||
| 74 | |||
| 75 | You can also start, stop, and restart the Syncthing server via the | ||
| 76 | command-line or web dashboard. If you're running Syncthing on a device | ||
| 77 | with =systemd=, you can use the following commands: | ||
| 78 | |||
| 79 | #+begin_src sh | ||
| 80 | sudo systemctl start syncthing@username.service | ||
| 81 | sudo systemctl restart syncthing@username.service | ||
| 82 | sudo systemctl stop syncthing@username.service | ||
| 83 | #+end_src | ||
| 84 | |||
| 85 | ** Syncthing Dashboard | ||
| 86 | |||
| 87 | This biggest part of Syncthing is the admin GUI website that runs on | ||
| 88 | each device (note that mobile devices will use the Syncthing app rather | ||
| 89 | than the web GUI). The admin GUI is available through the web browser on | ||
| 90 | the local device that is running Syncthing - simply go to | ||
| 91 | =http://localhost:8384= or =http://127.0.0.1:8384=. This web page is the | ||
| 92 | place where you will change settings, add/modify synced files, and | ||
| 93 | add/modify connected devices. | ||
| 94 | |||
| 95 | Here's an example web GUI dashboard: | ||
| 96 | |||
| 97 | #+caption: Syncthing Dashboard | ||
| 98 | [[https://img.cleberg.net/blog/20221020-syncthing/syncthing_gui.png]] | ||
| 99 | |||
| 100 | ** Remote Devices | ||
| 101 | |||
| 102 | A cloud storage solution wouldn't be very useful if you aren't able to | ||
| 103 | share data among various devices. Syncthing does this by sharing Device | ||
| 104 | IDs to connect servers, and then by manually sharing Folders with | ||
| 105 | devices that have been connected. | ||
| 106 | |||
| 107 | For instance, if you have a laptop running Syncthing and then install | ||
| 108 | the Syncthing mobile app on a phone, you could scan the laptop's QR code | ||
| 109 | for Device ID and then accept the authentication on the laptop's | ||
| 110 | dashboard. Next, you can use either device to select a folder for | ||
| 111 | sharing and dictating which device should send, receive, or both. | ||
| 112 | |||
| 113 | When you connect devices, you can set one device as an "Introducer," | ||
| 114 | which can add devices from the introducer to the device list, for | ||
| 115 | mutually shared folders. You can also configure Auto Accept, | ||
| 116 | compression, rate limits, and more settings per device. | ||
| 117 | |||
| 118 | * My Personal Cloud Storage Set-up | ||
| 119 | |||
| 120 | Personally, I use a model similar to a traditional cloud storage | ||
| 121 | service. I have a "centralized" server running 24/7 that acts as an | ||
| 122 | Introducer for my Syncthing network. I think of this as my main storage | ||
| 123 | and all other devices as tertiary client devices. I will likely add | ||
| 124 | additional servers as backups as time goes on so that I don't have to | ||
| 125 | rely on my laptop or phone as the only backups. | ||
| 126 | |||
| 127 | Currently, I have one desktop and one mobile device connected to the | ||
| 128 | network, both running intermittently as they are not powered-on 24/7. | ||
| 129 | |||
| 130 | The initial set-up of the software was easy enough, but data transfer | ||
| 131 | rates were incredibly slow for me due to the Wi-Fi. Instead, I plugged | ||
| 132 | my laptop into the ethernet network that my server is on and manually | ||
| 133 | copied my folders over to the server with =scp=. Once complete, | ||
| 134 | Syncthing validated that all files were there and not missing, and it | ||
| 135 | did not need to transfer any data through the WAN. | ||
| 136 | |||
| 137 | As slow as the transfer was going, this probably saved me a few days of | ||
| 138 | waiting for my ~100GB sync. | ||
| 139 | |||
| 140 | * Pros & Cons | ||
| 141 | |||
| 142 | I've put together a short list of pros and cons for Syncthing. I thought | ||
| 143 | about my experiences with Nextcloud, WebDAV, proprietary services | ||
| 144 | (Google Drive, iCloud, etc.), and privacy-focused cloud solutions | ||
| 145 | (pCloud, Tresorit, etc.). | ||
| 146 | |||
| 147 | *Pros:* | ||
| 148 | |||
| 149 | - I've faced no data loss at all through my two-month trial run. | ||
| 150 | - No third-parties store your data on their servers. | ||
| 151 | - You have full control over your data and can take your data and leave | ||
| 152 | at any time. | ||
| 153 | - It's possible to encrypt client-side easily with software like | ||
| 154 | Cryptomator. | ||
| 155 | - No proprietary clients or mounted volumes, just plain files and | ||
| 156 | folders. | ||
| 157 | |||
| 158 | *Cons:* | ||
| 159 | |||
| 160 | - The learning curve is steeper than traditional cloud services and is | ||
| 161 | focused on a technical audience. | ||
| 162 | - If a device needs to modify files in a Folder, the devices will need | ||
| 163 | to sync ALL files from the folder, which may be large. To avoid size | ||
| 164 | restraints, split large folders into smaller folders for syncing. | ||
| 165 | - Syncing can be slow due to the clients/servers initially connecting or | ||
| 166 | re-connecting after sleeping. | ||
| 167 | - Multiple personal devices are required and require the user to own or | ||
| 168 | rent them as no third-party servers are involved in the storage of | ||
| 169 | data. | ||
| 170 | |||
| 171 | Overall, I've had a great experience with Syncthing so far. I've had no | ||
| 172 | data loss, syncing has been quick and easy when changes are made to | ||
| 173 | files, device connections are reliable, and I love the freedom of | ||
| 174 | controlling the clients and servers as I choose. | ||
| 175 | |||
| 176 | Not to mention that I appreciate that I - or someone else - could pull | ||
| 177 | the Syncthing [[https://github.com/syncthing][source code]] and continue | ||
| 178 | development/support if the Syncthing Foundation decides to stop | ||
| 179 | developing the software or sells the business. | ||
content/blog/2022-10-22-alpine-linux.md deleted −266
| @@ -1,266 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-10-22 | ||
| 3 | title = "Alpine Linux: My New Server OS" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Alpine Linux | ||
| 9 | |||
| 10 | [Alpine Linux](https://alpinelinux.org) is a very small distro, built on musl | ||
| 11 | libc and busybox. It uses ash as the default shell, OpenRC as the init system, | ||
| 12 | and apk as the package manager. According to their website, an Alpine container | ||
| 13 | "requires no more than 8 MB and a minimal installation to disk requires around | ||
| 14 | 130 MB of storage." An actual bare metal machine is recommended to have 100 MB | ||
| 15 | of RAM and 0-700 MB of storage space. | ||
| 16 | |||
| 17 | Historically, I've used Ubuntu's minimal installation image as my server OS for | ||
| 18 | the last five years. Ubuntu worked well and helped as my original server | ||
| 19 | contained an nVidia GPU and no onboard graphics, so quite a few distros won't | ||
| 20 | boot or install without a lot of tinkering. | ||
| 21 | |||
| 22 | Alpine has given me a huge increase in performance across my Docker apps and | ||
| 23 | Nginx websites. CPU load for the new server I'm using to test Alpine hovers | ||
| 24 | around 0-5% on average with an Intel(R) Core(TM) i3-6100 CPU @ 3.70GHz. | ||
| 25 | |||
| 26 | The only services I haven't moved over to Alpine are Plex Media Server and | ||
| 27 | Syncthing, which may increase CPU load quite a bit depending on how many streams | ||
| 28 | are running. | ||
| 29 | |||
| 30 | ## Installation | ||
| 31 | |||
| 32 | In terms of installation, Alpine has an incredibly useful | ||
| 33 | [wiki](https://wiki.alpinelinux.org/wiki/Installation) that will guide a user | ||
| 34 | throughout the installation and post-installation processes, as well as various | ||
| 35 | other articles and guides. | ||
| 36 | |||
| 37 | To install Alpine, find an appropriate [image to | ||
| 38 | download](https://alpinelinux.org/downloads/) and flash it to a USB using | ||
| 39 | software such as Rufus or Etcher. I opted to use the Standard image for my | ||
| 40 | x86~64~ architecture. | ||
| 41 | |||
| 42 | Once the USB is ready, plug it into the machine and reboot. Note that you may | ||
| 43 | have to use a key such as `Esc` or `F1-12` to access the boot menu. The Alpine | ||
| 44 | Linux terminal will load quickly and for a login. | ||
| 45 | |||
| 46 | To log in to the installation image, use the `root` account; there is no | ||
| 47 | password. Once logged-in, execute the setup command: | ||
| 48 | |||
| 49 | ```sh | ||
| 50 | setup-alpine | ||
| 51 | ``` | ||
| 52 | |||
| 53 | The setup script will ask a series of questions to configure the system. Be sure | ||
| 54 | to answer carefully or else you may have to re-configure the system after boot. | ||
| 55 | |||
| 56 | - Keyboard Layout (Local keyboard language and usage mode, e.g., us and | ||
| 57 | variant of us-nodeadkeys.) | ||
| 58 | - Hostname (The name for the computer.) | ||
| 59 | - Network (For example, automatic IP address discovery with the "DHCP" | ||
| 60 | protocol.) | ||
| 61 | - DNS Servers (Domain Name Servers to query. For privacy reasons, it is NOT | ||
| 62 | recommended to route every local request to servers like Google's 8.8.8.8.) | ||
| 63 | - Timezone | ||
| 64 | - Proxy (Proxy server to use for accessing the web. Use "none" for direct | ||
| 65 | connections to the internet.) | ||
| 66 | - Mirror (From where to download packages. Choose the organization you trust | ||
| 67 | giving your usage patterns to.) | ||
| 68 | - SSH (Secure SHell remote access server. "Openssh" is part of the default | ||
| 69 | install image. Use "none" to disable remote login, e.g. on laptops.) | ||
| 70 | - NTP (Network Time Protocol client used for keeping the system clock in sync | ||
| 71 | with a time-server. Package "chrony" is part of the default install image.) | ||
| 72 | - Disk Mode (Select between diskless (disk="none"), "data" or "sys", as | ||
| 73 | described above.) | ||
| 74 | |||
| 75 | Once the setup script is finished, be sure to reboot the machine and remove the | ||
| 76 | USB device. | ||
| 77 | |||
| 78 | ```sh | ||
| 79 | reboot | ||
| 80 | ``` | ||
| 81 | |||
| 82 | ## Post-Installation | ||
| 83 | |||
| 84 | There are many things you can do once your Alpine Linux system is up and | ||
| 85 | running, and it largely depends on what you'll use the machine for. I'm going to | ||
| 86 | walk through my personal post-installation setup for my web server. | ||
| 87 | |||
| 88 | 1. Upgrade the System | ||
| 89 | |||
| 90 | First, login as `root` in order to update and upgrade the system: | ||
| 91 | |||
| 92 | ```sh | ||
| 93 | apk -U upgrade | ||
| 94 | ``` | ||
| 95 | |||
| 96 | 2. Adding a User | ||
| 97 | |||
| 98 | I needed to add a user so that I don't need to log in as root. Note that if | ||
| 99 | you're used to using the `sudo` command, you will now need to use the `doas` | ||
| 100 | command on Alpine Linux. | ||
| 101 | |||
| 102 | ```sh | ||
| 103 | apk add doas | ||
| 104 | adduser <username> | ||
| 105 | adduser <username> wheel | ||
| 106 | ``` | ||
| 107 | |||
| 108 | You can now log out and log back in using the newly-created user: | ||
| 109 | |||
| 110 | ```sh | ||
| 111 | exit | ||
| 112 | ``` | ||
| 113 | |||
| 114 | 3. Enable Community Packages | ||
| 115 | |||
| 116 | In order to install more common packages that aren't found in the `main` | ||
| 117 | repository, you will need to enable the `community` repository: | ||
| 118 | |||
| 119 | ```sh | ||
| 120 | doas nano /etc/apk/repositories | ||
| 121 | ``` | ||
| 122 | |||
| 123 | Uncomment the community line for whichever version of Alpine you're running: | ||
| 124 | |||
| 125 | ```sh | ||
| 126 | /media/usb/apks | ||
| 127 | http://dl-cdn.alpinelinux.org/alpine/v3.16/main | ||
| 128 | http://dl-cdn.alpinelinux.org/alpine/v3.16/community | ||
| 129 | #http://dl-cdn.alpinelinux.org/alpine/edge/main | ||
| 130 | #http://dl-cdn.alpinelinux.org/alpine/edge/community | ||
| 131 | #http://dl-cdn.alpinelinux.org/alpine/edge/testing | ||
| 132 | ``` | ||
| 133 | |||
| 134 | 4. Install Required Packages | ||
| 135 | |||
| 136 | Now that the community packages are available, you can install any packages | ||
| 137 | you need. In my case, I installed the web server packages I need for my | ||
| 138 | services: | ||
| 139 | |||
| 140 | ```sh | ||
| 141 | doas apk add nano nginx docker docker-compose ufw | ||
| 142 | ``` | ||
| 143 | |||
| 144 | 5. SSH | ||
| 145 | |||
| 146 | If you didn't install OpenSSH as part of the installation, you can do so | ||
| 147 | now: | ||
| 148 | |||
| 149 | ```sh | ||
| 150 | doas apk add openssh | ||
| 151 | ``` | ||
| 152 | |||
| 153 | Next, either create a new key or copy your SSH key to the server from your | ||
| 154 | current machines: | ||
| 155 | |||
| 156 | ```sh | ||
| 157 | # Create a new key | ||
| 158 | ssh-keygen | ||
| 159 | ``` | ||
| 160 | |||
| 161 | If you need to copy an existing SSH key from a current machine: | ||
| 162 | |||
| 163 | ```sh | ||
| 164 | # Copy key from existing machines | ||
| 165 | ssh-copy-id <username>@<ip_address> | ||
| 166 | ``` | ||
| 167 | |||
| 168 | 6. Firewall | ||
| 169 | |||
| 170 | Lastly, I installed `ufw` above as my firewall. To set up, default to deny | ||
| 171 | incoming and allow outgoing connections. Then selectively allow other ports | ||
| 172 | or apps as needed. | ||
| 173 | |||
| 174 | ```sh | ||
| 175 | doas ufw default deny incoming | ||
| 176 | doas ufw default allow outgoing | ||
| 177 | doas ufw allow SSH | ||
| 178 | doas ufw allow "WWW Full" | ||
| 179 | doas ufw allow 9418 # Git server port | ||
| 180 | ``` | ||
| 181 | |||
| 182 | 7. Change Hostname | ||
| 183 | |||
| 184 | If you don't like the hostname set during installation, you just need to | ||
| 185 | edit two files. First, edit the simple hostname file: | ||
| 186 | |||
| 187 | ```sh | ||
| 188 | doas nano /etc/hostname | ||
| 189 | ``` | ||
| 190 | |||
| 191 | ```sh | ||
| 192 | <hostname> | ||
| 193 | ``` | ||
| 194 | |||
| 195 | Next, edit the `hosts` file: | ||
| 196 | |||
| 197 | ```sh | ||
| 198 | doas nano /etc/hosts | ||
| 199 | ``` | ||
| 200 | |||
| 201 | ```sh | ||
| 202 | 127.0.0.1 <hostname>.local <hostname> localhost.local localhost | ||
| 203 | ::1 <hostname> <hostname>.local | ||
| 204 | ``` | ||
| 205 | |||
| 206 | # Nginx Web Server | ||
| 207 | |||
| 208 | To set up my web server, I simply created the `www` user and created the | ||
| 209 | necessary files. | ||
| 210 | |||
| 211 | ```sh | ||
| 212 | doas adduser -D -g 'www' www | ||
| 213 | mkdir /www | ||
| 214 | doas mkdir /www | ||
| 215 | doas chown -R www:www /var/lib/nginx/ | ||
| 216 | doas chown -R www:www /www | ||
| 217 | ``` | ||
| 218 | |||
| 219 | If you're running a simple webroot, you can alter the main `nginx.conf` file. | ||
| 220 | Otherwise, you can drop configuration files in the following directory. You | ||
| 221 | don't need to enable or symlink the configuration file like you do in other | ||
| 222 | systems. | ||
| 223 | |||
| 224 | ```sh | ||
| 225 | doas nano /etc/nginx/http.d/example_website.conf | ||
| 226 | ``` | ||
| 227 | |||
| 228 | Once the configuration is set and pointed at the `/www` directory to serve | ||
| 229 | files, enable the Nginx service: | ||
| 230 | |||
| 231 | ```sh | ||
| 232 | # Note that 'default' must be included or Nginx will not start on boot | ||
| 233 | doas rc-update add nginx default | ||
| 234 | ``` | ||
| 235 | |||
| 236 | # Docker Containers | ||
| 237 | |||
| 238 | Docker works exactly the same as other systems. Either execute a `docker run` | ||
| 239 | command or create a `docker-compose.yml` file and do `docker-compose up -d`. | ||
| 240 | |||
| 241 | # Git Server | ||
| 242 | |||
| 243 | I went in-depth on how to self-host a git server in another post: [Self-Hosting | ||
| 244 | a Personal Git Server](../git-server/). | ||
| 245 | |||
| 246 | However, there are a few differences with Alpine. First note that in order to | ||
| 247 | change the `git` user's shell, you must do a few things a little different: | ||
| 248 | |||
| 249 | ```sh | ||
| 250 | doas apk add libuser | ||
| 251 | doas touch /etc/login.defs | ||
| 252 | doas mkdir /etc/default | ||
| 253 | doas touch /etc/default/useradd | ||
| 254 | doas lchsh git | ||
| 255 | ``` | ||
| 256 | |||
| 257 | # Thoughts on Alpine | ||
| 258 | |||
| 259 | So far, I love Alpine Linux. I have no complaints about anything at this point, | ||
| 260 | but I'm not completely finished with the migration yet. Once I'm able to upgrade | ||
| 261 | my hardware to a rack-mounted server, I will migrate Plex and Syncthing over to | ||
| 262 | Alpine as well - possibly putting Plex into a container or VM. | ||
| 263 | |||
| 264 | The performance is stellar, the `apk` package manager is seamless, and system | ||
| 265 | administration tasks are effortless. My only regret is that I didn't install | ||
| 266 | Alpine sooner. | ||
content/blog/2022-10-22-alpine-linux.org added +276
| @@ -0,0 +1,276 @@ | |||
| 1 | #+date: <2022-10-22> | ||
| 2 | #+title: Alpine Linux: My New Server OS | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Alpine Linux | ||
| 7 | |||
| 8 | [[https://alpinelinux.org][Alpine Linux]] is a very small distro, built | ||
| 9 | on musl libc and busybox. It uses ash as the default shell, OpenRC as | ||
| 10 | the init system, and apk as the package manager. According to their | ||
| 11 | website, an Alpine container "requires no more than 8 MB and a minimal | ||
| 12 | installation to disk requires around 130 MB of storage." An actual bare | ||
| 13 | metal machine is recommended to have 100 MB of RAM and 0-700 MB of | ||
| 14 | storage space. | ||
| 15 | |||
| 16 | Historically, I've used Ubuntu's minimal installation image as my server | ||
| 17 | OS for the last five years. Ubuntu worked well and helped as my original | ||
| 18 | server contained an nVidia GPU and no onboard graphics, so quite a few | ||
| 19 | distros won't boot or install without a lot of tinkering. | ||
| 20 | |||
| 21 | Alpine has given me a huge increase in performance across my Docker apps | ||
| 22 | and Nginx websites. CPU load for the new server I'm using to test Alpine | ||
| 23 | hovers around 0-5% on average with an Intel(R) Core(TM) i3-6100 CPU @ | ||
| 24 | 3.70GHz. | ||
| 25 | |||
| 26 | The only services I haven't moved over to Alpine are Plex Media Server | ||
| 27 | and Syncthing, which may increase CPU load quite a bit depending on how | ||
| 28 | many streams are running. | ||
| 29 | |||
| 30 | ** Installation | ||
| 31 | |||
| 32 | In terms of installation, Alpine has an incredibly useful | ||
| 33 | [[https://wiki.alpinelinux.org/wiki/Installation][wiki]] that will guide | ||
| 34 | a user throughout the installation and post-installation processes, as | ||
| 35 | well as various other articles and guides. | ||
| 36 | |||
| 37 | To install Alpine, find an appropriate | ||
| 38 | [[https://alpinelinux.org/downloads/][image to download]] and flash it | ||
| 39 | to a USB using software such as Rufus or Etcher. I opted to use the | ||
| 40 | Standard image for my x86_{64} architecture. | ||
| 41 | |||
| 42 | Once the USB is ready, plug it into the machine and reboot. Note that | ||
| 43 | you may have to use a key such as =Esc= or =F1-12= to access the boot | ||
| 44 | menu. The Alpine Linux terminal will load quickly and for a login. | ||
| 45 | |||
| 46 | To log in to the installation image, use the =root= account; there is no | ||
| 47 | password. Once logged-in, execute the setup command: | ||
| 48 | |||
| 49 | #+begin_src sh | ||
| 50 | setup-alpine | ||
| 51 | #+end_src | ||
| 52 | |||
| 53 | The setup script will ask a series of questions to configure the system. | ||
| 54 | Be sure to answer carefully or else you may have to re-configure the | ||
| 55 | system after boot. | ||
| 56 | |||
| 57 | - Keyboard Layout (Local keyboard language and usage mode, e.g., us and | ||
| 58 | variant of us-nodeadkeys.) | ||
| 59 | - Hostname (The name for the computer.) | ||
| 60 | - Network (For example, automatic IP address discovery with the "DHCP" | ||
| 61 | protocol.) | ||
| 62 | - DNS Servers (Domain Name Servers to query. For privacy reasons, it is | ||
| 63 | NOT recommended to route every local request to servers like Google's | ||
| 64 | 8.8.8.8.) | ||
| 65 | - Timezone | ||
| 66 | - Proxy (Proxy server to use for accessing the web. Use "none" for | ||
| 67 | direct connections to the internet.) | ||
| 68 | - Mirror (From where to download packages. Choose the organization you | ||
| 69 | trust giving your usage patterns to.) | ||
| 70 | - SSH (Secure SHell remote access server. "Openssh" is part of the | ||
| 71 | default install image. Use "none" to disable remote login, e.g. on | ||
| 72 | laptops.) | ||
| 73 | - NTP (Network Time Protocol client used for keeping the system clock in | ||
| 74 | sync with a time-server. Package "chrony" is part of the default | ||
| 75 | install image.) | ||
| 76 | - Disk Mode (Select between diskless (disk="none"), "data" or "sys", as | ||
| 77 | described above.) | ||
| 78 | |||
| 79 | Once the setup script is finished, be sure to reboot the machine and | ||
| 80 | remove the USB device. | ||
| 81 | |||
| 82 | #+begin_src sh | ||
| 83 | reboot | ||
| 84 | #+end_src | ||
| 85 | |||
| 86 | ** Post-Installation | ||
| 87 | |||
| 88 | There are many things you can do once your Alpine Linux system is up and | ||
| 89 | running, and it largely depends on what you'll use the machine for. I'm | ||
| 90 | going to walk through my personal post-installation setup for my web | ||
| 91 | server. | ||
| 92 | |||
| 93 | 1. Upgrade the System | ||
| 94 | |||
| 95 | First, login as =root= in order to update and upgrade the system: | ||
| 96 | |||
| 97 | #+begin_src sh | ||
| 98 | apk -U upgrade | ||
| 99 | #+end_src | ||
| 100 | |||
| 101 | 2. Adding a User | ||
| 102 | |||
| 103 | I needed to add a user so that I don't need to log in as root. Note | ||
| 104 | that if you're used to using the =sudo= command, you will now need to | ||
| 105 | use the =doas= command on Alpine Linux. | ||
| 106 | |||
| 107 | #+begin_src sh | ||
| 108 | apk add doas | ||
| 109 | adduser <username> | ||
| 110 | adduser <username> wheel | ||
| 111 | #+end_src | ||
| 112 | |||
| 113 | You can now log out and log back in using the newly-created user: | ||
| 114 | |||
| 115 | #+begin_src sh | ||
| 116 | exit | ||
| 117 | #+end_src | ||
| 118 | |||
| 119 | 3. Enable Community Packages | ||
| 120 | |||
| 121 | In order to install more common packages that aren't found in the | ||
| 122 | =main= repository, you will need to enable the =community= | ||
| 123 | repository: | ||
| 124 | |||
| 125 | #+begin_src sh | ||
| 126 | doas nano /etc/apk/repositories | ||
| 127 | #+end_src | ||
| 128 | |||
| 129 | Uncomment the community line for whichever version of Alpine you're | ||
| 130 | running: | ||
| 131 | |||
| 132 | #+begin_src sh | ||
| 133 | /media/usb/apks | ||
| 134 | http://dl-cdn.alpinelinux.org/alpine/v3.16/main | ||
| 135 | http://dl-cdn.alpinelinux.org/alpine/v3.16/community | ||
| 136 | #http://dl-cdn.alpinelinux.org/alpine/edge/main | ||
| 137 | #http://dl-cdn.alpinelinux.org/alpine/edge/community | ||
| 138 | #http://dl-cdn.alpinelinux.org/alpine/edge/testing | ||
| 139 | #+end_src | ||
| 140 | |||
| 141 | 4. Install Required Packages | ||
| 142 | |||
| 143 | Now that the community packages are available, you can install any | ||
| 144 | packages you need. In my case, I installed the web server packages I | ||
| 145 | need for my services: | ||
| 146 | |||
| 147 | #+begin_src sh | ||
| 148 | doas apk add nano nginx docker docker-compose ufw | ||
| 149 | #+end_src | ||
| 150 | |||
| 151 | 5. SSH | ||
| 152 | |||
| 153 | If you didn't install OpenSSH as part of the installation, you can do | ||
| 154 | so now: | ||
| 155 | |||
| 156 | #+begin_src sh | ||
| 157 | doas apk add openssh | ||
| 158 | #+end_src | ||
| 159 | |||
| 160 | Next, either create a new key or copy your SSH key to the server from | ||
| 161 | your current machines: | ||
| 162 | |||
| 163 | #+begin_src sh | ||
| 164 | # Create a new key | ||
| 165 | ssh-keygen | ||
| 166 | #+end_src | ||
| 167 | |||
| 168 | If you need to copy an existing SSH key from a current machine: | ||
| 169 | |||
| 170 | #+begin_src sh | ||
| 171 | # Copy key from existing machines | ||
| 172 | ssh-copy-id <username>@<ip_address> | ||
| 173 | #+end_src | ||
| 174 | |||
| 175 | 6. Firewall | ||
| 176 | |||
| 177 | Lastly, I installed =ufw= above as my firewall. To set up, default to | ||
| 178 | deny incoming and allow outgoing connections. Then selectively allow | ||
| 179 | other ports or apps as needed. | ||
| 180 | |||
| 181 | #+begin_src sh | ||
| 182 | doas ufw default deny incoming | ||
| 183 | doas ufw default allow outgoing | ||
| 184 | doas ufw allow SSH | ||
| 185 | doas ufw allow "WWW Full" | ||
| 186 | doas ufw allow 9418 # Git server port | ||
| 187 | #+end_src | ||
| 188 | |||
| 189 | 7. Change Hostname | ||
| 190 | |||
| 191 | If you don't like the hostname set during installation, you just need | ||
| 192 | to edit two files. First, edit the simple hostname file: | ||
| 193 | |||
| 194 | #+begin_src sh | ||
| 195 | doas nano /etc/hostname | ||
| 196 | #+end_src | ||
| 197 | |||
| 198 | #+begin_src sh | ||
| 199 | <hostname> | ||
| 200 | #+end_src | ||
| 201 | |||
| 202 | Next, edit the =hosts= file: | ||
| 203 | |||
| 204 | #+begin_src sh | ||
| 205 | doas nano /etc/hosts | ||
| 206 | #+end_src | ||
| 207 | |||
| 208 | #+begin_src sh | ||
| 209 | 127.0.0.1 <hostname>.local <hostname> localhost.local localhost | ||
| 210 | ::1 <hostname> <hostname>.local | ||
| 211 | #+end_src | ||
| 212 | |||
| 213 | * Nginx Web Server | ||
| 214 | |||
| 215 | To set up my web server, I simply created the =www= user and created the | ||
| 216 | necessary files. | ||
| 217 | |||
| 218 | #+begin_src sh | ||
| 219 | doas adduser -D -g 'www' www | ||
| 220 | mkdir /www | ||
| 221 | doas mkdir /www | ||
| 222 | doas chown -R www:www /var/lib/nginx/ | ||
| 223 | doas chown -R www:www /www | ||
| 224 | #+end_src | ||
| 225 | |||
| 226 | If you're running a simple webroot, you can alter the main =nginx.conf= | ||
| 227 | file. Otherwise, you can drop configuration files in the following | ||
| 228 | directory. You don't need to enable or symlink the configuration file | ||
| 229 | like you do in other systems. | ||
| 230 | |||
| 231 | #+begin_src sh | ||
| 232 | doas nano /etc/nginx/http.d/example_website.conf | ||
| 233 | #+end_src | ||
| 234 | |||
| 235 | Once the configuration is set and pointed at the =/www= directory to | ||
| 236 | serve files, enable the Nginx service: | ||
| 237 | |||
| 238 | #+begin_src sh | ||
| 239 | # Note that 'default' must be included or Nginx will not start on boot | ||
| 240 | doas rc-update add nginx default | ||
| 241 | #+end_src | ||
| 242 | |||
| 243 | * Docker Containers | ||
| 244 | |||
| 245 | Docker works exactly the same as other systems. Either execute a | ||
| 246 | =docker run= command or create a =docker-compose.yml= file and do | ||
| 247 | =docker-compose up -d=. | ||
| 248 | |||
| 249 | * Git Server | ||
| 250 | |||
| 251 | I went in-depth on how to self-host a git server in another post: | ||
| 252 | [[../git-server/][Self-Hosting a Personal Git Server]]. | ||
| 253 | |||
| 254 | However, there are a few differences with Alpine. First note that in | ||
| 255 | order to change the =git= user's shell, you must do a few things a | ||
| 256 | little different: | ||
| 257 | |||
| 258 | #+begin_src sh | ||
| 259 | doas apk add libuser | ||
| 260 | doas touch /etc/login.defs | ||
| 261 | doas mkdir /etc/default | ||
| 262 | doas touch /etc/default/useradd | ||
| 263 | doas lchsh git | ||
| 264 | #+end_src | ||
| 265 | |||
| 266 | * Thoughts on Alpine | ||
| 267 | |||
| 268 | So far, I love Alpine Linux. I have no complaints about anything at this | ||
| 269 | point, but I'm not completely finished with the migration yet. Once I'm | ||
| 270 | able to upgrade my hardware to a rack-mounted server, I will migrate | ||
| 271 | Plex and Syncthing over to Alpine as well - possibly putting Plex into a | ||
| 272 | container or VM. | ||
| 273 | |||
| 274 | The performance is stellar, the =apk= package manager is seamless, and | ||
| 275 | system administration tasks are effortless. My only regret is that I | ||
| 276 | didn't install Alpine sooner. | ||
content/blog/2022-10-30-linux-display-manager.md deleted −77
| @@ -1,77 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-10-30 | ||
| 3 | title = "How to Disable or Change the Display Manager on Void Linux" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Display Manager Services | ||
| 9 | |||
| 10 | In order to change the [display | ||
| 11 | manager](https://en.wikipedia.org/wiki/Display_manager) on Void Linux - or any | ||
| 12 | other Linux distro - you need to identify the currently enabled display manager. | ||
| 13 | |||
| 14 | ## Disabling the Current Display Manager | ||
| 15 | |||
| 16 | Void Linux only has one ISO available for download with a pre-built display | ||
| 17 | manager at the time of this post: the XFCE ISO. If you've installed this | ||
| 18 | version, the pre-assigned display manager is `lxdm`. If you installed another | ||
| 19 | display manager, replace `lxdm` in the following command with the display | ||
| 20 | manager you have installed. | ||
| 21 | |||
| 22 | To disable `lxdm`, simply remove the service symlink: | ||
| 23 | |||
| 24 | ```sh | ||
| 25 | sudo rm /var/service/lxdm | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Enabling a New Display Manager | ||
| 29 | |||
| 30 | If you want to enable a new display manager, you can do so after `lxdm` is | ||
| 31 | disabled. Make sure to replace `<new_display_manager>` with your new DM, such as | ||
| 32 | `gdm`, `xdm`, etc. | ||
| 33 | |||
| 34 | ```sh | ||
| 35 | sudo ln -s /etc/sv/<new_display_manager> /var/service | ||
| 36 | ``` | ||
| 37 | |||
| 38 | # Set Up `.xinitrc` | ||
| 39 | |||
| 40 | Depending on your setup, you may need to create a few X files, such as | ||
| 41 | `~/.xinitrc`. For my personal set-up, I created this file to launch the i3wm as | ||
| 42 | my desktop. | ||
| 43 | |||
| 44 | ```sh | ||
| 45 | nano ~/.xinitrc | ||
| 46 | ``` | ||
| 47 | |||
| 48 | ```sh | ||
| 49 | #!/bin/sh | ||
| 50 | |||
| 51 | exec i3 | ||
| 52 | ``` | ||
| 53 | |||
| 54 | If you run a desktop other than i3, simply replace `i3` with the shell command | ||
| 55 | that launches that desktop. | ||
| 56 | |||
| 57 | # Set Up Your Shell Profile | ||
| 58 | |||
| 59 | Finally, in order to automatically launch an X session upon login, you will need | ||
| 60 | to edit the `.bash_profile` (bash) or `.zprofile` (zsh) files for your shell: | ||
| 61 | |||
| 62 | ```sh | ||
| 63 | nano ~/.zprofile | ||
| 64 | ``` | ||
| 65 | |||
| 66 | Add the following snippet to the end of the shell profile file. This will | ||
| 67 | execute the `startx` command upon login. | ||
| 68 | |||
| 69 | ```sh | ||
| 70 | if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then | ||
| 71 | exec startx | ||
| 72 | fi | ||
| 73 | ``` | ||
| 74 | |||
| 75 | Alternatively, you can ignore this step and simply choose to manually execute | ||
| 76 | `startx` upon login. This can be useful if you have issues with your desktop or | ||
| 77 | like to manually launch different desktops by choice. | ||
content/blog/2022-10-30-linux-display-manager.org added +77
| @@ -0,0 +1,77 @@ | |||
| 1 | #+date: <2022-10-30> | ||
| 2 | #+title: How to Disable or Change the Display Manager on Void Linux | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Display Manager Services | ||
| 7 | |||
| 8 | In order to change the | ||
| 9 | [[https://en.wikipedia.org/wiki/Display_manager][display manager]] on | ||
| 10 | Void Linux - or any other Linux distro - you need to identify the | ||
| 11 | currently enabled display manager. | ||
| 12 | |||
| 13 | ** Disabling the Current Display Manager | ||
| 14 | |||
| 15 | Void Linux only has one ISO available for download with a pre-built | ||
| 16 | display manager at the time of this post: the XFCE ISO. If you've | ||
| 17 | installed this version, the pre-assigned display manager is =lxdm=. If | ||
| 18 | you installed another display manager, replace =lxdm= in the following | ||
| 19 | command with the display manager you have installed. | ||
| 20 | |||
| 21 | To disable =lxdm=, simply remove the service symlink: | ||
| 22 | |||
| 23 | #+begin_src sh | ||
| 24 | sudo rm /var/service/lxdm | ||
| 25 | #+end_src | ||
| 26 | |||
| 27 | ** Enabling a New Display Manager | ||
| 28 | |||
| 29 | If you want to enable a new display manager, you can do so after =lxdm= | ||
| 30 | is disabled. Make sure to replace =<new_display_manager>= with your new | ||
| 31 | DM, such as =gdm=, =xdm=, etc. | ||
| 32 | |||
| 33 | #+begin_src sh | ||
| 34 | sudo ln -s /etc/sv/<new_display_manager> /var/service | ||
| 35 | #+end_src | ||
| 36 | |||
| 37 | * Set Up =.xinitrc= | ||
| 38 | |||
| 39 | Depending on your setup, you may need to create a few X files, such as | ||
| 40 | =~/.xinitrc=. For my personal set-up, I created this file to launch the | ||
| 41 | i3wm as my desktop. | ||
| 42 | |||
| 43 | #+begin_src sh | ||
| 44 | nano ~/.xinitrc | ||
| 45 | #+end_src | ||
| 46 | |||
| 47 | #+begin_src sh | ||
| 48 | #!/bin/sh | ||
| 49 | |||
| 50 | exec i3 | ||
| 51 | #+end_src | ||
| 52 | |||
| 53 | If you run a desktop other than i3, simply replace =i3= with the shell | ||
| 54 | command that launches that desktop. | ||
| 55 | |||
| 56 | * Set Up Your Shell Profile | ||
| 57 | |||
| 58 | Finally, in order to automatically launch an X session upon login, you | ||
| 59 | will need to edit the =.bash_profile= (bash) or =.zprofile= (zsh) files | ||
| 60 | for your shell: | ||
| 61 | |||
| 62 | #+begin_src sh | ||
| 63 | nano ~/.zprofile | ||
| 64 | #+end_src | ||
| 65 | |||
| 66 | Add the following snippet to the end of the shell profile file. This | ||
| 67 | will execute the =startx= command upon login. | ||
| 68 | |||
| 69 | #+begin_src sh | ||
| 70 | if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then | ||
| 71 | exec startx | ||
| 72 | fi | ||
| 73 | #+end_src | ||
| 74 | |||
| 75 | Alternatively, you can ignore this step and simply choose to manually | ||
| 76 | execute =startx= upon login. This can be useful if you have issues with | ||
| 77 | your desktop or like to manually launch different desktops by choice. | ||
content/blog/2022-11-07-self-hosting-matrix.md → content/blog/2022-11-07-self-hosting-matrix.org renamed +88 −85
| @@ -1,103 +1,104 @@ | |||
| 1 | +++ | 1 | #+date: <2022-11-07> |
| 2 | date = 2022-11-07 | 2 | #+title: Self-Hosting Matrix Synapse on Alpine Linux |
| 3 | title = "Self-Hosting Matrix Synapse on Alpine Linux" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Synapse | 5 | |
| 6 | * Synapse | ||
| 9 | 7 | ||
| 10 | If you're reading this, you likely know that | 8 | If you're reading this, you likely know that |
| 11 | [Synapse](https://github.com/matrix-org/synapse/) is a popular | 9 | [[https://github.com/matrix-org/synapse/][Synapse]] is a popular |
| 12 | [Matrix](https://matrix.org/) home server software that allows users to run | 10 | [[https://matrix.org/][Matrix]] home server software that allows users |
| 13 | their own Matrix home server. | 11 | to run their own Matrix home server. |
| 14 | 12 | ||
| 15 | This post is a short guide describing how I was able to get Synapse working in a | 13 | This post is a short guide describing how I was able to get Synapse |
| 16 | minimally-usable state on Alpine Linux. | 14 | working in a minimally-usable state on Alpine Linux. |
| 17 | 15 | ||
| 18 | # Installation Process | 16 | * Installation Process |
| 19 | 17 | ||
| 20 | ## Dependencies | 18 | ** Dependencies |
| 21 | 19 | ||
| 22 | First, since there is no Alpine-specific package for Synapse, we need to ensure | 20 | First, since there is no Alpine-specific package for Synapse, we need to |
| 23 | that Alpine has the required dependencies for the Python-based installation | 21 | ensure that Alpine has the required dependencies for the Python-based |
| 24 | method. | 22 | installation method. |
| 25 | 23 | ||
| 26 | ```sh | 24 | #+begin_src sh |
| 27 | doas apk -U update | 25 | doas apk -U update |
| 28 | doas apk add python3 py3-virtualenv | 26 | doas apk add python3 py3-virtualenv |
| 29 | ``` | 27 | #+end_src |
| 30 | 28 | ||
| 31 | Next, we need to set up a Python virtual environment for Synapse: | 29 | Next, we need to set up a Python virtual environment for Synapse: |
| 32 | 30 | ||
| 33 | ```sh | 31 | #+begin_src sh |
| 34 | mkdir -p ~/synapse && cd ~/synapse | 32 | mkdir -p ~/synapse && cd ~/synapse |
| 35 | virtualenv -p python3 ~/synapse/env | 33 | virtualenv -p python3 ~/synapse/env |
| 36 | source ~/synapse/env/bin/activate | 34 | source ~/synapse/env/bin/activate |
| 37 | pip install --upgrade pip | 35 | pip install --upgrade pip |
| 38 | pip install --upgrade setuptools | 36 | pip install --upgrade setuptools |
| 39 | pip install matrix-synapse | 37 | pip install matrix-synapse |
| 40 | ``` | 38 | #+end_src |
| 41 | 39 | ||
| 42 | ## Running Synapse | 40 | ** Running Synapse |
| 43 | 41 | ||
| 44 | Once installed, running Synapse is easy. Simply execute the following command, | 42 | Once installed, running Synapse is easy. Simply execute the following |
| 45 | replacing `example.com` with the domain name that will be used with this home | 43 | command, replacing =example.com= with the domain name that will be used |
| 46 | server. This will generate the configuration files needed to run the server. | 44 | with this home server. This will generate the configuration files needed |
| 45 | to run the server. | ||
| 47 | 46 | ||
| 48 | ```sh | 47 | #+begin_src sh |
| 49 | python -m synapse.app.homeserver \ | 48 | python -m synapse.app.homeserver \ |
| 50 | --server-name example.com \ | 49 | --server-name example.com \ |
| 51 | --config-path homeserver.yaml \ | 50 | --config-path homeserver.yaml \ |
| 52 | --generate-config \ | 51 | --generate-config \ |
| 53 | --report-stats=no | 52 | --report-stats=no |
| 54 | ``` | 53 | #+end_src |
| 55 | 54 | ||
| 56 | Once the configuration is generated, we can start up the Synapse server: | 55 | Once the configuration is generated, we can start up the Synapse server: |
| 57 | 56 | ||
| 58 | ```sh | 57 | #+begin_src sh |
| 59 | synctl start | 58 | synctl start |
| 60 | ``` | 59 | #+end_src |
| 61 | 60 | ||
| 62 | ## Configuring Synapse | 61 | ** Configuring Synapse |
| 63 | 62 | ||
| 64 | To make any change to Synapse, we need to edit the `YAML` configuration file: | 63 | To make any change to Synapse, we need to edit the =YAML= configuration |
| 64 | file: | ||
| 65 | 65 | ||
| 66 | ```sh | 66 | #+begin_src sh |
| 67 | nano ~/synapse/homeserver.yaml | 67 | nano ~/synapse/homeserver.yaml |
| 68 | ``` | 68 | #+end_src |
| 69 | 69 | ||
| 70 | For now, we just need to ensure the `server_name` is accurate. However, there | 70 | For now, we just need to ensure the =server_name= is accurate. However, |
| 71 | are a lot of other configuration options found in the [Configuring | 71 | there are a lot of other configuration options found in the |
| 72 | Synapse](https://matrix-org.github.io/synapse/develop/usage/configuration/config_documentation.html) | 72 | [[https://matrix-org.github.io/synapse/develop/usage/configuration/config_documentation.html][Configuring |
| 73 | documentation that can be enabled/disabled at any point. | 73 | Synapse]] documentation that can be enabled/disabled at any point. |
| 74 | 74 | ||
| 75 | ```yaml | 75 | #+begin_src yaml |
| 76 | server_name: "example.com" | 76 | server_name: "example.com" |
| 77 | ``` | 77 | #+end_src |
| 78 | 78 | ||
| 79 | Make sure to restart Synapse when you make changes to the configuration: | 79 | Make sure to restart Synapse when you make changes to the configuration: |
| 80 | 80 | ||
| 81 | ```sh | 81 | #+begin_src sh |
| 82 | synctl restart | 82 | synctl restart |
| 83 | ``` | 83 | #+end_src |
| 84 | 84 | ||
| 85 | ## Nginx Reverse-Proxy | 85 | ** Nginx Reverse-Proxy |
| 86 | 86 | ||
| 87 | To ensure that Synapse is reachable from the public, we need to connect our | 87 | To ensure that Synapse is reachable from the public, we need to connect |
| 88 | domain to the Synapse server. In my case, I use a Nginx reverse-proxy for this | 88 | our domain to the Synapse server. In my case, I use a Nginx |
| 89 | purpose. | 89 | reverse-proxy for this purpose. |
| 90 | 90 | ||
| 91 | To use Nginx, we need to create a reverse-proxy configuration file: | 91 | To use Nginx, we need to create a reverse-proxy configuration file: |
| 92 | 92 | ||
| 93 | ```sh | 93 | #+begin_src sh |
| 94 | doas nano /etc/nginx/http.d/example.com.conf | 94 | doas nano /etc/nginx/http.d/example.com.conf |
| 95 | ``` | 95 | #+end_src |
| 96 | 96 | ||
| 97 | If you already have TLS certificates for this domain (`example.com`), you can | 97 | If you already have TLS certificates for this domain (=example.com=), |
| 98 | simply use the SSL configuration and point toward your TLS certificates. | 98 | you can simply use the SSL configuration and point toward your TLS |
| 99 | certificates. | ||
| 99 | 100 | ||
| 100 | ```conf | 101 | #+begin_src conf |
| 101 | server { | 102 | server { |
| 102 | listen 443 ssl http2; | 103 | listen 443 ssl http2; |
| 103 | listen [::]:443 ssl http2; | 104 | listen [::]:443 ssl http2; |
| @@ -136,14 +137,15 @@ server { | |||
| 136 | listen 80; | 137 | listen 80; |
| 137 | return 404; | 138 | return 404; |
| 138 | } | 139 | } |
| 139 | ``` | 140 | #+end_src |
| 140 | 141 | ||
| 141 | If you need to generate TLS certificates (I recommend | 142 | If you need to generate TLS certificates (I recommend |
| 142 | [Certbot](https://certbot.eff.org/)), you'll need a more minimal Nginx conf file | 143 | [[https://certbot.eff.org/][Certbot]]), you'll need a more minimal Nginx |
| 143 | before you can use the TLS-enabled example above. Instead, use this | 144 | conf file before you can use the TLS-enabled example above. Instead, use |
| 144 | configuration file during the Certbot certificate generation process: | 145 | this configuration file during the Certbot certificate generation |
| 146 | process: | ||
| 145 | 147 | ||
| 146 | ```conf | 148 | #+begin_src conf |
| 147 | server { | 149 | server { |
| 148 | server_name example.com; | 150 | server_name example.com; |
| 149 | location / { | 151 | location / { |
| @@ -151,62 +153,63 @@ server { | |||
| 151 | } | 153 | } |
| 152 | listen 80; | 154 | listen 80; |
| 153 | } | 155 | } |
| 154 | ``` | 156 | #+end_src |
| 155 | 157 | ||
| 156 | Once you're done editing the Nginx conf file, restart Nginx: | 158 | Once you're done editing the Nginx conf file, restart Nginx: |
| 157 | 159 | ||
| 158 | ```sh | 160 | #+begin_src sh |
| 159 | doas rc-service nginx restart | 161 | doas rc-service nginx restart |
| 160 | ``` | 162 | #+end_src |
| 161 | 163 | ||
| 162 | If you still need to generate TLS certificates, run `certbot` now and obtain the | 164 | If you still need to generate TLS certificates, run =certbot= now and |
| 163 | certificates. Certbot will ask if you want to use a webroot or spin up a | 165 | obtain the certificates. Certbot will ask if you want to use a webroot |
| 164 | temporary web server. I **highly** recommend using the temporary web server due | 166 | or spin up a temporary web server. I *highly* recommend using the |
| 165 | to the many issues with using a webroot. | 167 | temporary web server due to the many issues with using a webroot. |
| 166 | 168 | ||
| 167 | You will need to stop Nginx in order to user the temporary web server option | 169 | You will need to stop Nginx in order to user the temporary web server |
| 168 | with Certbot: | 170 | option with Certbot: |
| 169 | 171 | ||
| 170 | ```sh | 172 | #+begin_src sh |
| 171 | # Stop Nginx so certbot can spin up a temp webserver for cert generation | 173 | # Stop Nginx so certbot can spin up a temp webserver for cert generation |
| 172 | doas rc-service nginx stop | 174 | doas rc-service nginx stop |
| 173 | doas certbot certonly -v | 175 | doas certbot certonly -v |
| 174 | doas rc-service nginx start | 176 | doas rc-service nginx start |
| 175 | ``` | 177 | #+end_src |
| 176 | 178 | ||
| 177 | ## Open Firewall & Router Ports | 179 | ** Open Firewall & Router Ports |
| 178 | 180 | ||
| 179 | If you use a firewall on the server, open the `8448` port for discovery and | 181 | If you use a firewall on the server, open the =8448= port for discovery |
| 180 | federation, as well as the normal web server ports if you're using a reverse | 182 | and federation, as well as the normal web server ports if you're using a |
| 181 | proxy. If you want additional services, such as voice calls, you will need to | 183 | reverse proxy. If you want additional services, such as voice calls, you |
| 182 | read the Synapse documentation to see which ports need to be opened for those | 184 | will need to read the Synapse documentation to see which ports need to |
| 183 | features. | 185 | be opened for those features. |
| 184 | 186 | ||
| 185 | Here's an example of the Universal Firewall (UFW) software: | 187 | Here's an example of the Universal Firewall (UFW) software: |
| 186 | 188 | ||
| 187 | ```sh | 189 | #+begin_src sh |
| 188 | # Matrix port | 190 | # Matrix port |
| 189 | doas ufw allow 8448 | 191 | doas ufw allow 8448 |
| 190 | # Standard web server ports | 192 | # Standard web server ports |
| 191 | doas ufw allow "Nginx Full" | 193 | doas ufw allow "Nginx Full" |
| 192 | ``` | 194 | #+end_src |
| 193 | 195 | ||
| 194 | Remember to forward any Synapse ports, such as `8448`, `80`, and `443`, in your | 196 | Remember to forward any Synapse ports, such as =8448=, =80=, and =443=, |
| 195 | Router from the internet to your server's IP address. | 197 | in your Router from the internet to your server's IP address. |
| 196 | 198 | ||
| 197 | ## Adding Matrix Users | 199 | ** Adding Matrix Users |
| 198 | 200 | ||
| 199 | Finally, if you didn't enable public registration in the `homeserver.yaml` file, | 201 | Finally, if you didn't enable public registration in the |
| 200 | you can manually create users via the command-line: | 202 | =homeserver.yaml= file, you can manually create users via the |
| 203 | command-line: | ||
| 201 | 204 | ||
| 202 | ```sh | 205 | #+begin_src sh |
| 203 | cd ~/synapse | 206 | cd ~/synapse |
| 204 | register_new_matrix_user -c homeserver.yaml | 207 | register_new_matrix_user -c homeserver.yaml |
| 205 | ``` | 208 | #+end_src |
| 206 | 209 | ||
| 207 | Remember that the format for federated Matrix usernames is | 210 | Remember that the format for federated Matrix usernames is |
| 208 | `@username:example.com` when logging in to client applications. | 211 | =@username:example.com= when logging in to client applications. |
| 209 | 212 | ||
| 210 | Once Synapse is running, and you have a username, you are ready to log in to a | 213 | Once Synapse is running, and you have a username, you are ready to log |
| 211 | Matrix client and start sending messages, joining rooms, and utilizing your very | 214 | in to a Matrix client and start sending messages, joining rooms, and |
| 212 | own Matrix server. | 215 | utilizing your very own Matrix server. |
content/blog/2022-11-11-nginx-tmp-errors.md deleted −77
| @@ -1,77 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-11-11 | ||
| 3 | title = "Fixing Permission Errors in /var/lib/nginx" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | _This is a brief post so that I personally remember the solution as it has | ||
| 9 | occurred multiple times for me._ | ||
| 10 | |||
| 11 | # The Problem | ||
| 12 | |||
| 13 | After migrating to a new server OS, I started receiving quite a few permission | ||
| 14 | errors like the one below. These popped up for various different websites I'm | ||
| 15 | serving via Nginx on this server, but did not prevent the website from loading. | ||
| 16 | |||
| 17 | I found the errors in the standard log file: | ||
| 18 | |||
| 19 | ```sh | ||
| 20 | cat /var/log/nginx/error.log | ||
| 21 | ``` | ||
| 22 | |||
| 23 | ```sh | ||
| 24 | 2022/11/11 11:30:34 [crit] 8970#8970: *10 open() "/var/lib/nginx/tmp/proxy/3/00/0000000003" failed (13: Permission denied) while reading upstream, client: 169.150.203.10, server: cyberchef.example.com, request: "GET /assets/main.css HTTP/2.0", upstream: "http://127.0.0.1:8111/assets/main.css", host: "cyberchef.example.com", referrer: "https://cyberchef.example.com/" | ||
| 25 | ``` | ||
| 26 | |||
| 27 | You can see that the error is `13: Permission denied` and it occurs in the | ||
| 28 | `/var/lib/nginx/tmp/` directory. In my case, I had thousands of errors where | ||
| 29 | Nginx was denied permission to read/write files in this directory. | ||
| 30 | |||
| 31 | So how do I fix it? | ||
| 32 | |||
| 33 | # The Solution | ||
| 34 | |||
| 35 | In order to resolve the issue, I had to ensure the `/var/lib/nginx` directory is | ||
| 36 | owned by Nginx. Mine was owned by the `www` user and Nginx was not able to read | ||
| 37 | or write files within that directory. This prevented Nginx from caching | ||
| 38 | temporary files. | ||
| 39 | |||
| 40 | ```sh | ||
| 41 | # Alpine Linux | ||
| 42 | doas chown -R nginx:nginx /var/lib/nginx | ||
| 43 | |||
| 44 | # Other Distros | ||
| 45 | sudo chown -R nginx:nginx /var/lib/nginx | ||
| 46 | ``` | ||
| 47 | |||
| 48 | You _may_ also be able to change the `proxy_temp_path` in your Nginx config, but | ||
| 49 | I did not try this. Here's a suggestion I found online that may work if the | ||
| 50 | above solution does not: | ||
| 51 | |||
| 52 | ```sh | ||
| 53 | nano /etc/nginx/http.d/example.com.conf | ||
| 54 | ``` | ||
| 55 | |||
| 56 | ```conf | ||
| 57 | server { | ||
| 58 | ... | ||
| 59 | |||
| 60 | # Set the proxy_temp_path to your preference, make sure it's owned by the | ||
| 61 | # `nginx` user | ||
| 62 | proxy_temp_path /tmp; | ||
| 63 | |||
| 64 | ... | ||
| 65 | } | ||
| 66 | ``` | ||
| 67 | |||
| 68 | Finally, restart Nginx and your server should be able to cache temporary files | ||
| 69 | again. | ||
| 70 | |||
| 71 | ```sh | ||
| 72 | # Alpine Linux (OpenRC) | ||
| 73 | doas rc-service nginx restart | ||
| 74 | |||
| 75 | # Other Distros (systemd) | ||
| 76 | sudo systemctl restart nginx | ||
| 77 | ``` | ||
content/blog/2022-11-11-nginx-tmp-errors.org added +77
| @@ -0,0 +1,77 @@ | |||
| 1 | #+date: <2022-11-11> | ||
| 2 | #+title: Fixing Permission Errors in /var/lib/nginx | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | /This is a brief post so that I personally remember the solution as it | ||
| 7 | has occurred multiple times for me./ | ||
| 8 | |||
| 9 | * The Problem | ||
| 10 | |||
| 11 | After migrating to a new server OS, I started receiving quite a few | ||
| 12 | permission errors like the one below. These popped up for various | ||
| 13 | different websites I'm serving via Nginx on this server, but did not | ||
| 14 | prevent the website from loading. | ||
| 15 | |||
| 16 | I found the errors in the standard log file: | ||
| 17 | |||
| 18 | #+begin_src sh | ||
| 19 | cat /var/log/nginx/error.log | ||
| 20 | #+end_src | ||
| 21 | |||
| 22 | #+begin_src sh | ||
| 23 | 2022/11/11 11:30:34 [crit] 8970#8970: *10 open() "/var/lib/nginx/tmp/proxy/3/00/0000000003" failed (13: Permission denied) while reading upstream, client: 169.150.203.10, server: cyberchef.example.com, request: "GET /assets/main.css HTTP/2.0", upstream: "http://127.0.0.1:8111/assets/main.css", host: "cyberchef.example.com", referrer: "https://cyberchef.example.com/" | ||
| 24 | #+end_src | ||
| 25 | |||
| 26 | You can see that the error is =13: Permission denied= and it occurs in | ||
| 27 | the =/var/lib/nginx/tmp/= directory. In my case, I had thousands of | ||
| 28 | errors where Nginx was denied permission to read/write files in this | ||
| 29 | directory. | ||
| 30 | |||
| 31 | So how do I fix it? | ||
| 32 | |||
| 33 | * The Solution | ||
| 34 | |||
| 35 | In order to resolve the issue, I had to ensure the =/var/lib/nginx= | ||
| 36 | directory is owned by Nginx. Mine was owned by the =www= user and Nginx | ||
| 37 | was not able to read or write files within that directory. This | ||
| 38 | prevented Nginx from caching temporary files. | ||
| 39 | |||
| 40 | #+begin_src sh | ||
| 41 | # Alpine Linux | ||
| 42 | doas chown -R nginx:nginx /var/lib/nginx | ||
| 43 | |||
| 44 | # Other Distros | ||
| 45 | sudo chown -R nginx:nginx /var/lib/nginx | ||
| 46 | #+end_src | ||
| 47 | |||
| 48 | You /may/ also be able to change the =proxy_temp_path= in your Nginx | ||
| 49 | config, but I did not try this. Here's a suggestion I found online that | ||
| 50 | may work if the above solution does not: | ||
| 51 | |||
| 52 | #+begin_src sh | ||
| 53 | nano /etc/nginx/http.d/example.com.conf | ||
| 54 | #+end_src | ||
| 55 | |||
| 56 | #+begin_src conf | ||
| 57 | server { | ||
| 58 | ... | ||
| 59 | |||
| 60 | # Set the proxy_temp_path to your preference, make sure it's owned by the | ||
| 61 | # `nginx` user | ||
| 62 | proxy_temp_path /tmp; | ||
| 63 | |||
| 64 | ... | ||
| 65 | } | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | Finally, restart Nginx and your server should be able to cache temporary | ||
| 69 | files again. | ||
| 70 | |||
| 71 | #+begin_src sh | ||
| 72 | # Alpine Linux (OpenRC) | ||
| 73 | doas rc-service nginx restart | ||
| 74 | |||
| 75 | # Other Distros (systemd) | ||
| 76 | sudo systemctl restart nginx | ||
| 77 | #+end_src | ||
content/blog/2022-11-27-server-build.md deleted −143
| @@ -1,143 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-11-27 | ||
| 3 | title = "Building a Custom Rack-Mounted Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Dilemma | ||
| 9 | |||
| 10 | For years, I have been using desktops and a Raspberry Pi as the backbone of my | ||
| 11 | homelab. I have always wanted to move toward a single dedicated server that | ||
| 12 | could handle all of my tasks, but was often put off by the complexity of the | ||
| 13 | choices (and financial roadblocks at some times). | ||
| 14 | |||
| 15 | However, after purchasing a small server rack this past year, I have been | ||
| 16 | researching to see what kind of rack-mounted server I could buy. I initially | ||
| 17 | bought a Dell R720XD loaded up with everything I could ever want in a server - | ||
| 18 | but I did not care for it. It was far too loud, and the age of the CPU/RAM was | ||
| 19 | something I wanted to improve upon. | ||
| 20 | |||
| 21 | After returning the R720XD, I decided that I wanted to build my own server with | ||
| 22 | modern, consumer-grade PC components. This time, I am very happy with the | ||
| 23 | results of my server. | ||
| 24 | |||
| 25 | # Components | ||
| 26 | |||
| 27 | I'll start by listing all the components I used for this server build: | ||
| 28 | |||
| 29 | - **Case**: [Rosewill RSV-R4100U 4U Server Chassis Rackmount | ||
| 30 | Case](https://www.rosewill.com/rosewill-rsv-r4100u-black/p/9SIA072GJ92825) | ||
| 31 | - **Motherboard**: [NZXT B550](https://nzxt.com/product/n7-b550) | ||
| 32 | - **CPU**: AMD Ryzen 7 5700G with Radeon Graphics | ||
| 33 | - **GPU**: N/A - I specifically chose one of the few AMD CPUs that support | ||
| 34 | onboard graphics. | ||
| 35 | - **RAM**: 64GB RAM (2x32GB) _Max of 128GB RAM on this motherboard_ | ||
| 36 | - **Boot Drive**: Western Digital 500GB M.2 NVME SSD | ||
| 37 | - **HDD Bay**: | ||
| 38 | - 10TB WD White _(shucked, moved from previous server)_ | ||
| 39 | - 8TB WD White _(shucked, moved from previous server)_ | ||
| 40 | - 2 x 8TB WD Red Plus _(Black Friday lined up perfectly with this build, | ||
| 41 | so I grabbed two of these)_ | ||
| 42 | - **PSU**: Corsair RM850 PSU | ||
| 43 | - **Extras**: | ||
| 44 | - Corsair TM3Q Thermal Paste | ||
| 45 | - Noctua 120mm fan _(replacement for front case fan)_ | ||
| 46 | - 2 x Noctua 80mm fans _(replacement for rear case fans)_ | ||
| 47 | - CableMatters 6Gbps SATA Cables | ||
| 48 | |||
| 49 | # Building the Server | ||
| 50 | |||
| 51 | This took quite a while for me to build (in my opinion of time), totaling around | ||
| 52 | 3 hours from start to finish. The case has some peculiar construction, so you | ||
| 53 | have to completely remove the ODD & HDD cages to install the motherboard and | ||
| 54 | other components first. | ||
| 55 | |||
| 56 | Now, I've never built a computer of any kind before, so I was quite nervous. | ||
| 57 | Personally, the only challenging part was getting the CPU cooler to screw into | ||
| 58 | the motherboard without sliding the thermal paste around too much underneath. | ||
| 59 | I'm still not entirely sure if I did a great job of it, but nothing's broken | ||
| 60 | yet. | ||
| 61 | |||
| 62 | The main components were all fine and simple. However, installing the hard | ||
| 63 | drives is slightly tedious as I need to power off the server and completely | ||
| 64 | unscrew the HDD cage to install or remove any drives. Additionally, the drives | ||
| 65 | are screwed directly into the metal cage with small screws, which are quite a | ||
| 66 | bit different from the HDD trays I'm used to in other machines. | ||
| 67 | |||
| 68 | Seeing that the cases with hot-swap bays were 3-4x the price, I'm okay dealing | ||
| 69 | with the tedium of removing the cage to install new drives. | ||
| 70 | |||
| 71 | # Software | ||
| 72 | |||
| 73 | I'm not going to dive into the software as I have done so in other recent posts. | ||
| 74 | However, I wanted to note that I am using Alpine Linux on this server and | ||
| 75 | hosting most services inside Docker. No virtual machines (VMs) and very few | ||
| 76 | bare-metal services. | ||
| 77 | |||
| 78 | # The Results | ||
| 79 | |||
| 80 | How did my build turn out? Well, after migrating my other servers and their | ||
| 81 | services over, I found that my server is blazing fast. The heaviest of my | ||
| 82 | applications, Plex, is handled with ease. Even 4k streaming seems to be | ||
| 83 | effortless. | ||
| 84 | |||
| 85 | I am very happy with the results and will likely continue to improve on this | ||
| 86 | server as the years go by rather than buying another used server online. | ||
| 87 | |||
| 88 | ## Mistakes I Made | ||
| 89 | |||
| 90 | This post wouldn't be complete unless I wrote about the mistakes I made while | ||
| 91 | building. The only real mistake I made beyond a "whoops I dropped a screw" | ||
| 92 | related to airflow and fan direction. | ||
| 93 | |||
| 94 | While installing the two new hard drives that showed up on 2022-11-30 and | ||
| 95 | getting ready to install the case in my rack, I noticed that the hard drive | ||
| 96 | temperatures were quite high. | ||
| 97 | |||
| 98 | I used the `smartctl` command for each of my drives (`/dev/sda` through | ||
| 99 | `/dev/sdd`): | ||
| 100 | |||
| 101 | ```sh | ||
| 102 | doas smartctl -a /dev/sda | grep Temperature_Celsius | ||
| 103 | ``` | ||
| 104 | |||
| 105 | The results were unusual - all four drives were idling at ~44-46 degrees | ||
| 106 | Celsius. The only drive that was cooler was my 10TB drive, which was at 38 | ||
| 107 | degrees Celsius. I noted that this 10TB drive was also closest to the case fan. | ||
| 108 | |||
| 109 | ```sh | ||
| 110 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE | ||
| 111 | # /dev/sda | ||
| 112 | 194 Temperature_Celsius 0x0002 147 147 000 Old_age Always - 44 (Min/Max 22/46) | ||
| 113 | # /dev/sdb | ||
| 114 | 194 Temperature_Celsius 0x0002 141 141 000 Old_age Always - 46 (Min/Max 21/48) | ||
| 115 | # /dev/sdc | ||
| 116 | 194 Temperature_Celsius 0x0002 144 144 000 Old_age Always - 45 (Min/Max 19/61) | ||
| 117 | # /dev/sdd | ||
| 118 | 194 Temperature_Celsius 0x0002 171 171 000 Old_age Always - 38 (Min/Max 14/56) | ||
| 119 | ``` | ||
| 120 | |||
| 121 | After looking to see if I could fit more fans into the case, I noticed that the | ||
| 122 | 120mm fan used for intake from the front of the case was actually pushing air | ||
| 123 | out of the case by mistake. This fan sits right in front of the hard drive bay. | ||
| 124 | |||
| 125 | Once I flipped the fan around to act as an intake fan, the temperatures dropped | ||
| 126 | immediately! They are now idling at ~31-33 degrees Celsius. A single fan | ||
| 127 | spinning the wrong way caused my drives to idle 10-15 degrees higher than they | ||
| 128 | should have. | ||
| 129 | |||
| 130 | ```sh | ||
| 131 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE | ||
| 132 | # /dev/sda | ||
| 133 | 194 Temperature_Celsius 0x0002 209 209 000 Old_age Always - 31 (Min/Max 14/56) | ||
| 134 | # /dev/sdb | ||
| 135 | 194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 19/61) | ||
| 136 | # /dev/sdc | ||
| 137 | 194 Temperature_Celsius 0x0002 203 203 000 Old_age Always - 32 (Min/Max 21/48) | ||
| 138 | # /dev/sdd | ||
| 139 | 194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 22/46) | ||
| 140 | ``` | ||
| 141 | |||
| 142 | This was a silly error to make, but I'm glad I found it today before I screwed | ||
| 143 | the case into the rack and made things a lot more tedious to fix. | ||
content/blog/2022-11-27-server-build.org added +147
| @@ -0,0 +1,147 @@ | |||
| 1 | #+date: <2022-11-27> | ||
| 2 | #+title: Building a Custom Rack-Mounted Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Dilemma | ||
| 7 | |||
| 8 | For years, I have been using desktops and a Raspberry Pi as the backbone | ||
| 9 | of my homelab. I have always wanted to move toward a single dedicated | ||
| 10 | server that could handle all of my tasks, but was often put off by the | ||
| 11 | complexity of the choices (and financial roadblocks at some times). | ||
| 12 | |||
| 13 | However, after purchasing a small server rack this past year, I have | ||
| 14 | been researching to see what kind of rack-mounted server I could buy. I | ||
| 15 | initially bought a Dell R720XD loaded up with everything I could ever | ||
| 16 | want in a server - but I did not care for it. It was far too loud, and | ||
| 17 | the age of the CPU/RAM was something I wanted to improve upon. | ||
| 18 | |||
| 19 | After returning the R720XD, I decided that I wanted to build my own | ||
| 20 | server with modern, consumer-grade PC components. This time, I am very | ||
| 21 | happy with the results of my server. | ||
| 22 | |||
| 23 | * Components | ||
| 24 | |||
| 25 | I'll start by listing all the components I used for this server build: | ||
| 26 | |||
| 27 | - *Case*: | ||
| 28 | [[https://www.rosewill.com/rosewill-rsv-r4100u-black/p/9SIA072GJ92825][Rosewill | ||
| 29 | RSV-R4100U 4U Server Chassis Rackmount Case]] | ||
| 30 | - *Motherboard*: [[https://nzxt.com/product/n7-b550][NZXT B550]] | ||
| 31 | - *CPU*: AMD Ryzen 7 5700G with Radeon Graphics | ||
| 32 | - *GPU*: N/A - I specifically chose one of the few AMD CPUs that support | ||
| 33 | onboard graphics. | ||
| 34 | - *RAM*: 64GB RAM (2x32GB) /Max of 128GB RAM on this motherboard/ | ||
| 35 | - *Boot Drive*: Western Digital 500GB M.2 NVME SSD | ||
| 36 | - *HDD Bay*: | ||
| 37 | - 10TB WD White /(shucked, moved from previous server)/ | ||
| 38 | - 8TB WD White /(shucked, moved from previous server)/ | ||
| 39 | - 2 x 8TB WD Red Plus /(Black Friday lined up perfectly with this | ||
| 40 | build, so I grabbed two of these)/ | ||
| 41 | - *PSU*: Corsair RM850 PSU | ||
| 42 | - *Extras*: | ||
| 43 | - Corsair TM3Q Thermal Paste | ||
| 44 | - Noctua 120mm fan /(replacement for front case fan)/ | ||
| 45 | - 2 x Noctua 80mm fans /(replacement for rear case fans)/ | ||
| 46 | - CableMatters 6Gbps SATA Cables | ||
| 47 | |||
| 48 | * Building the Server | ||
| 49 | |||
| 50 | This took quite a while for me to build (in my opinion of time), | ||
| 51 | totaling around 3 hours from start to finish. The case has some peculiar | ||
| 52 | construction, so you have to completely remove the ODD & HDD cages to | ||
| 53 | install the motherboard and other components first. | ||
| 54 | |||
| 55 | Now, I've never built a computer of any kind before, so I was quite | ||
| 56 | nervous. Personally, the only challenging part was getting the CPU | ||
| 57 | cooler to screw into the motherboard without sliding the thermal paste | ||
| 58 | around too much underneath. I'm still not entirely sure if I did a great | ||
| 59 | job of it, but nothing's broken yet. | ||
| 60 | |||
| 61 | The main components were all fine and simple. However, installing the | ||
| 62 | hard drives is slightly tedious as I need to power off the server and | ||
| 63 | completely unscrew the HDD cage to install or remove any drives. | ||
| 64 | Additionally, the drives are screwed directly into the metal cage with | ||
| 65 | small screws, which are quite a bit different from the HDD trays I'm | ||
| 66 | used to in other machines. | ||
| 67 | |||
| 68 | Seeing that the cases with hot-swap bays were 3-4x the price, I'm okay | ||
| 69 | dealing with the tedium of removing the cage to install new drives. | ||
| 70 | |||
| 71 | * Software | ||
| 72 | |||
| 73 | I'm not going to dive into the software as I have done so in other | ||
| 74 | recent posts. However, I wanted to note that I am using Alpine Linux on | ||
| 75 | this server and hosting most services inside Docker. No virtual machines | ||
| 76 | (VMs) and very few bare-metal services. | ||
| 77 | |||
| 78 | * The Results | ||
| 79 | |||
| 80 | How did my build turn out? Well, after migrating my other servers and | ||
| 81 | their services over, I found that my server is blazing fast. The | ||
| 82 | heaviest of my applications, Plex, is handled with ease. Even 4k | ||
| 83 | streaming seems to be effortless. | ||
| 84 | |||
| 85 | I am very happy with the results and will likely continue to improve on | ||
| 86 | this server as the years go by rather than buying another used server | ||
| 87 | online. | ||
| 88 | |||
| 89 | ** Mistakes I Made | ||
| 90 | |||
| 91 | This post wouldn't be complete unless I wrote about the mistakes I made | ||
| 92 | while building. The only real mistake I made beyond a "whoops I dropped | ||
| 93 | a screw" related to airflow and fan direction. | ||
| 94 | |||
| 95 | While installing the two new hard drives that showed up on 2022-11-30 | ||
| 96 | and getting ready to install the case in my rack, I noticed that the | ||
| 97 | hard drive temperatures were quite high. | ||
| 98 | |||
| 99 | I used the =smartctl= command for each of my drives (=/dev/sda= through | ||
| 100 | =/dev/sdd=): | ||
| 101 | |||
| 102 | #+begin_src sh | ||
| 103 | doas smartctl -a /dev/sda | grep Temperature_Celsius | ||
| 104 | #+end_src | ||
| 105 | |||
| 106 | The results were unusual - all four drives were idling at ~44-46 degrees | ||
| 107 | Celsius. The only drive that was cooler was my 10TB drive, which was at | ||
| 108 | 38 degrees Celsius. I noted that this 10TB drive was also closest to the | ||
| 109 | case fan. | ||
| 110 | |||
| 111 | #+begin_src sh | ||
| 112 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE | ||
| 113 | # /dev/sda | ||
| 114 | 194 Temperature_Celsius 0x0002 147 147 000 Old_age Always - 44 (Min/Max 22/46) | ||
| 115 | # /dev/sdb | ||
| 116 | 194 Temperature_Celsius 0x0002 141 141 000 Old_age Always - 46 (Min/Max 21/48) | ||
| 117 | # /dev/sdc | ||
| 118 | 194 Temperature_Celsius 0x0002 144 144 000 Old_age Always - 45 (Min/Max 19/61) | ||
| 119 | # /dev/sdd | ||
| 120 | 194 Temperature_Celsius 0x0002 171 171 000 Old_age Always - 38 (Min/Max 14/56) | ||
| 121 | #+end_src | ||
| 122 | |||
| 123 | After looking to see if I could fit more fans into the case, I noticed | ||
| 124 | that the 120mm fan used for intake from the front of the case was | ||
| 125 | actually pushing air out of the case by mistake. This fan sits right in | ||
| 126 | front of the hard drive bay. | ||
| 127 | |||
| 128 | Once I flipped the fan around to act as an intake fan, the temperatures | ||
| 129 | dropped immediately! They are now idling at ~31-33 degrees Celsius. A | ||
| 130 | single fan spinning the wrong way caused my drives to idle 10-15 degrees | ||
| 131 | higher than they should have. | ||
| 132 | |||
| 133 | #+begin_src sh | ||
| 134 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE | ||
| 135 | # /dev/sda | ||
| 136 | 194 Temperature_Celsius 0x0002 209 209 000 Old_age Always - 31 (Min/Max 14/56) | ||
| 137 | # /dev/sdb | ||
| 138 | 194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 19/61) | ||
| 139 | # /dev/sdc | ||
| 140 | 194 Temperature_Celsius 0x0002 203 203 000 Old_age Always - 32 (Min/Max 21/48) | ||
| 141 | # /dev/sdd | ||
| 142 | 194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 22/46) | ||
| 143 | #+end_src | ||
| 144 | |||
| 145 | This was a silly error to make, but I'm glad I found it today before I | ||
| 146 | screwed the case into the rack and made things a lot more tedious to | ||
| 147 | fix. | ||
content/blog/2022-11-29-nginx-referrer-ban-list.md deleted −136
| @@ -1,136 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-11-29 | ||
| 3 | title = "Creating a Referrer Ban List in Nginx" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Creating the Ban List | ||
| 9 | |||
| 10 | In order to ban list referral domains or websites with Nginx, you need to create | ||
| 11 | a ban list file. The file below will accept regexes for different domains or | ||
| 12 | websites you wish to block. | ||
| 13 | |||
| 14 | First, create the file in your nginx directory: | ||
| 15 | |||
| 16 | ```sh | ||
| 17 | doas nano /etc/nginx/banlist.conf | ||
| 18 | ``` | ||
| 19 | |||
| 20 | Next, paste the following contents in and fill out the regexes with whichever | ||
| 21 | domains you're blocking. | ||
| 22 | |||
| 23 | ```conf | ||
| 24 | # /etc/nginx/banlist.conf | ||
| 25 | |||
| 26 | map $http_referer $bad_referer { | ||
| 27 | hostnames; | ||
| 28 | |||
| 29 | default 0; | ||
| 30 | |||
| 31 | # Put regexes for undesired referrers here | ||
| 32 | "~news.ycombinator.com" 1; | ||
| 33 | } | ||
| 34 | ``` | ||
| 35 | |||
| 36 | # Configuring Nginx | ||
| 37 | |||
| 38 | In order for the ban list to work, Nginx needs to know it exists and how to | ||
| 39 | handle it. For this, edit the `nginx.conf` file. | ||
| 40 | |||
| 41 | ```sh | ||
| 42 | doas nano /etc/nginx/nginx.conf | ||
| 43 | ``` | ||
| 44 | |||
| 45 | Within this file, find the `http` block and add your ban list file location to | ||
| 46 | the end of the block. | ||
| 47 | |||
| 48 | ```conf | ||
| 49 | # /etc/nginx/nginx.conf | ||
| 50 | |||
| 51 | http { | ||
| 52 | ... | ||
| 53 | |||
| 54 | # Include ban list | ||
| 55 | include /etc/nginx/banlist.conf; | ||
| 56 | } | ||
| 57 | ``` | ||
| 58 | |||
| 59 | # Enabling the Ban List | ||
| 60 | |||
| 61 | Finally, we need to take action when a bad referral site is found. To do so, | ||
| 62 | edit the configuration file for your website. For example, I have all website | ||
| 63 | configuration files in the `http.d` directory. You may have them in the | ||
| 64 | `sites-available` directory on some distributions. | ||
| 65 | |||
| 66 | ```sh | ||
| 67 | doas nano /etc/nginx/http.d/example.com.conf | ||
| 68 | ``` | ||
| 69 | |||
| 70 | Within each website's configuration file, edit the `server` blocks that are | ||
| 71 | listening to ports 80 and 443 and create a check for the `$bad_referrer` | ||
| 72 | variable we created in the ban list file. | ||
| 73 | |||
| 74 | If a matching site is found, you can return any [HTTP Status | ||
| 75 | Code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) you want. Code | ||
| 76 | 403 (Forbidden) is logical in this case since you are preventing a client | ||
| 77 | connection due to a banned domain. | ||
| 78 | |||
| 79 | ```conf | ||
| 80 | server { | ||
| 81 | ... | ||
| 82 | |||
| 83 | # If a referral site is banned, return an error | ||
| 84 | if ($bad_referer) { | ||
| 85 | return 403; | ||
| 86 | } | ||
| 87 | |||
| 88 | ... | ||
| 89 | } | ||
| 90 | ``` | ||
| 91 | |||
| 92 | # Restart Nginx | ||
| 93 | |||
| 94 | Lastly, restart Nginx to enable all changes made. | ||
| 95 | |||
| 96 | ```sh | ||
| 97 | doas rc-service nginx restart | ||
| 98 | ``` | ||
| 99 | |||
| 100 | # Testing Results | ||
| 101 | |||
| 102 | In order to test the results, let's curl the contents of our site. To start, | ||
| 103 | I'll curl the site normally: | ||
| 104 | |||
| 105 | ```sh | ||
| 106 | curl https://cleberg.net | ||
| 107 | ``` | ||
| 108 | |||
| 109 | The HTML contents of the page come back successfully: | ||
| 110 | |||
| 111 | ```html | ||
| 112 | <!doctype html>...</html> | ||
| 113 | ``` | ||
| 114 | |||
| 115 | Next, let's include a banned referrer: | ||
| 116 | |||
| 117 | ```sh | ||
| 118 | curl --referer https://news.ycombinator.com https://cleberg.net | ||
| 119 | ``` | ||
| 120 | |||
| 121 | This time, I'm met with a 403 Forbidden response page. That means we are | ||
| 122 | successful and any clients being referred from a banned domain will be met with | ||
| 123 | this same response code. | ||
| 124 | |||
| 125 | ```html | ||
| 126 | <html> | ||
| 127 | <head> | ||
| 128 | <title>403 Forbidden</title> | ||
| 129 | </head> | ||
| 130 | <body> | ||
| 131 | <center><h1>403 Forbidden</h1></center> | ||
| 132 | <hr /> | ||
| 133 | <center>nginx</center> | ||
| 134 | </body> | ||
| 135 | </html> | ||
| 136 | ``` | ||
content/blog/2022-11-29-nginx-referrer-ban-list.org added +134
| @@ -0,0 +1,134 @@ | |||
| 1 | #+date: <2022-11-29> | ||
| 2 | #+title: Creating a Referrer Ban List in Nginx | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Creating the Ban List | ||
| 7 | |||
| 8 | In order to ban list referral domains or websites with Nginx, you need | ||
| 9 | to create a ban list file. The file below will accept regexes for | ||
| 10 | different domains or websites you wish to block. | ||
| 11 | |||
| 12 | First, create the file in your nginx directory: | ||
| 13 | |||
| 14 | #+begin_src sh | ||
| 15 | doas nano /etc/nginx/banlist.conf | ||
| 16 | #+end_src | ||
| 17 | |||
| 18 | Next, paste the following contents in and fill out the regexes with | ||
| 19 | whichever domains you're blocking. | ||
| 20 | |||
| 21 | #+begin_src conf | ||
| 22 | # /etc/nginx/banlist.conf | ||
| 23 | |||
| 24 | map $http_referer $bad_referer { | ||
| 25 | hostnames; | ||
| 26 | |||
| 27 | default 0; | ||
| 28 | |||
| 29 | # Put regexes for undesired referrers here | ||
| 30 | "~news.ycombinator.com" 1; | ||
| 31 | } | ||
| 32 | #+end_src | ||
| 33 | |||
| 34 | * Configuring Nginx | ||
| 35 | |||
| 36 | In order for the ban list to work, Nginx needs to know it exists and how | ||
| 37 | to handle it. For this, edit the =nginx.conf= file. | ||
| 38 | |||
| 39 | #+begin_src sh | ||
| 40 | doas nano /etc/nginx/nginx.conf | ||
| 41 | #+end_src | ||
| 42 | |||
| 43 | Within this file, find the =http= block and add your ban list file | ||
| 44 | location to the end of the block. | ||
| 45 | |||
| 46 | #+begin_src conf | ||
| 47 | # /etc/nginx/nginx.conf | ||
| 48 | |||
| 49 | http { | ||
| 50 | ... | ||
| 51 | |||
| 52 | # Include ban list | ||
| 53 | include /etc/nginx/banlist.conf; | ||
| 54 | } | ||
| 55 | #+end_src | ||
| 56 | |||
| 57 | * Enabling the Ban List | ||
| 58 | |||
| 59 | Finally, we need to take action when a bad referral site is found. To do | ||
| 60 | so, edit the configuration file for your website. For example, I have | ||
| 61 | all website configuration files in the =http.d= directory. You may have | ||
| 62 | them in the =sites-available= directory on some distributions. | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | doas nano /etc/nginx/http.d/example.com.conf | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | Within each website's configuration file, edit the =server= blocks that | ||
| 69 | are listening to ports 80 and 443 and create a check for the | ||
| 70 | =$bad_referrer= variable we created in the ban list file. | ||
| 71 | |||
| 72 | If a matching site is found, you can return any | ||
| 73 | [[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes][HTTP Status | ||
| 74 | Code]] you want. Code 403 (Forbidden) is logical in this case since you | ||
| 75 | are preventing a client connection due to a banned domain. | ||
| 76 | |||
| 77 | #+begin_src conf | ||
| 78 | server { | ||
| 79 | ... | ||
| 80 | |||
| 81 | # If a referral site is banned, return an error | ||
| 82 | if ($bad_referer) { | ||
| 83 | return 403; | ||
| 84 | } | ||
| 85 | |||
| 86 | ... | ||
| 87 | } | ||
| 88 | #+end_src | ||
| 89 | |||
| 90 | * Restart Nginx | ||
| 91 | |||
| 92 | Lastly, restart Nginx to enable all changes made. | ||
| 93 | |||
| 94 | #+begin_src sh | ||
| 95 | doas rc-service nginx restart | ||
| 96 | #+end_src | ||
| 97 | |||
| 98 | * Testing Results | ||
| 99 | |||
| 100 | In order to test the results, let's curl the contents of our site. To | ||
| 101 | start, I'll curl the site normally: | ||
| 102 | |||
| 103 | #+begin_src sh | ||
| 104 | curl https://cleberg.net | ||
| 105 | #+end_src | ||
| 106 | |||
| 107 | The HTML contents of the page come back successfully: | ||
| 108 | |||
| 109 | #+begin_src html | ||
| 110 | <!doctype html>...</html> | ||
| 111 | #+end_src | ||
| 112 | |||
| 113 | Next, let's include a banned referrer: | ||
| 114 | |||
| 115 | #+begin_src sh | ||
| 116 | curl --referer https://news.ycombinator.com https://cleberg.net | ||
| 117 | #+end_src | ||
| 118 | |||
| 119 | This time, I'm met with a 403 Forbidden response page. That means we are | ||
| 120 | successful and any clients being referred from a banned domain will be | ||
| 121 | met with this same response code. | ||
| 122 | |||
| 123 | #+begin_src html | ||
| 124 | <html> | ||
| 125 | <head> | ||
| 126 | <title>403 Forbidden</title> | ||
| 127 | </head> | ||
| 128 | <body> | ||
| 129 | <center><h1>403 Forbidden</h1></center> | ||
| 130 | <hr /> | ||
| 131 | <center>nginx</center> | ||
| 132 | </body> | ||
| 133 | </html> | ||
| 134 | #+end_src | ||
content/blog/2022-12-01-nginx-compression.md deleted −76
| @@ -1,76 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-12-01 | ||
| 3 | title = "Enable GZIP Compression in Nginx" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Text Compression | ||
| 9 | |||
| 10 | Text compression allows a web server to serve text-based resources faster than | ||
| 11 | uncompressed data. This can speed up things like First Contentful Paint, Tie to | ||
| 12 | Interactive, and Speed Index. | ||
| 13 | |||
| 14 | # Enable Nginx Compression with gzip | ||
| 15 | |||
| 16 | In order to enable text compression on Nginx, we need to enable it within the | ||
| 17 | configuration file: | ||
| 18 | |||
| 19 | ```sh | ||
| 20 | nano /etc/nginx/nginx.conf | ||
| 21 | ``` | ||
| 22 | |||
| 23 | Within the `http` block, find the section that shows something like the block | ||
| 24 | below. This is the default gzip configuration I found in my `nginx.conf` file on | ||
| 25 | Alpine Linux 3.17. Yours may look slightly different, just make sure that you're | ||
| 26 | not creating any duplicate gzip options. | ||
| 27 | |||
| 28 | ```conf | ||
| 29 | # Enable gzipping of responses. | ||
| 30 | #gzip on; | ||
| 31 | |||
| 32 | # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. | ||
| 33 | gzip_vary on; | ||
| 34 | ``` | ||
| 35 | |||
| 36 | Remove the default gzip lines and replace them with the following: | ||
| 37 | |||
| 38 | ```conf | ||
| 39 | # Enable gzipping of responses. | ||
| 40 | gzip on; | ||
| 41 | gzip_vary on; | ||
| 42 | gzip_min_length 10240; | ||
| 43 | gzip_proxied expired no-cache no-store private auth; | ||
| 44 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; | ||
| 45 | gzip_disable "MSIE [1-6]"; | ||
| 46 | ``` | ||
| 47 | |||
| 48 | # Explanations of ngx~httpgzipmodule~ Options | ||
| 49 | |||
| 50 | Each of the lines above enables a different aspect of the gzip response for | ||
| 51 | Nginx. Here are the full explanations: | ||
| 52 | |||
| 53 | - `gzip` -- Enables or disables gzipping of responses. | ||
| 54 | - `gzip_vary` -- Enables or disables inserting the "Vary: Accept-Encoding" | ||
| 55 | response header field if the directives gzip, gzip~static~, or gunzip are | ||
| 56 | active. | ||
| 57 | - `gzip_min_length` -- Sets the minimum length of a response that will be | ||
| 58 | gzipped. The length is determined only from the "Content-Length" response | ||
| 59 | header field. | ||
| 60 | - `gzip_proxied` -- Enables or disables gzipping of responses for proxied | ||
| 61 | requests depending on the request and response. The fact that the request is | ||
| 62 | proxied is determined by the presence of the "Via" request header field. | ||
| 63 | - `gzip_types` -- Enables gzipping of responses for the specified MIME types | ||
| 64 | in addition to "text/html". The special value "\*" matches any MIME type | ||
| 65 | (0.8.29). Responses with the "text/html" type are always compressed. | ||
| 66 | - `gzip_disable` -- Disables gzipping of responses for requests with | ||
| 67 | "User-Agent" header fields matching any of the specified regular | ||
| 68 | expressions. | ||
| 69 | - The special mask "msie6" (0.7.12) corresponds to the regular expression | ||
| 70 | "MSIE [4-6].", but works faster. Starting from version 0.8.11, "MSIE | ||
| 71 | 6.0; ... SV1" is excluded from this mask. | ||
| 72 | |||
| 73 | More information on these directives and their options can be found on the | ||
| 74 | [Module | ||
| 75 | ngx~httpgzipmodule~](https://nginx.org/en/docs/http/ngx_http_gzip_module.html) | ||
| 76 | page in Nginx's documentation. | ||
content/blog/2022-12-01-nginx-compression.org added +76
| @@ -0,0 +1,76 @@ | |||
| 1 | #+date: <2022-12-01> | ||
| 2 | #+title: Enable GZIP Compression in Nginx | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Text Compression | ||
| 7 | |||
| 8 | Text compression allows a web server to serve text-based resources | ||
| 9 | faster than uncompressed data. This can speed up things like First | ||
| 10 | Contentful Paint, Tie to Interactive, and Speed Index. | ||
| 11 | |||
| 12 | * Enable Nginx Compression with gzip | ||
| 13 | |||
| 14 | In order to enable text compression on Nginx, we need to enable it | ||
| 15 | within the configuration file: | ||
| 16 | |||
| 17 | #+begin_src sh | ||
| 18 | nano /etc/nginx/nginx.conf | ||
| 19 | #+end_src | ||
| 20 | |||
| 21 | Within the =http= block, find the section that shows something like the | ||
| 22 | block below. This is the default gzip configuration I found in my | ||
| 23 | =nginx.conf= file on Alpine Linux 3.17. Yours may look slightly | ||
| 24 | different, just make sure that you're not creating any duplicate gzip | ||
| 25 | options. | ||
| 26 | |||
| 27 | #+begin_src conf | ||
| 28 | # Enable gzipping of responses. | ||
| 29 | #gzip on; | ||
| 30 | |||
| 31 | # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. | ||
| 32 | gzip_vary on; | ||
| 33 | #+end_src | ||
| 34 | |||
| 35 | Remove the default gzip lines and replace them with the following: | ||
| 36 | |||
| 37 | #+begin_src conf | ||
| 38 | # Enable gzipping of responses. | ||
| 39 | gzip on; | ||
| 40 | gzip_vary on; | ||
| 41 | gzip_min_length 10240; | ||
| 42 | gzip_proxied expired no-cache no-store private auth; | ||
| 43 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; | ||
| 44 | gzip_disable "MSIE [1-6]"; | ||
| 45 | #+end_src | ||
| 46 | |||
| 47 | * Explanations of ngx_{httpgzipmodule} Options | ||
| 48 | |||
| 49 | Each of the lines above enables a different aspect of the gzip response | ||
| 50 | for Nginx. Here are the full explanations: | ||
| 51 | |||
| 52 | - =gzip= -- Enables or disables gzipping of responses. | ||
| 53 | - =gzip_vary= -- Enables or disables inserting the "Vary: | ||
| 54 | Accept-Encoding" response header field if the directives gzip, | ||
| 55 | gzip_{static}, or gunzip are active. | ||
| 56 | - =gzip_min_length= -- Sets the minimum length of a response that will | ||
| 57 | be gzipped. The length is determined only from the "Content-Length" | ||
| 58 | response header field. | ||
| 59 | - =gzip_proxied= -- Enables or disables gzipping of responses for | ||
| 60 | proxied requests depending on the request and response. The fact that | ||
| 61 | the request is proxied is determined by the presence of the "Via" | ||
| 62 | request header field. | ||
| 63 | - =gzip_types= -- Enables gzipping of responses for the specified MIME | ||
| 64 | types in addition to "text/html". The special value "*" matches any | ||
| 65 | MIME type (0.8.29). Responses with the "text/html" type are always | ||
| 66 | compressed. | ||
| 67 | - =gzip_disable= -- Disables gzipping of responses for requests with | ||
| 68 | "User-Agent" header fields matching any of the specified regular | ||
| 69 | expressions. | ||
| 70 | - The special mask "msie6" (0.7.12) corresponds to the regular | ||
| 71 | expression "MSIE [4-6].", but works faster. Starting from version | ||
| 72 | 0.8.11, "MSIE 6.0; ... SV1" is excluded from this mask. | ||
| 73 | |||
| 74 | More information on these directives and their options can be found on | ||
| 75 | the [[https://nginx.org/en/docs/http/ngx_http_gzip_module.html][Module | ||
| 76 | ngx_{httpgzipmodule}]] page in Nginx's documentation. | ||
content/blog/2022-12-07-nginx-wildcard-redirect.md → content/blog/2022-12-07-nginx-wildcard-redirect.org renamed +49 −49
| @@ -1,26 +1,24 @@ | |||
| 1 | +++ | 1 | #+date: <2022-12-07> |
| 2 | date = 2022-12-07 | 2 | #+title: Redirect Nginx Subdomains and Trailing Content with Regex |
| 3 | title = "Redirect Nginx Subdomains and Trailing Content with Regex" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Problem | ||
| 9 | 5 | ||
| 10 | I recently migrated domains and replaced the old webpage with a simple info page | 6 | * Problem |
| 11 | with instructions to users on how to edit their bookmarks and URLs to get to the | ||
| 12 | page they were seeking. | ||
| 13 | 7 | ||
| 14 | This was not ideal as it left the work up to the user and may have caused | 8 | I recently migrated domains and replaced the old webpage with a simple |
| 15 | friction for users who accessed my RSS feed. | 9 | info page with instructions to users on how to edit their bookmarks and |
| 10 | URLs to get to the page they were seeking. | ||
| 16 | 11 | ||
| 17 | # Solution | 12 | This was not ideal as it left the work up to the user and may have |
| 13 | caused friction for users who accessed my RSS feed. | ||
| 18 | 14 | ||
| 19 | Instead, I finally found a solution that allows me to redirect both subdomains | 15 | * Solution |
| 20 | AND trailing content. For example, both of these URLs now redirect properly | ||
| 21 | using the logic I'll explain below: | ||
| 22 | 16 | ||
| 23 | ```txt | 17 | Instead, I finally found a solution that allows me to redirect both |
| 18 | subdomains AND trailing content. For example, both of these URLs now | ||
| 19 | redirect properly using the logic I'll explain below: | ||
| 20 | |||
| 21 | #+begin_src txt | ||
| 24 | # Example 1 - Simple base domain redirect with trailing content | 22 | # Example 1 - Simple base domain redirect with trailing content |
| 25 | https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/ | 23 | https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/ |
| 26 | 24 | ||
| @@ -28,23 +26,23 @@ https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/ | |||
| 28 | https://libreddit.domain1.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ | 26 | https://libreddit.domain1.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ |
| 29 | -> | 27 | -> |
| 30 | https://libreddit.domain2.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ | 28 | https://libreddit.domain2.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ |
| 31 | ``` | 29 | #+end_src |
| 32 | 30 | ||
| 33 | Go ahead, try the URLs if you want to test them. | 31 | Go ahead, try the URLs if you want to test them. |
| 34 | 32 | ||
| 35 | ## Nginx Config | 33 | ** Nginx Config |
| 36 | 34 | ||
| 37 | To make this possible. I needed to configure a proper redirect scheme in my | 35 | To make this possible. I needed to configure a proper redirect scheme in |
| 38 | Nginx configuration. | 36 | my Nginx configuration. |
| 39 | 37 | ||
| 40 | ```sh | 38 | #+begin_src sh |
| 41 | doas nano /etc/nginx/http.d/domain1.conf | 39 | doas nano /etc/nginx/http.d/domain1.conf |
| 42 | ``` | 40 | #+end_src |
| 43 | 41 | ||
| 44 | Within this file, I had one block configured to redirect HTTP requests to HTTPS | 42 | Within this file, I had one block configured to redirect HTTP requests |
| 45 | for the base domain and all subdomains. | 43 | to HTTPS for the base domain and all subdomains. |
| 46 | 44 | ||
| 47 | ```conf | 45 | #+begin_src conf |
| 48 | server { | 46 | server { |
| 49 | listen [::]:80; | 47 | listen [::]:80; |
| 50 | listen 80; | 48 | listen 80; |
| @@ -60,13 +58,14 @@ server { | |||
| 60 | 58 | ||
| 61 | return 404; | 59 | return 404; |
| 62 | } | 60 | } |
| 63 | ``` | 61 | #+end_src |
| 64 | 62 | ||
| 65 | For the base domain, I have another `server` block dedicated to redirecting all | 63 | For the base domain, I have another =server= block dedicated to |
| 66 | base domain requests. You can see that the `rewrite` line is instructing Nginx | 64 | redirecting all base domain requests. You can see that the =rewrite= |
| 67 | to gather all trailing content and append it to the new `domain2.com` URL. | 65 | line is instructing Nginx to gather all trailing content and append it |
| 66 | to the new =domain2.com= URL. | ||
| 68 | 67 | ||
| 69 | ```conf | 68 | #+begin_src conf |
| 70 | server { | 69 | server { |
| 71 | listen [::]:443 ssl http2; | 70 | listen [::]:443 ssl http2; |
| 72 | listen 443 ssl http2; | 71 | listen 443 ssl http2; |
| @@ -78,20 +77,21 @@ server { | |||
| 78 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; | 77 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; |
| 79 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; | 78 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; |
| 80 | } | 79 | } |
| 81 | ``` | 80 | #+end_src |
| 82 | 81 | ||
| 83 | Finally, the tricky part is figuring out how to tell Nginx to redirect while | 82 | Finally, the tricky part is figuring out how to tell Nginx to redirect |
| 84 | keeping both a subdomain and trailing content intact. I found that the easiest | 83 | while keeping both a subdomain and trailing content intact. I found that |
| 85 | way to do this is to give it a `server` block of its own. | 84 | the easiest way to do this is to give it a =server= block of its own. |
| 86 | 85 | ||
| 87 | Within this block, we need to do some regex on the `server_name` line before we | 86 | Within this block, we need to do some regex on the =server_name= line |
| 88 | can rewrite anything. This creates a variable called `subdomain`. | 87 | before we can rewrite anything. This creates a variable called |
| 88 | =subdomain=. | ||
| 89 | 89 | ||
| 90 | Once the server gets to the `rewrite` line, it pulls the `subdomain` variable | 90 | Once the server gets to the =rewrite= line, it pulls the =subdomain= |
| 91 | from above and uses it on the new `domain2.com` domain before appending the | 91 | variable from above and uses it on the new =domain2.com= domain before |
| 92 | trailing content (`$request_uri`). | 92 | appending the trailing content (=$request_uri=). |
| 93 | 93 | ||
| 94 | ```conf | 94 | #+begin_src conf |
| 95 | server { | 95 | server { |
| 96 | listen [::]:443 ssl http2; | 96 | listen [::]:443 ssl http2; |
| 97 | listen 443 ssl http2; | 97 | listen 443 ssl http2; |
| @@ -103,17 +103,17 @@ server { | |||
| 103 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; | 103 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; |
| 104 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; | 104 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; |
| 105 | } | 105 | } |
| 106 | ``` | 106 | #+end_src |
| 107 | 107 | ||
| 108 | That's all there is to it. With this, I simply restarted Nginx and watched the | 108 | That's all there is to it. With this, I simply restarted Nginx and |
| 109 | redirections work in-action. | 109 | watched the redirections work in-action. |
| 110 | 110 | ||
| 111 | ```sh | 111 | #+begin_src sh |
| 112 | doas rc-service nginx restart | 112 | doas rc-service nginx restart |
| 113 | ``` | 113 | #+end_src |
| 114 | 114 | ||
| 115 | Looking back on it, I wish I had done this sooner. Who knows how many people | 115 | Looking back on it, I wish I had done this sooner. Who knows how many |
| 116 | went looking for my sites or bookmarks and gave up when they saw the redirect | 116 | people went looking for my sites or bookmarks and gave up when they saw |
| 117 | instructions page. | 117 | the redirect instructions page. |
| 118 | 118 | ||
| 119 | Oh well, it's done now. Live and learn. | 119 | Oh well, it's done now. Live and learn. |
content/blog/2022-12-17-st.md deleted −91
| @@ -1,91 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2022-12-17 | ||
| 3 | title = "Simple Terminal" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # st | ||
| 9 | |||
| 10 | [st](https://st.suckless.org) standards for Simple Terminal, a simple terminal | ||
| 11 | implementation for X made by the [suckless](https://suckless.org) team. | ||
| 12 | |||
| 13 | This post walks through the dependencies needed and process to build and install | ||
| 14 | `st` on Fedora Workstation. | ||
| 15 | |||
| 16 | ## Obtain Files | ||
| 17 | |||
| 18 | To start, obtain the source files for `st` via `git clone`. | ||
| 19 | |||
| 20 | ```sh | ||
| 21 | mkdir ~/suckless && cd ~/suckless | ||
| 22 | git clone https://git.suckless.org/st && cd st | ||
| 23 | ``` | ||
| 24 | |||
| 25 | ## Dependencies | ||
| 26 | |||
| 27 | Once you have the files and are in the `st` directory, ensure the following | ||
| 28 | packages are installed. | ||
| 29 | |||
| 30 | ```sh | ||
| 31 | sudo dnf update && sudo dnf upgrade | ||
| 32 | sudo dnf install gcc patch libX11-devel libXft-devel | ||
| 33 | ``` | ||
| 34 | |||
| 35 | ## Building | ||
| 36 | |||
| 37 | Before building, ensure that you read the README file. | ||
| 38 | |||
| 39 | ```sh | ||
| 40 | cat README | ||
| 41 | ``` | ||
| 42 | |||
| 43 | Once you've read the instructions, open the `config.mk` file and ensure it | ||
| 44 | matches your setup. If you're not sure, leave the default options within the | ||
| 45 | file. | ||
| 46 | |||
| 47 | Finally, you can build `st` with the following command. Ensure you run as root | ||
| 48 | (e.g., `sudo`) or else you may not end up with a usable application file. | ||
| 49 | |||
| 50 | ```sh | ||
| 51 | sudo make clean install | ||
| 52 | ``` | ||
| 53 | |||
| 54 | ## Customization (Patches) | ||
| 55 | |||
| 56 | Note that customizing `st` requires you to modify the source files or to | ||
| 57 | download one of the [available patches](https://st.suckless.org/patches/) for | ||
| 58 | suckless.org. | ||
| 59 | |||
| 60 | If you've already installed `st` and want to customize or install a patch, start | ||
| 61 | by uninstalling the current program. | ||
| 62 | |||
| 63 | ```sh | ||
| 64 | cd ~/suckless/st | ||
| 65 | sudo make uninstall | ||
| 66 | ``` | ||
| 67 | |||
| 68 | Next, grab the `<path>.diff` file from the page of the patch you chose. For | ||
| 69 | example, I will be using the | ||
| 70 | [defaultfontsize](https://st.suckless.org/patches/defaultfontsize/) patch in the | ||
| 71 | below example. | ||
| 72 | |||
| 73 | ```sh | ||
| 74 | wget https://st.suckless.org/patches/defaultfontsize/st-defaultfontsize-20210225-4ef0cbd.diff | ||
| 75 | ``` | ||
| 76 | |||
| 77 | Once the file is downloaded inside the `st` folder, apply the patch and | ||
| 78 | re-install the program. You may need to install the `patch` command if you don't | ||
| 79 | have it installed already (you should have installed it above). | ||
| 80 | |||
| 81 | ```sh | ||
| 82 | patch -i st-defaultfontsize-20210225-4ef0cbd.diff | ||
| 83 | sudo make clean install | ||
| 84 | ``` | ||
| 85 | |||
| 86 | Once installed, you can use the default font size patch to launch `st` with any | ||
| 87 | font size you wish: | ||
| 88 | |||
| 89 | ```sh | ||
| 90 | st -z 16 | ||
| 91 | ``` | ||
content/blog/2022-12-17-st.org added +92
| @@ -0,0 +1,92 @@ | |||
| 1 | #+date: <2022-12-17> | ||
| 2 | #+title: Simple Terminal | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * st | ||
| 7 | |||
| 8 | [[https://st.suckless.org][st]] standards for Simple Terminal, a simple | ||
| 9 | terminal implementation for X made by the | ||
| 10 | [[https://suckless.org][suckless]] team. | ||
| 11 | |||
| 12 | This post walks through the dependencies needed and process to build and | ||
| 13 | install =st= on Fedora Workstation. | ||
| 14 | |||
| 15 | ** Obtain Files | ||
| 16 | |||
| 17 | To start, obtain the source files for =st= via =git clone=. | ||
| 18 | |||
| 19 | #+begin_src sh | ||
| 20 | mkdir ~/suckless && cd ~/suckless | ||
| 21 | git clone https://git.suckless.org/st && cd st | ||
| 22 | #+end_src | ||
| 23 | |||
| 24 | ** Dependencies | ||
| 25 | |||
| 26 | Once you have the files and are in the =st= directory, ensure the | ||
| 27 | following packages are installed. | ||
| 28 | |||
| 29 | #+begin_src sh | ||
| 30 | sudo dnf update && sudo dnf upgrade | ||
| 31 | sudo dnf install gcc patch libX11-devel libXft-devel | ||
| 32 | #+end_src | ||
| 33 | |||
| 34 | ** Building | ||
| 35 | |||
| 36 | Before building, ensure that you read the README file. | ||
| 37 | |||
| 38 | #+begin_src sh | ||
| 39 | cat README | ||
| 40 | #+end_src | ||
| 41 | |||
| 42 | Once you've read the instructions, open the =config.mk= file and ensure | ||
| 43 | it matches your setup. If you're not sure, leave the default options | ||
| 44 | within the file. | ||
| 45 | |||
| 46 | Finally, you can build =st= with the following command. Ensure you run | ||
| 47 | as root (e.g., =sudo=) or else you may not end up with a usable | ||
| 48 | application file. | ||
| 49 | |||
| 50 | #+begin_src sh | ||
| 51 | sudo make clean install | ||
| 52 | #+end_src | ||
| 53 | |||
| 54 | ** Customization (Patches) | ||
| 55 | |||
| 56 | Note that customizing =st= requires you to modify the source files or to | ||
| 57 | download one of the [[https://st.suckless.org/patches/][available | ||
| 58 | patches]] for suckless.org. | ||
| 59 | |||
| 60 | If you've already installed =st= and want to customize or install a | ||
| 61 | patch, start by uninstalling the current program. | ||
| 62 | |||
| 63 | #+begin_src sh | ||
| 64 | cd ~/suckless/st | ||
| 65 | sudo make uninstall | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | Next, grab the =<path>.diff= file from the page of the patch you chose. | ||
| 69 | For example, I will be using the | ||
| 70 | [[https://st.suckless.org/patches/defaultfontsize/][defaultfontsize]] | ||
| 71 | patch in the below example. | ||
| 72 | |||
| 73 | #+begin_src sh | ||
| 74 | wget https://st.suckless.org/patches/defaultfontsize/st-defaultfontsize-20210225-4ef0cbd.diff | ||
| 75 | #+end_src | ||
| 76 | |||
| 77 | Once the file is downloaded inside the =st= folder, apply the patch and | ||
| 78 | re-install the program. You may need to install the =patch= command if | ||
| 79 | you don't have it installed already (you should have installed it | ||
| 80 | above). | ||
| 81 | |||
| 82 | #+begin_src sh | ||
| 83 | patch -i st-defaultfontsize-20210225-4ef0cbd.diff | ||
| 84 | sudo make clean install | ||
| 85 | #+end_src | ||
| 86 | |||
| 87 | Once installed, you can use the default font size patch to launch =st= | ||
| 88 | with any font size you wish: | ||
| 89 | |||
| 90 | #+begin_src sh | ||
| 91 | st -z 16 | ||
| 92 | #+end_src | ||
content/blog/2022-12-23-alpine-desktop.md → content/blog/2022-12-23-alpine-desktop.org renamed +104 −102
| @@ -1,90 +1,89 @@ | |||
| 1 | +++ | 1 | #+date: <2022-12-23> |
| 2 | date = 2022-12-23 | 2 | #+title: Alpine Linux as a Desktop OS |
| 3 | title = "Alpine Linux as a Desktop OS" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Isn't Alpine Linux for Servers? | ||
| 9 | 5 | ||
| 10 | This is a question I see a lot when people are presented with an example of | 6 | * Isn't Alpine Linux for Servers? |
| 11 | Alpine Linux running as a desktop OS. | ||
| 12 | 7 | ||
| 13 | While Alpine is small, fast, and minimal, that doesn't stop it from functioning | 8 | This is a question I see a lot when people are presented with an example |
| 14 | at a productive level for desktop users. | 9 | of Alpine Linux running as a desktop OS. |
| 15 | 10 | ||
| 16 | This post is documentation of how I installed and modified Alpine Linux to | 11 | While Alpine is small, fast, and minimal, that doesn't stop it from |
| 17 | become my daily desktop OS. | 12 | functioning at a productive level for desktop users. |
| 18 | 13 | ||
| 19 | # Installation | 14 | This post is documentation of how I installed and modified Alpine Linux |
| 15 | to become my daily desktop OS. | ||
| 20 | 16 | ||
| 21 | Note that I cover the installation of Alpine Linux in my other post, so I won't | 17 | * Installation |
| 22 | repeat it here: [Alpine Linux: My New Server OS](../alpine-linux/). | ||
| 23 | 18 | ||
| 24 | Basically, get a bootable USB or whatever you prefer with Alpine on it, boot the | 19 | Note that I cover the installation of Alpine Linux in my other post, so |
| 25 | ISO, and run the setup script. | 20 | I won't repeat it here: [[../alpine-linux/][Alpine Linux: My New Server |
| 21 | OS]]. | ||
| 26 | 22 | ||
| 27 | ```sh | 23 | Basically, get a bootable USB or whatever you prefer with Alpine on it, |
| 24 | boot the ISO, and run the setup script. | ||
| 25 | |||
| 26 | #+begin_src sh | ||
| 28 | setup-alpine | 27 | setup-alpine |
| 29 | ``` | 28 | #+end_src |
| 30 | 29 | ||
| 31 | Once you have gone through all the options and installer finishes without | 30 | Once you have gone through all the options and installer finishes |
| 32 | errors, reboot. | 31 | without errors, reboot. |
| 33 | 32 | ||
| 34 | ```sh | 33 | #+begin_src sh |
| 35 | reboot | 34 | reboot |
| 36 | ``` | 35 | #+end_src |
| 37 | 36 | ||
| 38 | # Initial Setup | 37 | * Initial Setup |
| 39 | 38 | ||
| 40 | Once Alpine is installed and the machine has rebooted, login is as root | 39 | Once Alpine is installed and the machine has rebooted, login is as root |
| 41 | initially or `su` to root once you log in as your user. From here, you should | 40 | initially or =su= to root once you log in as your user. From here, you |
| 42 | start by updating and upgrading the system in case the ISO was not fully | 41 | should start by updating and upgrading the system in case the ISO was |
| 43 | up-to-date. | 42 | not fully up-to-date. |
| 44 | 43 | ||
| 45 | ```sh | 44 | #+begin_src sh |
| 46 | # Update and upgrade system | 45 | # Update and upgrade system |
| 47 | apk -U update && apk -U upgrade | 46 | apk -U update && apk -U upgrade |
| 48 | 47 | ||
| 49 | # Add an editor so we can enable the community repository | 48 | # Add an editor so we can enable the community repository |
| 50 | apk add nano | 49 | apk add nano |
| 51 | ``` | 50 | #+end_src |
| 52 | 51 | ||
| 53 | You need to uncomment the `community` repository for your version of Alpine | 52 | You need to uncomment the =community= repository for your version of |
| 54 | Linux. | 53 | Alpine Linux. |
| 55 | 54 | ||
| 56 | For v3.17, the `repositories` file should look like this: | 55 | For v3.17, the =repositories= file should look like this: |
| 57 | 56 | ||
| 58 | ```sh | 57 | #+begin_src sh |
| 59 | nano /etc/apk/repositories | 58 | nano /etc/apk/repositories |
| 60 | ``` | 59 | #+end_src |
| 61 | 60 | ||
| 62 | ```conf | 61 | #+begin_src conf |
| 63 | #/media/sda/apks | 62 | #/media/sda/apks |
| 64 | http://mirrors.gigenet.com/alpinelinux/v3.17/main | 63 | http://mirrors.gigenet.com/alpinelinux/v3.17/main |
| 65 | http://mirrors.gigenet.com/alpinelinux/v3.17/community | 64 | http://mirrors.gigenet.com/alpinelinux/v3.17/community |
| 66 | #http://mirrors.gigenet.com/alpinelinux/edge/main | 65 | #http://mirrors.gigenet.com/alpinelinux/edge/main |
| 67 | #http://mirrors.gigenet.com/alpinelinux/edge/community | 66 | #http://mirrors.gigenet.com/alpinelinux/edge/community |
| 68 | #http://mirrors.gigenet.com/alpinelinux/edge/testing | 67 | #http://mirrors.gigenet.com/alpinelinux/edge/testing |
| 69 | ``` | 68 | #+end_src |
| 70 | 69 | ||
| 71 | ```sh | 70 | #+begin_src sh |
| 72 | # Add the rest of your packages | 71 | # Add the rest of your packages |
| 73 | apk add linux-firmware iwd doas git curl wget | 72 | apk add linux-firmware iwd doas git curl wget |
| 74 | 73 | ||
| 75 | # Add yourself to the wheel group so you can use the doas command | 74 | # Add yourself to the wheel group so you can use the doas command |
| 76 | adduser $USER wheel | 75 | adduser $USER wheel |
| 77 | ``` | 76 | #+end_src |
| 78 | 77 | ||
| 79 | # Window Manager (Desktop) | 78 | * Window Manager (Desktop) |
| 80 | 79 | ||
| 81 | The [Sway installation guide](https://wiki.alpinelinux.org/wiki/Sway) has | 80 | The [[https://wiki.alpinelinux.org/wiki/Sway][Sway installation guide]] |
| 82 | everything you need to get Sway working on Alpine. | 81 | has everything you need to get Sway working on Alpine. |
| 83 | 82 | ||
| 84 | However, I'll include a brief list of the commands I ran and their purpose for | 83 | However, I'll include a brief list of the commands I ran and their |
| 85 | posterity here. | 84 | purpose for posterity here. |
| 86 | 85 | ||
| 87 | ```sh | 86 | #+begin_src sh |
| 88 | # Add eudev and set it up | 87 | # Add eudev and set it up |
| 89 | apk add eudev | 88 | apk add eudev |
| 90 | setup-devd udev | 89 | setup-devd udev |
| @@ -121,21 +120,21 @@ apk add \ # Install optional dependencies: | |||
| 121 | swaylock swaylockd \ # lockscreen tool | 120 | swaylock swaylockd \ # lockscreen tool |
| 122 | swaybg \ # wallpaper daemon | 121 | swaybg \ # wallpaper daemon |
| 123 | swayidle # idle management (DPMS) daemon | 122 | swayidle # idle management (DPMS) daemon |
| 124 | ``` | 123 | #+end_src |
| 125 | 124 | ||
| 126 | Once you have the packages installed and set-up, you need to export the | 125 | Once you have the packages installed and set-up, you need to export the |
| 127 | `XDG_RUNTIME_DIR` upon login. To do this, edit your `.profile` file. | 126 | =XDG_RUNTIME_DIR= upon login. To do this, edit your =.profile= file. |
| 128 | 127 | ||
| 129 | If you use another shell, such as `zsh`, you need to edit that shell's profile | 128 | If you use another shell, such as =zsh=, you need to edit that shell's |
| 130 | (e.g., `~/.zprofile`)! | 129 | profile (e.g., =~/.zprofile=)! |
| 131 | 130 | ||
| 132 | ```sh | 131 | #+begin_src sh |
| 133 | nano ~/.profile | 132 | nano ~/.profile |
| 134 | ``` | 133 | #+end_src |
| 135 | 134 | ||
| 136 | Within the file, paste this: | 135 | Within the file, paste this: |
| 137 | 136 | ||
| 138 | ```sh | 137 | #+begin_src sh |
| 139 | if test -z "${XDG_RUNTIME_DIR}"; then | 138 | if test -z "${XDG_RUNTIME_DIR}"; then |
| 140 | export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir | 139 | export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir |
| 141 | if ! test -d "${XDG_RUNTIME_DIR}"; then | 140 | if ! test -d "${XDG_RUNTIME_DIR}"; then |
| @@ -143,19 +142,20 @@ if test -z "${XDG_RUNTIME_DIR}"; then | |||
| 143 | chmod 0700 "${XDG_RUNTIME_DIR}" | 142 | chmod 0700 "${XDG_RUNTIME_DIR}" |
| 144 | fi | 143 | fi |
| 145 | fi | 144 | fi |
| 146 | ``` | 145 | #+end_src |
| 147 | 146 | ||
| 148 | Once that's complete, you can launch Sway manually. | 147 | Once that's complete, you can launch Sway manually. |
| 149 | 148 | ||
| 150 | ```sh | 149 | #+begin_src sh |
| 151 | dbus-run-session -- sway | 150 | dbus-run-session -- sway |
| 152 | ``` | 151 | #+end_src |
| 153 | 152 | ||
| 154 | ## Personal Touches | 153 | ** Personal Touches |
| 155 | 154 | ||
| 156 | I also added the following packages, per my personal preferences and situation. | 155 | I also added the following packages, per my personal preferences and |
| 156 | situation. | ||
| 157 | 157 | ||
| 158 | ```sh | 158 | #+begin_src sh |
| 159 | doas apk add brightnessctl \ # Brightness controller | 159 | doas apk add brightnessctl \ # Brightness controller |
| 160 | zsh \ # Shell | 160 | zsh \ # Shell |
| 161 | firefox \ # Browser | 161 | firefox \ # Browser |
| @@ -165,90 +165,92 @@ doas apk add brightnessctl \ # Brightness controller | |||
| 165 | neomutt \ # CLI email client | 165 | neomutt \ # CLI email client |
| 166 | thunderbird \ # GUI email client | 166 | thunderbird \ # GUI email client |
| 167 | gnupg # GPG key manager | 167 | gnupg # GPG key manager |
| 168 | ``` | 168 | #+end_src |
| 169 | 169 | ||
| 170 | From here, I use my Syncthing storage to pull all the configuration files I | 170 | From here, I use my Syncthing storage to pull all the configuration |
| 171 | stored from prior desktops, such as [my | 171 | files I stored from prior desktops, such as |
| 172 | dotfiles](https://git.sr.ht/~cyborg/dotfiles). | 172 | [[https://git.sr.ht/~cyborg/dotfiles][my dotfiles]]. |
| 173 | 173 | ||
| 174 | # Resolving Issues | 174 | * Resolving Issues |
| 175 | 175 | ||
| 176 | ## WiFi Issues | 176 | ** WiFi Issues |
| 177 | 177 | ||
| 178 | I initially tried to set up my Wi-Fi the standard way with `iwd`, but it didn't | 178 | I initially tried to set up my Wi-Fi the standard way with =iwd=, but it |
| 179 | work. | 179 | didn't work. |
| 180 | 180 | ||
| 181 | Here is what I initially tried (I did all of this as `root`): | 181 | Here is what I initially tried (I did all of this as =root=): |
| 182 | 182 | ||
| 183 | ```sh | 183 | #+begin_src sh |
| 184 | apk add iwd | 184 | apk add iwd |
| 185 | rc-service iwd start | 185 | rc-service iwd start |
| 186 | iwctl station wlan0 connect <SSID> # This will prompt for the password | 186 | iwctl station wlan0 connect <SSID> # This will prompt for the password |
| 187 | rc-update add iwd boot && rc-update add dbus boot | 187 | rc-update add iwd boot && rc-update add dbus boot |
| 188 | ``` | 188 | #+end_src |
| 189 | 189 | ||
| 190 | Then, I added the Wi-Fi entry to the bottom of the networking interface file: | 190 | Then, I added the Wi-Fi entry to the bottom of the networking interface |
| 191 | file: | ||
| 191 | 192 | ||
| 192 | ```sh | 193 | #+begin_src sh |
| 193 | nano /etc/network/interfaces | 194 | nano /etc/network/interfaces |
| 194 | ``` | 195 | #+end_src |
| 195 | 196 | ||
| 196 | ```conf | 197 | #+begin_src conf |
| 197 | auto wlan0 | 198 | auto wlan0 |
| 198 | iface wlan0 inet dhcp | 199 | iface wlan0 inet dhcp |
| 199 | ``` | 200 | #+end_src |
| 200 | 201 | ||
| 201 | Finally, restart the networking service: | 202 | Finally, restart the networking service: |
| 202 | 203 | ||
| 203 | ```sh | 204 | #+begin_src sh |
| 204 | rc-service networking restart | 205 | rc-service networking restart |
| 205 | ``` | 206 | #+end_src |
| 206 | 207 | ||
| 207 | My Wi-Fi interface would receive an IP address from the router, but it could not | 208 | My Wi-Fi interface would receive an IP address from the router, but it |
| 208 | ping anything in the network. To solve the Wi-Fi issues, I originally upgraded | 209 | could not ping anything in the network. To solve the Wi-Fi issues, I |
| 209 | to Alpine's `edge` repositories, which was unnecessary. | 210 | originally upgraded to Alpine's =edge= repositories, which was |
| 211 | unnecessary. | ||
| 210 | 212 | ||
| 211 | Really, the solution was to enable the `NameResolvingService=resolvconf` in | 213 | Really, the solution was to enable the =NameResolvingService=resolvconf= |
| 212 | `/etc/iwd/main.conf`. | 214 | in =/etc/iwd/main.conf=. |
| 213 | 215 | ||
| 214 | ```sh | 216 | #+begin_src sh |
| 215 | doas nano /etc/iwd/main.conf | 217 | doas nano /etc/iwd/main.conf |
| 216 | ``` | 218 | #+end_src |
| 217 | 219 | ||
| 218 | ```conf | 220 | #+begin_src conf |
| 219 | [Network] | 221 | [Network] |
| 220 | 222 | ||
| 221 | NameResolvingService=resolvconf | 223 | NameResolvingService=resolvconf |
| 222 | ``` | 224 | #+end_src |
| 223 | 225 | ||
| 224 | Once I finished this process, my Wi-Fi is working flawlessly. | 226 | Once I finished this process, my Wi-Fi is working flawlessly. |
| 225 | 227 | ||
| 226 | ## Sound Issues | 228 | ** Sound Issues |
| 227 | 229 | ||
| 228 | Same as with the Wi-Fi, I had no sound and could not control the mute/unmute or | 230 | Same as with the Wi-Fi, I had no sound and could not control the |
| 229 | volume buttons on my laptop. | 231 | mute/unmute or volume buttons on my laptop. |
| 230 | 232 | ||
| 231 | To resolve this, I installed | 233 | To resolve this, I installed |
| 232 | [pipewire](https://wiki.alpinelinux.org/wiki/PipeWire). | 234 | [[https://wiki.alpinelinux.org/wiki/PipeWire][pipewire]]. |
| 233 | 235 | ||
| 234 | ```sh | 236 | #+begin_src sh |
| 235 | # Add your user to the following groups | 237 | # Add your user to the following groups |
| 236 | addgroup $USER audio | 238 | addgroup $USER audio |
| 237 | addgroup $USER video | 239 | addgroup $USER video |
| 238 | 240 | ||
| 239 | # Install pipewire and other useful packages | 241 | # Install pipewire and other useful packages |
| 240 | apk add pipewire wireplumber pipewire-pulse pipewire-jack pipewire-alsa | 242 | apk add pipewire wireplumber pipewire-pulse pipewire-jack pipewire-alsa |
| 241 | ``` | 243 | #+end_src |
| 242 | 244 | ||
| 243 | Finally, I needed to add `/usr/libexec/pipewire-launcher` to my | 245 | Finally, I needed to add =/usr/libexec/pipewire-launcher= to my |
| 244 | `.config/sway/config` file so that Pipewire would run every time I launched | 246 | =.config/sway/config= file so that Pipewire would run every time I |
| 245 | sway. | 247 | launched sway. |
| 246 | 248 | ||
| 247 | ```sh | 249 | #+begin_src sh |
| 248 | nano ~/.config/sway/config | 250 | nano ~/.config/sway/config |
| 249 | ``` | 251 | #+end_src |
| 250 | 252 | ||
| 251 | ```conf | 253 | #+begin_src conf |
| 252 | # Run pipewire audio server | 254 | # Run pipewire audio server |
| 253 | exec /usr/libexec/pipewire-launcher | 255 | exec /usr/libexec/pipewire-launcher |
| 254 | 256 | ||
| @@ -257,10 +259,10 @@ bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT | |||
| 257 | bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5% | 259 | bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5% |
| 258 | bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle | 260 | bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle |
| 259 | bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle | 261 | bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle |
| 260 | ``` | 262 | #+end_src |
| 261 | 263 | ||
| 262 | Note that I do not use bluetooth or screen sharing, so I won't cover those | 264 | Note that I do not use bluetooth or screen sharing, so I won't cover |
| 263 | options in this post. | 265 | those options in this post. |
| 264 | 266 | ||
| 265 | Other than these issues, I have a working Alpine desktop. No other complaints | 267 | Other than these issues, I have a working Alpine desktop. No other |
| 266 | thus far! | 268 | complaints thus far! |
content/blog/2023-01-03-recent-website-changes.md deleted −82
| @@ -1,82 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-01-03 | ||
| 3 | title = "Recent Website Changes" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The State of This Website | ||
| 9 | |||
| 10 | Over the years, this website has changed URLs, styles, content focus, and much | ||
| 11 | more. However, it seems that I am never done, as I am yet again changing this | ||
| 12 | page to fit my current needs and wants. | ||
| 13 | |||
| 14 | While this site was already minimal (~12kb), it contained a lot of disorganized | ||
| 15 | content and some poorly chosen color schemes. | ||
| 16 | |||
| 17 | The recent updates attempt to fix these items while focusing on what I truly | ||
| 18 | care about here: the content within each page. | ||
| 19 | |||
| 20 | # Recent Changes | ||
| 21 | |||
| 22 | I've made quite a few changes to the style of this website today, both in the | ||
| 23 | CSS and in the HTML. | ||
| 24 | |||
| 25 | Here are some of the key takeaways from today's changes: | ||
| 26 | |||
| 27 | 1. Reduce the width of the website from `60em` to `40em`. | ||
| 28 | 2. Remove breadcrumb navigation and replaced with a simple "Return Home" link | ||
| 29 | when visiting anything other than the homepage. | ||
| 30 | 3. Remove syntax highlighting from code blocks. CSS now loads from a single | ||
| 31 | file. | ||
| 32 | 4. Move blog posts on the homepage to the top, pushing tertiary content down. | ||
| 33 | 5. Update font-family from `monospace` to `sans-serif` for readability. | ||
| 34 | |||
| 35 | # Future Focus | ||
| 36 | |||
| 37 | ## Accessibility | ||
| 38 | |||
| 39 | My main focus amidst all the changes this past year was accessibility. I tried | ||
| 40 | to improve the accessibility of all pages on this site as well as I know how. | ||
| 41 | For example, I added aria labels to identify different navigation menus in | ||
| 42 | November. | ||
| 43 | |||
| 44 | I want to continue this in 2023 and learn more about the accessibility features | ||
| 45 | I can utilize to help those who may need them. | ||
| 46 | |||
| 47 | ## Content First | ||
| 48 | |||
| 49 | Beyond accessibility, I am making a concerted effort to bring the content of | ||
| 50 | this site to the forefront and push the tertiary information (e.g., About Me, | ||
| 51 | Services, etc.) down below the content. | ||
| 52 | |||
| 53 | Further, I want to review and edit previous blog posts for grammar, accuracy, | ||
| 54 | dead links, and more. Where necessary, I may even add a blurb at the top of old | ||
| 55 | posts that are no longer accurate or helpful. | ||
| 56 | |||
| 57 | ## Style | ||
| 58 | |||
| 59 | As always, I will be searching for ways to reduce distractions and bring a | ||
| 60 | minimalistic approach to this site. While there are certainly more drastic | ||
| 61 | measures I could take, such as making this a [Shinobi | ||
| 62 | Website](https://shinobi.bt.ht) or a [No CSS Website](https://nocss.club), I | ||
| 63 | prefer to keep some modern features here. | ||
| 64 | |||
| 65 | Two ideas have been floating around in my head, but I am not sure how I feel | ||
| 66 | about these approaches yet: | ||
| 67 | |||
| 68 | 1. Going all-in with Emacs org-mode and creating a script to auto-publish all my | ||
| 69 | blog posts from org-mode to plain HTML files and then letting my CSS style it | ||
| 70 | after the fact. | ||
| 71 | 2. Focus on publishing for Gemini or Gopher and then utilize a conversion | ||
| 72 | program to translate the pages to HTML. | ||
| 73 | |||
| 74 | # Looking Back | ||
| 75 | |||
| 76 | As I sit here and finalize the changes, as well as this post, I am happy with | ||
| 77 | the current state of this website. It's certainly a long way from my first | ||
| 78 | attempts (parallax background images, anyone?) and it's good to see the | ||
| 79 | progress. | ||
| 80 | |||
| 81 | I can only hope and work hard to ensure that I make more meaningful progress by | ||
| 82 | this time next year. | ||
content/blog/2023-01-03-recent-website-changes.org added +81
| @@ -0,0 +1,81 @@ | |||
| 1 | #+date: <2023-01-03> | ||
| 2 | #+title: Recent Website Changes | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The State of This Website | ||
| 7 | |||
| 8 | Over the years, this website has changed URLs, styles, content focus, | ||
| 9 | and much more. However, it seems that I am never done, as I am yet again | ||
| 10 | changing this page to fit my current needs and wants. | ||
| 11 | |||
| 12 | While this site was already minimal (~12kb), it contained a lot of | ||
| 13 | disorganized content and some poorly chosen color schemes. | ||
| 14 | |||
| 15 | The recent updates attempt to fix these items while focusing on what I | ||
| 16 | truly care about here: the content within each page. | ||
| 17 | |||
| 18 | * Recent Changes | ||
| 19 | |||
| 20 | I've made quite a few changes to the style of this website today, both | ||
| 21 | in the CSS and in the HTML. | ||
| 22 | |||
| 23 | Here are some of the key takeaways from today's changes: | ||
| 24 | |||
| 25 | 1. Reduce the width of the website from =60em= to =40em=. | ||
| 26 | 2. Remove breadcrumb navigation and replaced with a simple "Return Home" | ||
| 27 | link when visiting anything other than the homepage. | ||
| 28 | 3. Remove syntax highlighting from code blocks. CSS now loads from a | ||
| 29 | single file. | ||
| 30 | 4. Move blog posts on the homepage to the top, pushing tertiary content | ||
| 31 | down. | ||
| 32 | 5. Update font-family from =monospace= to =sans-serif= for readability. | ||
| 33 | |||
| 34 | * Future Focus | ||
| 35 | |||
| 36 | ** Accessibility | ||
| 37 | |||
| 38 | My main focus amidst all the changes this past year was accessibility. I | ||
| 39 | tried to improve the accessibility of all pages on this site as well as | ||
| 40 | I know how. For example, I added aria labels to identify different | ||
| 41 | navigation menus in November. | ||
| 42 | |||
| 43 | I want to continue this in 2023 and learn more about the accessibility | ||
| 44 | features I can utilize to help those who may need them. | ||
| 45 | |||
| 46 | ** Content First | ||
| 47 | |||
| 48 | Beyond accessibility, I am making a concerted effort to bring the | ||
| 49 | content of this site to the forefront and push the tertiary information | ||
| 50 | (e.g., About Me, Services, etc.) down below the content. | ||
| 51 | |||
| 52 | Further, I want to review and edit previous blog posts for grammar, | ||
| 53 | accuracy, dead links, and more. Where necessary, I may even add a blurb | ||
| 54 | at the top of old posts that are no longer accurate or helpful. | ||
| 55 | |||
| 56 | ** Style | ||
| 57 | |||
| 58 | As always, I will be searching for ways to reduce distractions and bring | ||
| 59 | a minimalistic approach to this site. While there are certainly more | ||
| 60 | drastic measures I could take, such as making this a | ||
| 61 | [[https://shinobi.bt.ht][Shinobi Website]] or a [[https://nocss.club][No | ||
| 62 | CSS Website]], I prefer to keep some modern features here. | ||
| 63 | |||
| 64 | Two ideas have been floating around in my head, but I am not sure how I | ||
| 65 | feel about these approaches yet: | ||
| 66 | |||
| 67 | 1. Going all-in with Emacs org-mode and creating a script to | ||
| 68 | auto-publish all my blog posts from org-mode to plain HTML files and | ||
| 69 | then letting my CSS style it after the fact. | ||
| 70 | 2. Focus on publishing for Gemini or Gopher and then utilize a | ||
| 71 | conversion program to translate the pages to HTML. | ||
| 72 | |||
| 73 | * Looking Back | ||
| 74 | |||
| 75 | As I sit here and finalize the changes, as well as this post, I am happy | ||
| 76 | with the current state of this website. It's certainly a long way from | ||
| 77 | my first attempts (parallax background images, anyone?) and it's good to | ||
| 78 | see the progress. | ||
| 79 | |||
| 80 | I can only hope and work hard to ensure that I make more meaningful | ||
| 81 | progress by this time next year. | ||
content/blog/2023-01-05-mass-unlike-tumblr-posts.md deleted −92
| @@ -1,92 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-01-05 | ||
| 3 | title = " How to Easily Mass Unlike Tumblr Posts with Javascript" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Dilemma | ||
| 9 | |||
| 10 | The dilemma I had was pretty simple: I wanted to unlike all the posts I have | ||
| 11 | liked on Tumblr so that I could follow a new focus on blogs and start fresh. | ||
| 12 | Otherwise, Tumblr will keep recommending content based on your previous likes. | ||
| 13 | |||
| 14 | # The Solution | ||
| 15 | |||
| 16 | I searched the web for a while and noted that most solutions referenced Tumblr | ||
| 17 | setting and dashboard pages that no longer exist. Additionally, I did not want | ||
| 18 | to install a third party extension to do this, as some suggested. | ||
| 19 | |||
| 20 | Luckily, I used Javascript for a while a few years ago and figured it would be | ||
| 21 | easy enough to script a solution, as long as Tumblr had a system for the unlike | ||
| 22 | buttons. | ||
| 23 | |||
| 24 | ## Identifying Unlike Buttons | ||
| 25 | |||
| 26 | Tumblr's unlike buttons are structured as you can see in the following code | ||
| 27 | block. All unlike buttons have an `aria-label` with a value of `Unlike`. | ||
| 28 | |||
| 29 | ```html | ||
| 30 | <button class="TRX6J" aria-label="Unlike"> | ||
| 31 | <span class="EvhBA B1Z5w ztpfZ" tabindex="-1"> | ||
| 32 | <svg | ||
| 33 | xmlns="http://www.w3.org/2000/svg" | ||
| 34 | height="21" | ||
| 35 | width="23" | ||
| 36 | role="presentation" | ||
| 37 | > | ||
| 38 | <use href="#managed-icon__like-filled"></use> | ||
| 39 | </svg> | ||
| 40 | </span> | ||
| 41 | </button> | ||
| 42 | ``` | ||
| 43 | |||
| 44 | ## Running a Script to Unlike All Likes | ||
| 45 | |||
| 46 | To run this script, you will need to load the [Likes \| | ||
| 47 | Tumblr](https://www.tumblr.com/likes) page while logged in to your account. | ||
| 48 | |||
| 49 | Further, be sure to scroll down to the bottom and force Tumblr to load more | ||
| 50 | posts so that this script unlikes more posts at a time. | ||
| 51 | |||
| 52 | Once you are logged in and the page is loaded, open the Developer Tools and be | ||
| 53 | sure you're on the "Console" tab. It should look something like this (this is in | ||
| 54 | Firefox, Chromium should be similar): | ||
| 55 | |||
| 56 |  | ||
| 58 | |||
| 59 | All you need to do is paste the following snippet into the dev console. This | ||
| 60 | code will collect all unlike buttons (`elements`) and then click each button to | ||
| 61 | unlike it. | ||
| 62 | |||
| 63 | Optionally, you can comment-out the line `elements[i].click();` and uncomment | ||
| 64 | the `console.log()` lines to simply print out information without performing any | ||
| 65 | actions. This can be useful to debug issues or confirm that the code below isn't | ||
| 66 | doing anything you don't want it to. | ||
| 67 | |||
| 68 | ```javascript | ||
| 69 | const elements = document.querySelectorAll('[aria-label="Unlike"]'); | ||
| 70 | // console.log(elements); // 👉 [button] | ||
| 71 | |||
| 72 | for (let i = 0; i < elements.length; i++) { | ||
| 73 | // console.log(elements[i]); | ||
| 74 | elements[i].click(); | ||
| 75 | } | ||
| 76 | ``` | ||
| 77 | |||
| 78 | # Results | ||
| 79 | |||
| 80 | The results were quick for my situation, as it unliked ~200 posts within 2-3 | ||
| 81 | seconds. I am not sure how this will perform on larger sets of likes (or if | ||
| 82 | Tumblr has a limit to unliking posts). | ||
| 83 | |||
| 84 | You can see the below screenshot showing that I pasted the snippet into the | ||
| 85 | console, pressed Enter, and then the posts are automatically unliked. | ||
| 86 | |||
| 87 |  | ||
| 89 | |||
| 90 | Thinking about this further, I would bet that this would be fairly simple to | ||
| 91 | package into a browser add-on so that users could install the add-on, go to | ||
| 92 | their Likes page, and click a button to run the script. Food for thought. | ||
content/blog/2023-01-05-mass-unlike-tumblr-posts.org added +96
| @@ -0,0 +1,96 @@ | |||
| 1 | #+date: <2023-01-05> | ||
| 2 | #+title: How to Easily Mass Unlike Tumblr Posts with Javascript | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Dilemma | ||
| 7 | |||
| 8 | The dilemma I had was pretty simple: I wanted to unlike all the posts I | ||
| 9 | have liked on Tumblr so that I could follow a new focus on blogs and | ||
| 10 | start fresh. Otherwise, Tumblr will keep recommending content based on | ||
| 11 | your previous likes. | ||
| 12 | |||
| 13 | * The Solution | ||
| 14 | |||
| 15 | I searched the web for a while and noted that most solutions referenced | ||
| 16 | Tumblr setting and dashboard pages that no longer exist. Additionally, I | ||
| 17 | did not want to install a third party extension to do this, as some | ||
| 18 | suggested. | ||
| 19 | |||
| 20 | Luckily, I used Javascript for a while a few years ago and figured it | ||
| 21 | would be easy enough to script a solution, as long as Tumblr had a | ||
| 22 | system for the unlike buttons. | ||
| 23 | |||
| 24 | ** Identifying Unlike Buttons | ||
| 25 | |||
| 26 | Tumblr's unlike buttons are structured as you can see in the following | ||
| 27 | code block. All unlike buttons have an =aria-label= with a value of | ||
| 28 | =Unlike=. | ||
| 29 | |||
| 30 | #+begin_src html | ||
| 31 | <button class="TRX6J" aria-label="Unlike"> | ||
| 32 | <span class="EvhBA B1Z5w ztpfZ" tabindex="-1"> | ||
| 33 | <svg | ||
| 34 | xmlns="http://www.w3.org/2000/svg" | ||
| 35 | height="21" | ||
| 36 | width="23" | ||
| 37 | role="presentation" | ||
| 38 | > | ||
| 39 | <use href="#managed-icon__like-filled"></use> | ||
| 40 | </svg> | ||
| 41 | </span> | ||
| 42 | </button> | ||
| 43 | #+end_src | ||
| 44 | |||
| 45 | ** Running a Script to Unlike All Likes | ||
| 46 | |||
| 47 | To run this script, you will need to load the | ||
| 48 | [[https://www.tumblr.com/likes][Likes | Tumblr]] page while logged in to | ||
| 49 | your account. | ||
| 50 | |||
| 51 | Further, be sure to scroll down to the bottom and force Tumblr to load | ||
| 52 | more posts so that this script unlikes more posts at a time. | ||
| 53 | |||
| 54 | Once you are logged in and the page is loaded, open the Developer Tools | ||
| 55 | and be sure you're on the "Console" tab. It should look something like | ||
| 56 | this (this is in Firefox, Chromium should be similar): | ||
| 57 | |||
| 58 | #+caption: Firefox Dev !Tools | ||
| 59 | [[https:///img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/dev_console.png]] | ||
| 60 | |||
| 61 | All you need to do is paste the following snippet into the dev console. | ||
| 62 | This code will collect all unlike buttons (=elements=) and then click | ||
| 63 | each button to unlike it. | ||
| 64 | |||
| 65 | Optionally, you can comment-out the line =elements[i].click();= and | ||
| 66 | uncomment the =console.log()= lines to simply print out information | ||
| 67 | without performing any actions. This can be useful to debug issues or | ||
| 68 | confirm that the code below isn't doing anything you don't want it to. | ||
| 69 | |||
| 70 | #+begin_src javascript | ||
| 71 | const elements = document.querySelectorAll('[aria-label="Unlike"]'); | ||
| 72 | // console.log(elements); // 👉 [button] | ||
| 73 | |||
| 74 | for (let i = 0; i < elements.length; i++) { | ||
| 75 | // console.log(elements[i]); | ||
| 76 | elements[i].click(); | ||
| 77 | } | ||
| 78 | #+end_src | ||
| 79 | |||
| 80 | * Results | ||
| 81 | |||
| 82 | The results were quick for my situation, as it unliked ~200 posts within | ||
| 83 | 2-3 seconds. I am not sure how this will perform on larger sets of likes | ||
| 84 | (or if Tumblr has a limit to unliking posts). | ||
| 85 | |||
| 86 | You can see the below screenshot showing that I pasted the snippet into | ||
| 87 | the console, pressed Enter, and then the posts are automatically | ||
| 88 | unliked. | ||
| 89 | |||
| 90 | #+caption: Script !Results | ||
| 91 | [[https:///img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/script_results.png]] | ||
| 92 | |||
| 93 | Thinking about this further, I would bet that this would be fairly | ||
| 94 | simple to package into a browser add-on so that users could install the | ||
| 95 | add-on, go to their Likes page, and click a button to run the script. | ||
| 96 | Food for thought. | ||
content/blog/2023-01-08-fedora-login-manager.md deleted −45
| @@ -1,45 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-01-08 | ||
| 3 | title = "How to Remove the Login Manager from Fedora i3" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Fedora i3's Login Manager | ||
| 9 | |||
| 10 | Since I use the i3 spin of Fedora Workstation, I don't like to have a login | ||
| 11 | manager installed by default. As of the current version of Fedora i3, the | ||
| 12 | default login manager is LightDM. | ||
| 13 | |||
| 14 | If this is no longer the case, you can search for currently-installed packages | ||
| 15 | with the following command and see if you can identify a different login | ||
| 16 | manager. | ||
| 17 | |||
| 18 | ```sh | ||
| 19 | sudo dnf list installed | ||
| 20 | ``` | ||
| 21 | |||
| 22 | # Removing the Login Manager | ||
| 23 | |||
| 24 | In order to remove the login manager, simply uninstall the package. | ||
| 25 | |||
| 26 | ```sh | ||
| 27 | sudo dnf remove lightdm | ||
| 28 | ``` | ||
| 29 | |||
| 30 | # Launching i3 Manually | ||
| 31 | |||
| 32 | In order to launch i3 manually, you need to set up your X session properly. To | ||
| 33 | start, create or edit the `~/.xinitrc` file to include the following at the | ||
| 34 | bottom. | ||
| 35 | |||
| 36 | ```config | ||
| 37 | exec i3 | ||
| 38 | ``` | ||
| 39 | |||
| 40 | Now, whenever you log in to the TTY, you can launch your desktop with the | ||
| 41 | following command. | ||
| 42 | |||
| 43 | ```sh | ||
| 44 | startx | ||
| 45 | ``` | ||
content/blog/2023-01-08-fedora-login-manager.org added +43
| @@ -0,0 +1,43 @@ | |||
| 1 | #+date: <2023-01-08> | ||
| 2 | #+title: How to Remove the Login Manager from Fedora i3 | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Fedora i3's Login Manager | ||
| 7 | |||
| 8 | Since I use the i3 spin of Fedora Workstation, I don't like to have a | ||
| 9 | login manager installed by default. As of the current version of Fedora | ||
| 10 | i3, the default login manager is LightDM. | ||
| 11 | |||
| 12 | If this is no longer the case, you can search for currently-installed | ||
| 13 | packages with the following command and see if you can identify a | ||
| 14 | different login manager. | ||
| 15 | |||
| 16 | #+begin_src sh | ||
| 17 | sudo dnf list installed | ||
| 18 | #+end_src | ||
| 19 | |||
| 20 | * Removing the Login Manager | ||
| 21 | |||
| 22 | In order to remove the login manager, simply uninstall the package. | ||
| 23 | |||
| 24 | #+begin_src sh | ||
| 25 | sudo dnf remove lightdm | ||
| 26 | #+end_src | ||
| 27 | |||
| 28 | * Launching i3 Manually | ||
| 29 | |||
| 30 | In order to launch i3 manually, you need to set up your X session | ||
| 31 | properly. To start, create or edit the =~/.xinitrc= file to include the | ||
| 32 | following at the bottom. | ||
| 33 | |||
| 34 | #+begin_src config | ||
| 35 | exec i3 | ||
| 36 | #+end_src | ||
| 37 | |||
| 38 | Now, whenever you log in to the TTY, you can launch your desktop with | ||
| 39 | the following command. | ||
| 40 | |||
| 41 | #+begin_src sh | ||
| 42 | startx | ||
| 43 | #+end_src | ||
content/blog/2023-01-21-flatpak-symlinks.md deleted −46
| @@ -1,46 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-01-21 | ||
| 3 | title = "Running Flatpak Apps with Symlinks" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Running Flatpak Apps Should Be Faster | ||
| 9 | |||
| 10 | If you're like me and use Flatpak for those pesky apps that cannot run on your | ||
| 11 | system for one reason or another, you likely get annoyed with opening a terminal | ||
| 12 | and manually running the Flatpak app with the lengthy `flatpak run ...` command. | ||
| 13 | |||
| 14 | In the past, I manually created aliases in my `.zshrc` file for certain apps. | ||
| 15 | For example, an alias would look like the example below. | ||
| 16 | |||
| 17 | This would allow me to run the command fast within the terminal, but it wouldn't | ||
| 18 | allow me to run it in an application launcher. | ||
| 19 | |||
| 20 | ```sh | ||
| 21 | # ~/.zshrc | ||
| 22 | alias librewolf = "flatpak run io.gitlab.librewolf-community" | ||
| 23 | ``` | ||
| 24 | |||
| 25 | However, I now use a much faster and better method that integrates with the | ||
| 26 | tiling WMs I use and their application launchers - `dmenu` and `bemenu`. | ||
| 27 | |||
| 28 | # Creating Symlinks for Flatpak Apps | ||
| 29 | |||
| 30 | Let's use the example of Librewolf below. I can install the application like so: | ||
| 31 | |||
| 32 | ```sh | ||
| 33 | flatpak install flathub io.gitlab.librewolf-community | ||
| 34 | ``` | ||
| 35 | |||
| 36 | Once installed, I can create a symlink to link the flatpak app to my new symlink | ||
| 37 | in a location commonly included in your PATH. In this case, I chose `/usr/bin`. | ||
| 38 | You may need to choose a different location if `/usr/bin` isn't in your PATH. | ||
| 39 | |||
| 40 | ```sh | ||
| 41 | ln -s /var/lib/flatpak/exports/bin/io.gitlab.librewolf-community /usr/bin/librewolf | ||
| 42 | ``` | ||
| 43 | |||
| 44 | Once complete, you should be able to launch the app using the command name you | ||
| 45 | chose above in the symlink (`librewolf`) from a terminal or from your | ||
| 46 | application launcher! | ||
content/blog/2023-01-21-flatpak-symlinks.org added +48
| @@ -0,0 +1,48 @@ | |||
| 1 | #+date: <2023-01-21> | ||
| 2 | #+title: Running Flatpak Apps with Symlinks | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Running Flatpak Apps Should Be Faster | ||
| 7 | |||
| 8 | If you're like me and use Flatpak for those pesky apps that cannot run | ||
| 9 | on your system for one reason or another, you likely get annoyed with | ||
| 10 | opening a terminal and manually running the Flatpak app with the lengthy | ||
| 11 | =flatpak run ...= command. | ||
| 12 | |||
| 13 | In the past, I manually created aliases in my =.zshrc= file for certain | ||
| 14 | apps. For example, an alias would look like the example below. | ||
| 15 | |||
| 16 | This would allow me to run the command fast within the terminal, but it | ||
| 17 | wouldn't allow me to run it in an application launcher. | ||
| 18 | |||
| 19 | #+begin_src sh | ||
| 20 | # ~/.zshrc | ||
| 21 | alias librewolf = "flatpak run io.gitlab.librewolf-community" | ||
| 22 | #+end_src | ||
| 23 | |||
| 24 | However, I now use a much faster and better method that integrates with | ||
| 25 | the tiling WMs I use and their application launchers - =dmenu= and | ||
| 26 | =bemenu=. | ||
| 27 | |||
| 28 | * Creating Symlinks for Flatpak Apps | ||
| 29 | |||
| 30 | Let's use the example of Librewolf below. I can install the application | ||
| 31 | like so: | ||
| 32 | |||
| 33 | #+begin_src sh | ||
| 34 | flatpak install flathub io.gitlab.librewolf-community | ||
| 35 | #+end_src | ||
| 36 | |||
| 37 | Once installed, I can create a symlink to link the flatpak app to my new | ||
| 38 | symlink in a location commonly included in your PATH. In this case, I | ||
| 39 | chose =/usr/bin=. You may need to choose a different location if | ||
| 40 | =/usr/bin= isn't in your PATH. | ||
| 41 | |||
| 42 | #+begin_src sh | ||
| 43 | ln -s /var/lib/flatpak/exports/bin/io.gitlab.librewolf-community /usr/bin/librewolf | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | Once complete, you should be able to launch the app using the command | ||
| 47 | name you chose above in the symlink (=librewolf=) from a terminal or | ||
| 48 | from your application launcher! | ||
content/blog/2023-01-23-random-wireguard.md → content/blog/2023-01-23-random-wireguard.org renamed +53 −53
| @@ -1,47 +1,47 @@ | |||
| 1 | +++ | 1 | #+date: <2023-01-23> |
| 2 | date = 2023-01-23 | 2 | #+title: Connecting to a Random Mullvad Wireguard Host on Boot |
| 3 | title = "Connecting to a Random Mullvad Wireguard Host on Boot" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Mullvad Wireguard | ||
| 9 | 5 | ||
| 10 | If you're using an OS that does not support one of Mullvad's apps, you're likely | 6 | * Mullvad Wireguard |
| 11 | using the Wireguard configuration files instead. | ||
| 12 | 7 | ||
| 13 | If not, the first step is to visit Mullvad's [Wireguard configuration | 8 | If you're using an OS that does not support one of Mullvad's apps, |
| 14 | files](https://mullvad.net/en/account/#/wireguard-config) page and download a | 9 | you're likely using the Wireguard configuration files instead. |
| 15 | ZIP of the configuration files you want to use. | ||
| 16 | 10 | ||
| 17 | Personally, I downloaded all configuration files across the world and chose my | 11 | If not, the first step is to visit Mullvad's |
| 18 | connections using the script below. | 12 | [[https://mullvad.net/en/account/#/wireguard-config][Wireguard |
| 13 | configuration files]] page and download a ZIP of the configuration files | ||
| 14 | you want to use. | ||
| 19 | 15 | ||
| 20 | Once the files are downloaded, unzip them and move them to your preferred | 16 | Personally, I downloaded all configuration files across the world and |
| 21 | location: | 17 | chose my connections using the script below. |
| 22 | 18 | ||
| 23 | ```sh | 19 | Once the files are downloaded, unzip them and move them to your |
| 20 | preferred location: | ||
| 21 | |||
| 22 | #+begin_src sh | ||
| 24 | cd Downloads | 23 | cd Downloads |
| 25 | unzip mullvad_wireguard_linux_all_all.zip | 24 | unzip mullvad_wireguard_linux_all_all.zip |
| 26 | mkdir ~/mullvad && mv ~/Downloads/*.conf ~/mullvad/ | 25 | mkdir ~/mullvad && mv ~/Downloads/*.conf ~/mullvad/ |
| 27 | ``` | 26 | #+end_src |
| 28 | 27 | ||
| 29 | ### Creating a Script to Connect to a Random Host | 28 | *** Creating a Script to Connect to a Random Host |
| 30 | 29 | ||
| 31 | Once you have a folder of Wireguard configuration files from Mullvad, you can | 30 | Once you have a folder of Wireguard configuration files from Mullvad, |
| 32 | create a script to randomly connect to any one of the locations. | 31 | you can create a script to randomly connect to any one of the locations. |
| 33 | 32 | ||
| 34 | Start by creating a shell script - mine is called `vpn.sh`. | 33 | Start by creating a shell script - mine is called =vpn.sh=. |
| 35 | 34 | ||
| 36 | ```sh | 35 | #+begin_src sh |
| 37 | nano ~/vpn.sh | 36 | nano ~/vpn.sh |
| 38 | ``` | 37 | #+end_src |
| 39 | 38 | ||
| 40 | Within this script, you can paste the following info. Note that I specify `us-*` | 39 | Within this script, you can paste the following info. Note that I |
| 41 | in my script, which means that it will only consider US-based VPN locations. You | 40 | specify =us-*= in my script, which means that it will only consider |
| 42 | can alter this or simply change it `*` to consider all locations. | 41 | US-based VPN locations. You can alter this or simply change it =*= to |
| 42 | consider all locations. | ||
| 43 | 43 | ||
| 44 | ```sh | 44 | #+begin_src sh |
| 45 | #!/bin/sh | 45 | #!/bin/sh |
| 46 | 46 | ||
| 47 | ls /home/$USER/mullvad/us-** |sort -R |tail -n 1 |while read file; do | 47 | ls /home/$USER/mullvad/us-** |sort -R |tail -n 1 |while read file; do |
| @@ -52,19 +52,19 @@ ls /home/$USER/mullvad/us-** |sort -R |tail -n 1 |while read file; do | |||
| 52 | printf "\n\nPrinting new IP info:\n" | 52 | printf "\n\nPrinting new IP info:\n" |
| 53 | curl https://am.i.mullvad.net/connected | 53 | curl https://am.i.mullvad.net/connected |
| 54 | done | 54 | done |
| 55 | ``` | 55 | #+end_src |
| 56 | 56 | ||
| 57 | Once you've modified the script to your liking, add executable permissions and | 57 | Once you've modified the script to your liking, add executable |
| 58 | run the script: | 58 | permissions and run the script: |
| 59 | 59 | ||
| 60 | ```sh | 60 | #+begin_src sh |
| 61 | chmod +x ~/vpn.sh | 61 | chmod +x ~/vpn.sh |
| 62 | ~/vpn.sh | 62 | ~/vpn.sh |
| 63 | ``` | 63 | #+end_src |
| 64 | 64 | ||
| 65 | The output should look like the following: | 65 | The output should look like the following: |
| 66 | 66 | ||
| 67 | ```txt | 67 | #+begin_src txt |
| 68 | doas (user@host) password: | 68 | doas (user@host) password: |
| 69 | 69 | ||
| 70 | # ... The script will process all of the iptables and wg commands here | 70 | # ... The script will process all of the iptables and wg commands here |
| @@ -73,40 +73,40 @@ Created Mullvad wireguard connection with file: /home/user/mullvad/us-nyc-wg-210 | |||
| 73 | 73 | ||
| 74 | Printing new IP info: | 74 | Printing new IP info: |
| 75 | You are connected to Mullvad (server country-city-wg-num). Your IP address is 12.345.678.99 | 75 | You are connected to Mullvad (server country-city-wg-num). Your IP address is 12.345.678.99 |
| 76 | ``` | 76 | #+end_src |
| 77 | 77 | ||
| 78 | That's all there is to it. You can see your new location and IP via the `printf` | 78 | That's all there is to it. You can see your new location and IP via the |
| 79 | and `curl` commands included in the script. | 79 | =printf= and =curl= commands included in the script. |
| 80 | 80 | ||
| 81 | You can also go to the [Connection Check \| | 81 | You can also go to the [[https://mullvad.net/en/check/][Connection Check |
| 82 | Mullvad](https://mullvad.net/en/check/) page to see if you are fully connected | 82 | | Mullvad]] page to see if you are fully connected to Mullvad and if any |
| 83 | to Mullvad and if any leaks exist. | 83 | leaks exist. |
| 84 | 84 | ||
| 85 | # Disconnecting from the Wireguard Connection | 85 | * Disconnecting from the Wireguard Connection |
| 86 | 86 | ||
| 87 | If you forget which connection you're using, you can execute the following | 87 | If you forget which connection you're using, you can execute the |
| 88 | command to see where Wireguard is currently connected: | 88 | following command to see where Wireguard is currently connected: |
| 89 | 89 | ||
| 90 | ```sh | 90 | #+begin_src sh |
| 91 | wg show | 91 | wg show |
| 92 | ``` | 92 | #+end_src |
| 93 | 93 | ||
| 94 | This command will show you the Wireguard interfaces and should output a | 94 | This command will show you the Wireguard interfaces and should output a |
| 95 | connection like so: `interface: us-lax-wg-104`. | 95 | connection like so: =interface: us-lax-wg-104=. |
| 96 | 96 | ||
| 97 | Once you have this, just disconnect using that files' full path: | 97 | Once you have this, just disconnect using that files' full path: |
| 98 | 98 | ||
| 99 | ```sh | 99 | #+begin_src sh |
| 100 | wg-quick down /home/user/mullvad/us-lax-wg-104.conf | 100 | wg-quick down /home/user/mullvad/us-lax-wg-104.conf |
| 101 | ``` | 101 | #+end_src |
| 102 | 102 | ||
| 103 | I have a TODO item on figuring out how to easily export an environment variable | 103 | I have a TODO item on figuring out how to easily export an environment |
| 104 | that contains the configuration file's full name, so that I can just execute the | 104 | variable that contains the configuration file's full name, so that I can |
| 105 | following: | 105 | just execute the following: |
| 106 | 106 | ||
| 107 | ```sh | 107 | #+begin_src sh |
| 108 | # Ideal situation if I can export the $file variable to the environment | 108 | # Ideal situation if I can export the $file variable to the environment |
| 109 | wg-quick down $file | 109 | wg-quick down $file |
| 110 | ``` | 110 | #+end_src |
| 111 | 111 | ||
| 112 | If you have an idea on how to do this, email me! | 112 | If you have an idea on how to do this, email me! |
content/blog/2023-01-28-self-hosting-wger.md deleted −142
| @@ -1,142 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-01-28 | ||
| 3 | title = "Self-Hosting Wger Workout Manager" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Wger: The Self-Hosted Workout Manager | ||
| 9 | |||
| 10 | [Wger Workout Manager](https://wger.de) is a fitness tracking tool for those who | ||
| 11 | enjoy self-hosting their data. You can also register an account on their main | ||
| 12 | website if you'd prefer to try without self-hosting. | ||
| 13 | |||
| 14 | ## Features | ||
| 15 | |||
| 16 | I didn't see a full listing of features anywhere, so I compiled this list of my | ||
| 17 | own after installing wger: | ||
| 18 | |||
| 19 | 1. Dashboard | ||
| 20 | |||
| 21 | - Dashboard view of Workout Schedule, Nutrition Plan, Weight Graph, & last | ||
| 22 | 5 Weight Logs | ||
| 23 | |||
| 24 | 2. Training | ||
| 25 | |||
| 26 | - Workout Log | ||
| 27 | - Workout Schedule | ||
| 28 | - Calendar (shows weight logs and Bad/Neutral/Good days) | ||
| 29 | - Gallery (shows images you upload) | ||
| 30 | - Workout templates | ||
| 31 | - Public templates | ||
| 32 | - Exercises | ||
| 33 | |||
| 34 | 3. Nutrition | ||
| 35 | |||
| 36 | - Nutrition plans | ||
| 37 | - BMI calculator | ||
| 38 | - Daily calories calculator | ||
| 39 | - Ingredient overview | ||
| 40 | |||
| 41 | 4. Body Weight | ||
| 42 | |||
| 43 | - Weight overview | ||
| 44 | |||
| 45 | ## Documentation | ||
| 46 | |||
| 47 | In order to self-host wger, I opted to use the Docker version of the | ||
| 48 | application. You can read the README within the | ||
| 49 | [wger-project/docker](https://github.com/wger-project/docker) project on GitHub | ||
| 50 | for information and installation instructions. | ||
| 51 | |||
| 52 | ## Installation | ||
| 53 | |||
| 54 | To start the installation, I created a folder for wger and started creating the | ||
| 55 | three necessary files: | ||
| 56 | |||
| 57 | ```sh | ||
| 58 | mkdir ~/wger && mkdir ~/wger/config | ||
| 59 | touch ~/wger/docker-compose.yml && \ | ||
| 60 | touch ~/wger/config/prod.env && \ | ||
| 61 | touch ~/wger/config/nginx.conf | ||
| 62 | ``` | ||
| 63 | |||
| 64 | Once you have the folders and files created, you will need to copy the contents | ||
| 65 | of the `docker-compose.yml`, `prod.env`, and `nginx.conf` from the GitHub link | ||
| 66 | above. | ||
| 67 | |||
| 68 | A few notes to explain the changes I made to the default files: | ||
| 69 | |||
| 70 | - I updated the `ALLOW_REGISTRAION` variable in `prod.env` to `False` after I | ||
| 71 | created an account via my LAN connection, **before** I connected this app to | ||
| 72 | a publicly-available domain. | ||
| 73 | - I uncommented and updated `CSRF_TRUSTED_ORIGINS` to be equal to the public | ||
| 74 | version of this app: `https://wger.example.com`. | ||
| 75 | - I updated the port within `docker-compose.yml`, within the `nginx` block. | ||
| 76 | The port I updated this to will be reflected in my nginx configuration file | ||
| 77 | on the server (NOT the wger nginx.conf file). | ||
| 78 | |||
| 79 | ## Deploy | ||
| 80 | |||
| 81 | Once all files are created and modified to your needs, simply start the | ||
| 82 | container. | ||
| 83 | |||
| 84 | ```sh | ||
| 85 | docker-compose up -d | ||
| 86 | ``` | ||
| 87 | |||
| 88 | You can now visit the website on your LAN by going to `localhost:YOUR_PORT` or | ||
| 89 | by the server's IP, if you're not on the same machine that is running the | ||
| 90 | container. | ||
| 91 | |||
| 92 | If you wish to connect this app to a public domain name, you'll need to point an | ||
| 93 | `A` DNS record from the domain to your server's public IP. You'll then need to | ||
| 94 | create a configuration file for whichever web server or reverse proxy you're | ||
| 95 | using. | ||
| 96 | |||
| 97 | Wger's README suggests the following reverse proxy configuration for Nginx: | ||
| 98 | |||
| 99 | ```conf | ||
| 100 | upstream wger { | ||
| 101 | # This port should match the port in the `nginx` block of docker-compose.yml | ||
| 102 | # If the container is running on this same machine, replace this with | ||
| 103 | # server 127.0.0.1:8080 | ||
| 104 | server 123.456.789.0:8080; | ||
| 105 | } | ||
| 106 | |||
| 107 | server { | ||
| 108 | listen 80; | ||
| 109 | listen [::]:443 ssl; | ||
| 110 | listen 443 ssl; | ||
| 111 | |||
| 112 | location / { | ||
| 113 | proxy_pass http://wger; | ||
| 114 | proxy_set_header Host $http_host; | ||
| 115 | proxy_set_header X-Real-IP $remote_addr; | ||
| 116 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 117 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 118 | proxy_redirect off; | ||
| 119 | } | ||
| 120 | |||
| 121 | server_name my.domain.example.com; | ||
| 122 | |||
| 123 | ssl_certificate /path/to/https/certificate.crt; | ||
| 124 | ssl_certificate_key /path/to/https/certificate.key; | ||
| 125 | } | ||
| 126 | ``` | ||
| 127 | |||
| 128 | # Thoughts on Wger | ||
| 129 | |||
| 130 | I'm still playing around with the app itself, but it seems to be a solid | ||
| 131 | all-around workout manager, weight log, and food log. | ||
| 132 | |||
| 133 | I like that the weight log graph is fluid and updates quickly. You can also | ||
| 134 | import or export data in CSV format if you'd like to move your data elsewhere. | ||
| 135 | |||
| 136 | The workout manager is slightly odd, as it requires you to enter sets and reps | ||
| 137 | for each exercise when you enter it into the plan. Then, when you add a log | ||
| 138 | entry for performing a workout, you then add what you actually performed, in | ||
| 139 | terms of reps and weight. | ||
| 140 | |||
| 141 | I haven't tried the food log yet and I likely will not, at least for a while. I | ||
| 142 | have no need for a food log or calorie tracker at the moment. | ||
content/blog/2023-01-28-self-hosting-wger.org added +143
| @@ -0,0 +1,143 @@ | |||
| 1 | #+date: <2023-01-28> | ||
| 2 | #+title: Self-Hosting Wger Workout Manager | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Wger: The Self-Hosted Workout Manager | ||
| 7 | |||
| 8 | [[https://wger.de][Wger Workout Manager]] is a fitness tracking tool for | ||
| 9 | those who enjoy self-hosting their data. You can also register an | ||
| 10 | account on their main website if you'd prefer to try without | ||
| 11 | self-hosting. | ||
| 12 | |||
| 13 | ** Features | ||
| 14 | |||
| 15 | I didn't see a full listing of features anywhere, so I compiled this | ||
| 16 | list of my own after installing wger: | ||
| 17 | |||
| 18 | 1. Dashboard | ||
| 19 | |||
| 20 | - Dashboard view of Workout Schedule, Nutrition Plan, Weight Graph, & | ||
| 21 | last 5 Weight Logs | ||
| 22 | |||
| 23 | 2. Training | ||
| 24 | |||
| 25 | - Workout Log | ||
| 26 | - Workout Schedule | ||
| 27 | - Calendar (shows weight logs and Bad/Neutral/Good days) | ||
| 28 | - Gallery (shows images you upload) | ||
| 29 | - Workout templates | ||
| 30 | - Public templates | ||
| 31 | - Exercises | ||
| 32 | |||
| 33 | 3. Nutrition | ||
| 34 | |||
| 35 | - Nutrition plans | ||
| 36 | - BMI calculator | ||
| 37 | - Daily calories calculator | ||
| 38 | - Ingredient overview | ||
| 39 | |||
| 40 | 4. Body Weight | ||
| 41 | |||
| 42 | - Weight overview | ||
| 43 | |||
| 44 | ** Documentation | ||
| 45 | |||
| 46 | In order to self-host wger, I opted to use the Docker version of the | ||
| 47 | application. You can read the README within the | ||
| 48 | [[https://github.com/wger-project/docker][wger-project/docker]] project | ||
| 49 | on GitHub for information and installation instructions. | ||
| 50 | |||
| 51 | ** Installation | ||
| 52 | |||
| 53 | To start the installation, I created a folder for wger and started | ||
| 54 | creating the three necessary files: | ||
| 55 | |||
| 56 | #+begin_src sh | ||
| 57 | mkdir ~/wger && mkdir ~/wger/config | ||
| 58 | touch ~/wger/docker-compose.yml && \ | ||
| 59 | touch ~/wger/config/prod.env && \ | ||
| 60 | touch ~/wger/config/nginx.conf | ||
| 61 | #+end_src | ||
| 62 | |||
| 63 | Once you have the folders and files created, you will need to copy the | ||
| 64 | contents of the =docker-compose.yml=, =prod.env=, and =nginx.conf= from | ||
| 65 | the GitHub link above. | ||
| 66 | |||
| 67 | A few notes to explain the changes I made to the default files: | ||
| 68 | |||
| 69 | - I updated the =ALLOW_REGISTRAION= variable in =prod.env= to =False= | ||
| 70 | after I created an account via my LAN connection, *before* I connected | ||
| 71 | this app to a publicly-available domain. | ||
| 72 | - I uncommented and updated =CSRF_TRUSTED_ORIGINS= to be equal to the | ||
| 73 | public version of this app: =https://wger.example.com=. | ||
| 74 | - I updated the port within =docker-compose.yml=, within the =nginx= | ||
| 75 | block. The port I updated this to will be reflected in my nginx | ||
| 76 | configuration file on the server (NOT the wger nginx.conf file). | ||
| 77 | |||
| 78 | ** Deploy | ||
| 79 | |||
| 80 | Once all files are created and modified to your needs, simply start the | ||
| 81 | container. | ||
| 82 | |||
| 83 | #+begin_src sh | ||
| 84 | docker-compose up -d | ||
| 85 | #+end_src | ||
| 86 | |||
| 87 | You can now visit the website on your LAN by going to | ||
| 88 | =localhost:YOUR_PORT= or by the server's IP, if you're not on the same | ||
| 89 | machine that is running the container. | ||
| 90 | |||
| 91 | If you wish to connect this app to a public domain name, you'll need to | ||
| 92 | point an =A= DNS record from the domain to your server's public IP. | ||
| 93 | You'll then need to create a configuration file for whichever web server | ||
| 94 | or reverse proxy you're using. | ||
| 95 | |||
| 96 | Wger's README suggests the following reverse proxy configuration for | ||
| 97 | Nginx: | ||
| 98 | |||
| 99 | #+begin_src conf | ||
| 100 | upstream wger { | ||
| 101 | # This port should match the port in the `nginx` block of docker-compose.yml | ||
| 102 | # If the container is running on this same machine, replace this with | ||
| 103 | # server 127.0.0.1:8080 | ||
| 104 | server 123.456.789.0:8080; | ||
| 105 | } | ||
| 106 | |||
| 107 | server { | ||
| 108 | listen 80; | ||
| 109 | listen [::]:443 ssl; | ||
| 110 | listen 443 ssl; | ||
| 111 | |||
| 112 | location / { | ||
| 113 | proxy_pass http://wger; | ||
| 114 | proxy_set_header Host $http_host; | ||
| 115 | proxy_set_header X-Real-IP $remote_addr; | ||
| 116 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 117 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 118 | proxy_redirect off; | ||
| 119 | } | ||
| 120 | |||
| 121 | server_name my.domain.example.com; | ||
| 122 | |||
| 123 | ssl_certificate /path/to/https/certificate.crt; | ||
| 124 | ssl_certificate_key /path/to/https/certificate.key; | ||
| 125 | } | ||
| 126 | #+end_src | ||
| 127 | |||
| 128 | * Thoughts on Wger | ||
| 129 | |||
| 130 | I'm still playing around with the app itself, but it seems to be a solid | ||
| 131 | all-around workout manager, weight log, and food log. | ||
| 132 | |||
| 133 | I like that the weight log graph is fluid and updates quickly. You can | ||
| 134 | also import or export data in CSV format if you'd like to move your data | ||
| 135 | elsewhere. | ||
| 136 | |||
| 137 | The workout manager is slightly odd, as it requires you to enter sets | ||
| 138 | and reps for each exercise when you enter it into the plan. Then, when | ||
| 139 | you add a log entry for performing a workout, you then add what you | ||
| 140 | actually performed, in terms of reps and weight. | ||
| 141 | |||
| 142 | I haven't tried the food log yet and I likely will not, at least for a | ||
| 143 | while. I have no need for a food log or calorie tracker at the moment. | ||
content/blog/2023-02-02-exploring-hare.md deleted −161
| @@ -1,161 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-02-02 | ||
| 3 | title = "Exploring the Hare Programming Language" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # A Quick Note | ||
| 9 | |||
| 10 | By no means am I a professional developer, so this post will be rather short. I | ||
| 11 | won't be going into depth on the specification or anything that technical. | ||
| 12 | |||
| 13 | Instead, I will simply be talking about how I (a relatively basic hobbyist | ||
| 14 | programmer) have been playing with Hare and what intrigues me about the | ||
| 15 | language. | ||
| 16 | |||
| 17 | # Hare | ||
| 18 | |||
| 19 | The [Hare](https://harelang.org) programming language is a straightforward | ||
| 20 | language that should look familiar if you've ever programmed with C, Rust, or | ||
| 21 | other languages that aim to build software at the system-level. | ||
| 22 | |||
| 23 | The Hare homepage states the following: | ||
| 24 | |||
| 25 | > Hare is a systems programming language designed to be simple, stable, and | ||
| 26 | > robust. Hare uses a static type system, manual memory management, and minimal | ||
| 27 | > runtime. It is well-suited to writing operating systems, system tools, | ||
| 28 | > compilers, networking software, and other low-level, high performance tasks. | ||
| 29 | |||
| 30 | I have found this all to be true while playing with it for the first time today. | ||
| 31 | In the next few sections, I'm going to walk through my installation and first | ||
| 32 | program. | ||
| 33 | |||
| 34 | ## Installation | ||
| 35 | |||
| 36 | I'm currently running Alpine Linux on my Thinkpad, so the installation was quite | ||
| 37 | easy as there is a package for Hare in the `apk` repositories. | ||
| 38 | |||
| 39 | ```sh | ||
| 40 | doas apk add hare hare-doc | ||
| 41 | ``` | ||
| 42 | |||
| 43 | However, I was able to install Hare from scratch on Fedora Linux a short while | ||
| 44 | ago, which was also very easy to do. If you need further instructions and Hare | ||
| 45 | doesn't have a package on your system, take a look at the [Hare | ||
| 46 | Installation](https://harelang.org/installation/) page. | ||
| 47 | |||
| 48 | ## Creating a Test Project | ||
| 49 | |||
| 50 | In order to play with the language, I created | ||
| 51 | [hare-test](https://git.sr.ht/~cyborg/learning-hare) and will be putting any of my | ||
| 52 | Hare-related adventures in here. | ||
| 53 | |||
| 54 | > **Update:** I also created a simple Hare program for creating a file from user | ||
| 55 | > input: | ||
| 56 | > [files.ha](hhttps://git.sr.ht/~cyborg/learning-hare/blob/main/files/files.ha) | ||
| 57 | |||
| 58 | Luckily, Hare doesn't require any complex set-up tools or build environment. | ||
| 59 | Once you have Hare installed, you simply need to create a file ending with `.ha` | ||
| 60 | and you can run a Hare program. | ||
| 61 | |||
| 62 | I created a file called `rgb.ha` in order to test out the random number | ||
| 63 | generation and passing parameters between functions. | ||
| 64 | |||
| 65 | ```sh | ||
| 66 | nano rgb.ha | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Within this file, I was able to easily import a few of the [standard library | ||
| 70 | modules](https://harelang.org/tutorials/stdlib/): `fmt`, `math::random`, and | ||
| 71 | `datetime`. | ||
| 72 | |||
| 73 | With these modules, I created two functions: | ||
| 74 | |||
| 75 | 1. `main`: This function calls the `generate_rgb` function and then prints out | ||
| 76 | the returned values. | ||
| 77 | 2. `generate_rgb`: This function uses the current Unix epoch time to generate a | ||
| 78 | pseudo-random value and uses this value to create three more random values | ||
| 79 | between 0 and 255. These three numbers represent a color in RGB format. | ||
| 80 | |||
| 81 | > **Note**: Some syntax coloring may look odd, as Zola currently doesn't have a | ||
| 82 | > syntax highlighting theme for Hare. Instead, I'm using the C theme, which may | ||
| 83 | > not be exactly accurate when coloring the code below. | ||
| 84 | |||
| 85 | ```c | ||
| 86 | use datetime; | ||
| 87 | use fmt; | ||
| 88 | use math::random; | ||
| 89 | |||
| 90 | export fn main() void = { | ||
| 91 | const rgb = generate_rgb(); | ||
| 92 | fmt::printfln("RGB: ({}, {}, {})", rgb[0], rgb[1], rgb[2])!; | ||
| 93 | }; | ||
| 94 | |||
| 95 | fn generate_rgb() []u64 = { | ||
| 96 | // Use the current Unix epoch time as the seed value | ||
| 97 | let datetime = datetime::epochunix(&datetime::now()); | ||
| 98 | |||
| 99 | // Generate initial pseudo-random value | ||
| 100 | // You must cast the datetime from int to u64 | ||
| 101 | let x = random::init(datetime: u64); | ||
| 102 | |||
| 103 | // Generate RGB values between (0, 255) using pseudo-random init value | ||
| 104 | let r = random::u64n(&x, 255); | ||
| 105 | let g = random::u64n(&x, 255); | ||
| 106 | let b = random::u64n(&x, 255); | ||
| 107 | |||
| 108 | // Structure data as array and return | ||
| 109 | let rgb_array: [3]u64 = [r, g, b]; | ||
| 110 | return rgb_array; | ||
| 111 | }; | ||
| 112 | ``` | ||
| 113 | |||
| 114 | ## Running a Program | ||
| 115 | |||
| 116 | Once you have a Hare file written and ready to run, you simply need to run it: | ||
| 117 | |||
| 118 | ```sh | ||
| 119 | hare run file.ha | ||
| 120 | ``` | ||
| 121 | |||
| 122 | You can also compile the program into an executable: | ||
| 123 | |||
| 124 | ```sh | ||
| 125 | hare build -o example file.ha | ||
| 126 | ./example | ||
| 127 | ``` | ||
| 128 | |||
| 129 | ## Initial Thoughts | ||
| 130 | |||
| 131 | 1. Documentation Improvements Would Help | ||
| 132 | |||
| 133 | While I was able to piece everything together eventually, the biggest | ||
| 134 | downfall right now in Hare's documentation. For such a new project, the | ||
| 135 | documentation is in a great spot. However, bare specifications don't help as | ||
| 136 | much as a brief examples section would. | ||
| 137 | |||
| 138 | For example, it took me a while to figure out what the `u64n` function was | ||
| 139 | looking for. I could tell that it took two parameters and the second was my | ||
| 140 | max value (255), but couldn't figure out what the first value should be. | ||
| 141 | Eventually, I inspected the `random.ha` file in the [Hare source | ||
| 142 | code](https://git.sr.ht/~sircmpwn/hare/tree/master/item/math/random/random.ha) | ||
| 143 | and found the test suite that helped me discover that it needed an `init()` | ||
| 144 | value in the form of `&var`. | ||
| 145 | |||
| 146 | 2. More Basic Modules | ||
| 147 | |||
| 148 | This is another point that comes from Hare being new and awaiting more | ||
| 149 | contributions, but there are some basic functions that I would personally | ||
| 150 | enjoy seeing in Hare, such as one to convert decimal (base 10) values to | ||
| 151 | hexadecimal (base 16). | ||
| 152 | |||
| 153 | If I'm feeling comfortable with my math, I may work on the list of functions | ||
| 154 | I want and see if any can make it into the Hare source code. | ||
| 155 | |||
| 156 | 3. Overall Thoughts | ||
| 157 | |||
| 158 | Overall, I actually really enjoy Hare. It's not as tedious to get a project | ||
| 159 | up and running as Rust, but it's also simpler and more user-friendly than | ||
| 160 | learning C. I am going to continue playing with it and see if I can make | ||
| 161 | anything of particular value. | ||
content/blog/2023-02-02-exploring-hare.org added +172
| @@ -0,0 +1,172 @@ | |||
| 1 | #+date: <2023-02-02> | ||
| 2 | #+title: Exploring the Hare Programming Language | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * A Quick Note | ||
| 7 | |||
| 8 | By no means am I a professional developer, so this post will be rather | ||
| 9 | short. I won't be going into depth on the specification or anything that | ||
| 10 | technical. | ||
| 11 | |||
| 12 | Instead, I will simply be talking about how I (a relatively basic | ||
| 13 | hobbyist programmer) have been playing with Hare and what intrigues me | ||
| 14 | about the language. | ||
| 15 | |||
| 16 | * Hare | ||
| 17 | |||
| 18 | The [[https://harelang.org][Hare]] programming language is a | ||
| 19 | straightforward language that should look familiar if you've ever | ||
| 20 | programmed with C, Rust, or other languages that aim to build software | ||
| 21 | at the system-level. | ||
| 22 | |||
| 23 | The Hare homepage states the following: | ||
| 24 | |||
| 25 | #+begin_quote | ||
| 26 | Hare is a systems programming language designed to be simple, stable, | ||
| 27 | and robust. Hare uses a static type system, manual memory management, | ||
| 28 | and minimal runtime. It is well-suited to writing operating systems, | ||
| 29 | system tools, compilers, networking software, and other low-level, high | ||
| 30 | performance tasks. | ||
| 31 | #+end_quote | ||
| 32 | |||
| 33 | I have found this all to be true while playing with it for the first | ||
| 34 | time today. In the next few sections, I'm going to walk through my | ||
| 35 | installation and first program. | ||
| 36 | |||
| 37 | ** Installation | ||
| 38 | |||
| 39 | I'm currently running Alpine Linux on my Thinkpad, so the installation | ||
| 40 | was quite easy as there is a package for Hare in the =apk= repositories. | ||
| 41 | |||
| 42 | #+begin_src sh | ||
| 43 | doas apk add hare hare-doc | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | However, I was able to install Hare from scratch on Fedora Linux a short | ||
| 47 | while ago, which was also very easy to do. If you need further | ||
| 48 | instructions and Hare doesn't have a package on your system, take a look | ||
| 49 | at the [[https://harelang.org/installation/][Hare Installation]] page. | ||
| 50 | |||
| 51 | ** Creating a Test Project | ||
| 52 | |||
| 53 | In order to play with the language, I created | ||
| 54 | [[https://git.sr.ht/~cyborg/learning-hare][hare-test]] and will be | ||
| 55 | putting any of my Hare-related adventures in here. | ||
| 56 | |||
| 57 | #+begin_quote | ||
| 58 | *Update:* I also created a simple Hare program for creating a file from | ||
| 59 | user input: | ||
| 60 | [[https://git.sr.ht/~cyborg/learning-hare/blob/main/files/files.ha][files.ha]] | ||
| 61 | #+end_quote | ||
| 62 | |||
| 63 | Luckily, Hare doesn't require any complex set-up tools or build | ||
| 64 | environment. Once you have Hare installed, you simply need to create a | ||
| 65 | file ending with =.ha= and you can run a Hare program. | ||
| 66 | |||
| 67 | I created a file called =rgb.ha= in order to test out the random number | ||
| 68 | generation and passing parameters between functions. | ||
| 69 | |||
| 70 | #+begin_src sh | ||
| 71 | nano rgb.ha | ||
| 72 | #+end_src | ||
| 73 | |||
| 74 | Within this file, I was able to easily import a few of the | ||
| 75 | [[https://harelang.org/tutorials/stdlib/][standard library modules]]: | ||
| 76 | =fmt=, =math::random=, and =datetime=. | ||
| 77 | |||
| 78 | With these modules, I created two functions: | ||
| 79 | |||
| 80 | 1. =main=: This function calls the =generate_rgb= function and then | ||
| 81 | prints out the returned values. | ||
| 82 | 2. =generate_rgb=: This function uses the current Unix epoch time to | ||
| 83 | generate a pseudo-random value and uses this value to create three | ||
| 84 | more random values between 0 and 255. These three numbers represent a | ||
| 85 | color in RGB format. | ||
| 86 | |||
| 87 | #+begin_quote | ||
| 88 | *Note*: Some syntax coloring may look odd, as Zola currently doesn't | ||
| 89 | have a syntax highlighting theme for Hare. Instead, I'm using the C | ||
| 90 | theme, which may not be exactly accurate when coloring the code below. | ||
| 91 | #+end_quote | ||
| 92 | |||
| 93 | #+begin_src C | ||
| 94 | use datetime; | ||
| 95 | use fmt; | ||
| 96 | use math::random; | ||
| 97 | |||
| 98 | export fn main() void = { | ||
| 99 | const rgb = generate_rgb(); | ||
| 100 | fmt::printfln("RGB: ({}, {}, {})", rgb[0], rgb[1], rgb[2])!; | ||
| 101 | }; | ||
| 102 | |||
| 103 | fn generate_rgb() []u64 = { | ||
| 104 | // Use the current Unix epoch time as the seed value | ||
| 105 | let datetime = datetime::epochunix(&datetime::now()); | ||
| 106 | |||
| 107 | // Generate initial pseudo-random value | ||
| 108 | // You must cast the datetime from int to u64 | ||
| 109 | let x = random::init(datetime: u64); | ||
| 110 | |||
| 111 | // Generate RGB values between (0, 255) using pseudo-random init value | ||
| 112 | let r = random::u64n(&x, 255); | ||
| 113 | let g = random::u64n(&x, 255); | ||
| 114 | let b = random::u64n(&x, 255); | ||
| 115 | |||
| 116 | // Structure data as array and return | ||
| 117 | let rgb_array: [3]u64 = [r, g, b]; | ||
| 118 | return rgb_array; | ||
| 119 | }; | ||
| 120 | #+end_src | ||
| 121 | |||
| 122 | ** Running a Program | ||
| 123 | |||
| 124 | Once you have a Hare file written and ready to run, you simply need to | ||
| 125 | run it: | ||
| 126 | |||
| 127 | #+begin_src sh | ||
| 128 | hare run file.ha | ||
| 129 | #+end_src | ||
| 130 | |||
| 131 | You can also compile the program into an executable: | ||
| 132 | |||
| 133 | #+begin_src sh | ||
| 134 | hare build -o example file.ha | ||
| 135 | ./example | ||
| 136 | #+end_src | ||
| 137 | |||
| 138 | ** Initial Thoughts | ||
| 139 | |||
| 140 | 1. Documentation Improvements Would Help | ||
| 141 | |||
| 142 | While I was able to piece everything together eventually, the biggest | ||
| 143 | downfall right now in Hare's documentation. For such a new project, | ||
| 144 | the documentation is in a great spot. However, bare specifications | ||
| 145 | don't help as much as a brief examples section would. | ||
| 146 | |||
| 147 | For example, it took me a while to figure out what the =u64n= | ||
| 148 | function was looking for. I could tell that it took two parameters | ||
| 149 | and the second was my max value (255), but couldn't figure out what | ||
| 150 | the first value should be. Eventually, I inspected the =random.ha= | ||
| 151 | file in the | ||
| 152 | [[https://git.sr.ht/~sircmpwn/hare/tree/master/item/math/random/random.ha][Hare | ||
| 153 | source code]] and found the test suite that helped me discover that | ||
| 154 | it needed an =init()= value in the form of =&var=. | ||
| 155 | |||
| 156 | 2. More Basic Modules | ||
| 157 | |||
| 158 | This is another point that comes from Hare being new and awaiting | ||
| 159 | more contributions, but there are some basic functions that I would | ||
| 160 | personally enjoy seeing in Hare, such as one to convert decimal | ||
| 161 | (base 10) values to hexadecimal (base 16). | ||
| 162 | |||
| 163 | If I'm feeling comfortable with my math, I may work on the list of | ||
| 164 | functions I want and see if any can make it into the Hare source | ||
| 165 | code. | ||
| 166 | |||
| 167 | 3. Overall Thoughts | ||
| 168 | |||
| 169 | Overall, I actually really enjoy Hare. It's not as tedious to get a | ||
| 170 | project up and running as Rust, but it's also simpler and more | ||
| 171 | user-friendly than learning C. I am going to continue playing with it | ||
| 172 | and see if I can make anything of particular value. | ||
content/blog/2023-05-22-burnout.md deleted −45
| @@ -1,45 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-05-22 | ||
| 3 | title = "RE: Burnout" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # RE: Burnout | ||
| 9 | |||
| 10 | I recently read [Drew DeVault's post on | ||
| 11 | burnout](https://drewdevault.com/2023/05/01/2023-05-01-Burnout.html) around the | ||
| 12 | same time I was pulling out of a burnout rut myself earlier this month. Finally, | ||
| 13 | seeing the light at the end of my burnout tunnel made me want to write my first | ||
| 14 | post back on this topic. | ||
| 15 | |||
| 16 | # Busy Seasons on Busy Seasons | ||
| 17 | |||
| 18 | My career deals with busy seasons, generally driven by client demand. This last | ||
| 19 | year, I dealt with a harsh busy season from Aug to Oct 2022 to issue a few SOC | ||
| 20 | reports for the period ending 2022-09-30. Immediately following that, I had to | ||
| 21 | pivot and found another busy season from Oct to Jan for financial statement | ||
| 22 | audits ending on 2022-12-31. Then again, supporting other clients from Jan to | ||
| 23 | Mar 2023, followed by my current client workload aiming for SOC reports due on | ||
| 24 | 2023-06-30. | ||
| 25 | |||
| 26 | The result? A busy season that has lasted from August 2022 through today. I will | ||
| 27 | likely be rushing throughout the next month or two before I have a brief break | ||
| 28 | and need to focus on the 2023-09-30 SOC reports again. While auditing and | ||
| 29 | consulting always involve a busy season, this is the first time I've had one | ||
| 30 | last 9+ months without a break. | ||
| 31 | |||
| 32 | While it's been tough, I have a handful of breaks pre-planned throughout this | ||
| 33 | next cycle and should be able to moderate the level of commitment required for | ||
| 34 | each client. | ||
| 35 | |||
| 36 | # Refocusing | ||
| 37 | |||
| 38 | Outside of work, I finally have time to work on hobbies such as this website, | ||
| 39 | programming, athletics, games, etc. | ||
| 40 | |||
| 41 | You may have noticed my absence if you're in the same channels, forums, and | ||
| 42 | rooms that I am, but I should finally be active again. | ||
| 43 | |||
| 44 | I'm hoping to break an item out of my backlog soon and start working on building | ||
| 45 | a new project or hack around with a stale one. | ||
content/blog/2023-05-22-burnout.org added +44
| @@ -0,0 +1,44 @@ | |||
| 1 | #+date: <2023-05-22> | ||
| 2 | #+title: RE: Burnout | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * RE: Burnout | ||
| 7 | |||
| 8 | I recently read | ||
| 9 | [[https://drewdevault.com/2023/05/01/2023-05-01-Burnout.html][Drew | ||
| 10 | DeVault's post on burnout]] around the same time I was pulling out of a | ||
| 11 | burnout rut myself earlier this month. Finally, seeing the light at the | ||
| 12 | end of my burnout tunnel made me want to write my first post back on | ||
| 13 | this topic. | ||
| 14 | |||
| 15 | * Busy Seasons on Busy Seasons | ||
| 16 | |||
| 17 | My career deals with busy seasons, generally driven by client demand. | ||
| 18 | This last year, I dealt with a harsh busy season from Aug to Oct 2022 to | ||
| 19 | issue a few SOC reports for the period ending 2022-09-30. Immediately | ||
| 20 | following that, I had to pivot and found another busy season from Oct to | ||
| 21 | Jan for financial statement audits ending on 2022-12-31. Then again, | ||
| 22 | supporting other clients from Jan to Mar 2023, followed by my current | ||
| 23 | client workload aiming for SOC reports due on 2023-06-30. | ||
| 24 | |||
| 25 | The result? A busy season that has lasted from August 2022 through | ||
| 26 | today. I will likely be rushing throughout the next month or two before | ||
| 27 | I have a brief break and need to focus on the 2023-09-30 SOC reports | ||
| 28 | again. While auditing and consulting always involve a busy season, this | ||
| 29 | is the first time I've had one last 9+ months without a break. | ||
| 30 | |||
| 31 | While it's been tough, I have a handful of breaks pre-planned throughout | ||
| 32 | this next cycle and should be able to moderate the level of commitment | ||
| 33 | required for each client. | ||
| 34 | |||
| 35 | * Refocusing | ||
| 36 | |||
| 37 | Outside of work, I finally have time to work on hobbies such as this | ||
| 38 | website, programming, athletics, games, etc. | ||
| 39 | |||
| 40 | You may have noticed my absence if you're in the same channels, forums, | ||
| 41 | and rooms that I am, but I should finally be active again. | ||
| 42 | |||
| 43 | I'm hoping to break an item out of my backlog soon and start working on | ||
| 44 | building a new project or hack around with a stale one. | ||
content/blog/2023-06-08-goaccess-geoip.md deleted −52
| @@ -1,52 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-06-08 | ||
| 3 | title = "Inspecting Nginx Logs with GoAccess and MaxMind GeoIP Data" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | [GoAccess](https://goaccess.io/) is an open source real-time web log analyzer | ||
| 11 | and interactive viewer that runs in a terminal in \*nix systems or through your | ||
| 12 | browser. | ||
| 13 | |||
| 14 | # Installation | ||
| 15 | |||
| 16 | To start, you'll need to install GoAccess for your OS. Here's an example for | ||
| 17 | Debian-based distros: | ||
| 18 | |||
| 19 | ```sh | ||
| 20 | sudo apt install goaccess | ||
| 21 | ``` | ||
| 22 | |||
| 23 | Next, find any number of the MaxMind GeoIP database files on GitHub or another | ||
| 24 | file hosting website. We're going to use P3TERX's version in this example: | ||
| 25 | |||
| 26 | ```sh | ||
| 27 | wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb | ||
| 28 | ``` | ||
| 29 | |||
| 30 | Be sure to save this file in an easy to remember location! | ||
| 31 | |||
| 32 | # Usage | ||
| 33 | |||
| 34 | In order to utilize the full capabilities of GoAccess and MMDB, start with the | ||
| 35 | command template below and customize as necessary. This will export an HTML view | ||
| 36 | of the GoAccess dashboard, showing all relevant information related to that | ||
| 37 | site's access log. You can also omit the `-o output.html` parameter if you | ||
| 38 | prefer to view the data within the CLI instead of creating an HTML file. | ||
| 39 | |||
| 40 | With the addition of the GeoIP Database parameter, section `16 - Geo Location` | ||
| 41 | will be added with the various countries that are associated with the collected | ||
| 42 | IP addresses. | ||
| 43 | |||
| 44 | ```sh | ||
| 45 | zcat /var/log/nginx/example.access.log.*.gz | goaccess \ | ||
| 46 | --geoip-database=/home/user/GeoLite2-City.mmdb \ | ||
| 47 | --date-format=%d/%b/%Y \ | ||
| 48 | --time-format=%H:%M:%S \ | ||
| 49 | --log-format=COMBINED \ | ||
| 50 | -o output.html \ | ||
| 51 | /var/log/nginx/example.access.log - | ||
| 52 | ```\ No newline at end of file | ||
content/blog/2023-06-08-goaccess-geoip.org added +52
| @@ -0,0 +1,52 @@ | |||
| 1 | #+date: <2023-06-08> | ||
| 2 | #+title: Inspecting Nginx Logs with GoAccess and MaxMind GeoIP Data | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | [[https://goaccess.io/][GoAccess]] is an open source real-time web log | ||
| 9 | analyzer and interactive viewer that runs in a terminal in *nix systems | ||
| 10 | or through your browser. | ||
| 11 | |||
| 12 | * Installation | ||
| 13 | |||
| 14 | To start, you'll need to install GoAccess for your OS. Here's an example | ||
| 15 | for Debian-based distros: | ||
| 16 | |||
| 17 | #+begin_src sh | ||
| 18 | sudo apt install goaccess | ||
| 19 | #+end_src | ||
| 20 | |||
| 21 | Next, find any number of the MaxMind GeoIP database files on GitHub or | ||
| 22 | another file hosting website. We're going to use P3TERX's version in | ||
| 23 | this example: | ||
| 24 | |||
| 25 | #+begin_src sh | ||
| 26 | wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb | ||
| 27 | #+end_src | ||
| 28 | |||
| 29 | Be sure to save this file in an easy to remember location! | ||
| 30 | |||
| 31 | * Usage | ||
| 32 | |||
| 33 | In order to utilize the full capabilities of GoAccess and MMDB, start | ||
| 34 | with the command template below and customize as necessary. This will | ||
| 35 | export an HTML view of the GoAccess dashboard, showing all relevant | ||
| 36 | information related to that site's access log. You can also omit the | ||
| 37 | =-o output.html= parameter if you prefer to view the data within the CLI | ||
| 38 | instead of creating an HTML file. | ||
| 39 | |||
| 40 | With the addition of the GeoIP Database parameter, section | ||
| 41 | =16 - Geo Location= will be added with the various countries that are | ||
| 42 | associated with the collected IP addresses. | ||
| 43 | |||
| 44 | #+begin_src sh | ||
| 45 | zcat /var/log/nginx/example.access.log.*.gz | goaccess \ | ||
| 46 | --geoip-database=/home/user/GeoLite2-City.mmdb \ | ||
| 47 | --date-format=%d/%b/%Y \ | ||
| 48 | --time-format=%H:%M:%S \ | ||
| 49 | --log-format=COMBINED \ | ||
| 50 | -o output.html \ | ||
| 51 | /var/log/nginx/example.access.log - | ||
| 52 | #+end_src | ||
content/blog/2023-06-08-self-hosting-baikal.md → content/blog/2023-06-08-self-hosting-baikal.org renamed +71 −71
| @@ -1,34 +1,32 @@ | |||
| 1 | +++ | 1 | #+date: <2023-06-08> |
| 2 | date = 2023-06-08 | 2 | #+title: Self-Hosting Baikal Server (CalDAV & CardDAV) |
| 3 | title = "Self-Hosting Baikal Server (CalDAV & CardDAV)" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # What is Baikal? | ||
| 9 | 5 | ||
| 10 | [Baikal](https://sabre.io/baikal/) is a lightweight CalDAV + CardDAV server that | 6 | * What is Baikal? |
| 11 | you can self-host on your own machine. While I have tried (& failed) to get this | ||
| 12 | CalDAV + CardDAV server running before, it was quite easy this time. Not really | ||
| 13 | sure what I did differently this time, but I'm documenting my process here to | ||
| 14 | ensure I don't forget. | ||
| 15 | 7 | ||
| 16 | # Installation | 8 | [[https://sabre.io/baikal/][Baikal]] is a lightweight CalDAV + CardDAV |
| 9 | server that you can self-host on your own machine. While I have tried (& | ||
| 10 | failed) to get this CalDAV + CardDAV server running before, it was quite | ||
| 11 | easy this time. Not really sure what I did differently this time, but | ||
| 12 | I'm documenting my process here to ensure I don't forget. | ||
| 17 | 13 | ||
| 18 | First, create a folder on your server and open a `docker-compose.yml` file for | 14 | * Installation |
| 19 | editing: | ||
| 20 | 15 | ||
| 21 | ```sh | 16 | First, create a folder on your server and open a =docker-compose.yml= |
| 17 | file for editing: | ||
| 18 | |||
| 19 | #+begin_src sh | ||
| 22 | mkdir baikal && cd baikal | 20 | mkdir baikal && cd baikal |
| 23 | nano docker-compose.yml | 21 | nano docker-compose.yml |
| 24 | ``` | 22 | #+end_src |
| 25 | 23 | ||
| 26 | Within this file, you'll need to paste the information below. You can customize | 24 | Within this file, you'll need to paste the information below. You can |
| 27 | the `ports` section to use any port on your server to pass through to port 80 in | 25 | customize the =ports= section to use any port on your server to pass |
| 28 | the container. You can also edit the `volumes` section to use docker volumes | 26 | through to port 80 in the container. You can also edit the =volumes= |
| 29 | instead of local folders. | 27 | section to use docker volumes instead of local folders. |
| 30 | 28 | ||
| 31 | ```conf | 29 | #+begin_src conf |
| 32 | version: "2" | 30 | version: "2" |
| 33 | services: | 31 | services: |
| 34 | baikal: | 32 | baikal: |
| @@ -39,58 +37,59 @@ services: | |||
| 39 | volumes: | 37 | volumes: |
| 40 | - ./config:/var/www/baikal/config | 38 | - ./config:/var/www/baikal/config |
| 41 | - ./data:/var/www/baikal/Specific | 39 | - ./data:/var/www/baikal/Specific |
| 42 | ``` | 40 | #+end_src |
| 43 | 41 | ||
| 44 | Once finished with editing, save and close the file. Then, launch the docker | 42 | Once finished with editing, save and close the file. Then, launch the |
| 45 | container: | 43 | docker container: |
| 46 | 44 | ||
| 47 | ```sh | 45 | #+begin_src sh |
| 48 | sudo docker-compose up -d | 46 | sudo docker-compose up -d |
| 49 | ``` | 47 | #+end_src |
| 50 | 48 | ||
| 51 | # Intial Setup | 49 | * Intial Setup |
| 52 | 50 | ||
| 53 | As long as no issues came up when starting the container, you should be able to | 51 | As long as no issues came up when starting the container, you should be |
| 54 | visit the server's set-up page at `http://<server_ip>:<port>`. The application | 52 | able to visit the server's set-up page at =http://<server_ip>:<port>=. |
| 55 | will ask you to create an administrator account and choose the database type for | 53 | The application will ask you to create an administrator account and |
| 56 | your storage. Personally, I opted to use SQLite. | 54 | choose the database type for your storage. Personally, I opted to use |
| 55 | SQLite. | ||
| 57 | 56 | ||
| 58 | Make sure the administrator credentials are adequate to protect against common | 57 | Make sure the administrator credentials are adequate to protect against |
| 59 | attacks. | 58 | common attacks. |
| 60 | 59 | ||
| 61 | # Creating Users | 60 | * Creating Users |
| 62 | 61 | ||
| 63 | Once you've set up the application, you will be greeted by the Dashboard page, | 62 | Once you've set up the application, you will be greeted by the Dashboard |
| 64 | which will show the version of the app, status of the admin/CalDAV/CardDAV | 63 | page, which will show the version of the app, status of the |
| 65 | services, and the number of users, calendars, events, address books, and | 64 | admin/CalDAV/CardDAV services, and the number of users, calendars, |
| 66 | contacts. | 65 | events, address books, and contacts. |
| 67 | 66 | ||
| 68 | To create a new user, navigate to the `Users and resources` page. This process | 67 | To create a new user, navigate to the =Users and resources= page. This |
| 69 | is as simple as entering a username, password, and email. | 68 | process is as simple as entering a username, password, and email. |
| 70 | 69 | ||
| 71 | Once a user has been created, you can create any number of calendars and address | 70 | Once a user has been created, you can create any number of calendars and |
| 72 | books for user, as well as inspect their information. | 71 | address books for user, as well as inspect their information. |
| 73 | 72 | ||
| 74 | # Setting Up a Public URL | 73 | * Setting Up a Public URL |
| 75 | 74 | ||
| 76 | Once your application is working locally, you can open access remotely via a URL | 75 | Once your application is working locally, you can open access remotely |
| 77 | by using a reverse-proxy like Nginx. | 76 | via a URL by using a reverse-proxy like Nginx. |
| 78 | 77 | ||
| 79 | As a prerequisite, you must own a domain name and set up DNS records to point a | 78 | As a prerequisite, you must own a domain name and set up DNS records to |
| 80 | domain name to the server hosting Baikal. | 79 | point a domain name to the server hosting Baikal. |
| 81 | 80 | ||
| 82 | Start by navigating to your web server's configuration directory and create a | 81 | Start by navigating to your web server's configuration directory and |
| 83 | new file for this application. | 82 | create a new file for this application. |
| 84 | 83 | ||
| 85 | ```sh | 84 | #+begin_src sh |
| 86 | cd /etc/nginx/sites-available/ | 85 | cd /etc/nginx/sites-available/ |
| 87 | nano dav | 86 | nano dav |
| 88 | ``` | 87 | #+end_src |
| 89 | 88 | ||
| 90 | Within this file, paste in the configuration from below and change | 89 | Within this file, paste in the configuration from below and change |
| 91 | `dav.example.com` to match the URL you'll be using. | 90 | =dav.example.com= to match the URL you'll be using. |
| 92 | 91 | ||
| 93 | ```conf | 92 | #+begin_src conf |
| 94 | server { | 93 | server { |
| 95 | if ($host ~ ^[^.]+\.example\.com$) { | 94 | if ($host ~ ^[^.]+\.example\.com$) { |
| 96 | return 301 https://$host$request_uri; | 95 | return 301 https://$host$request_uri; |
| @@ -120,34 +119,35 @@ server { | |||
| 120 | include /etc/letsencrypt/options-ssl-nginx.conf; | 119 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 121 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | 120 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 122 | } | 121 | } |
| 123 | ``` | 122 | #+end_src |
| 124 | 123 | ||
| 125 | For Nginx on Ubuntu, you'll need to symlink the configuration file to the | 124 | For Nginx on Ubuntu, you'll need to symlink the configuration file to |
| 126 | `sites-enabled` directory and then restart Nginx. | 125 | the =sites-enabled= directory and then restart Nginx. |
| 127 | 126 | ||
| 128 | ```sh | 127 | #+begin_src sh |
| 129 | sudo ln -s /etc/nginx/sites-available/dav /etc/nginx/sites-enabled/dav | 128 | sudo ln -s /etc/nginx/sites-available/dav /etc/nginx/sites-enabled/dav |
| 130 | sudo systemctl restart nginx.service | 129 | sudo systemctl restart nginx.service |
| 131 | ``` | 130 | #+end_src |
| 132 | 131 | ||
| 133 | At this point, the Baikal server should be available over the internet at the | 132 | At this point, the Baikal server should be available over the internet |
| 134 | URL configured above! | 133 | at the URL configured above! |
| 135 | 134 | ||
| 136 | # Configuring Clients | 135 | * Configuring Clients |
| 137 | 136 | ||
| 138 | Lastly, configuring clients and applications is essential to making sure the DAV | 137 | Lastly, configuring clients and applications is essential to making sure |
| 139 | server is being fully utilized. | 138 | the DAV server is being fully utilized. |
| 140 | 139 | ||
| 141 | You can also use the `Users and resources` page to inspect a user's personal | 140 | You can also use the =Users and resources= page to inspect a user's |
| 142 | link to their calendars and address books by clicking the info (i) button. It | 141 | personal link to their calendars and address books by clicking the info |
| 143 | will show a URI like `/dav.php/calendars/your-user/default/`. | 142 | (i) button. It will show a URI like |
| 143 | =/dav.php/calendars/your-user/default/=. | ||
| 144 | 144 | ||
| 145 | However, I found that the following URL works for most applications: | 145 | However, I found that the following URL works for most applications: |
| 146 | `/dav.php/principals/your-user/`. | 146 | =/dav.php/principals/your-user/=. |
| 147 | 147 | ||
| 148 | I used the `principals` URL above for Thunderbird (calendar, tasks, and | 148 | I used the =principals= URL above for Thunderbird (calendar, tasks, and |
| 149 | contacts), as well as iOS (calendar, tasks, and contacts) and everything works | 149 | contacts), as well as iOS (calendar, tasks, and contacts) and everything |
| 150 | flawlessly so far. | 150 | works flawlessly so far. |
| 151 | 151 | ||
| 152 | Syncing is quick between the server and clients, and I haven't seen any | 152 | Syncing is quick between the server and clients, and I haven't seen any |
| 153 | disruptions in the service or data integrity. | 153 | disruptions in the service or data integrity. |
content/blog/2023-06-18-unifi-ip-blocklist.md deleted −70
| @@ -1,70 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-06-18 | ||
| 3 | title = "Block IP Addresses and Subnets with Unifi Network Firewall" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Identifying Abusive IPs | ||
| 9 | |||
| 10 | If you're like me and use Unifi network equipment at the edge of the network you | ||
| 11 | manage, you may know that Unifi is only somewhat decent at identifying and | ||
| 12 | blocking IPs that represent abusive or threat actors. | ||
| 13 | |||
| 14 | While Unifi has a [threat | ||
| 15 | management](https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Threat-Management) | ||
| 16 | tool inside their Network application, it can be lacking in functionality and | ||
| 17 | identification. For example, I have my UDM Pro set to identify and block almost | ||
| 18 | all categories of threats available within the Unifi settings. However, I | ||
| 19 | regularly identify abusive actors on my web server via the server logs. | ||
| 20 | |||
| 21 | In addition, I have identified IP addresses and subnets directly within Unifi's | ||
| 22 | logs that the UDM did not block for whatever reason. | ||
| 23 | |||
| 24 | This guide is meant to be another step in the process to manually block abusive | ||
| 25 | IP addresses or subnets that you have identified but are not being automatically | ||
| 26 | blocked yet. | ||
| 27 | |||
| 28 | # Create an IP Group Profile | ||
| 29 | |||
| 30 | To start, login to the Unifi machine's web GUI and navigate to the Network app > Settings > Profiles. | ||
| 31 | |||
| 32 | Within this page, choose the `IP Groups` tab and click `Create New`. | ||
| 33 | |||
| 34 | Each IP Group profile can be used as one of three options: | ||
| 35 | |||
| 36 | 1. Port Group | ||
| 37 | 2. IPv4 Address/Subnet | ||
| 38 | 3. IPv6 Address/Subnet | ||
| 39 | |||
| 40 | In this example, I'm creating an IPv4 Address/Subnet group and adding a few | ||
| 41 | different IP addresses and a subnet. Once you've added all IP addresses and | ||
| 42 | subnets, click the `Apply` button that should appear at the bottom. | ||
| 43 | |||
| 44 | At this point, the IPv4 Address/Subnet has been created but not yet used. | ||
| 45 | |||
| 46 | # Drop IP Group Profile via the Unifi Firewall | ||
| 47 | |||
| 48 | To instruct the Unifi machine to block the profile we just created, we need to | ||
| 49 | navigate to the Network app > Settings > Firewall & Security. | ||
| 50 | |||
| 51 | Within this screen, find the Firewall Rules table and click `Create Entry`. This | ||
| 52 | entry should contain the following settings: | ||
| 53 | |||
| 54 | - Type: `Internet In` | ||
| 55 | - Description: `<Your Custom Rule>` | ||
| 56 | - Rule Applied: `Before Predefined Rules` | ||
| 57 | - Action: `Drop` | ||
| 58 | - Source Type: `Port/IP Group` | ||
| 59 | - IPv4 Address Group: `<Name of the Group Profile You Created Above>` | ||
| 60 | |||
| 61 | Customize the remaining configurations to your liking, and then save and enable | ||
| 62 | the firewall rule. | ||
| 63 | |||
| 64 | Once enabled, the Unifi machine will be able to drop all incoming connections | ||
| 65 | from the defined IP addresses and subnets within the created profile. | ||
| 66 | |||
| 67 | > As a personal aside to this topic, I'm looking for a convenient way to update | ||
| 68 | > the firewall rules or profiles remotely (within the LAN) from the web server | ||
| 69 | > to accelerate this process. If you have an idea on how to automatically update | ||
| 70 | > Unifi IP groups or firewall rules, let me know! | ||
content/blog/2023-06-18-unifi-ip-blocklist.org added +75
| @@ -0,0 +1,75 @@ | |||
| 1 | #+date: <2023-06-18> | ||
| 2 | #+title: Block IP Addresses and Subnets with Unifi Network Firewall | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Identifying Abusive IPs | ||
| 7 | |||
| 8 | If you're like me and use Unifi network equipment at the edge of the | ||
| 9 | network you manage, you may know that Unifi is only somewhat decent at | ||
| 10 | identifying and blocking IPs that represent abusive or threat actors. | ||
| 11 | |||
| 12 | While Unifi has a | ||
| 13 | [[https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Threat-Management][threat | ||
| 14 | management]] tool inside their Network application, it can be lacking in | ||
| 15 | functionality and identification. For example, I have my UDM Pro set to | ||
| 16 | identify and block almost all categories of threats available within the | ||
| 17 | Unifi settings. However, I regularly identify abusive actors on my web | ||
| 18 | server via the server logs. | ||
| 19 | |||
| 20 | In addition, I have identified IP addresses and subnets directly within | ||
| 21 | Unifi's logs that the UDM did not block for whatever reason. | ||
| 22 | |||
| 23 | This guide is meant to be another step in the process to manually block | ||
| 24 | abusive IP addresses or subnets that you have identified but are not | ||
| 25 | being automatically blocked yet. | ||
| 26 | |||
| 27 | * Create an IP Group Profile | ||
| 28 | |||
| 29 | To start, login to the Unifi machine's web GUI and navigate to the | ||
| 30 | Network app > Settings > Profiles. | ||
| 31 | |||
| 32 | Within this page, choose the =IP Groups= tab and click =Create New=. | ||
| 33 | |||
| 34 | Each IP Group profile can be used as one of three options: | ||
| 35 | |||
| 36 | 1. Port Group | ||
| 37 | 2. IPv4 Address/Subnet | ||
| 38 | 3. IPv6 Address/Subnet | ||
| 39 | |||
| 40 | In this example, I'm creating an IPv4 Address/Subnet group and adding a | ||
| 41 | few different IP addresses and a subnet. Once you've added all IP | ||
| 42 | addresses and subnets, click the =Apply= button that should appear at | ||
| 43 | the bottom. | ||
| 44 | |||
| 45 | At this point, the IPv4 Address/Subnet has been created but not yet | ||
| 46 | used. | ||
| 47 | |||
| 48 | * Drop IP Group Profile via the Unifi Firewall | ||
| 49 | |||
| 50 | To instruct the Unifi machine to block the profile we just created, we | ||
| 51 | need to navigate to the Network app > Settings > Firewall & Security. | ||
| 52 | |||
| 53 | Within this screen, find the Firewall Rules table and click | ||
| 54 | =Create Entry=. This entry should contain the following settings: | ||
| 55 | |||
| 56 | - Type: =Internet In= | ||
| 57 | - Description: =<Your Custom Rule>= | ||
| 58 | - Rule Applied: =Before Predefined Rules= | ||
| 59 | - Action: =Drop= | ||
| 60 | - Source Type: =Port/IP Group= | ||
| 61 | - IPv4 Address Group: =<Name of the Group Profile You Created Above>= | ||
| 62 | |||
| 63 | Customize the remaining configurations to your liking, and then save and | ||
| 64 | enable the firewall rule. | ||
| 65 | |||
| 66 | Once enabled, the Unifi machine will be able to drop all incoming | ||
| 67 | connections from the defined IP addresses and subnets within the created | ||
| 68 | profile. | ||
| 69 | |||
| 70 | #+begin_quote | ||
| 71 | As a personal aside to this topic, I'm looking for a convenient way to | ||
| 72 | update the firewall rules or profiles remotely (within the LAN) from the | ||
| 73 | web server to accelerate this process. If you have an idea on how to | ||
| 74 | automatically update Unifi IP groups or firewall rules, let me know! | ||
| 75 | #+end_quote | ||
content/blog/2023-06-20-audit-review-template.md deleted −77
| @@ -1,77 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-06-20 | ||
| 3 | title = "Audit Testing Review Template" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | This post is a _very_ brief overview on the basic process to review audit test | ||
| 11 | results, focusing on work done as part of a financial statement audit (FSA) or | ||
| 12 | service organization controls (SOC) report. | ||
| 13 | |||
| 14 | While there are numerous different things to review and look for - all varying | ||
| 15 | wildly depending on the report, client, and tester - this list serves as a solid | ||
| 16 | base foundation for a reviewer. | ||
| 17 | |||
| 18 | I have used this throughout my career as a starting point to my reviews, and it | ||
| 19 | has worked wonders for creating a consistent and objective template to my | ||
| 20 | reviews. The goal is to keep this base high-level enough to be used on a wide | ||
| 21 | variety of engagements, while still ensuring that all key areas are covered. | ||
| 22 | |||
| 23 | # Review Template | ||
| 24 | |||
| 25 | 1. [ ] Check all documents for spelling and grammar. | ||
| 26 | 2. [ ] Ensure all acronyms are fully explained upon first use. | ||
| 27 | 3. [ ] For all people referenced, use their full names and job titles upon first | ||
| 28 | use. | ||
| 29 | 4. [ ] All supporting documents must cross-reference to the lead sheet and | ||
| 30 | vice-versa. | ||
| 31 | 5. [ ] Verify that the control has been adequately tested: | ||
| 32 | - [ ] **Test of Design**: Did the tester obtain information regarding how | ||
| 33 | the control should perform normally and abnormally (e.g., emergency | ||
| 34 | scenarios)? | ||
| 35 | - [ ] **Test of Operating Effectiveness**: Did the tester inquire, observe, | ||
| 36 | inspect, or re-perform sufficient evidence to support their conclusion | ||
| 37 | over the control? Inquiry alone is not adequate! | ||
| 38 | 6. [ ] For any information used in the control, whether by the control operator | ||
| 39 | or by the tester, did the tester appropriately document the source | ||
| 40 | (system or person), extraction method, parameters, and completeness and | ||
| 41 | accuracy (C&A)? | ||
| 42 | - [ ] For any reports, queries, etc. used in the extraction, did the tester | ||
| 43 | include a copy and notate C&A considerations? | ||
| 44 | 7. [ ] Did the tester document the specific criteria that the control is being | ||
| 45 | tested against? | ||
| 46 | 8. [ ] Did the tester notate in the supporting documents where each criterion | ||
| 47 | was satisfied? | ||
| 48 | 9. [ ] If testing specific policies or procedures, are the documents adequate? | ||
| 49 | - [ ] e.g., a test to validate that a review of policy XYZ occurs | ||
| 50 | periodically should also evaluate the sufficiency of the policy | ||
| 51 | itself, if meant to cover the risk that such a policy does not exist | ||
| 52 | and is not reviewed. | ||
| 53 | 10. [ ] Does the test cover the appropriate period under review? | ||
| 54 | - [ ] If the test is meant to cover only a portion of the audit period, do | ||
| 55 | other controls exist to mitigate the risks that exist for the | ||
| 56 | remainder of the period? | ||
| 57 | 11. [ ] For any computer-aided audit tools (CAATs) or other automation | ||
| 58 | techniques used in the test, is the use of such tools explained and | ||
| 59 | appropriately documented? | ||
| 60 | 12. [ ] If prior-period documentation exists, are there any missing pieces of | ||
| 61 | evidence that would further enhance the quality of the test? | ||
| 62 | 13. [ ] Was any information discovered during the walkthrough or inquiry phase | ||
| 63 | that was not incorporated into the test? | ||
| 64 | 14. [ ] Are there new rules or expectations from your company's internal | ||
| 65 | guidance or your regulatory bodies that would affect the audit approach | ||
| 66 | for this control? | ||
| 67 | 15. [ ] Was an exception, finding, or deficiency identified as a result of this | ||
| 68 | test? | ||
| 69 | - [ ] Was the control deficient in design, operation, or both? | ||
| 70 | - [ ] What was the root cause of the finding? | ||
| 71 | - [ ] Does the finding indicate other findings or potential fraud? | ||
| 72 | - [ ] What's the severity and scope of the finding? | ||
| 73 | - [ ] Do other controls exist as a form of compensation against the | ||
| 74 | finding's severity, and do they mitigate the risk within the control | ||
| 75 | objective? | ||
| 76 | - [ ] Does the finding exist at the end of the period, or was it resolved | ||
| 77 | within the audit period? | ||
content/blog/2023-06-20-audit-review-template.org added +78
| @@ -0,0 +1,78 @@ | |||
| 1 | #+date: <2023-06-20> | ||
| 2 | #+title: Audit Testing Review Template | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | This post is a /very/ brief overview on the basic process to review | ||
| 9 | audit test results, focusing on work done as part of a financial | ||
| 10 | statement audit (FSA) or service organization controls (SOC) report. | ||
| 11 | |||
| 12 | While there are numerous different things to review and look for - all | ||
| 13 | varying wildly depending on the report, client, and tester - this list | ||
| 14 | serves as a solid base foundation for a reviewer. | ||
| 15 | |||
| 16 | I have used this throughout my career as a starting point to my reviews, | ||
| 17 | and it has worked wonders for creating a consistent and objective | ||
| 18 | template to my reviews. The goal is to keep this base high-level enough | ||
| 19 | to be used on a wide variety of engagements, while still ensuring that | ||
| 20 | all key areas are covered. | ||
| 21 | |||
| 22 | * Review Template | ||
| 23 | |||
| 24 | 1. [ ] Check all documents for spelling and grammar. | ||
| 25 | 2. [ ] Ensure all acronyms are fully explained upon first use. | ||
| 26 | 3. [ ] For all people referenced, use their full names and job titles | ||
| 27 | upon first use. | ||
| 28 | 4. [ ] All supporting documents must cross-reference to the lead sheet | ||
| 29 | and vice-versa. | ||
| 30 | 5. [ ] Verify that the control has been adequately tested: | ||
| 31 | - [ ] *Test of Design*: Did the tester obtain information regarding | ||
| 32 | how the control should perform normally and abnormally (e.g., | ||
| 33 | emergency scenarios)? | ||
| 34 | - [ ] *Test of Operating Effectiveness*: Did the tester inquire, | ||
| 35 | observe, inspect, or re-perform sufficient evidence to support | ||
| 36 | their conclusion over the control? Inquiry alone is not adequate! | ||
| 37 | 6. [ ] For any information used in the control, whether by the control | ||
| 38 | operator or by the tester, did the tester appropriately document the | ||
| 39 | source (system or person), extraction method, parameters, and | ||
| 40 | completeness and accuracy (C&A)? | ||
| 41 | - [ ] For any reports, queries, etc. used in the extraction, did the | ||
| 42 | tester include a copy and notate C&A considerations? | ||
| 43 | 7. [ ] Did the tester document the specific criteria that the control is | ||
| 44 | being tested against? | ||
| 45 | 8. [ ] Did the tester notate in the supporting documents where each | ||
| 46 | criterion was satisfied? | ||
| 47 | 9. [ ] If testing specific policies or procedures, are the documents | ||
| 48 | adequate? | ||
| 49 | - [ ] e.g., a test to validate that a review of policy XYZ occurs | ||
| 50 | periodically should also evaluate the sufficiency of the policy | ||
| 51 | itself, if meant to cover the risk that such a policy does not | ||
| 52 | exist and is not reviewed. | ||
| 53 | 10. [ ] Does the test cover the appropriate period under review? | ||
| 54 | - [ ] If the test is meant to cover only a portion of the audit | ||
| 55 | period, do other controls exist to mitigate the risks that exist | ||
| 56 | for the remainder of the period? | ||
| 57 | 11. [ ] For any computer-aided audit tools (CAATs) or other automation | ||
| 58 | techniques used in the test, is the use of such tools explained and | ||
| 59 | appropriately documented? | ||
| 60 | 12. [ ] If prior-period documentation exists, are there any missing | ||
| 61 | pieces of evidence that would further enhance the quality of the | ||
| 62 | test? | ||
| 63 | 13. [ ] Was any information discovered during the walkthrough or inquiry | ||
| 64 | phase that was not incorporated into the test? | ||
| 65 | 14. [ ] Are there new rules or expectations from your company's internal | ||
| 66 | guidance or your regulatory bodies that would affect the audit | ||
| 67 | approach for this control? | ||
| 68 | 15. [ ] Was an exception, finding, or deficiency identified as a result | ||
| 69 | of this test? | ||
| 70 | - [ ] Was the control deficient in design, operation, or both? | ||
| 71 | - [ ] What was the root cause of the finding? | ||
| 72 | - [ ] Does the finding indicate other findings or potential fraud? | ||
| 73 | - [ ] What's the severity and scope of the finding? | ||
| 74 | - [ ] Do other controls exist as a form of compensation against the | ||
| 75 | finding's severity, and do they mitigate the risk within the | ||
| 76 | control objective? | ||
| 77 | - [ ] Does the finding exist at the end of the period, or was it | ||
| 78 | resolved within the audit period? | ||
content/blog/2023-06-23-byobu.md → content/blog/2023-06-23-byobu.org renamed +26 −26
| @@ -1,44 +1,44 @@ | |||
| 1 | +++ | 1 | #+date: <2023-06-23> |
| 2 | date = 2023-06-23 | 2 | #+title: Byobu |
| 3 | title = "Byobu" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Byobu | ||
| 9 | 5 | ||
| 10 | [byobu](https://www.byobu.org) is a command-line tool that allows you to use | 6 | * Byobu |
| 11 | numerous screens within a single terminal emulator instance. More specifically, | ||
| 12 | it's a text based window manager, using either `screen` or `tmux`. | ||
| 13 | 7 | ||
| 14 | This post is mostly just a self-reference as I explore byobu, so I may come back | 8 | [[https://www.byobu.org][byobu]] is a command-line tool that allows you |
| 15 | later and update this post with more content. | 9 | to use numerous screens within a single terminal emulator instance. More |
| 10 | specifically, it's a text based window manager, using either =screen= or | ||
| 11 | =tmux=. | ||
| 16 | 12 | ||
| 17 | ## Screenshot | 13 | This post is mostly just a self-reference as I explore byobu, so I may |
| 14 | come back later and update this post with more content. | ||
| 18 | 15 | ||
| 19 | Take a look below at my current multi-window set-up in byobu while I write this | 16 | ** Screenshot |
| 20 | blog post: | ||
| 21 | 17 | ||
| 22 |  | 18 | Take a look below at my current multi-window set-up in byobu while I |
| 19 | write this blog post: | ||
| 23 | 20 | ||
| 24 | ### Keybindings | 21 | #+caption: byobu |
| 22 | [[https://img.cleberg.net/blog/20230623-byobu/byobu.png]] | ||
| 25 | 23 | ||
| 26 | You can open the help menu with either of the following commands; they will both | 24 | *** Keybindings |
| 27 | open the same manpage: | ||
| 28 | 25 | ||
| 29 | ```sh | 26 | You can open the help menu with either of the following commands; they |
| 27 | will both open the same manpage: | ||
| 28 | |||
| 29 | #+begin_src sh | ||
| 30 | byobu --help | 30 | byobu --help |
| 31 | # or | 31 | # or |
| 32 | man byobu | 32 | man byobu |
| 33 | ``` | 33 | #+end_src |
| 34 | 34 | ||
| 35 | While the manpage contains a ton of information about the functionality of byobu | 35 | While the manpage contains a ton of information about the functionality |
| 36 | (such as status notifications, sessions, and windows), the first location to | 36 | of byobu (such as status notifications, sessions, and windows), the |
| 37 | explore should be the keybindings section. | 37 | first location to explore should be the keybindings section. |
| 38 | 38 | ||
| 39 | The keybindings are configured as follows: | 39 | The keybindings are configured as follows: |
| 40 | 40 | ||
| 41 | ```txt | 41 | #+begin_src txt |
| 42 | byobu keybindings can be user defined in /usr/share/byobu/keybindings/ (or | 42 | byobu keybindings can be user defined in /usr/share/byobu/keybindings/ (or |
| 43 | within .screenrc if byobu-export was used). The common key bindings are: | 43 | within .screenrc if byobu-export was used). The common key bindings are: |
| 44 | 44 | ||
| @@ -66,4 +66,4 @@ Ctrl-a R - Reload profile | |||
| 66 | Ctrl-a ! - Toggle key bindings on and off | 66 | Ctrl-a ! - Toggle key bindings on and off |
| 67 | Ctrl-a k - Kill the current window | 67 | Ctrl-a k - Kill the current window |
| 68 | Ctrl-a ~ - Save the current window's scrollback buffer | 68 | Ctrl-a ~ - Save the current window's scrollback buffer |
| 69 | ``` | 69 | #+end_src |
content/blog/2023-06-23-self-hosting-convos.md deleted −161
| @@ -1,161 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-06-23 | ||
| 3 | title = "Self-Hosting Convos IRC Web Client" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Convos | ||
| 9 | |||
| 10 | [Convos](https://convos.chat/) is an always-online web client for IRC. It has a | ||
| 11 | few features that made it attractive to me as a self-hosted option: | ||
| 12 | |||
| 13 | - Extremely simple Docker Compose installation method. | ||
| 14 | - Runs in the background and monitors chats even while you're not logged in. | ||
| 15 | - Neatly organized sidebar for conversation and client settings. | ||
| 16 | - Ability to connect to different hosts and create profiles for hosts. | ||
| 17 | - By default, registration is closed to the public. You can enable public | ||
| 18 | registration on the Settings page or generate invitation links on the Users | ||
| 19 | page. | ||
| 20 | - Customization of the client theme, organization name and URL, admin email, | ||
| 21 | and video service. | ||
| 22 | |||
| 23 | # Docker Installation | ||
| 24 | |||
| 25 | To install Convos, simply create a directory and a `docker-compose.yml` file: | ||
| 26 | |||
| 27 | ```sh | ||
| 28 | mkdir ~/convos && cd ~/convos | ||
| 29 | nano docker-compose.yml | ||
| 30 | ``` | ||
| 31 | |||
| 32 | With the `docker-compose.yml` file open, paste the configuration below into the | ||
| 33 | file. You can customize the host port to be something unique, such as | ||
| 34 | `21897:3000`. You can also change the `data` folder to be a docker volume | ||
| 35 | instead, if you prefer. | ||
| 36 | |||
| 37 | ```config | ||
| 38 | version: '3' | ||
| 39 | |||
| 40 | services: | ||
| 41 | convos: | ||
| 42 | image: 'convos/convos:stable' | ||
| 43 | ports: | ||
| 44 | - '3000:3000' | ||
| 45 | volumes: | ||
| 46 | - './data:/data' | ||
| 47 | environment: | ||
| 48 | - CONVOS_REVERSE_PROXY=1 | ||
| 49 | restart: always | ||
| 50 | ``` | ||
| 51 | |||
| 52 | Save the `docker-compose.yml` file and bring the container up: | ||
| 53 | |||
| 54 | ```sh | ||
| 55 | sudo docker-compose up -d | ||
| 56 | ``` | ||
| 57 | |||
| 58 | At this point, Convos is available at `<server_ip>:3000` but not available to | ||
| 59 | the public. | ||
| 60 | |||
| 61 | # Nginx Reverse Proxy | ||
| 62 | |||
| 63 | If you're using Nginx, you can create a configuration file for the convos | ||
| 64 | application. Start by opening the file: | ||
| 65 | |||
| 66 | ```sh | ||
| 67 | nano /etc/nginx/sites-available/convos | ||
| 68 | ``` | ||
| 69 | |||
| 70 | Within the nginx configuration file, paste the following content and be sure to | ||
| 71 | update `convos.example.com` to match your domain and `127.0.0.1:3000` to match | ||
| 72 | the port you opened in the `docker-compose.yml` file. | ||
| 73 | |||
| 74 | ```config | ||
| 75 | # Host and port where convos is running | ||
| 76 | upstream convos_upstream { server 127.0.0.1:3000; } | ||
| 77 | |||
| 78 | server { | ||
| 79 | if ($host ~ ^[^.]+\.example\.com$) { | ||
| 80 | return 301 https://$host$request_uri; | ||
| 81 | } | ||
| 82 | |||
| 83 | listen [::]:80; | ||
| 84 | listen 80; | ||
| 85 | server_name convos.example.com; | ||
| 86 | return 404; | ||
| 87 | } | ||
| 88 | |||
| 89 | server { | ||
| 90 | listen [::]:443 ssl http2; | ||
| 91 | listen 443 ssl http2; | ||
| 92 | server_name convos.example.com; | ||
| 93 | access_log /var/log/nginx/convos.access.log; | ||
| 94 | error_log /var/log/nginx/convos.error.log; | ||
| 95 | |||
| 96 | location / { | ||
| 97 | proxy_pass http://convos_upstream; | ||
| 98 | proxy_http_version 1.1; | ||
| 99 | proxy_set_header Upgrade $http_upgrade; | ||
| 100 | proxy_set_header Connection "upgrade"; | ||
| 101 | client_max_body_size 0; | ||
| 102 | proxy_set_header Host $host; | ||
| 103 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 104 | proxy_set_header X-Request-Base "$scheme://$host/"; | ||
| 105 | } | ||
| 106 | |||
| 107 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; | ||
| 108 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; | ||
| 109 | include /etc/letsencrypt/options-ssl-nginx.conf; | ||
| 110 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | ||
| 111 | } | ||
| 112 | ``` | ||
| 113 | |||
| 114 | Once the file is saved, link it to the `sites-enabled` directory and restart | ||
| 115 | Nginx. | ||
| 116 | |||
| 117 | ```sh | ||
| 118 | sudo ln -s /etc/nginx/sites-available/convos /etc/nginx/sites-enabled/convos | ||
| 119 | sudo systemctl restart nginx.service | ||
| 120 | ``` | ||
| 121 | |||
| 122 | # Screenshots | ||
| 123 | |||
| 124 | See the screenshots below for an example of the Convos homepage and an excerpt | ||
| 125 | of the chat screen. | ||
| 126 | |||
| 127 | There are numerous themes to choose from; the theme shown in the images below is | ||
| 128 | Dracula. | ||
| 129 | |||
| 130 |  | ||
| 131 | |||
| 132 |  | ||
| 133 | |||
| 134 | # Registering a Nickname | ||
| 135 | |||
| 136 | Hop into the server channel so that you can message NickServ. In the case of | ||
| 137 | Convos, the default sever is libera.chat. Simply click the `libera` conversation | ||
| 138 | at the top of the sidebar to open it. Once the chat is open, you can claim a | ||
| 139 | nickname by typing: | ||
| 140 | |||
| 141 | ```txt | ||
| 142 | /nick <nick> | ||
| 143 | ``` | ||
| 144 | |||
| 145 | If the nickname is available, and you'd like to register the nickname to | ||
| 146 | yourself, you'll need to type another command: | ||
| 147 | |||
| 148 | ```txt | ||
| 149 | /msg NickServ REGISTER | ||
| 150 | <password> <email> | ||
| 151 | ``` | ||
| 152 | |||
| 153 | On libera.chat, the server will send a confirmation email with a command that | ||
| 154 | you must message in IRC to verify registration of the nickname: | ||
| 155 | |||
| 156 | ```txt | ||
| 157 | /msg NickServ VERIFY REGISTER <nick> <verification_code> | ||
| 158 | ``` | ||
| 159 | |||
| 160 | Once entered, the server should confirm registration of the nickname to the | ||
| 161 | supplied email with the password specified. | ||
content/blog/2023-06-23-self-hosting-convos.org added +165
| @@ -0,0 +1,165 @@ | |||
| 1 | #+date: <2023-06-23> | ||
| 2 | #+title: Self-Hosting Convos IRC Web Client | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Convos | ||
| 7 | |||
| 8 | [[https://convos.chat/][Convos]] is an always-online web client for IRC. | ||
| 9 | It has a few features that made it attractive to me as a self-hosted | ||
| 10 | option: | ||
| 11 | |||
| 12 | - Extremely simple Docker Compose installation method. | ||
| 13 | - Runs in the background and monitors chats even while you're not logged | ||
| 14 | in. | ||
| 15 | - Neatly organized sidebar for conversation and client settings. | ||
| 16 | - Ability to connect to different hosts and create profiles for hosts. | ||
| 17 | - By default, registration is closed to the public. You can enable | ||
| 18 | public registration on the Settings page or generate invitation links | ||
| 19 | on the Users page. | ||
| 20 | - Customization of the client theme, organization name and URL, admin | ||
| 21 | email, and video service. | ||
| 22 | |||
| 23 | * Docker Installation | ||
| 24 | |||
| 25 | To install Convos, simply create a directory and a =docker-compose.yml= | ||
| 26 | file: | ||
| 27 | |||
| 28 | #+begin_src sh | ||
| 29 | mkdir ~/convos && cd ~/convos | ||
| 30 | nano docker-compose.yml | ||
| 31 | #+end_src | ||
| 32 | |||
| 33 | With the =docker-compose.yml= file open, paste the configuration below | ||
| 34 | into the file. You can customize the host port to be something unique, | ||
| 35 | such as =21897:3000=. You can also change the =data= folder to be a | ||
| 36 | docker volume instead, if you prefer. | ||
| 37 | |||
| 38 | #+begin_src config | ||
| 39 | version: '3' | ||
| 40 | |||
| 41 | services: | ||
| 42 | convos: | ||
| 43 | image: 'convos/convos:stable' | ||
| 44 | ports: | ||
| 45 | - '3000:3000' | ||
| 46 | volumes: | ||
| 47 | - './data:/data' | ||
| 48 | environment: | ||
| 49 | - CONVOS_REVERSE_PROXY=1 | ||
| 50 | restart: always | ||
| 51 | #+end_src | ||
| 52 | |||
| 53 | Save the =docker-compose.yml= file and bring the container up: | ||
| 54 | |||
| 55 | #+begin_src sh | ||
| 56 | sudo docker-compose up -d | ||
| 57 | #+end_src | ||
| 58 | |||
| 59 | At this point, Convos is available at =<server_ip>:3000= but not | ||
| 60 | available to the public. | ||
| 61 | |||
| 62 | * Nginx Reverse Proxy | ||
| 63 | |||
| 64 | If you're using Nginx, you can create a configuration file for the | ||
| 65 | convos application. Start by opening the file: | ||
| 66 | |||
| 67 | #+begin_src sh | ||
| 68 | nano /etc/nginx/sites-available/convos | ||
| 69 | #+end_src | ||
| 70 | |||
| 71 | Within the nginx configuration file, paste the following content and be | ||
| 72 | sure to update =convos.example.com= to match your domain and | ||
| 73 | =127.0.0.1:3000= to match the port you opened in the | ||
| 74 | =docker-compose.yml= file. | ||
| 75 | |||
| 76 | #+begin_src config | ||
| 77 | # Host and port where convos is running | ||
| 78 | upstream convos_upstream { server 127.0.0.1:3000; } | ||
| 79 | |||
| 80 | server { | ||
| 81 | if ($host ~ ^[^.]+\.example\.com$) { | ||
| 82 | return 301 https://$host$request_uri; | ||
| 83 | } | ||
| 84 | |||
| 85 | listen [::]:80; | ||
| 86 | listen 80; | ||
| 87 | server_name convos.example.com; | ||
| 88 | return 404; | ||
| 89 | } | ||
| 90 | |||
| 91 | server { | ||
| 92 | listen [::]:443 ssl http2; | ||
| 93 | listen 443 ssl http2; | ||
| 94 | server_name convos.example.com; | ||
| 95 | access_log /var/log/nginx/convos.access.log; | ||
| 96 | error_log /var/log/nginx/convos.error.log; | ||
| 97 | |||
| 98 | location / { | ||
| 99 | proxy_pass http://convos_upstream; | ||
| 100 | proxy_http_version 1.1; | ||
| 101 | proxy_set_header Upgrade $http_upgrade; | ||
| 102 | proxy_set_header Connection "upgrade"; | ||
| 103 | client_max_body_size 0; | ||
| 104 | proxy_set_header Host $host; | ||
| 105 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 106 | proxy_set_header X-Request-Base "$scheme://$host/"; | ||
| 107 | } | ||
| 108 | |||
| 109 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; | ||
| 110 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; | ||
| 111 | include /etc/letsencrypt/options-ssl-nginx.conf; | ||
| 112 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | ||
| 113 | } | ||
| 114 | #+end_src | ||
| 115 | |||
| 116 | Once the file is saved, link it to the =sites-enabled= directory and | ||
| 117 | restart Nginx. | ||
| 118 | |||
| 119 | #+begin_src sh | ||
| 120 | sudo ln -s /etc/nginx/sites-available/convos /etc/nginx/sites-enabled/convos | ||
| 121 | sudo systemctl restart nginx.service | ||
| 122 | #+end_src | ||
| 123 | |||
| 124 | * Screenshots | ||
| 125 | |||
| 126 | See the screenshots below for an example of the Convos homepage and an | ||
| 127 | excerpt of the chat screen. | ||
| 128 | |||
| 129 | There are numerous themes to choose from; the theme shown in the images | ||
| 130 | below is Dracula. | ||
| 131 | |||
| 132 | #+caption: Convos Home | ||
| 133 | [[https://img.cleberg.net/blog/20230623-convos/convos_home.png]] | ||
| 134 | |||
| 135 | #+caption: Convos Chat | ||
| 136 | [[https://img.cleberg.net/blog/20230623-convos/convos_chat.png]] | ||
| 137 | |||
| 138 | * Registering a Nickname | ||
| 139 | |||
| 140 | Hop into the server channel so that you can message NickServ. In the | ||
| 141 | case of Convos, the default sever is libera.chat. Simply click the | ||
| 142 | =libera= conversation at the top of the sidebar to open it. Once the | ||
| 143 | chat is open, you can claim a nickname by typing: | ||
| 144 | |||
| 145 | #+begin_src txt | ||
| 146 | /nick <nick> | ||
| 147 | #+end_src | ||
| 148 | |||
| 149 | If the nickname is available, and you'd like to register the nickname to | ||
| 150 | yourself, you'll need to type another command: | ||
| 151 | |||
| 152 | #+begin_src txt | ||
| 153 | /msg NickServ REGISTER | ||
| 154 | <password> <email> | ||
| 155 | #+end_src | ||
| 156 | |||
| 157 | On libera.chat, the server will send a confirmation email with a command | ||
| 158 | that you must message in IRC to verify registration of the nickname: | ||
| 159 | |||
| 160 | #+begin_src txt | ||
| 161 | /msg NickServ VERIFY REGISTER <nick> <verification_code> | ||
| 162 | #+end_src | ||
| 163 | |||
| 164 | Once entered, the server should confirm registration of the nickname to | ||
| 165 | the supplied email with the password specified. | ||
content/blog/2023-06-28-backblaze-b2.md deleted −181
| @@ -1,181 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-06-28 | ||
| 3 | title = "Getting Started with Backblaze B2 Cloud Storage" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Overview | ||
| 9 | |||
| 10 | Backblaze [B2 Cloud Storage](https://www.backblaze.com/b2/cloud-storage.html) is | ||
| 11 | an inexpensive and reliable on-demand cloud storage and backup solution. | ||
| 12 | |||
| 13 | The service starts at $5/TB/month ($0.005/GB/month) with a download rate of | ||
| 14 | $0.01/GB/month. | ||
| 15 | |||
| 16 | However, there are free tiers: | ||
| 17 | |||
| 18 | - The first 10 GB of storage is free. | ||
| 19 | - The first 1 GB of data downloaded each day is free. | ||
| 20 | - Class A transactions are free. | ||
| 21 | - The first 2500 Class B transactions each day are free. | ||
| 22 | - The first 2500 Class C transactions each day are free. | ||
| 23 | |||
| 24 | You can see which API calls fall into categories A, B, or C here: [Pricing | ||
| 25 | Organized by API | ||
| 26 | Calls](https://www.backblaze.com/b2/b2-transactions-price.html). | ||
| 27 | |||
| 28 | For someone like me, who wants an offsite backup of their server's `/home/` | ||
| 29 | directory and various other server configs that fall under 10 GB total, | ||
| 30 | Backblaze is a great solution from a financial perspective. | ||
| 31 | |||
| 32 | # Create An Account | ||
| 33 | |||
| 34 | To start with Backblaze, you'll need to [create a free | ||
| 35 | account](https://www.backblaze.com/b2/sign-up.html) - no payment method is | ||
| 36 | required to sign up. | ||
| 37 | |||
| 38 | Once you have an account, you can test out the service with their web GUI, their | ||
| 39 | mobile app, or their CLI tool. I'm going to use the CLI tool below to test a | ||
| 40 | file upload and then sync an entire directory to my Backblaze bucket. | ||
| 41 | |||
| 42 | # Create a Bucket | ||
| 43 | |||
| 44 | Before you can start uploading, you need to create a bucket. If you're familiar | ||
| 45 | with other object storage services, this will feel familiar. If not, it's pretty | ||
| 46 | simple to create one. | ||
| 47 | |||
| 48 | As their webpage says: | ||
| 49 | |||
| 50 | > A bucket is a container that holds files that are uploaded into B2 Cloud | ||
| 51 | > Storage. The bucket name must be globally unique and must have a minimum of 6 | ||
| 52 | > characters. A limit of 100 buckets may be created per account. An unlimited | ||
| 53 | > number of files may be uploaded into a bucket. | ||
| 54 | |||
| 55 | Once you click the `Create a Bucket` button on their webpage or mobile app, you | ||
| 56 | need to provide the following: | ||
| 57 | |||
| 58 | - Bucket Unique Name | ||
| 59 | - Files in Bucket are: `Private` or `Public` | ||
| 60 | - Default Encryption: `Disable` or `Enable` | ||
| 61 | - Object Lock: `Disable` or `Enable` | ||
| 62 | |||
| 63 | For my bucket, I created a private bucket with encryption enabled and object | ||
| 64 | lock disabled. | ||
| 65 | |||
| 66 | Once your bucket is created, you can test the upload/download feature on their | ||
| 67 | web GUI or mobile app! At this point, you have a fully functional bucket and | ||
| 68 | account. | ||
| 69 | |||
| 70 | # Linux CLI Tool | ||
| 71 | |||
| 72 | ## Installation | ||
| 73 | |||
| 74 | To install the `b2` CLI tool, you'll need to download it from the [CLI | ||
| 75 | Tools](https://www.backblaze.com/docs/cloud-storage-command-line-tools) page. I | ||
| 76 | recommend copying the URL from the link that says `Linux` and using wget to | ||
| 77 | download it, as shown below. | ||
| 78 | |||
| 79 | Once downloaded, make the file executable and move it to a location on your | ||
| 80 | `$PATH`, so that you can execute that command from anywhere on the machine. | ||
| 81 | |||
| 82 | ```sh | ||
| 83 | wget <b2_cli_url> | ||
| 84 | chmod +x b2_linux | ||
| 85 | mv b2_linux /usr/bin/b2 | ||
| 86 | ``` | ||
| 87 | |||
| 88 | ## Log In | ||
| 89 | |||
| 90 | The first step after installation is to log in. To do this, execute the | ||
| 91 | following command and provide your `<applicationKeyId>` and `<applicationKey>`. | ||
| 92 | |||
| 93 | If you don't want to provide these values in the command itself, you can simply | ||
| 94 | execute the base command and it will request them in an interactive prompt. | ||
| 95 | |||
| 96 | ```sh | ||
| 97 | # if you want to provide the keys directly: | ||
| 98 | b2 authorize-account [<applicationKeyId>] [<applicationKey>] | ||
| 99 | |||
| 100 | # or, if you don't want your keys in your shell history: | ||
| 101 | b2 authorize-account | ||
| 102 | ``` | ||
| 103 | |||
| 104 | ## Upload a Test File | ||
| 105 | |||
| 106 | In order to test the functionality of the CLI tool, I'll start by uploading a | ||
| 107 | single test file to the bucket I created above. We can do this with the | ||
| 108 | `upload_file` function. | ||
| 109 | |||
| 110 | The command is issued as follows: | ||
| 111 | |||
| 112 | ```sh | ||
| 113 | b2 upload_file <bucket_name> <local_file> <remote_file> | ||
| 114 | ``` | ||
| 115 | |||
| 116 | In my situation, I executed the following command with my username. | ||
| 117 | |||
| 118 | ```sh | ||
| 119 | b2 upload_file my_unique_bucket /home/<user>/test.md test.md | ||
| 120 | ``` | ||
| 121 | |||
| 122 | To confirm that the file was uploaded successfully, list the files in your | ||
| 123 | bucket: | ||
| 124 | |||
| 125 | ```sh | ||
| 126 | b2 ls <bucket_name> | ||
| 127 | ``` | ||
| 128 | |||
| 129 | ```txt | ||
| 130 | test.md | ||
| 131 | ``` | ||
| 132 | |||
| 133 | ## Sync a Directory | ||
| 134 | |||
| 135 | If you have numerous files, you can use the `sync` function to perform | ||
| 136 | functionality similar to `rsync`, where you can check what's in your bucket and | ||
| 137 | sync anything that is new or modified. | ||
| 138 | |||
| 139 | The command is issued as follows: | ||
| 140 | |||
| 141 | ```sh | ||
| 142 | b2 sync <source file location> <B2 bucket destination> | ||
| 143 | ``` | ||
| 144 | |||
| 145 | In my case, I can sync my user's entire home directory to my bucket without | ||
| 146 | specifying any of the files directly: | ||
| 147 | |||
| 148 | ```sh | ||
| 149 | b2 sync /home/<user>/ "b2://<bucketName>/home/<user>" | ||
| 150 | ``` | ||
| 151 | |||
| 152 | # Caveats | ||
| 153 | |||
| 154 | ## Timing of Updates to the Web GUI | ||
| 155 | |||
| 156 | When performing actions over a bucket, there is a slight delay in the web GUI | ||
| 157 | when inspecting a bucket or its file. Note that simple actions such as uploading | ||
| 158 | or deleting files may have a delay of a few minutes up to 24 hours. In my | ||
| 159 | experience (\<10 GB and ~20,000 files), any actions took only a few minutes to | ||
| 160 | update across clients. | ||
| 161 | |||
| 162 | ## Symlinks | ||
| 163 | |||
| 164 | Note that symlinks are resolved by b2, so if you have a link from | ||
| 165 | `/home/<user>/nas-storage` that symlinks out to a `/mnt/nas-storage` folder that | ||
| 166 | has 10TB of data, `b2` will resolve that link and start uploading all 10TB of | ||
| 167 | data linked within the folder. | ||
| 168 | |||
| 169 | If you're not sure if you have any symlinks, a symlink will look like this (note | ||
| 170 | the `->` symbol): | ||
| 171 | |||
| 172 | ```sh | ||
| 173 | > ls -lha | ||
| 174 | lrwxrwxrwx 1 root root 20 Jun 28 13:32 nas -> /mnt/nas-storage/ | ||
| 175 | ``` | ||
| 176 | |||
| 177 | You can recursively find symlink in a path with the following command: | ||
| 178 | |||
| 179 | ```sh | ||
| 180 | ls -lR /path/to/search | grep '^l' | ||
| 181 | ``` | ||
content/blog/2023-06-28-backblaze-b2.org added +186
| @@ -0,0 +1,186 @@ | |||
| 1 | #+date: <2023-06-28> | ||
| 2 | #+title: Getting Started with Backblaze B2 Cloud Storage | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Overview | ||
| 7 | |||
| 8 | Backblaze [[https://www.backblaze.com/b2/cloud-storage.html][B2 Cloud | ||
| 9 | Storage]] is an inexpensive and reliable on-demand cloud storage and | ||
| 10 | backup solution. | ||
| 11 | |||
| 12 | The service starts at $5/TB/month ($0.005/GB/month) with a download rate | ||
| 13 | of $0.01/GB/month. | ||
| 14 | |||
| 15 | However, there are free tiers: | ||
| 16 | |||
| 17 | - The first 10 GB of storage is free. | ||
| 18 | - The first 1 GB of data downloaded each day is free. | ||
| 19 | - Class A transactions are free. | ||
| 20 | - The first 2500 Class B transactions each day are free. | ||
| 21 | - The first 2500 Class C transactions each day are free. | ||
| 22 | |||
| 23 | You can see which API calls fall into categories A, B, or C here: | ||
| 24 | [[https://www.backblaze.com/b2/b2-transactions-price.html][Pricing | ||
| 25 | Organized by API Calls]]. | ||
| 26 | |||
| 27 | For someone like me, who wants an offsite backup of their server's | ||
| 28 | =/home/= directory and various other server configs that fall under 10 | ||
| 29 | GB total, Backblaze is a great solution from a financial perspective. | ||
| 30 | |||
| 31 | * Create An Account | ||
| 32 | |||
| 33 | To start with Backblaze, you'll need to | ||
| 34 | [[https://www.backblaze.com/b2/sign-up.html][create a free account]] - | ||
| 35 | no payment method is required to sign up. | ||
| 36 | |||
| 37 | Once you have an account, you can test out the service with their web | ||
| 38 | GUI, their mobile app, or their CLI tool. I'm going to use the CLI tool | ||
| 39 | below to test a file upload and then sync an entire directory to my | ||
| 40 | Backblaze bucket. | ||
| 41 | |||
| 42 | * Create a Bucket | ||
| 43 | |||
| 44 | Before you can start uploading, you need to create a bucket. If you're | ||
| 45 | familiar with other object storage services, this will feel familiar. If | ||
| 46 | not, it's pretty simple to create one. | ||
| 47 | |||
| 48 | As their webpage says: | ||
| 49 | |||
| 50 | #+begin_quote | ||
| 51 | A bucket is a container that holds files that are uploaded into B2 Cloud | ||
| 52 | Storage. The bucket name must be globally unique and must have a minimum | ||
| 53 | of 6 characters. A limit of 100 buckets may be created per account. An | ||
| 54 | unlimited number of files may be uploaded into a bucket. | ||
| 55 | #+end_quote | ||
| 56 | |||
| 57 | Once you click the =Create a Bucket= button on their webpage or mobile | ||
| 58 | app, you need to provide the following: | ||
| 59 | |||
| 60 | - Bucket Unique Name | ||
| 61 | - Files in Bucket are: =Private= or =Public= | ||
| 62 | - Default Encryption: =Disable= or =Enable= | ||
| 63 | - Object Lock: =Disable= or =Enable= | ||
| 64 | |||
| 65 | For my bucket, I created a private bucket with encryption enabled and | ||
| 66 | object lock disabled. | ||
| 67 | |||
| 68 | Once your bucket is created, you can test the upload/download feature on | ||
| 69 | their web GUI or mobile app! At this point, you have a fully functional | ||
| 70 | bucket and account. | ||
| 71 | |||
| 72 | * Linux CLI Tool | ||
| 73 | |||
| 74 | ** Installation | ||
| 75 | |||
| 76 | To install the =b2= CLI tool, you'll need to download it from the | ||
| 77 | [[https://www.backblaze.com/docs/cloud-storage-command-line-tools][CLI | ||
| 78 | Tools]] page. I recommend copying the URL from the link that says | ||
| 79 | =Linux= and using wget to download it, as shown below. | ||
| 80 | |||
| 81 | Once downloaded, make the file executable and move it to a location on | ||
| 82 | your =$PATH=, so that you can execute that command from anywhere on the | ||
| 83 | machine. | ||
| 84 | |||
| 85 | #+begin_src sh | ||
| 86 | wget <b2_cli_url> | ||
| 87 | chmod +x b2_linux | ||
| 88 | mv b2_linux /usr/bin/b2 | ||
| 89 | #+end_src | ||
| 90 | |||
| 91 | ** Log In | ||
| 92 | |||
| 93 | The first step after installation is to log in. To do this, execute the | ||
| 94 | following command and provide your =<applicationKeyId>= and | ||
| 95 | =<applicationKey>=. | ||
| 96 | |||
| 97 | If you don't want to provide these values in the command itself, you can | ||
| 98 | simply execute the base command and it will request them in an | ||
| 99 | interactive prompt. | ||
| 100 | |||
| 101 | #+begin_src sh | ||
| 102 | # if you want to provide the keys directly: | ||
| 103 | b2 authorize-account [<applicationKeyId>] [<applicationKey>] | ||
| 104 | |||
| 105 | # or, if you don't want your keys in your shell history: | ||
| 106 | b2 authorize-account | ||
| 107 | #+end_src | ||
| 108 | |||
| 109 | ** Upload a Test File | ||
| 110 | |||
| 111 | In order to test the functionality of the CLI tool, I'll start by | ||
| 112 | uploading a single test file to the bucket I created above. We can do | ||
| 113 | this with the =upload_file= function. | ||
| 114 | |||
| 115 | The command is issued as follows: | ||
| 116 | |||
| 117 | #+begin_src sh | ||
| 118 | b2 upload_file <bucket_name> <local_file> <remote_file> | ||
| 119 | #+end_src | ||
| 120 | |||
| 121 | In my situation, I executed the following command with my username. | ||
| 122 | |||
| 123 | #+begin_src sh | ||
| 124 | b2 upload_file my_unique_bucket /home/<user>/test.md test.md | ||
| 125 | #+end_src | ||
| 126 | |||
| 127 | To confirm that the file was uploaded successfully, list the files in | ||
| 128 | your bucket: | ||
| 129 | |||
| 130 | #+begin_src sh | ||
| 131 | b2 ls <bucket_name> | ||
| 132 | #+end_src | ||
| 133 | |||
| 134 | #+begin_src txt | ||
| 135 | test.md | ||
| 136 | #+end_src | ||
| 137 | |||
| 138 | ** Sync a Directory | ||
| 139 | |||
| 140 | If you have numerous files, you can use the =sync= function to perform | ||
| 141 | functionality similar to =rsync=, where you can check what's in your | ||
| 142 | bucket and sync anything that is new or modified. | ||
| 143 | |||
| 144 | The command is issued as follows: | ||
| 145 | |||
| 146 | #+begin_src sh | ||
| 147 | b2 sync <source file location> <B2 bucket destination> | ||
| 148 | #+end_src | ||
| 149 | |||
| 150 | In my case, I can sync my user's entire home directory to my bucket | ||
| 151 | without specifying any of the files directly: | ||
| 152 | |||
| 153 | #+begin_src sh | ||
| 154 | b2 sync /home/<user>/ "b2://<bucketName>/home/<user>" | ||
| 155 | #+end_src | ||
| 156 | |||
| 157 | * Caveats | ||
| 158 | |||
| 159 | ** Timing of Updates to the Web GUI | ||
| 160 | |||
| 161 | When performing actions over a bucket, there is a slight delay in the | ||
| 162 | web GUI when inspecting a bucket or its file. Note that simple actions | ||
| 163 | such as uploading or deleting files may have a delay of a few minutes up | ||
| 164 | to 24 hours. In my experience (<10 GB and ~20,000 files), any actions | ||
| 165 | took only a few minutes to update across clients. | ||
| 166 | |||
| 167 | ** Symlinks | ||
| 168 | |||
| 169 | Note that symlinks are resolved by b2, so if you have a link from | ||
| 170 | =/home/<user>/nas-storage= that symlinks out to a =/mnt/nas-storage= | ||
| 171 | folder that has 10TB of data, =b2= will resolve that link and start | ||
| 172 | uploading all 10TB of data linked within the folder. | ||
| 173 | |||
| 174 | If you're not sure if you have any symlinks, a symlink will look like | ||
| 175 | this (note the =->= symbol): | ||
| 176 | |||
| 177 | #+begin_src sh | ||
| 178 | > ls -lha | ||
| 179 | lrwxrwxrwx 1 root root 20 Jun 28 13:32 nas -> /mnt/nas-storage/ | ||
| 180 | #+end_src | ||
| 181 | |||
| 182 | You can recursively find symlink in a path with the following command: | ||
| 183 | |||
| 184 | #+begin_src sh | ||
| 185 | ls -lR /path/to/search | grep '^l' | ||
| 186 | #+end_src | ||
content/blog/2023-06-30-self-hosting-voyager.md → content/blog/2023-06-30-self-hosting-voyager.org renamed +50 −50
| @@ -1,54 +1,53 @@ | |||
| 1 | +++ | 1 | #+date: <2023-06-30> |
| 2 | date = 2023-06-30 | 2 | #+title: Self-Hosting Voyager - A Lemmy Web Client |
| 3 | title = "Self-Hosting Voyager - A Lemmy Web Client" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Installation Guide | ||
| 9 | 5 | ||
| 10 | [Voyager](https://github.com/aeharding/voyager) is a mobile-first Lemmy web | 6 | * Installation Guide |
| 11 | client, based on iOS design standards. It follows very closely to Apollo's | ||
| 12 | design. | ||
| 13 | 7 | ||
| 14 | This post is a guide showing how I was able to build and launch my own instance | 8 | [[https://github.com/aeharding/voyager][Voyager]] is a mobile-first |
| 15 | of Voyager via Docker Compose. | 9 | Lemmy web client, based on iOS design standards. It follows very closely |
| 10 | to Apollo's design. | ||
| 16 | 11 | ||
| 17 | ## Clone the Repository | 12 | This post is a guide showing how I was able to build and launch my own |
| 13 | instance of Voyager via Docker Compose. | ||
| 14 | |||
| 15 | ** Clone the Repository | ||
| 18 | 16 | ||
| 19 | Start by cloning the repository and entering it: | 17 | Start by cloning the repository and entering it: |
| 20 | 18 | ||
| 21 | ```sh | 19 | #+begin_src sh |
| 22 | git clone https://github.com/aeharding/voyager | 20 | git clone https://github.com/aeharding/voyager |
| 23 | cd voyager | 21 | cd voyager |
| 24 | ``` | 22 | #+end_src |
| 25 | 23 | ||
| 26 | ## Build the Image | 24 | ** Build the Image |
| 27 | 25 | ||
| 28 | With this repository, you can build the image yourself without any further | 26 | With this repository, you can build the image yourself without any |
| 29 | configuration. When complete, it'll give you the image ID for you to run. | 27 | further configuration. When complete, it'll give you the image ID for |
| 28 | you to run. | ||
| 30 | 29 | ||
| 31 | ```sh | 30 | #+begin_src sh |
| 32 | sudo docker build . | 31 | sudo docker build . |
| 33 | # Successfully built 5f00723cb5be | 32 | # Successfully built 5f00723cb5be |
| 34 | ``` | 33 | #+end_src |
| 35 | 34 | ||
| 36 | With the image ID above, you can run the container and pass the requested port | 35 | With the image ID above, you can run the container and pass the |
| 37 | `5314` through or use a custom port, if you wish. | 36 | requested port =5314= through or use a custom port, if you wish. |
| 38 | 37 | ||
| 39 | You can also set the `CUSTOM_LEMMY_SERVERS` environment variable if you want to | 38 | You can also set the =CUSTOM_LEMMY_SERVERS= environment variable if you |
| 40 | add to the default suggested login servers. This must be set with a comma | 39 | want to add to the default suggested login servers. This must be set |
| 41 | separated list of suggested servers. The first instance in the list will be the | 40 | with a comma separated list of suggested servers. The first instance in |
| 42 | default view for logged-out users. | 41 | the list will be the default view for logged-out users. |
| 43 | 42 | ||
| 44 | I will be using a `docker-compose.yml` file to run this container, instead of a | 43 | I will be using a =docker-compose.yml= file to run this container, |
| 45 | `docker run` command. | 44 | instead of a =docker run= command. |
| 46 | 45 | ||
| 47 | ```sh | 46 | #+begin_src sh |
| 48 | nano docker-compose.yml | 47 | nano docker-compose.yml |
| 49 | ``` | 48 | #+end_src |
| 50 | 49 | ||
| 51 | ```conf | 50 | #+begin_src conf |
| 52 | version: "2" | 51 | version: "2" |
| 53 | services: | 52 | services: |
| 54 | voyager: | 53 | voyager: |
| @@ -58,29 +57,30 @@ services: | |||
| 58 | - "<custom_port>:5314" | 57 | - "<custom_port>:5314" |
| 59 | environment: | 58 | environment: |
| 60 | - CUSTOM_LEMMY_SERVERS=lemmy.dbzer0.com,lemmy.world,lemmy.ml,beehaw.org | 59 | - CUSTOM_LEMMY_SERVERS=lemmy.dbzer0.com,lemmy.world,lemmy.ml,beehaw.org |
| 61 | ``` | 60 | #+end_src |
| 62 | 61 | ||
| 63 | ```sh | 62 | #+begin_src sh |
| 64 | sudo docker-compose up -d | 63 | sudo docker-compose up -d |
| 65 | ``` | 64 | #+end_src |
| 66 | 65 | ||
| 67 | The web app will now be available at the following address: | 66 | The web app will now be available at the following address: |
| 68 | `<machine_ip>:<custom_port>`. If you are running it on your local device, try | 67 | =<machine_ip>:<custom_port>=. If you are running it on your local |
| 69 | `localhost:<custom_port>`. | 68 | device, try =localhost:<custom_port>=. |
| 70 | 69 | ||
| 71 | ## Reverse Proxy | 70 | ** Reverse Proxy |
| 72 | 71 | ||
| 73 | If you want to visit this app via an external URL or domain name, you'll need to | 72 | If you want to visit this app via an external URL or domain name, you'll |
| 74 | set up a reverse proxy. The example below uses Nginx as a reverse proxy. | 73 | need to set up a reverse proxy. The example below uses Nginx as a |
| 74 | reverse proxy. | ||
| 75 | 75 | ||
| 76 | Simply create the configuration file, paste the contents below, save the file, | 76 | Simply create the configuration file, paste the contents below, save the |
| 77 | symlink the file, and restart Nginx. | 77 | file, symlink the file, and restart Nginx. |
| 78 | 78 | ||
| 79 | ```sh | 79 | #+begin_src sh |
| 80 | sudo nano /etc/nginx/sites-available/voyager | 80 | sudo nano /etc/nginx/sites-available/voyager |
| 81 | ``` | 81 | #+end_src |
| 82 | 82 | ||
| 83 | ```conf | 83 | #+begin_src conf |
| 84 | server { | 84 | server { |
| 85 | if ($host ~ ^[^.]+\.example\.com$) { | 85 | if ($host ~ ^[^.]+\.example\.com$) { |
| 86 | return 301 https://$host$request_uri; | 86 | return 301 https://$host$request_uri; |
| @@ -110,14 +110,14 @@ server { | |||
| 110 | include /etc/letsencrypt/options-ssl-nginx.conf; | 110 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 111 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | 111 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 112 | } | 112 | } |
| 113 | ``` | 113 | #+end_src |
| 114 | 114 | ||
| 115 | ```sh | 115 | #+begin_src sh |
| 116 | sudo ln sudo ln -s /etc/nginx/sites-available/voyager /etc/nginx/sites-enabled/voyager | 116 | sudo ln sudo ln -s /etc/nginx/sites-available/voyager /etc/nginx/sites-enabled/voyager |
| 117 | sudo systemctl restart nginx.service | 117 | sudo systemctl restart nginx.service |
| 118 | ``` | 118 | #+end_src |
| 119 | 119 | ||
| 120 | The site will now be available at the `server_name` you specified above! | 120 | The site will now be available at the =server_name= you specified above! |
| 121 | 121 | ||
| 122 | You can visit my instance at [voyager.cleberg.net](https://voyager.cleberg.net) | 122 | You can visit my instance at |
| 123 | for an example. | 123 | [[https://voyager.cleberg.net][voyager.cleberg.net]] for an example. |
content/blog/2023-07-12-wireguard-lan.md → content/blog/2023-07-12-wireguard-lan.org renamed +62 −58
| @@ -1,31 +1,30 @@ | |||
| 1 | +++ | 1 | #+date: <2023-07-12> |
| 2 | date = 2023-07-12 | 2 | #+title: Enable LAN Access in Mullvad Wireguard Configuration Files |
| 3 | title = "Enable LAN Access in Mullvad Wireguard Configuration Files" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Download Configuration Files from Mullvad | ||
| 9 | 5 | ||
| 10 | To begin, you'll need [Wireguard configuration files from | 6 | * Download Configuration Files from Mullvad |
| 11 | Mullvad](https://mullvad.net/account/wireguard-config). You can choose any of | ||
| 12 | the options as you download them. For example, I enabled the kill switch, | ||
| 13 | selected all countries, and selected a few content filters. | ||
| 14 | 7 | ||
| 15 | Once downloaded, unzip the files and move them to the Wireguard folder on your | 8 | To begin, you'll need |
| 16 | system. | 9 | [[https://mullvad.net/account/wireguard-config][Wireguard configuration |
| 10 | files from Mullvad]]. You can choose any of the options as you download | ||
| 11 | them. For example, I enabled the kill switch, selected all countries, | ||
| 12 | and selected a few content filters. | ||
| 17 | 13 | ||
| 18 | ```sh | 14 | Once downloaded, unzip the files and move them to the Wireguard folder |
| 15 | on your system. | ||
| 16 | |||
| 17 | #+begin_src sh | ||
| 19 | cd ~/Downloads | 18 | cd ~/Downloads |
| 20 | unzip mullvad_wireguard_linux_all_all.zip | 19 | unzip mullvad_wireguard_linux_all_all.zip |
| 21 | doas mv *.conf /etc/wireguard/ | 20 | doas mv *.conf /etc/wireguard/ |
| 22 | ``` | 21 | #+end_src |
| 23 | 22 | ||
| 24 | ## Configuration File Layout | 23 | ** Configuration File Layout |
| 25 | 24 | ||
| 26 | The default configuration files will look something like this: | 25 | The default configuration files will look something like this: |
| 27 | 26 | ||
| 28 | ```conf | 27 | #+begin_src conf |
| 29 | [Interface] | 28 | [Interface] |
| 30 | # Device: <redacted> | 29 | # Device: <redacted> |
| 31 | PrivateKey = <redacted> | 30 | PrivateKey = <redacted> |
| @@ -38,42 +37,46 @@ PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m ad | |||
| 38 | PublicKey = <redacted> | 37 | PublicKey = <redacted> |
| 39 | AllowedIPs = <redacted> | 38 | AllowedIPs = <redacted> |
| 40 | Endpoint = <redacted> | 39 | Endpoint = <redacted> |
| 41 | ``` | 40 | #+end_src |
| 42 | 41 | ||
| 43 | > Note: If you didn't select the kill switch option, you won't see the `PostUp` | 42 | #+begin_quote |
| 44 | > and `PreDown` lines. In this case, you'll need to modify the script below to | 43 | Note: If you didn't select the kill switch option, you won't see the |
| 45 | > simply append those lines to the `[Interface]` block. | 44 | =PostUp= and =PreDown= lines. In this case, you'll need to modify the |
| 45 | script below to simply append those lines to the =[Interface]= block. | ||
| 46 | #+end_quote | ||
| 46 | 47 | ||
| 47 | # Editing the Configuration Files | 48 | * Editing the Configuration Files |
| 48 | 49 | ||
| 49 | Once you have the files, you'll need to edit them and replace the `PostUp` and | 50 | Once you have the files, you'll need to edit them and replace the |
| 50 | `PreDown` lines to enable LAN access. | 51 | =PostUp= and =PreDown= lines to enable LAN access. |
| 51 | 52 | ||
| 52 | I recommend that you do this process as root, since you'll need to be able to | 53 | I recommend that you do this process as root, since you'll need to be |
| 53 | access files in `/etc/wireguard`, which are generally owned by root. You can | 54 | able to access files in =/etc/wireguard=, which are generally owned by |
| 54 | also try using `sudo` or `doas`, but I didn't test that scenario so you may need | 55 | root. You can also try using =sudo= or =doas=, but I didn't test that |
| 55 | to adjust, as necessary. | 56 | scenario so you may need to adjust, as necessary. |
| 56 | 57 | ||
| 57 | ```sh | 58 | #+begin_src sh |
| 58 | su | 59 | su |
| 59 | ``` | 60 | #+end_src |
| 60 | 61 | ||
| 61 | Create the Python file that we'll be using to update the Wireguard configuration | 62 | Create the Python file that we'll be using to update the Wireguard |
| 62 | files. | 63 | configuration files. |
| 63 | 64 | ||
| 64 | ```sh | 65 | #+begin_src sh |
| 65 | nano replace.py | 66 | nano replace.py |
| 66 | ``` | 67 | #+end_src |
| 67 | 68 | ||
| 68 | Within the Python file, copy and paste the logic below. This script will open a | 69 | Within the Python file, copy and paste the logic below. This script will |
| 69 | directory, loop through every configuration file within the directory, and | 70 | open a directory, loop through every configuration file within the |
| 70 | replace the `PostUp` and `PreDown` lines with the new LAN-enabled iptables | 71 | directory, and replace the =PostUp= and =PreDown= lines with the new |
| 71 | commands. | 72 | LAN-enabled iptables commands. |
| 72 | 73 | ||
| 73 | > Note: If your LAN is on a subnet other than `192.168.1.0/24`, you'll need to | 74 | #+begin_quote |
| 74 | > update the Python script below appropriately. | 75 | Note: If your LAN is on a subnet other than =192.168.1.0/24=, you'll |
| 76 | need to update the Python script below appropriately. | ||
| 77 | #+end_quote | ||
| 75 | 78 | ||
| 76 | ```python | 79 | #+begin_src python |
| 77 | import os | 80 | import os |
| 78 | import fileinput | 81 | import fileinput |
| 79 | 82 | ||
| @@ -92,25 +95,25 @@ for file in os.listdir(dir): | |||
| 92 | print(line, end="") | 95 | print(line, end="") |
| 93 | 96 | ||
| 94 | print("--- done ---") | 97 | print("--- done ---") |
| 95 | ``` | 98 | #+end_src |
| 96 | 99 | ||
| 97 | Once you're done, save and close the file. You can now run the Python script and | 100 | Once you're done, save and close the file. You can now run the Python |
| 98 | watch as each file is updated. | 101 | script and watch as each file is updated. |
| 99 | 102 | ||
| 100 | ```sh | 103 | #+begin_src sh |
| 101 | python3 replace.py | 104 | python3 replace.py |
| 102 | ``` | 105 | #+end_src |
| 103 | 106 | ||
| 104 | To confirm it worked, you can `cat` one of the configuration files to inspect | 107 | To confirm it worked, you can =cat= one of the configuration files to |
| 105 | the new logic and connect to one to test it out. | 108 | inspect the new logic and connect to one to test it out. |
| 106 | 109 | ||
| 107 | ```sh | 110 | #+begin_src sh |
| 108 | cat /etc/wireguard/us-chi-wg-001.conf | 111 | cat /etc/wireguard/us-chi-wg-001.conf |
| 109 | ``` | 112 | #+end_src |
| 110 | 113 | ||
| 111 | The configuration files should now look like this: | 114 | The configuration files should now look like this: |
| 112 | 115 | ||
| 113 | ```conf | 116 | #+begin_src conf |
| 114 | [Interface] | 117 | [Interface] |
| 115 | # Device: <redacted> | 118 | # Device: <redacted> |
| 116 | PrivateKey = <redacted> | 119 | PrivateKey = <redacted> |
| @@ -123,19 +126,20 @@ PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m ad | |||
| 123 | PublicKey = <redacted> | 126 | PublicKey = <redacted> |
| 124 | AllowedIPs = <redacted> | 127 | AllowedIPs = <redacted> |
| 125 | Endpoint = <redacted> | 128 | Endpoint = <redacted> |
| 126 | ``` | 129 | #+end_src |
| 127 | 130 | ||
| 128 | If you connect to a Wireguard interface, such as `us-chi-wg-001`, you can test | 131 | If you connect to a Wireguard interface, such as =us-chi-wg-001=, you |
| 129 | your SSH functionality and see that it works even while on the VPN. | 132 | can test your SSH functionality and see that it works even while on the |
| 133 | VPN. | ||
| 130 | 134 | ||
| 131 | ```sh | 135 | #+begin_src sh |
| 132 | wg-quick up us-chi-wg-001 | 136 | wg-quick up us-chi-wg-001 |
| 133 | ssh user@lan-host | 137 | ssh user@lan-host |
| 134 | ``` | 138 | #+end_src |
| 135 | 139 | ||
| 136 | To confirm your VPN connection, you can curl Mullvad's connection API: | 140 | To confirm your VPN connection, you can curl Mullvad's connection API: |
| 137 | 141 | ||
| 138 | ```sh | 142 | #+begin_src sh |
| 139 | curl https://am.i.mullvad.net/connected | 143 | curl https://am.i.mullvad.net/connected |
| 140 | # You are connected to Mullvad (server us-chi-wg-001). Your IP address is <redacted> | 144 | # You are connected to Mullvad (server us-chi-wg-001). Your IP address is <redacted> |
| 141 | ``` | 145 | #+end_src |
content/blog/2023-07-19-plex-transcoder-errors.md → content/blog/2023-07-19-plex-transcoder-errors.org renamed +38 −37
| @@ -1,61 +1,62 @@ | |||
| 1 | +++ | 1 | #+date: <2023-07-19> |
| 2 | date = 2023-07-19 | 2 | #+title: How to Avoid Plex Error: 'Conversion failed. The transcoder failed to start up.' |
| 3 | title = "How to Avoid Plex Error: 'Conversion failed. The transcoder failed to start up.'" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Plex Transcoder Error | ||
| 9 | 5 | ||
| 10 | Occasionally, you may see an error in your Plex client that references a failure | 6 | * Plex Transcoder Error |
| 11 | with the transcoder conversion process. The specific error wording is: | ||
| 12 | 7 | ||
| 13 | ```txt | 8 | Occasionally, you may see an error in your Plex client that references a |
| 9 | failure with the transcoder conversion process. The specific error | ||
| 10 | wording is: | ||
| 11 | |||
| 12 | #+begin_src txt | ||
| 14 | Conversion failed. The transcoder failed to start up. | 13 | Conversion failed. The transcoder failed to start up. |
| 15 | ``` | 14 | #+end_src |
| 16 | 15 | ||
| 17 | # Debugging the Cause | 16 | * Debugging the Cause |
| 18 | 17 | ||
| 19 | In order to get a better look at what is causing the error, I'm going to observe | 18 | In order to get a better look at what is causing the error, I'm going to |
| 20 | the Plex console while the error occurs. To do this, open the Plex web client, | 19 | observe the Plex console while the error occurs. To do this, open the |
| 21 | go to `Settings` > `Manage` > `Console`. Now, try to play the title again and | 20 | Plex web client, go to =Settings= > =Manage= > =Console=. Now, try to |
| 22 | watch to see which errors occur. | 21 | play the title again and watch to see which errors occur. |
| 23 | 22 | ||
| 24 | In my case, you can see the errors below are related to a subtitle file (`.srt`) | 23 | In my case, you can see the errors below are related to a subtitle file |
| 25 | causing the transcoder to crash. | 24 | (=.srt=) causing the transcoder to crash. |
| 26 | 25 | ||
| 27 | ```txt | 26 | #+begin_src txt |
| 28 | Jul 19, 2023 16:49:34.945 [140184571120440] Error — Couldn't find the file to stream: /movies/Movie Title (2021)/Movie Title (2021).srt | 27 | Jul 19, 2023 16:49:34.945 [140184571120440] Error — Couldn't find the file to stream: /movies/Movie Title (2021)/Movie Title (2021).srt |
| 29 | Jul 19, 2023 16:49:34.947 [140184532732728] Error — [Req#7611/Transcode/42935159-67C1-4192-9336-DDC6F7BC9330] Error configuring transcoder: TPU: Failed to download sub-stream to temporary file | 28 | Jul 19, 2023 16:49:34.947 [140184532732728] Error — [Req#7611/Transcode/42935159-67C1-4192-9336-DDC6F7BC9330] Error configuring transcoder: TPU: Failed to download sub-stream to temporary file |
| 30 | Jul 19, 2023 16:49:35.225 [140184532732728] Warning — [Req#760d/Transcode] Got a request to stop a transcode session without a valid session GUID. | 29 | Jul 19, 2023 16:49:35.225 [140184532732728] Warning — [Req#760d/Transcode] Got a request to stop a transcode session without a valid session GUID. |
| 31 | Jul 19, 2023 16:49:45.561 [140184532732728] Info — [Req#7648] AutoUpdate: no updates available | 30 | Jul 19, 2023 16:49:45.561 [140184532732728] Info — [Req#7648] AutoUpdate: no updates available |
| 32 | Jul 19, 2023 16:51:23.402 [140184510081848] Info — Library section 1 (Movies) will be updated because of a change in "/movies/Movie Title (2021)/Movie Title (2021).srt" | 31 | Jul 19, 2023 16:51:23.402 [140184510081848] Info — Library section 1 (Movies) will be updated because of a change in "/movies/Movie Title (2021)/Movie Title (2021).srt" |
| 33 | ``` | 32 | #+end_src |
| 34 | 33 | ||
| 35 | # Solving the Error | 34 | * Solving the Error |
| 36 | 35 | ||
| 37 | In my case, I simply removed the subtitle file because it was not critical to | 36 | In my case, I simply removed the subtitle file because it was not |
| 38 | keep. You may also avoid this by turning off subtitles if you don't want to | 37 | critical to keep. You may also avoid this by turning off subtitles if |
| 39 | delete the file. | 38 | you don't want to delete the file. |
| 40 | 39 | ||
| 41 | ```sh | 40 | #+begin_src sh |
| 42 | rm -rf "/movies/Movie Title (2021)/Movie Title (2021).srt" | 41 | rm -rf "/movies/Movie Title (2021)/Movie Title (2021).srt" |
| 43 | ``` | 42 | #+end_src |
| 44 | 43 | ||
| 45 | Once the subtitle is removed from the directory or subtitles are turned off, try | 44 | Once the subtitle is removed from the directory or subtitles are turned |
| 46 | to play the title again. At this point, it should play without error. If not, | 45 | off, try to play the title again. At this point, it should play without |
| 47 | reopen or refresh your Plex client and the Plex server: | 46 | error. If not, reopen or refresh your Plex client and the Plex server: |
| 48 | 47 | ||
| 49 | ```sh | 48 | #+begin_src sh |
| 50 | sudo systemctl restart plexmediaserver.service | 49 | sudo systemctl restart plexmediaserver.service |
| 51 | ``` | 50 | #+end_src |
| 52 | 51 | ||
| 53 | # Related Discussion | 52 | * Related Discussion |
| 54 | 53 | ||
| 55 | Looking at [a related Plex forum | 54 | Looking at |
| 56 | post](https://forums.plex.tv/t/subtitles-crashing-plex-transcoder-samsung-q80-tv-with-or-without-hardware-transcode/741441/2), | 55 | [[https://forums.plex.tv/t/subtitles-crashing-plex-transcoder-samsung-q80-tv-with-or-without-hardware-transcode/741441/2][a |
| 57 | it seems that `.srt` subtitles are the core issue here. However, Plex does not | 56 | related Plex forum post]], it seems that =.srt= subtitles are the core |
| 58 | seem to have a resolution that fixes these errors. | 57 | issue here. However, Plex does not seem to have a resolution that fixes |
| 58 | these errors. | ||
| 59 | 59 | ||
| 60 | Unfortunately, I would suggest converting subtitle formats, burning the | 60 | Unfortunately, I would suggest converting subtitle formats, burning the |
| 61 | subtitles into the title, or simply removing subtitles when they cause crashes. | 61 | subtitles into the title, or simply removing subtitles when they cause |
| 62 | crashes. | ||
content/blog/2023-08-18-agile-auditing.md deleted −150
| @@ -1,150 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-08-18 | ||
| 3 | title = "Agile Auditing: An Introduction" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # What is Agile Auditing? | ||
| 9 | |||
| 10 | [Agile](https://en.wikipedia.org/wiki/Agile_software_development), the | ||
| 11 | collaborative philosophy behind many software development methods, has been | ||
| 12 | picking up steam as a beneficial tool to use in the external and internal | ||
| 13 | auditing world. | ||
| 14 | |||
| 15 | This blog post will walk through commonly used terms within Agile, Scrum, and | ||
| 16 | Kanban in order to translate these terms and roles into audit-specific terms. | ||
| 17 | |||
| 18 | Whether your team is in charge of a financial statement audit, an attestation | ||
| 19 | (SOC 1, SOC 2, etc.), or a unique internal audit, the terms used throughout this | ||
| 20 | post should still apply. | ||
| 21 | |||
| 22 | # Agile | ||
| 23 | |||
| 24 | To start, I'll take a look at Agile. | ||
| 25 | |||
| 26 | > The Agile methodology is a project management approach that involves breaking | ||
| 27 | > the project into phases and emphasizes continuous collaboration and | ||
| 28 | > improvement. Teams follow a cycle of planning, executing, and evaluating. | ||
| 29 | |||
| 30 | While this approach may seem familiar to what audit teams have historically | ||
| 31 | done, an audit team must make distinct changes in their mentality and how they | ||
| 32 | approach and manage a project. | ||
| 33 | |||
| 34 | ## Agile Values | ||
| 35 | |||
| 36 | The Agile Manifesto, written in 2001 at a summit in Utah, contain a set of four | ||
| 37 | main values that comprise the Agile approach: | ||
| 38 | |||
| 39 | 1. Individuals and interactions over processes and tools. | ||
| 40 | 2. Working software over comprehensive documentation. | ||
| 41 | 3. Customer collaboration over contract negotiation. | ||
| 42 | 4. Responding to change over following a plan. | ||
| 43 | |||
| 44 | Beyond the four values, [twelve | ||
| 45 | principles](https://agilemanifesto.org/principles.html) were also written as | ||
| 46 | part of the summit. | ||
| 47 | |||
| 48 | In order to relate these values to an audit or attestation engagement, we need | ||
| 49 | to shift the focus from software development to the main goal of an engagement: | ||
| 50 | completing sufficient audit testing to address to relevant risks over the | ||
| 51 | processes and controls at hand. | ||
| 52 | |||
| 53 | Audit Examples: | ||
| 54 | |||
| 55 | - Engagement teams must value the team members, client contacts, and their | ||
| 56 | interactions over the historical processes and tools that have been used. | ||
| 57 | - Engagement teams must value a final report that contains sufficient audit | ||
| 58 | documentation over excessive documentation or scope creep. | ||
| 59 | - Engagement teams must collaborate with the audit clients as much as feasible | ||
| 60 | to ensure that both sides are constantly updated with current knowledge of | ||
| 61 | the engagement's status and any potential findings, rather than waiting for | ||
| 62 | pre-set meetings or the end of the engagement to communicate. | ||
| 63 | - Engagement teams must be able to respond to change in an engagement's | ||
| 64 | schedule, scope, or environment to ensure that the project is completed in a | ||
| 65 | timely manner and that all relevant areas are tested. | ||
| 66 | - In terms of an audit department's portfolio, they must be able to | ||
| 67 | respond to changes in their company's or client's environment and be | ||
| 68 | able to dynamically change their audit plan accordingly. | ||
| 69 | |||
| 70 | # Scrum | ||
| 71 | |||
| 72 | The above section discusses the high-level details of the Agile philosophy and | ||
| 73 | how an audit team can potentially mold that mindset into the audit world, but | ||
| 74 | how does a team implement these ideas? | ||
| 75 | |||
| 76 | There are many methods that use an Agile mindset, but I prefer | ||
| 77 | [Scrum](<https://en.wikipedia.org/wiki/Scrum_(software_development)>). Scrum is | ||
| 78 | a framework based on Agile that enables a team to work through a project through | ||
| 79 | a series of roles, ceremonies, artifacts, and values. | ||
| 80 | |||
| 81 | Let's dive into each of these individually. | ||
| 82 | |||
| 83 | ## Scrum Team | ||
| 84 | |||
| 85 | A scrum project is only as good as the team running the project. Standard scrum | ||
| 86 | teams are separated into three distinct areas: | ||
| 87 | |||
| 88 | 1. **Product Owner (Client Contact)**: The client contact is the audit | ||
| 89 | equivalent of the product owner in Scrum. They are responsible for partnering | ||
| 90 | with the engagement or audit team to ensure progress is being made, | ||
| 91 | priorities are established, and clear guidance is given when questions or | ||
| 92 | findings arise within each sprint. | ||
| 93 | 2. **Scrum Master (Engagement Lead)**: The engagement or audit team lead is | ||
| 94 | responsible for coaching the team and the client contact on the scrum | ||
| 95 | process, tracking team progress against plan, scheduling necessary resources, | ||
| 96 | and helping remove obstacles. | ||
| 97 | 3. **Scrum Developers (Engagement Members)**: The engagement or audit team is | ||
| 98 | the set of team members responsible for getting the work done. These team | ||
| 99 | members will work on each task, report progress, resolve obstacles, and | ||
| 100 | collaborate with other team members and the client contact to ensure goals | ||
| 101 | are being met. | ||
| 102 | |||
| 103 | ## Scrum Ceremonies | ||
| 104 | |||
| 105 | Scrum ceremonies are events that are performed on a regular basis. | ||
| 106 | |||
| 107 | 1. **Sprint Planning**: The team works together to plan the upcoming sprint goal | ||
| 108 | and which user stories (tasks) will be added to the sprint to achieve that | ||
| 109 | goal. | ||
| 110 | 2. **Sprint**: The time period, typically at least one week and no more than one | ||
| 111 | month in length, where the team works on the stories and anything in the | ||
| 112 | backlog. | ||
| 113 | 3. **Daily Scrum**: A very short meeting held each day, typically 15 minutes, to | ||
| 114 | quickly emphasize alignment on the sprint goal and plan the next 24 hours. | ||
| 115 | Each team member may share what they did the day before, what they'll do | ||
| 116 | today, and any obstacles to their work. | ||
| 117 | 4. **Sprint Review**: At the end of each sprint, the team will gather and | ||
| 118 | discuss the progress, obstacles, and backlog from the previous sprint. | ||
| 119 | 5. **Sprint Retrospective**: More specific than the sprint review, the | ||
| 120 | retrospective is meant to discuss what worked and what did not work during | ||
| 121 | the sprint. This may be processes, tools, people, or even things related to | ||
| 122 | the Scrum ceremonies. | ||
| 123 | |||
| 124 | One additional ceremony that may be applicable is organizing the backlog. This | ||
| 125 | is typically the responsibility of the engagement leader and is meant to | ||
| 126 | prioritize and clarify what needs to be done to complete items in the backlog. | ||
| 127 | |||
| 128 | ## Artifacts | ||
| 129 | |||
| 130 | While artifacts are generally not customizable in the audit world (i.e., each | ||
| 131 | control test must include some kind of working paper with evidence supporting | ||
| 132 | the test results), I wanted to include some quick notes on associating scrum | ||
| 133 | artifact terms with an audit. | ||
| 134 | |||
| 135 | 1. **Product Backlog**: This is the overall backlog of unfinished audit tasks | ||
| 136 | from all prior sprints. | ||
| 137 | 2. **Sprint Backlog**: This is the backlog of unfinished audit tasks from one | ||
| 138 | individual sprint. | ||
| 139 | 3. **Increment**: This is the output of each sprint - generally this is best | ||
| 140 | thought of as any documentation prepared during the sprint, such as risk | ||
| 141 | assessments, control working papers, deficiency analysis, etc. | ||
| 142 | |||
| 143 | # Kanban | ||
| 144 | |||
| 145 | Last but not least, Kanban is a methodology that relies on boards to categorize | ||
| 146 | work into distinct, descriptive categories that allow an agile or scrum team to | ||
| 147 | effectively plan the work of a sprint or project. | ||
| 148 | |||
| 149 | See Atlassian's [Kanban](https://www.atlassian.com/agile/kanban) page for more | ||
| 150 | information. | ||
content/blog/2023-08-18-agile-auditing.org added +157
| @@ -0,0 +1,157 @@ | |||
| 1 | #+date: <2023-08-18> | ||
| 2 | #+title: Agile Auditing: An Introduction | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * What is Agile Auditing? | ||
| 7 | |||
| 8 | [[https://en.wikipedia.org/wiki/Agile_software_development][Agile]], the | ||
| 9 | collaborative philosophy behind many software development methods, has | ||
| 10 | been picking up steam as a beneficial tool to use in the external and | ||
| 11 | internal auditing world. | ||
| 12 | |||
| 13 | This blog post will walk through commonly used terms within Agile, | ||
| 14 | Scrum, and Kanban in order to translate these terms and roles into | ||
| 15 | audit-specific terms. | ||
| 16 | |||
| 17 | Whether your team is in charge of a financial statement audit, an | ||
| 18 | attestation (SOC 1, SOC 2, etc.), or a unique internal audit, the terms | ||
| 19 | used throughout this post should still apply. | ||
| 20 | |||
| 21 | * Agile | ||
| 22 | |||
| 23 | To start, I'll take a look at Agile. | ||
| 24 | |||
| 25 | #+begin_quote | ||
| 26 | The Agile methodology is a project management approach that involves | ||
| 27 | breaking the project into phases and emphasizes continuous collaboration | ||
| 28 | and improvement. Teams follow a cycle of planning, executing, and | ||
| 29 | evaluating. | ||
| 30 | #+end_quote | ||
| 31 | |||
| 32 | While this approach may seem familiar to what audit teams have | ||
| 33 | historically done, an audit team must make distinct changes in their | ||
| 34 | mentality and how they approach and manage a project. | ||
| 35 | |||
| 36 | ** Agile Values | ||
| 37 | |||
| 38 | The Agile Manifesto, written in 2001 at a summit in Utah, contain a set | ||
| 39 | of four main values that comprise the Agile approach: | ||
| 40 | |||
| 41 | 1. Individuals and interactions over processes and tools. | ||
| 42 | 2. Working software over comprehensive documentation. | ||
| 43 | 3. Customer collaboration over contract negotiation. | ||
| 44 | 4. Responding to change over following a plan. | ||
| 45 | |||
| 46 | Beyond the four values, | ||
| 47 | [[https://agilemanifesto.org/principles.html][twelve principles]] were | ||
| 48 | also written as part of the summit. | ||
| 49 | |||
| 50 | In order to relate these values to an audit or attestation engagement, | ||
| 51 | we need to shift the focus from software development to the main goal of | ||
| 52 | an engagement: completing sufficient audit testing to address to | ||
| 53 | relevant risks over the processes and controls at hand. | ||
| 54 | |||
| 55 | Audit Examples: | ||
| 56 | |||
| 57 | - Engagement teams must value the team members, client contacts, and | ||
| 58 | their interactions over the historical processes and tools that have | ||
| 59 | been used. | ||
| 60 | - Engagement teams must value a final report that contains sufficient | ||
| 61 | audit documentation over excessive documentation or scope creep. | ||
| 62 | - Engagement teams must collaborate with the audit clients as much as | ||
| 63 | feasible to ensure that both sides are constantly updated with current | ||
| 64 | knowledge of the engagement's status and any potential findings, | ||
| 65 | rather than waiting for pre-set meetings or the end of the engagement | ||
| 66 | to communicate. | ||
| 67 | - Engagement teams must be able to respond to change in an engagement's | ||
| 68 | schedule, scope, or environment to ensure that the project is | ||
| 69 | completed in a timely manner and that all relevant areas are tested. | ||
| 70 | - In terms of an audit department's portfolio, they must be able to | ||
| 71 | respond to changes in their company's or client's environment and be | ||
| 72 | able to dynamically change their audit plan accordingly. | ||
| 73 | |||
| 74 | * Scrum | ||
| 75 | |||
| 76 | The above section discusses the high-level details of the Agile | ||
| 77 | philosophy and how an audit team can potentially mold that mindset into | ||
| 78 | the audit world, but how does a team implement these ideas? | ||
| 79 | |||
| 80 | There are many methods that use an Agile mindset, but I prefer | ||
| 81 | [[https://en.wikipedia.org/wiki/Scrum_(software_development)][Scrum]]. | ||
| 82 | Scrum is a framework based on Agile that enables a team to work through | ||
| 83 | a project through a series of roles, ceremonies, artifacts, and values. | ||
| 84 | |||
| 85 | Let's dive into each of these individually. | ||
| 86 | |||
| 87 | ** Scrum Team | ||
| 88 | |||
| 89 | A scrum project is only as good as the team running the project. | ||
| 90 | Standard scrum teams are separated into three distinct areas: | ||
| 91 | |||
| 92 | 1. *Product Owner (Client Contact)*: The client contact is the audit | ||
| 93 | equivalent of the product owner in Scrum. They are responsible for | ||
| 94 | partnering with the engagement or audit team to ensure progress is | ||
| 95 | being made, priorities are established, and clear guidance is given | ||
| 96 | when questions or findings arise within each sprint. | ||
| 97 | 2. *Scrum Master (Engagement Lead)*: The engagement or audit team lead | ||
| 98 | is responsible for coaching the team and the client contact on the | ||
| 99 | scrum process, tracking team progress against plan, scheduling | ||
| 100 | necessary resources, and helping remove obstacles. | ||
| 101 | 3. *Scrum Developers (Engagement Members)*: The engagement or audit team | ||
| 102 | is the set of team members responsible for getting the work done. | ||
| 103 | These team members will work on each task, report progress, resolve | ||
| 104 | obstacles, and collaborate with other team members and the client | ||
| 105 | contact to ensure goals are being met. | ||
| 106 | |||
| 107 | ** Scrum Ceremonies | ||
| 108 | |||
| 109 | Scrum ceremonies are events that are performed on a regular basis. | ||
| 110 | |||
| 111 | 1. *Sprint Planning*: The team works together to plan the upcoming | ||
| 112 | sprint goal and which user stories (tasks) will be added to the | ||
| 113 | sprint to achieve that goal. | ||
| 114 | 2. *Sprint*: The time period, typically at least one week and no more | ||
| 115 | than one month in length, where the team works on the stories and | ||
| 116 | anything in the backlog. | ||
| 117 | 3. *Daily Scrum*: A very short meeting held each day, typically 15 | ||
| 118 | minutes, to quickly emphasize alignment on the sprint goal and plan | ||
| 119 | the next 24 hours. Each team member may share what they did the day | ||
| 120 | before, what they'll do today, and any obstacles to their work. | ||
| 121 | 4. *Sprint Review*: At the end of each sprint, the team will gather and | ||
| 122 | discuss the progress, obstacles, and backlog from the previous | ||
| 123 | sprint. | ||
| 124 | 5. *Sprint Retrospective*: More specific than the sprint review, the | ||
| 125 | retrospective is meant to discuss what worked and what did not work | ||
| 126 | during the sprint. This may be processes, tools, people, or even | ||
| 127 | things related to the Scrum ceremonies. | ||
| 128 | |||
| 129 | One additional ceremony that may be applicable is organizing the | ||
| 130 | backlog. This is typically the responsibility of the engagement leader | ||
| 131 | and is meant to prioritize and clarify what needs to be done to complete | ||
| 132 | items in the backlog. | ||
| 133 | |||
| 134 | ** Artifacts | ||
| 135 | |||
| 136 | While artifacts are generally not customizable in the audit world (i.e., | ||
| 137 | each control test must include some kind of working paper with evidence | ||
| 138 | supporting the test results), I wanted to include some quick notes on | ||
| 139 | associating scrum artifact terms with an audit. | ||
| 140 | |||
| 141 | 1. *Product Backlog*: This is the overall backlog of unfinished audit | ||
| 142 | tasks from all prior sprints. | ||
| 143 | 2. *Sprint Backlog*: This is the backlog of unfinished audit tasks from | ||
| 144 | one individual sprint. | ||
| 145 | 3. *Increment*: This is the output of each sprint - generally this is | ||
| 146 | best thought of as any documentation prepared during the sprint, such | ||
| 147 | as risk assessments, control working papers, deficiency analysis, | ||
| 148 | etc. | ||
| 149 | |||
| 150 | * Kanban | ||
| 151 | |||
| 152 | Last but not least, Kanban is a methodology that relies on boards to | ||
| 153 | categorize work into distinct, descriptive categories that allow an | ||
| 154 | agile or scrum team to effectively plan the work of a sprint or project. | ||
| 155 | |||
| 156 | See Atlassian's [[https://www.atlassian.com/agile/kanban][Kanban]] page | ||
| 157 | for more information. | ||
content/blog/2023-09-15-self-hosting-gitweb.md → content/blog/2023-09-15-self-hosting-gitweb.org renamed +30 −30
| @@ -1,36 +1,35 @@ | |||
| 1 | +++ | 1 | #+date: <2023-09-15> |
| 2 | date = 2023-09-15 | 2 | #+title: Self-Hosting Gitweb via Nginx |
| 3 | title = "Self-Hosting Gitweb via Nginx" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | ||
| 9 | 5 | ||
| 10 | [GitWeb](https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb) is a simple | 6 | * Overview |
| 11 | web-based visualizer for git repositories. By default, GitWeb will only run with | ||
| 12 | the `lighttpd` or `webrick` web servers. | ||
| 13 | 7 | ||
| 14 | However, this guide will show you how to keep GitWeb running in the background | 8 | [[https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb][GitWeb]] is a |
| 15 | and display information for all repositories in a chosen directory. | 9 | simple web-based visualizer for git repositories. By default, GitWeb |
| 10 | will only run with the =lighttpd= or =webrick= web servers. | ||
| 16 | 11 | ||
| 17 | # Install Dependencies | 12 | However, this guide will show you how to keep GitWeb running in the |
| 13 | background and display information for all repositories in a chosen | ||
| 14 | directory. | ||
| 15 | |||
| 16 | * Install Dependencies | ||
| 18 | 17 | ||
| 19 | To start, you'll need install the following packages: | 18 | To start, you'll need install the following packages: |
| 20 | 19 | ||
| 21 | ```sh | 20 | #+begin_src sh |
| 22 | sudo apt install git gitweb fcgiwrap nginx | 21 | sudo apt install git gitweb fcgiwrap nginx |
| 23 | ``` | 22 | #+end_src |
| 24 | 23 | ||
| 25 | # Configure Nginx | 24 | * Configure Nginx |
| 26 | 25 | ||
| 27 | Once installed, create an Nginx configuration file. | 26 | Once installed, create an Nginx configuration file. |
| 28 | 27 | ||
| 29 | ```sh | 28 | #+begin_src sh |
| 30 | sudo nano /etc/nginx/sites-available/git.example.com | 29 | sudo nano /etc/nginx/sites-available/git.example.com |
| 31 | ``` | 30 | #+end_src |
| 32 | 31 | ||
| 33 | ```conf | 32 | #+begin_src conf |
| 34 | server { | 33 | server { |
| 35 | listen 80; | 34 | listen 80; |
| 36 | server_name example.com; | 35 | server_name example.com; |
| @@ -49,23 +48,24 @@ server { | |||
| 49 | index index.cgi; | 48 | index index.cgi; |
| 50 | } | 49 | } |
| 51 | } | 50 | } |
| 52 | ``` | 51 | #+end_src |
| 53 | 52 | ||
| 54 | To make the configuration active, you need to symlink it and then restart Nginx. | 53 | To make the configuration active, you need to symlink it and then |
| 54 | restart Nginx. | ||
| 55 | 55 | ||
| 56 | ```sh | 56 | #+begin_src sh |
| 57 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/git.example.com | 57 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/git.example.com |
| 58 | sudo systemctl restart nginx.service | 58 | sudo systemctl restart nginx.service |
| 59 | ``` | 59 | #+end_src |
| 60 | 60 | ||
| 61 | The GitWeb application should now be available via the URL you set in the Nginx | 61 | The GitWeb application should now be available via the URL you set in |
| 62 | configuration above. | 62 | the Nginx configuration above. |
| 63 | 63 | ||
| 64 | # Customize GitWeb | 64 | * Customize GitWeb |
| 65 | 65 | ||
| 66 | If you need to, you can customize many things about Gitweb by editing the | 66 | If you need to, you can customize many things about Gitweb by editing |
| 67 | [gitweb.conf](https://git-scm.com/docs/gitweb.conf) file. | 67 | the [[https://git-scm.com/docs/gitweb.conf][gitweb.conf]] file. |
| 68 | 68 | ||
| 69 | ```sh | 69 | #+begin_src sh |
| 70 | sudo nano /etc/gitweb.conf | 70 | sudo nano /etc/gitweb.conf |
| 71 | ``` | 71 | #+end_src |
content/blog/2023-09-19-audit-sql-scripts.md → content/blog/2023-09-19-audit-sql-scripts.org renamed +62 −59
| @@ -1,24 +1,23 @@ | |||
| 1 | +++ | 1 | #+date: <2023-09-19> |
| 2 | date = 2023-09-19 | 2 | #+title: Useful SQL Scripts for Auditing Logical Access |
| 3 | title = "Useful SQL Scripts for Auditing Logical Access" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | ||
| 9 | 5 | ||
| 10 | When you have to scope a database into your engagement, you may be curious how | 6 | * Overview |
| 11 | to best extract the information from the database. While there are numerous | ||
| 12 | different methods to extract this type of information, I'm going to show an | ||
| 13 | example of how to gather all users and privileges from three main database | ||
| 14 | types: Oracle, Microsoft SQL, and MySQL. | ||
| 15 | 7 | ||
| 16 | # Oracle | 8 | When you have to scope a database into your engagement, you may be |
| 9 | curious how to best extract the information from the database. While | ||
| 10 | there are numerous different methods to extract this type of | ||
| 11 | information, I'm going to show an example of how to gather all users and | ||
| 12 | privileges from three main database types: Oracle, Microsoft SQL, and | ||
| 13 | MySQL. | ||
| 17 | 14 | ||
| 18 | You can use the following SQL script to see all users and their privileges in an | 15 | * Oracle |
| 19 | Oracle database: | ||
| 20 | 16 | ||
| 21 | ```sql | 17 | You can use the following SQL script to see all users and their |
| 18 | privileges in an Oracle database: | ||
| 19 | |||
| 20 | #+begin_src sql | ||
| 22 | SELECT | 21 | SELECT |
| 23 | grantee AS "User", | 22 | grantee AS "User", |
| 24 | privilege AS "Privilege" | 23 | privilege AS "Privilege" |
| @@ -34,37 +33,39 @@ FROM | |||
| 34 | dba_tab_privs | 33 | dba_tab_privs |
| 35 | WHERE | 34 | WHERE |
| 36 | grantee IN (SELECT DISTINCT grantee FROM dba_tab_privs); | 35 | grantee IN (SELECT DISTINCT grantee FROM dba_tab_privs); |
| 37 | ``` | 36 | #+end_src |
| 38 | 37 | ||
| 39 | This script queries the `dba_sys_privs` and `dba_tab_privs` views to retrieve | 38 | This script queries the =dba_sys_privs= and =dba_tab_privs= views to |
| 40 | system and table-level privileges respectively. It then combines the results | 39 | retrieve system and table-level privileges respectively. It then |
| 41 | using `UNION ALL` to show all users and their associated privileges. Please note | 40 | combines the results using =UNION ALL= to show all users and their |
| 42 | that this method does not extract information from the `dba_role_privs` table - | 41 | associated privileges. Please note that this method does not extract |
| 43 | use the method below for that data. | 42 | information from the =dba_role_privs= table - use the method below for |
| 43 | that data. | ||
| 44 | 44 | ||
| 45 | Please note that you might need appropriate privileges (e.g., DBA privileges) to | 45 | Please note that you might need appropriate privileges (e.g., DBA |
| 46 | access these views, and you should exercise caution when querying system tables | 46 | privileges) to access these views, and you should exercise caution when |
| 47 | in a production Oracle database. | 47 | querying system tables in a production Oracle database. |
| 48 | 48 | ||
| 49 | ## Alternative Oracle Query | 49 | ** Alternative Oracle Query |
| 50 | 50 | ||
| 51 | You can also extract each table's information separately and perform processing | 51 | You can also extract each table's information separately and perform |
| 52 | outside the database to explore and determine the information necessary for the | 52 | processing outside the database to explore and determine the information |
| 53 | audit: | 53 | necessary for the audit: |
| 54 | 54 | ||
| 55 | ```sql | 55 | #+begin_src sql |
| 56 | SELECT ** FROM sys.dba_role_privs; | 56 | SELECT ** FROM sys.dba_role_privs; |
| 57 | SELECT ** FROM sys.dba_sys_privs; | 57 | SELECT ** FROM sys.dba_sys_privs; |
| 58 | SELECT ** FROM sys.dba_tab_privs; | 58 | SELECT ** FROM sys.dba_tab_privs; |
| 59 | SELECT ** FROM sys.dba_users; | 59 | SELECT ** FROM sys.dba_users; |
| 60 | ``` | 60 | #+end_src |
| 61 | 61 | ||
| 62 | # Microsoft SQL | 62 | * Microsoft SQL |
| 63 | 63 | ||
| 64 | You can use the following SQL script to see all users and their privileges in a | 64 | You can use the following SQL script to see all users and their |
| 65 | Microsoft SQL Server database ([source](https://stackoverflow.com/a/30040784)): | 65 | privileges in a Microsoft SQL Server database |
| 66 | ([[https://stackoverflow.com/a/30040784][source]]): | ||
| 66 | 67 | ||
| 67 | ```sql | 68 | #+begin_src sql |
| 68 | /* | 69 | /* |
| 69 | Security Audit Report | 70 | Security Audit Report |
| 70 | 1) List all access provisioned to a sql user or windows user/group directly | 71 | 1) List all access provisioned to a sql user or windows user/group directly |
| @@ -95,7 +96,7 @@ ObjectName : Name of the object that the user/role is assigned permissions | |||
| 95 | definitions. | 96 | definitions. |
| 96 | ColumnName : Name of the column of the object that the user/role is assigned permissions on. This value | 97 | ColumnName : Name of the column of the object that the user/role is assigned permissions on. This value |
| 97 | is only populated if the object is a table, view or a table value function. | 98 | is only populated if the object is a table, view or a table value function. |
| 98 | */ | 99 | ,*/ |
| 99 | 100 | ||
| 100 | --List all access provisioned to a sql user or windows user/group directly | 101 | --List all access provisioned to a sql user or windows user/group directly |
| 101 | SELECT | 102 | SELECT |
| @@ -209,41 +210,43 @@ ORDER BY | |||
| 209 | perm.[permission_name], | 210 | perm.[permission_name], |
| 210 | perm.[state_desc], | 211 | perm.[state_desc], |
| 211 | obj.type_desc--perm.[class_desc] | 212 | obj.type_desc--perm.[class_desc] |
| 212 | ``` | 213 | #+end_src |
| 213 | 214 | ||
| 214 | # MySQL | 215 | * MySQL |
| 215 | 216 | ||
| 216 | You can use the following SQL script to see all users and their privileges in a | 217 | You can use the following SQL script to see all users and their |
| 217 | MySQL database: | 218 | privileges in a MySQL database: |
| 218 | 219 | ||
| 219 | ```sh | 220 | #+begin_src sh |
| 220 | mysql -u root -p | 221 | mysql -u root -p |
| 221 | ``` | 222 | #+end_src |
| 222 | 223 | ||
| 223 | Find all users and hosts with access to the database: | 224 | Find all users and hosts with access to the database: |
| 224 | 225 | ||
| 225 | ```sql | 226 | #+begin_src sql |
| 226 | SELECT ** FROM information_schema.user_privileges; | 227 | SELECT ** FROM information_schema.user_privileges; |
| 227 | ``` | 228 | #+end_src |
| 228 | 229 | ||
| 229 | This script retrieves user information and their associated database-level | 230 | This script retrieves user information and their associated |
| 230 | privileges from the `information_schema.user_privileges` table in MySQL. It | 231 | database-level privileges from the =information_schema.user_privileges= |
| 231 | lists various privileges such as SELECT, INSERT, UPDATE, DELETE, CREATE, and | 232 | table in MySQL. It lists various privileges such as SELECT, INSERT, |
| 232 | more for each user and database combination. | 233 | UPDATE, DELETE, CREATE, and more for each user and database combination. |
| 233 | 234 | ||
| 234 | Please note that you may need appropriate privileges (e.g., `SELECT` privileges | 235 | Please note that you may need appropriate privileges (e.g., =SELECT= |
| 235 | on `information_schema.user_privileges`) to access this information in a MySQL | 236 | privileges on =information_schema.user_privileges=) to access this |
| 236 | database. Additionally, some privileges like GRANT OPTION, EXECUTE, EVENT, and | 237 | information in a MySQL database. Additionally, some privileges like |
| 237 | TRIGGER may not be relevant for all users and databases. | 238 | GRANT OPTION, EXECUTE, EVENT, and TRIGGER may not be relevant for all |
| 239 | users and databases. | ||
| 238 | 240 | ||
| 239 | ## Alternative MySQL Query | 241 | ** Alternative MySQL Query |
| 240 | 242 | ||
| 241 | You can also grab individual sets of data from MySQL if you prefer to join them | 243 | You can also grab individual sets of data from MySQL if you prefer to |
| 242 | after extraction. I have marked the queries below with `SELECT ...` and excluded | 244 | join them after extraction. I have marked the queries below with |
| 243 | most `WHERE` clauses for brevity. You should determine the relevant privileges | 245 | =SELECT ...= and excluded most =WHERE= clauses for brevity. You should |
| 244 | in-scope and query for those privileges to reduce the length of time to query. | 246 | determine the relevant privileges in-scope and query for those |
| 247 | privileges to reduce the length of time to query. | ||
| 245 | 248 | ||
| 246 | ```sql | 249 | #+begin_src sql |
| 247 | -- Global Permissions | 250 | -- Global Permissions |
| 248 | SELECT ... FROM mysql.user; | 251 | SELECT ... FROM mysql.user; |
| 249 | 252 | ||
| @@ -262,4 +265,4 @@ WHERE db = @db_name; | |||
| 262 | -- Password Configuration | 265 | -- Password Configuration |
| 263 | SHOW GLOBAL VARIABLES LIKE 'validate_password%'; | 266 | SHOW GLOBAL VARIABLES LIKE 'validate_password%'; |
| 264 | SHOW VARIABLES LIKE 'validate_password%'; | 267 | SHOW VARIABLES LIKE 'validate_password%'; |
| 265 | ``` | 268 | #+end_src |
content/blog/2023-10-04-digital-minimalism.md deleted −99
| @@ -1,99 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-10-04 | ||
| 3 | title = "Digital Minimalism" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | I've written [a note about minimalism](file:///wiki/#digital-garden) before, but | ||
| 9 | I wanted to dedicate some time to reflect on digital minimalism and how I've | ||
| 10 | been able to minimize the impact of digital devices in my life. | ||
| 11 | |||
| 12 | > These changes crept up on us and happened fast, before we had a chance to step | ||
| 13 | > back and ask what we really wanted out of the rapid advances of the past | ||
| 14 | > decade. We added new technologies to the periphery of our experience for minor | ||
| 15 | > reasons, then woke one morning to discover that they had colonized the core of | ||
| 16 | > our daily life. We didn't, in other words, sign up for the digital world in | ||
| 17 | > which we're currently entrenched; we seem to have stumbled backward into it. | ||
| 18 | > | ||
| 19 | > _(Digital Minimalism, 2019)_ | ||
| 20 | |||
| 21 | # The Principles of Digital Minimalism | ||
| 22 | |||
| 23 | As noted in Cal Newport's book, _Digital Minimalism_, there are three main | ||
| 24 | principles to digital minimalism that I tend to agree with: | ||
| 25 | |||
| 26 | 1. Clutter is costly. | ||
| 27 | - Digital minimalists recognize that cluttering their time and attention | ||
| 28 | with too many devices, apps, and services creates an overall negative | ||
| 29 | cost that can swamp the small benefits that each individual item | ||
| 30 | provides in isolation. | ||
| 31 | 2. Optimization is important. | ||
| 32 | - Digital minimalists believe that deciding a particular technology | ||
| 33 | supports something they value is only the first step. To truly extract | ||
| 34 | its full potential benefit, it's necessary to think carefully about how | ||
| 35 | they'll use the technology. | ||
| 36 | 3. Intentionality is satisfying. | ||
| 37 | - Digital minimalists derive significant satisfaction from their general | ||
| 38 | commitment to being more intentional about how they engage with new | ||
| 39 | technologies. This source of satisfaction is independent of the specific | ||
| 40 | decisions they make and is one of the biggest reasons that minimalism | ||
| 41 | tends to be immensely meaningful to its practitioners. | ||
| 42 | |||
| 43 | # Taking Action | ||
| 44 | |||
| 45 | In order to put the logic into practice, I've created a few new habits and | ||
| 46 | continued performing old habits that are working well: | ||
| 47 | |||
| 48 | ## Using Devices With Intention | ||
| 49 | |||
| 50 | - I already rarely use "social media", mostly limited to forums such as Hacker | ||
| 51 | News and Tildes, so I've just tweaked my behavior to stop looking for | ||
| 52 | content in those places when I'm bored. | ||
| 53 | - Use devices with intention. Each time I pick up a digital device, there | ||
| 54 | should be an intention to use the device to improve my current situation. No | ||
| 55 | more endless scrolling or searching for something to interest me. | ||
| 56 | |||
| 57 | ## Prevent Distractions | ||
| 58 | |||
| 59 | - Disable (most) notifications on all devices. I spent 15-30 minutes going | ||
| 60 | through the notifications on my phone, watch, and computer to ensure that | ||
| 61 | only a select few apps have the ability to interrupt me: Calendar, Messages, | ||
| 62 | Phone, Reminders, & Signal. | ||
| 63 | - Disable badges for any apps except the ones mentioned in the bullet above. | ||
| 64 | - Set-up focus profiles across devices so that I can enable different modes, | ||
| 65 | such as Personal when I only want to see notifications from people I care | ||
| 66 | about or Do Not Disturb, where absolutely nothing can interrupt me. | ||
| 67 | - Clean up my home screens. This one was quite easy as I already maintain a | ||
| 68 | minimalist set-up, but I went extreme by limiting my phone to just eight | ||
| 69 | apps on the home screen and four in the dock. If I need another app, I'll | ||
| 70 | have to search or use the app library. | ||
| 71 | - Remove the work profile from my phone. This was a tough decision as having | ||
| 72 | my work profile on my device definitely makes my life easier at times, but | ||
| 73 | it also has quite a negative effect when I'm "always online" and can see the | ||
| 74 | notifications and team activity 24/7. I believe creating a distinct barrier | ||
| 75 | between my work and personal devices will be beneficial in the end. | ||
| 76 | |||
| 77 | ## Creating Alternative Activities | ||
| 78 | |||
| 79 | This is the most difficult piece, as most of my hobbies and interests lie in the | ||
| 80 | digital world. However, I'm making a concerted effort to put devices down unless | ||
| 81 | necessary and force myself to perform other activities in the physical world | ||
| 82 | instead. | ||
| 83 | |||
| 84 | I've started with a few basics that are always readily available to me: | ||
| 85 | |||
| 86 | - Do a chore, such as organizing or cleaning. | ||
| 87 | - Read a book, study a piece of art, etc. | ||
| 88 | - Exercise or get outdoors. | ||
| 89 | - Participate in a hobby, such as photography, birding, disc golf, etc. | ||
| 90 | - Let yourself be bored and wander into creativity. | ||
| 91 | |||
| 92 | # Making Progress | ||
| 93 | |||
| 94 | I'll be taking notes as I continue down this journey and hope to see positive | ||
| 95 | trends. I've always been a minimalist in the physical world and it feels | ||
| 96 | refreshing to filter out the clutter that has come to dominate my digital life | ||
| 97 | over the years. | ||
| 98 | |||
| 99 | I'm excited to see where this journey leads. | ||
content/blog/2023-10-04-digital-minimalism.org added +106
| @@ -0,0 +1,106 @@ | |||
| 1 | #+date: <2023-10-04> | ||
| 2 | #+title: Digital Minimalism | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | I've written [[file:///wiki/#digital-garden][a note about minimalism]] | ||
| 7 | before, but I wanted to dedicate some time to reflect on digital | ||
| 8 | minimalism and how I've been able to minimize the impact of digital | ||
| 9 | devices in my life. | ||
| 10 | |||
| 11 | #+begin_quote | ||
| 12 | These changes crept up on us and happened fast, before we had a chance | ||
| 13 | to step back and ask what we really wanted out of the rapid advances of | ||
| 14 | the past decade. We added new technologies to the periphery of our | ||
| 15 | experience for minor reasons, then woke one morning to discover that | ||
| 16 | they had colonized the core of our daily life. We didn't, in other | ||
| 17 | words, sign up for the digital world in which we're currently | ||
| 18 | entrenched; we seem to have stumbled backward into it. | ||
| 19 | |||
| 20 | /(Digital Minimalism, 2019)/ | ||
| 21 | #+end_quote | ||
| 22 | |||
| 23 | * The Principles of Digital Minimalism | ||
| 24 | |||
| 25 | As noted in Cal Newport's book, /Digital Minimalism/, there are three | ||
| 26 | main principles to digital minimalism that I tend to agree with: | ||
| 27 | |||
| 28 | 1. Clutter is costly. | ||
| 29 | - Digital minimalists recognize that cluttering their time and | ||
| 30 | attention with too many devices, apps, and services creates an | ||
| 31 | overall negative cost that can swamp the small benefits that each | ||
| 32 | individual item provides in isolation. | ||
| 33 | 2. Optimization is important. | ||
| 34 | - Digital minimalists believe that deciding a particular technology | ||
| 35 | supports something they value is only the first step. To truly | ||
| 36 | extract its full potential benefit, it's necessary to think | ||
| 37 | carefully about how they'll use the technology. | ||
| 38 | 3. Intentionality is satisfying. | ||
| 39 | - Digital minimalists derive significant satisfaction from their | ||
| 40 | general commitment to being more intentional about how they engage | ||
| 41 | with new technologies. This source of satisfaction is independent | ||
| 42 | of the specific decisions they make and is one of the biggest | ||
| 43 | reasons that minimalism tends to be immensely meaningful to its | ||
| 44 | practitioners. | ||
| 45 | |||
| 46 | * Taking Action | ||
| 47 | |||
| 48 | In order to put the logic into practice, I've created a few new habits | ||
| 49 | and continued performing old habits that are working well: | ||
| 50 | |||
| 51 | ** Using Devices With Intention | ||
| 52 | |||
| 53 | - I already rarely use "social media", mostly limited to forums such as | ||
| 54 | Hacker News and Tildes, so I've just tweaked my behavior to stop | ||
| 55 | looking for content in those places when I'm bored. | ||
| 56 | - Use devices with intention. Each time I pick up a digital device, | ||
| 57 | there should be an intention to use the device to improve my current | ||
| 58 | situation. No more endless scrolling or searching for something to | ||
| 59 | interest me. | ||
| 60 | |||
| 61 | ** Prevent Distractions | ||
| 62 | |||
| 63 | - Disable (most) notifications on all devices. I spent 15-30 minutes | ||
| 64 | going through the notifications on my phone, watch, and computer to | ||
| 65 | ensure that only a select few apps have the ability to interrupt me: | ||
| 66 | Calendar, Messages, Phone, Reminders, & Signal. | ||
| 67 | - Disable badges for any apps except the ones mentioned in the bullet | ||
| 68 | above. | ||
| 69 | - Set-up focus profiles across devices so that I can enable different | ||
| 70 | modes, such as Personal when I only want to see notifications from | ||
| 71 | people I care about or Do Not Disturb, where absolutely nothing can | ||
| 72 | interrupt me. | ||
| 73 | - Clean up my home screens. This one was quite easy as I already | ||
| 74 | maintain a minimalist set-up, but I went extreme by limiting my phone | ||
| 75 | to just eight apps on the home screen and four in the dock. If I need | ||
| 76 | another app, I'll have to search or use the app library. | ||
| 77 | - Remove the work profile from my phone. This was a tough decision as | ||
| 78 | having my work profile on my device definitely makes my life easier at | ||
| 79 | times, but it also has quite a negative effect when I'm "always | ||
| 80 | online" and can see the notifications and team activity 24/7. I | ||
| 81 | believe creating a distinct barrier between my work and personal | ||
| 82 | devices will be beneficial in the end. | ||
| 83 | |||
| 84 | ** Creating Alternative Activities | ||
| 85 | |||
| 86 | This is the most difficult piece, as most of my hobbies and interests | ||
| 87 | lie in the digital world. However, I'm making a concerted effort to put | ||
| 88 | devices down unless necessary and force myself to perform other | ||
| 89 | activities in the physical world instead. | ||
| 90 | |||
| 91 | I've started with a few basics that are always readily available to me: | ||
| 92 | |||
| 93 | - Do a chore, such as organizing or cleaning. | ||
| 94 | - Read a book, study a piece of art, etc. | ||
| 95 | - Exercise or get outdoors. | ||
| 96 | - Participate in a hobby, such as photography, birding, disc golf, etc. | ||
| 97 | - Let yourself be bored and wander into creativity. | ||
| 98 | |||
| 99 | * Making Progress | ||
| 100 | |||
| 101 | I'll be taking notes as I continue down this journey and hope to see | ||
| 102 | positive trends. I've always been a minimalist in the physical world and | ||
| 103 | it feels refreshing to filter out the clutter that has come to dominate | ||
| 104 | my digital life over the years. | ||
| 105 | |||
| 106 | I'm excited to see where this journey leads. | ||
content/blog/2023-10-11-self-hosting-authelia.md → content/blog/2023-10-11-self-hosting-authelia.org renamed +111 −109
| @@ -1,55 +1,54 @@ | |||
| 1 | +++ | 1 | #+date: <2023-10-11> |
| 2 | date = 2023-10-11 | 2 | #+title: Self-Hosting Authelia |
| 3 | title = "Self-Hosting Authelia" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | ||
| 9 | 5 | ||
| 10 | [Authelia](https://www.authelia.com/) is an open-source authentication service | 6 | * Overview |
| 11 | that allows you to place a portal between end users on the internet and | ||
| 12 | self-hosted services on your server. | ||
| 13 | 7 | ||
| 14 | You can require one factor (username+password) or two factor authentication for | 8 | [[https://www.authelia.com/][Authelia]] is an open-source authentication |
| 15 | any such user before allowing them to access a specific service on your domain. | 9 | service that allows you to place a portal between end users on the |
| 10 | internet and self-hosted services on your server. | ||
| 11 | |||
| 12 | You can require one factor (username+password) or two factor | ||
| 13 | authentication for any such user before allowing them to access a | ||
| 14 | specific service on your domain. | ||
| 16 | 15 | ||
| 17 | This guide will walk through a standard installation of Authelia for | 16 | This guide will walk through a standard installation of Authelia for |
| 18 | `example.com`, using `auth.example.com` as Authelia's authentication domain and | 17 | =example.com=, using =auth.example.com= as Authelia's authentication |
| 19 | `teddit.example.com` as the website we want to protect behind the authentication | 18 | domain and =teddit.example.com= as the website we want to protect behind |
| 20 | portal. | 19 | the authentication portal. |
| 21 | 20 | ||
| 22 | # Prerequisites | 21 | * Prerequisites |
| 23 | 22 | ||
| 24 | This guide assumes you have the following already set-up: | 23 | This guide assumes you have the following already set-up: |
| 25 | 24 | ||
| 26 | - A registered domain with DNS pointing to your server. | 25 | - A registered domain with DNS pointing to your server. |
| 27 | - A subdomain for Authelia (`auth.example.com`) and a subdomain to protect via | 26 | - A subdomain for Authelia (=auth.example.com=) and a subdomain to |
| 28 | Authelia (`app.example.com`). | 27 | protect via Authelia (=app.example.com=). |
| 29 | - A working Nginx web server. | 28 | - A working Nginx web server. |
| 30 | - Docker and docker-compose installed. | 29 | - Docker and docker-compose installed. |
| 31 | 30 | ||
| 32 | # Installation | 31 | * Installation |
| 33 | 32 | ||
| 34 | This guide will walk through each installation step one-by-one, starting with | 33 | This guide will walk through each installation step one-by-one, starting |
| 35 | the container and finishing by cleaning up external access via an Nginx reverse | 34 | with the container and finishing by cleaning up external access via an |
| 36 | proxy. | 35 | Nginx reverse proxy. |
| 37 | 36 | ||
| 38 | ## Docker-Compose | 37 | ** Docker-Compose |
| 39 | 38 | ||
| 40 | To start, create a directory for Authelia and create a `docker-compose.yml` | 39 | To start, create a directory for Authelia and create a |
| 41 | file. | 40 | =docker-compose.yml= file. |
| 42 | 41 | ||
| 43 | ```sh | 42 | #+begin_src sh |
| 44 | mkdir ~/authelia | 43 | mkdir ~/authelia |
| 45 | nano ~/authelia/docker-compose.yml | 44 | nano ~/authelia/docker-compose.yml |
| 46 | ``` | 45 | #+end_src |
| 47 | 46 | ||
| 48 | Within this file, paste the following content. If you prefer a different local | 47 | Within this file, paste the following content. If you prefer a different |
| 49 | port, modify the port on the left side of the colon on the `9091:9091` line. Be | 48 | local port, modify the port on the left side of the colon on the |
| 50 | sure to modify the `TZ` variable to your timezone. | 49 | =9091:9091= line. Be sure to modify the =TZ= variable to your timezone. |
| 51 | 50 | ||
| 52 | ```yml | 51 | #+begin_src yml |
| 53 | version: "3.3" | 52 | version: "3.3" |
| 54 | 53 | ||
| 55 | services: | 54 | services: |
| @@ -62,51 +61,53 @@ services: | |||
| 62 | - 9091:9091 | 61 | - 9091:9091 |
| 63 | environment: | 62 | environment: |
| 64 | - TZ=America/Chicago | 63 | - TZ=America/Chicago |
| 65 | ``` | 64 | #+end_src |
| 66 | 65 | ||
| 67 | Start the container with docker-compose: | 66 | Start the container with docker-compose: |
| 68 | 67 | ||
| 69 | ```sh | 68 | #+begin_src sh |
| 70 | sudo docker-compose up -d | 69 | sudo docker-compose up -d |
| 71 | ``` | 70 | #+end_src |
| 72 | 71 | ||
| 73 | After the first start, the container will automatically exit and require you to | 72 | After the first start, the container will automatically exit and require |
| 74 | modify the app's configuration files before continuing. Read on to learn more. | 73 | you to modify the app's configuration files before continuing. Read on |
| 74 | to learn more. | ||
| 75 | 75 | ||
| 76 | ## Authelia Configuration | 76 | ** Authelia Configuration |
| 77 | 77 | ||
| 78 | To configure Authelia before we restart the container, we need to open the | 78 | To configure Authelia before we restart the container, we need to open |
| 79 | `config` directory and modify the files. Start by editing the | 79 | the =config= directory and modify the files. Start by editing the |
| 80 | `configuration.yml` file, where all of Authelia's settings are stored. | 80 | =configuration.yml= file, where all of Authelia's settings are stored. |
| 81 | 81 | ||
| 82 | My personal preference is to copy the original configuration file to a backup | 82 | My personal preference is to copy the original configuration file to a |
| 83 | file and edit a fresh copy. | 83 | backup file and edit a fresh copy. |
| 84 | 84 | ||
| 85 | ```sh | 85 | #+begin_src sh |
| 86 | sudo cp ~/authelia/config/configuration.yml ~/authelia/config/configuration.yml.bk | 86 | sudo cp ~/authelia/config/configuration.yml ~/authelia/config/configuration.yml.bk |
| 87 | sudo nano ~/authelia/config/configuration.yml | 87 | sudo nano ~/authelia/config/configuration.yml |
| 88 | ``` | 88 | #+end_src |
| 89 | 89 | ||
| 90 | Within the blank `configuration.yml` file, paste the following information. You | 90 | Within the blank =configuration.yml= file, paste the following |
| 91 | will need to make quite a few updates, so be sure to read each line carefully | 91 | information. You will need to make quite a few updates, so be sure to |
| 92 | and modify as necessary. | 92 | read each line carefully and modify as necessary. |
| 93 | 93 | ||
| 94 | The major required changes are: | 94 | The major required changes are: |
| 95 | 95 | ||
| 96 | - Any instances of `example.com` should be replaced by your domain. | 96 | - Any instances of =example.com= should be replaced by your domain. |
| 97 | - `jwt_secret` - Use the `pwgen 40 1` command to generate a secret for | 97 | - =jwt_secret= - Use the =pwgen 40 1= command to generate a secret for |
| 98 | yourself. | 98 | yourself. |
| 99 | - `access_control` - Set the Authelia domain to bypass here, as well as any | 99 | - =access_control= - Set the Authelia domain to bypass here, as well as |
| 100 | subdomains you want to protect. | 100 | any subdomains you want to protect. |
| 101 | - `session` > `secret` - Use the `pwgen 40 1` command to generate a secret for | 101 | - =session= > =secret= - Use the =pwgen 40 1= command to generate a |
| 102 | yourself. | 102 | secret for yourself. |
| 103 | - `regulation` - Set the variables here to restrict login attempts and bans. | 103 | - =regulation= - Set the variables here to restrict login attempts and |
| 104 | - `storage` > `encryption_key` - Use the `pwgen 40 1` command to generate a | 104 | bans. |
| 105 | secret for yourself. | 105 | - =storage= > =encryption_key= - Use the =pwgen 40 1= command to |
| 106 | - `smtp` - If you have access to an SMTP service, set up the information here | 106 | generate a secret for yourself. |
| 107 | to active outgoing emails. | 107 | - =smtp= - If you have access to an SMTP service, set up the information |
| 108 | 108 | here to active outgoing emails. | |
| 109 | ```yml | 109 | |
| 110 | #+begin_src yml | ||
| 110 | # yamllint disable rule:comments-indentation | 111 | # yamllint disable rule:comments-indentation |
| 111 | --- | 112 | --- |
| 112 | ############################################################################### | 113 | ############################################################################### |
| @@ -194,25 +195,26 @@ notifier: | |||
| 194 | tls: | 195 | tls: |
| 195 | skip_verify: false | 196 | skip_verify: false |
| 196 | minimum_version: TLS1.2 | 197 | minimum_version: TLS1.2 |
| 197 | ``` | 198 | #+end_src |
| 198 | 199 | ||
| 199 | ## Authelia Users | 200 | ** Authelia Users |
| 200 | 201 | ||
| 201 | Next, create the users file for authentication. | 202 | Next, create the users file for authentication. |
| 202 | 203 | ||
| 203 | ```sh | 204 | #+begin_src sh |
| 204 | sudo nano ~/authelia/config/users_database.yml | 205 | sudo nano ~/authelia/config/users_database.yml |
| 205 | ``` | 206 | #+end_src |
| 206 | 207 | ||
| 207 | Within the file, you will need to create an entry for each user that needs | 208 | Within the file, you will need to create an entry for each user that |
| 208 | access to Authelia. The `my_username` entry will be the username used on the | 209 | needs access to Authelia. The =my_username= entry will be the username |
| 209 | login page. | 210 | used on the login page. |
| 210 | 211 | ||
| 211 | To generate the password, go to [Argon2 Hash Generator](https://argon2.online), | 212 | To generate the password, go to [[https://argon2.online][Argon2 Hash |
| 212 | generate a random salt, and make sure the rest of the settings match the | 213 | Generator]], generate a random salt, and make sure the rest of the |
| 213 | `authentication_backend` section of `configuration.yml` file. | 214 | settings match the =authentication_backend= section of |
| 215 | =configuration.yml= file. | ||
| 214 | 216 | ||
| 215 | ```yaml | 217 | #+begin_src yaml |
| 216 | users: | 218 | users: |
| 217 | my_username: | 219 | my_username: |
| 218 | displayname: "My User" | 220 | displayname: "My User" |
| @@ -223,31 +225,31 @@ users: | |||
| 223 | groups: | 225 | groups: |
| 224 | - admins | 226 | - admins |
| 225 | - dev | 227 | - dev |
| 226 | ``` | 228 | #+end_src |
| 227 | 229 | ||
| 228 | Once the app is configured, restart the container from scratch. | 230 | Once the app is configured, restart the container from scratch. |
| 229 | 231 | ||
| 230 | ```sh | 232 | #+begin_src sh |
| 231 | cd ~/authelia | 233 | cd ~/authelia |
| 232 | sudo docker-compose down && sudo docker-compose up -d | 234 | sudo docker-compose down && sudo docker-compose up -d |
| 233 | ``` | 235 | #+end_src |
| 234 | 236 | ||
| 235 | ## Nginx: Authelia Domain | 237 | ** Nginx: Authelia Domain |
| 236 | 238 | ||
| 237 | Once the container is running and configured, the final step is to configure | 239 | Once the container is running and configured, the final step is to |
| 238 | external access to the server via Nginx reverse proxy. | 240 | configure external access to the server via Nginx reverse proxy. |
| 239 | 241 | ||
| 240 | Start by creating the Authelia domain. | 242 | Start by creating the Authelia domain. |
| 241 | 243 | ||
| 242 | ```sh | 244 | #+begin_src sh |
| 243 | sudo nano /etc/nginx/sites-available/auth | 245 | sudo nano /etc/nginx/sites-available/auth |
| 244 | ``` | 246 | #+end_src |
| 245 | 247 | ||
| 246 | Within this file, paste the following information and be sure to update | 248 | Within this file, paste the following information and be sure to update |
| 247 | `example.com` to your domain. Make sure the `$upstream_authelia` variable | 249 | =example.com= to your domain. Make sure the =$upstream_authelia= |
| 248 | matches the location of your Authelia container. | 250 | variable matches the location of your Authelia container. |
| 249 | 251 | ||
| 250 | ```conf | 252 | #+begin_src conf |
| 251 | server { | 253 | server { |
| 252 | if ($host ~ ^[^.]+\.example\.com$) { | 254 | if ($host ~ ^[^.]+\.example\.com$) { |
| 253 | return 301 https://$host$request_uri; | 255 | return 301 https://$host$request_uri; |
| @@ -299,30 +301,30 @@ server { | |||
| 299 | } | 301 | } |
| 300 | 302 | ||
| 301 | } | 303 | } |
| 302 | ``` | 304 | #+end_src |
| 303 | 305 | ||
| 304 | Next, symlink the file and restart Nginx. If there are errors, be sure to | 306 | Next, symlink the file and restart Nginx. If there are errors, be sure |
| 305 | resolve those before moving on. | 307 | to resolve those before moving on. |
| 306 | 308 | ||
| 307 | ```sh | 309 | #+begin_src sh |
| 308 | sudo ln -s /etc/nginx/sites-available/auth /etc/nginx/sites-enabled/auth | 310 | sudo ln -s /etc/nginx/sites-available/auth /etc/nginx/sites-enabled/auth |
| 309 | sudo systemctl restart nginx.service | 311 | sudo systemctl restart nginx.service |
| 310 | ``` | 312 | #+end_src |
| 311 | 313 | ||
| 312 | ## Nginx: Protected Domain(s) | 314 | ** Nginx: Protected Domain(s) |
| 313 | 315 | ||
| 314 | Now that Authelia is accessible externally, you need to configure the domain you | 316 | Now that Authelia is accessible externally, you need to configure the |
| 315 | intend to protect with Authelia. In this example, I'm protecting | 317 | domain you intend to protect with Authelia. In this example, I'm |
| 316 | `teddit.example.com`. | 318 | protecting =teddit.example.com=. |
| 317 | 319 | ||
| 318 | Similar to the process above, paste the content and update the relevant | 320 | Similar to the process above, paste the content and update the relevant |
| 319 | variables. | 321 | variables. |
| 320 | 322 | ||
| 321 | ```sh | 323 | #+begin_src sh |
| 322 | sudo nano /etc/nginx/sites-available/teddit | 324 | sudo nano /etc/nginx/sites-available/teddit |
| 323 | ``` | 325 | #+end_src |
| 324 | 326 | ||
| 325 | ```conf | 327 | #+begin_src conf |
| 326 | server { | 328 | server { |
| 327 | if ($host ~ ^[^.]+\.example\.com$) { | 329 | if ($host ~ ^[^.]+\.example\.com$) { |
| 328 | return 301 https://$host$request_uri; | 330 | return 301 https://$host$request_uri; |
| @@ -424,26 +426,26 @@ server { | |||
| 424 | proxy_buffers 64 256k; | 426 | proxy_buffers 64 256k; |
| 425 | } | 427 | } |
| 426 | } | 428 | } |
| 427 | ``` | 429 | #+end_src |
| 428 | 430 | ||
| 429 | Same as before, symlink the file and restart Nginx. | 431 | Same as before, symlink the file and restart Nginx. |
| 430 | 432 | ||
| 431 | ```sh | 433 | #+begin_src sh |
| 432 | sudo ln -s /etc/nginx/sites-available/teddit /etc/nginx/sites-enabled/teddit | 434 | sudo ln -s /etc/nginx/sites-available/teddit /etc/nginx/sites-enabled/teddit |
| 433 | sudo systemctl restart nginx.service | 435 | sudo systemctl restart nginx.service |
| 434 | ``` | 436 | #+end_src |
| 435 | 437 | ||
| 436 | # Results | 438 | * Results |
| 437 | 439 | ||
| 438 | When visiting the protected domain, you will now be redirected to your | 440 | When visiting the protected domain, you will now be redirected to your |
| 439 | authentication domain and presented with the Authelia login portal. | 441 | authentication domain and presented with the Authelia login portal. |
| 440 | 442 | ||
| 441 |  | 444 | [[https://img.cleberg.net/blog/20231010-authelia/authelia_portal.png]] |
| 443 | 445 | ||
| 444 | Once you've successfully authenticated, you can visit your authentication domain | 446 | Once you've successfully authenticated, you can visit your |
| 445 | directly and see that you're currently authenticated to any domain protected by | 447 | authentication domain directly and see that you're currently |
| 446 | Authelia. | 448 | authenticated to any domain protected by Authelia. |
| 447 | 449 | ||
| 448 |  | 451 | [[https://img.cleberg.net/blog/20231010-authelia/authelia_success.png]] |
content/blog/2023-10-15-alpine-ssh-hardening.md → content/blog/2023-10-15-alpine-ssh-hardening.org renamed +35 −37
| @@ -1,69 +1,67 @@ | |||
| 1 | +++ | 1 | #+date: <2023-10-15> |
| 2 | date = 2023-10-15 | 2 | #+title: SSH Hardening for Alpine Linux |
| 3 | title = "SSH Hardening for Alpine Linux" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | 5 | |
| 6 | * Overview | ||
| 9 | 7 | ||
| 10 | This guide follows the standard | 8 | This guide follows the standard |
| 11 | [ssh-audit](https://www.ssh-audit.com/hardening_guides.html) hardening guide, | 9 | [[https://www.ssh-audit.com/hardening_guides.html][ssh-audit]] hardening |
| 12 | tweaked for Alpine Linux. | 10 | guide, tweaked for Alpine Linux. |
| 13 | 11 | ||
| 14 | # Hardening Guide | 12 | * Hardening Guide |
| 15 | 13 | ||
| 16 | These steps must be performed as root. You can try to use `doas` or `sudo`, but | 14 | These steps must be performed as root. You can try to use =doas= or |
| 17 | there may be issues. | 15 | =sudo=, but there may be issues. |
| 18 | 16 | ||
| 19 | 1. Re-generate the RSA and ED25519 keys | 17 | 1. Re-generate the RSA and ED25519 keys |
| 20 | 18 | ||
| 21 | ```sh | 19 | #+begin_src sh |
| 22 | rm /etc/ssh/ssh_host_* | 20 | rm /etc/ssh/ssh_host_* |
| 23 | ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N "" | 21 | ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N "" |
| 24 | ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" | 22 | ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" |
| 25 | ``` | 23 | #+end_src |
| 26 | 24 | ||
| 27 | 2. Remove small Diffie-Hellman moduli | 25 | 2. [@2] Remove small Diffie-Hellman moduli |
| 28 | 26 | ||
| 29 | ```sh | 27 | #+begin_src sh |
| 30 | awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe | 28 | awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe |
| 31 | mv /etc/ssh/moduli.safe /etc/ssh/moduli | 29 | mv /etc/ssh/moduli.safe /etc/ssh/moduli |
| 32 | ``` | 30 | #+end_src |
| 33 | 31 | ||
| 34 | 3. Enable the RSA and ED25519 HostKey directives in the /etc/ssh/sshd~config~ | 32 | 3. [@3] Enable the RSA and ED25519 HostKey directives in the |
| 35 | file | 33 | /etc/ssh/sshd_{config} file |
| 36 | 34 | ||
| 37 | ```sh | 35 | #+begin_src sh |
| 38 | sed -i 's/^\#HostKey \/etc\/ssh\/ssh_host_\(rsa\|ed25519\)_key$/HostKey \/etc\/ssh\/ssh_host_\1_key/g' /etc/ssh/sshd_config | 36 | sed -i 's/^\#HostKey \/etc\/ssh\/ssh_host_\(rsa\|ed25519\)_key$/HostKey \/etc\/ssh\/ssh_host_\1_key/g' /etc/ssh/sshd_config |
| 39 | ``` | 37 | #+end_src |
| 40 | 38 | ||
| 41 | 4. Restrict supported key exchange, cipher, and MAC algorithms | 39 | 4. [@4] Restrict supported key exchange, cipher, and MAC algorithms |
| 42 | 40 | ||
| 43 | ```sh | 41 | #+begin_src sh |
| 44 | echo -e "\n# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\n# hardening guide.\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\nHostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf | 42 | echo -e "\n# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\n# hardening guide.\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\nHostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf |
| 45 | ``` | 43 | #+end_src |
| 46 | 44 | ||
| 47 | 5. Include the /etc/ssh/sshd~config~.d directory | 45 | 5. [@5] Include the /etc/ssh/sshd_{config}.d directory |
| 48 | 46 | ||
| 49 | ```sh | 47 | #+begin_src sh |
| 50 | echo -e "Include /etc/ssh/sshd_config.d/*.conf" > /etc/ssh/sshd_config | 48 | echo -e "Include /etc/ssh/sshd_config.d/*.conf" > /etc/ssh/sshd_config |
| 51 | ``` | 49 | #+end_src |
| 52 | 50 | ||
| 53 | 6. Restart OpenSSH server | 51 | 6. [@6] Restart OpenSSH server |
| 54 | 52 | ||
| 55 | ```sh | 53 | #+begin_src sh |
| 56 | rc-service sshd restart | 54 | rc-service sshd restart |
| 57 | ``` | 55 | #+end_src |
| 58 | 56 | ||
| 59 | # Testing SSH | 57 | * Testing SSH |
| 60 | 58 | ||
| 61 | You can test the results with the `ssh-audit` python script. | 59 | You can test the results with the =ssh-audit= python script. |
| 62 | 60 | ||
| 63 | ```sh | 61 | #+begin_src sh |
| 64 | pip3 install ssh-audit | 62 | pip3 install ssh-audit |
| 65 | ssh-audit localhost | 63 | ssh-audit localhost |
| 66 | ``` | 64 | #+end_src |
| 67 | 65 | ||
| 68 | If everything succeeded, the results will show as all green. If anything is | 66 | If everything succeeded, the results will show as all green. If anything |
| 69 | yellow, orange, or red, you may need to tweak additional settings. | 67 | is yellow, orange, or red, you may need to tweak additional settings. |
content/blog/2023-10-17-self-hosting-anonymousoverflow.md → content/blog/2023-10-17-self-hosting-anonymousoverflow.org renamed +39 −40
| @@ -1,34 +1,33 @@ | |||
| 1 | +++ | 1 | #+date: <2023-10-17> |
| 2 | date = 2023-10-17 | 2 | #+title: Self-Hosting AnonymousOverflow |
| 3 | title = "Self-Hosting AnonymousOverflow" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | 5 | |
| 6 | * Overview | ||
| 9 | 7 | ||
| 10 | I recently launched an instance of AnonymousOverflow at | 8 | I recently launched an instance of AnonymousOverflow at |
| 11 | [ao.cleberg.net](https://ao.cleberg.net) and wanted to write a brief post on how | 9 | [[https://ao.cleberg.net][ao.cleberg.net]] and wanted to write a brief |
| 12 | easy it is to install with Docker Compose and Nginx. | 10 | post on how easy it is to install with Docker Compose and Nginx. |
| 13 | 11 | ||
| 14 | This guide uses Ubuntu server, Docker Compose, and Nginx as a reverse proxy. | 12 | This guide uses Ubuntu server, Docker Compose, and Nginx as a reverse |
| 13 | proxy. | ||
| 15 | 14 | ||
| 16 | # Installation | 15 | * Installation |
| 17 | 16 | ||
| 18 | ## Docker Compose | 17 | ** Docker Compose |
| 19 | 18 | ||
| 20 | To install AnonymousOverflow, start by creating a directory for the application | 19 | To install AnonymousOverflow, start by creating a directory for the |
| 21 | and create its `docker-compose.yml` file. | 20 | application and create its =docker-compose.yml= file. |
| 22 | 21 | ||
| 23 | ```sh | 22 | #+begin_src sh |
| 24 | mkdir ~/anonymousoverflow && cd ~/anonymousoverflow | 23 | mkdir ~/anonymousoverflow && cd ~/anonymousoverflow |
| 25 | nano docker-compose.yml | 24 | nano docker-compose.yml |
| 26 | ``` | 25 | #+end_src |
| 27 | 26 | ||
| 28 | Within this file, paste the following information. Be sure to change the | 27 | Within this file, paste the following information. Be sure to change the |
| 29 | `APP_URL`, `JWT_SIGNING_SECRET`, and `ports` to match your needs. | 28 | =APP_URL=, =JWT_SIGNING_SECRET=, and =ports= to match your needs. |
| 30 | 29 | ||
| 31 | ```yaml | 30 | #+begin_src yaml |
| 32 | version: "3" | 31 | version: "3" |
| 33 | 32 | ||
| 34 | services: | 33 | services: |
| @@ -41,31 +40,31 @@ services: | |||
| 41 | ports: | 40 | ports: |
| 42 | - "9380:8080" | 41 | - "9380:8080" |
| 43 | restart: "always" | 42 | restart: "always" |
| 44 | ``` | 43 | #+end_src |
| 45 | 44 | ||
| 46 | Save and exit the file when complete. You can now launch the container and | 45 | Save and exit the file when complete. You can now launch the container |
| 47 | access it via your local network. | 46 | and access it via your local network. |
| 48 | 47 | ||
| 49 | ```sh | 48 | #+begin_src sh |
| 50 | sudo docker-compose up -d | 49 | sudo docker-compose up -d |
| 51 | ``` | 50 | #+end_src |
| 52 | 51 | ||
| 53 | ## Nginx Reverse Proxy | 52 | ** Nginx Reverse Proxy |
| 54 | 53 | ||
| 55 | If you want to access this service outside the local network, I recommend using | 54 | If you want to access this service outside the local network, I |
| 56 | Nginx as a reverse proxy. | 55 | recommend using Nginx as a reverse proxy. |
| 57 | 56 | ||
| 58 | Let's start by creating a configuration file. | 57 | Let's start by creating a configuration file. |
| 59 | 58 | ||
| 60 | ```sh | 59 | #+begin_src sh |
| 61 | sudo nano /etc/nginx/sites-available/ao | 60 | sudo nano /etc/nginx/sites-available/ao |
| 62 | ``` | 61 | #+end_src |
| 63 | 62 | ||
| 64 | Within this file, paste the following content and repace `ao.example.com` with | 63 | Within this file, paste the following content and repace |
| 65 | your URL. You may need to update the SSL certificate statements if your | 64 | =ao.example.com= with your URL. You may need to update the SSL |
| 66 | certificates are in a different location. | 65 | certificate statements if your certificates are in a different location. |
| 67 | 66 | ||
| 68 | ```conf | 67 | #+begin_src conf |
| 69 | server { | 68 | server { |
| 70 | if ($host ~ ^[^.]+\.cleberg\.net$) { | 69 | if ($host ~ ^[^.]+\.cleberg\.net$) { |
| 71 | return 301 https://$host$request_uri; | 70 | return 301 https://$host$request_uri; |
| @@ -117,16 +116,16 @@ server { | |||
| 117 | proxy_buffers 64 256k; | 116 | proxy_buffers 64 256k; |
| 118 | } | 117 | } |
| 119 | } | 118 | } |
| 120 | ``` | 119 | #+end_src |
| 121 | 120 | ||
| 122 | Save and exit the file when complete. On Ubuntu, you will need to symlink the | 121 | Save and exit the file when complete. On Ubuntu, you will need to |
| 123 | configuration file before it will be recognized by Nginx. Once complete, simply | 122 | symlink the configuration file before it will be recognized by Nginx. |
| 124 | restart the web server. | 123 | Once complete, simply restart the web server. |
| 125 | 124 | ||
| 126 | ```sh | 125 | #+begin_src sh |
| 127 | sudo ln -s /etc/nginx/sites-available/ao /etc/nginx/sites-enabled/ao | 126 | sudo ln -s /etc/nginx/sites-available/ao /etc/nginx/sites-enabled/ao |
| 128 | sudo systemctl restart nginx.service | 127 | sudo systemctl restart nginx.service |
| 129 | ``` | 128 | #+end_src |
| 130 | 129 | ||
| 131 | The website will now be available publicly. Visit [my | 130 | The website will now be available publicly. Visit |
| 132 | instance](https://ao.cleberg.net) for an example. | 131 | [[https://ao.cleberg.net][my instance]] for an example. |
content/blog/2023-11-08-scli.md deleted −147
| @@ -1,147 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2023-11-08 | ||
| 3 | title = "Installing scli on Alpine Linux (musl)" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | [scli](https://github.com/isamert/scli) is a command-line tool that allows you | ||
| 9 | to connect to your Signal messenger account. This program utilizes a two-pane | ||
| 10 | display that shows you chats on the left and the focused conversation on the | ||
| 11 | right. | ||
| 12 | |||
| 13 | This guide will show you how to install `scli` and its dependencies on Alpine | ||
| 14 | Linux, which requires some extra work due to musl. | ||
| 15 | |||
| 16 | If you're using a non-musl system, you can likely following the `scli` README | ||
| 17 | and download the packaged binaries for an easier installation process. | ||
| 18 | |||
| 19 | # Dependencies | ||
| 20 | |||
| 21 | In order to use `scli`, you need a few dependencies: | ||
| 22 | |||
| 23 | - `openjdk17-jre` - Used as a dependency for the `signal-cli` tool. Version | ||
| 24 | may vary. | ||
| 25 | - `signal-cli` - Used as the backbone of the `scli` tool. | ||
| 26 | - `findutils` - Replaces the standard Busybox version of `xargs`. | ||
| 27 | - `urwid` - A console user interface library for Python. | ||
| 28 | - `urwid-readline` - For GNU emacs-like keybinds on the input line. | ||
| 29 | - `qrencode` - Displays a QR code in the terminal to link the device using | ||
| 30 | your phone. Not necessary if you're only linking on desktop and can | ||
| 31 | copy/paste the connection URL. | ||
| 32 | |||
| 33 | Let's start by installing the packages available via Alpine's repositories. Be | ||
| 34 | sure to install the latest version of `openjdk`. If you run into Java-related | ||
| 35 | issues, uninstall `openjdk` and install an older version. | ||
| 36 | |||
| 37 | ```sh | ||
| 38 | doas apk add openjdk17-jre findutils qrencode | ||
| 39 | ``` | ||
| 40 | |||
| 41 | Next, let's install `signal-cli`. Be sure to export the version of `signal-cli` | ||
| 42 | that you want. I use version `0.12.4` below, but that may be outdated by the | ||
| 43 | time you're reading this. | ||
| 44 | |||
| 45 | ```sh | ||
| 46 | export VERSION="0.12.4" | ||
| 47 | wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz | ||
| 48 | doas tar xf signal-cli-"${VERSION}".tar.gz -C /opt | ||
| 49 | doas ln -sf /opt/signal-cli-${VERSION}"/bin/signal/cli /usr/local/bin | ||
| 50 | ``` | ||
| 51 | |||
| 52 | Finally, install the `urwid` packages using the Python packaging utility. | ||
| 53 | |||
| 54 | ```sh | ||
| 55 | pip3 install urwid urwid-readline | ||
| 56 | ``` | ||
| 57 | |||
| 58 | # Installation | ||
| 59 | |||
| 60 | Now that we have all of the dependencies we need, we can install `scli`. Start | ||
| 61 | by simply cloning the repository. | ||
| 62 | |||
| 63 | ```sh | ||
| 64 | git clone https://github.com/isamert/scli | ||
| 65 | ``` | ||
| 66 | |||
| 67 | When I cloned this repository on 2023-11-08, I found a bug in the logic that | ||
| 68 | required a fix. You must edit the `scli` file and replace the one instance of | ||
| 69 | `RLIMIT_OFILE` with `RLIMIT_NOFILE`. | ||
| 70 | |||
| 71 | ```sh | ||
| 72 | cd scli | ||
| 73 | nano scli | ||
| 74 | ``` | ||
| 75 | |||
| 76 | Once complete, you can move this program to anywhere on your `$PATH`. I chose | ||
| 77 | the following directory. | ||
| 78 | |||
| 79 | ```sh | ||
| 80 | doas mv scli /usr/local/bin/scli | ||
| 81 | ``` | ||
| 82 | |||
| 83 | # Initial Setup | ||
| 84 | |||
| 85 | Now that everything is installed, we can login and configure the client. Start | ||
| 86 | by generating a connection link. | ||
| 87 | |||
| 88 | ```sh | ||
| 89 | signal-cli link -n "YOUR-DEVICE-NICKNAME" | tee >(xargs -L 1 qrencode -t utf8) | ||
| 90 | ``` | ||
| 91 | |||
| 92 | This will generate a connection link and related QR code for you to use to link | ||
| 93 | the devices together. Once complete, **wait patiently** for the connection | ||
| 94 | process to finish. | ||
| 95 | |||
| 96 | Once it completes, it will exit and return you to the prompt. From here, you | ||
| 97 | need to perform an initial `receive` command to start things off. The `USERNAME` | ||
| 98 | variable should be your phone number, such as `+15551237890`. | ||
| 99 | |||
| 100 | ```sh | ||
| 101 | signal-cli -u USERNAME receive | ||
| 102 | ``` | ||
| 103 | |||
| 104 | Also be sure to test the daemon to ensure it works properly. If no errors occur, | ||
| 105 | it's working. If you run into errors because you're not running a DBUS session, | ||
| 106 | see my notes below. | ||
| 107 | |||
| 108 | ```sh | ||
| 109 | signal-cli -u USERNAME daemon | ||
| 110 | ``` | ||
| 111 | |||
| 112 | Once the initial reception is complete, you are ready to use `scli`. | ||
| 113 | |||
| 114 | This process will differ depending on your desktop environment (DE). If you are | ||
| 115 | running a DE, you likely have a DBUS session running already and can simply | ||
| 116 | launch the program. | ||
| 117 | |||
| 118 | However, if you're like me and running your computer straight on the TTY without | ||
| 119 | a DE, you'll need to start a DBUS session for this program. | ||
| 120 | |||
| 121 | ```sh | ||
| 122 | # If you're not running a DBUS session yet, you need to start one for scli | ||
| 123 | dbus-run-session -- scli | ||
| 124 | |||
| 125 | # OR - If you're already running a DBUS session, simply run scli | ||
| 126 | scli | ||
| 127 | ``` | ||
| 128 | |||
| 129 | # Configuration | ||
| 130 | |||
| 131 | Lastly, there are a number of configuration options that you can pass via the | ||
| 132 | command or in the `~/.config/sclirc` file. See the Github README for more | ||
| 133 | information on configuration options. | ||
| 134 | |||
| 135 | ```sh | ||
| 136 | nano ~/.config/sclirc | ||
| 137 | ``` | ||
| 138 | |||
| 139 | ```conf | ||
| 140 | # ~/.config/sclirc | ||
| 141 | |||
| 142 | wrap-at = 80 | ||
| 143 | enable-notifications = true | ||
| 144 | ``` | ||
| 145 | |||
| 146 | That's it! Following this guide, I have a functional `scli` program that | ||
| 147 | successfully sends messages to my contacts and myself! | ||
content/blog/2023-11-08-scli.org added +149
| @@ -0,0 +1,149 @@ | |||
| 1 | #+date: <2023-11-08> | ||
| 2 | #+title: Installing scli on Alpine Linux (musl) | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | [[https://github.com/isamert/scli][scli]] is a command-line tool that | ||
| 7 | allows you to connect to your Signal messenger account. This program | ||
| 8 | utilizes a two-pane display that shows you chats on the left and the | ||
| 9 | focused conversation on the right. | ||
| 10 | |||
| 11 | This guide will show you how to install =scli= and its dependencies on | ||
| 12 | Alpine Linux, which requires some extra work due to musl. | ||
| 13 | |||
| 14 | If you're using a non-musl system, you can likely following the =scli= | ||
| 15 | README and download the packaged binaries for an easier installation | ||
| 16 | process. | ||
| 17 | |||
| 18 | * Dependencies | ||
| 19 | |||
| 20 | In order to use =scli=, you need a few dependencies: | ||
| 21 | |||
| 22 | - =openjdk17-jre= - Used as a dependency for the =signal-cli= tool. | ||
| 23 | Version may vary. | ||
| 24 | - =signal-cli= - Used as the backbone of the =scli= tool. | ||
| 25 | - =findutils= - Replaces the standard Busybox version of =xargs=. | ||
| 26 | - =urwid= - A console user interface library for Python. | ||
| 27 | - =urwid-readline= - For GNU emacs-like keybinds on the input line. | ||
| 28 | - =qrencode= - Displays a QR code in the terminal to link the device | ||
| 29 | using your phone. Not necessary if you're only linking on desktop and | ||
| 30 | can copy/paste the connection URL. | ||
| 31 | |||
| 32 | Let's start by installing the packages available via Alpine's | ||
| 33 | repositories. Be sure to install the latest version of =openjdk=. If you | ||
| 34 | run into Java-related issues, uninstall =openjdk= and install an older | ||
| 35 | version. | ||
| 36 | |||
| 37 | #+begin_src sh | ||
| 38 | doas apk add openjdk17-jre findutils qrencode | ||
| 39 | #+end_src | ||
| 40 | |||
| 41 | Next, let's install =signal-cli=. Be sure to export the version of | ||
| 42 | =signal-cli= that you want. I use version =0.12.4= below, but that may | ||
| 43 | be outdated by the time you're reading this. | ||
| 44 | |||
| 45 | #+begin_src sh | ||
| 46 | export VERSION="0.12.4" | ||
| 47 | wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz | ||
| 48 | doas tar xf signal-cli-"${VERSION}".tar.gz -C /opt | ||
| 49 | doas ln -sf /opt/signal-cli-${VERSION}"/bin/signal/cli /usr/local/bin | ||
| 50 | #+end_src | ||
| 51 | |||
| 52 | Finally, install the =urwid= packages using the Python packaging | ||
| 53 | utility. | ||
| 54 | |||
| 55 | #+begin_src sh | ||
| 56 | pip3 install urwid urwid-readline | ||
| 57 | #+end_src | ||
| 58 | |||
| 59 | * Installation | ||
| 60 | |||
| 61 | Now that we have all of the dependencies we need, we can install =scli=. | ||
| 62 | Start by simply cloning the repository. | ||
| 63 | |||
| 64 | #+begin_src sh | ||
| 65 | git clone https://github.com/isamert/scli | ||
| 66 | #+end_src | ||
| 67 | |||
| 68 | When I cloned this repository on 2023-11-08, I found a bug in the logic | ||
| 69 | that required a fix. You must edit the =scli= file and replace the one | ||
| 70 | instance of =RLIMIT_OFILE= with =RLIMIT_NOFILE=. | ||
| 71 | |||
| 72 | #+begin_src sh | ||
| 73 | cd scli | ||
| 74 | nano scli | ||
| 75 | #+end_src | ||
| 76 | |||
| 77 | Once complete, you can move this program to anywhere on your =$PATH=. I | ||
| 78 | chose the following directory. | ||
| 79 | |||
| 80 | #+begin_src sh | ||
| 81 | doas mv scli /usr/local/bin/scli | ||
| 82 | #+end_src | ||
| 83 | |||
| 84 | * Initial Setup | ||
| 85 | |||
| 86 | Now that everything is installed, we can login and configure the client. | ||
| 87 | Start by generating a connection link. | ||
| 88 | |||
| 89 | #+begin_src sh | ||
| 90 | signal-cli link -n "YOUR-DEVICE-NICKNAME" | tee >(xargs -L 1 qrencode -t utf8) | ||
| 91 | #+end_src | ||
| 92 | |||
| 93 | This will generate a connection link and related QR code for you to use | ||
| 94 | to link the devices together. Once complete, *wait patiently* for the | ||
| 95 | connection process to finish. | ||
| 96 | |||
| 97 | Once it completes, it will exit and return you to the prompt. From here, | ||
| 98 | you need to perform an initial =receive= command to start things off. | ||
| 99 | The =USERNAME= variable should be your phone number, such as | ||
| 100 | =+15551237890=. | ||
| 101 | |||
| 102 | #+begin_src sh | ||
| 103 | signal-cli -u USERNAME receive | ||
| 104 | #+end_src | ||
| 105 | |||
| 106 | Also be sure to test the daemon to ensure it works properly. If no | ||
| 107 | errors occur, it's working. If you run into errors because you're not | ||
| 108 | running a DBUS session, see my notes below. | ||
| 109 | |||
| 110 | #+begin_src sh | ||
| 111 | signal-cli -u USERNAME daemon | ||
| 112 | #+end_src | ||
| 113 | |||
| 114 | Once the initial reception is complete, you are ready to use =scli=. | ||
| 115 | |||
| 116 | This process will differ depending on your desktop environment (DE). If | ||
| 117 | you are running a DE, you likely have a DBUS session running already and | ||
| 118 | can simply launch the program. | ||
| 119 | |||
| 120 | However, if you're like me and running your computer straight on the TTY | ||
| 121 | without a DE, you'll need to start a DBUS session for this program. | ||
| 122 | |||
| 123 | #+begin_src sh | ||
| 124 | # If you're not running a DBUS session yet, you need to start one for scli | ||
| 125 | dbus-run-session -- scli | ||
| 126 | |||
| 127 | # OR - If you're already running a DBUS session, simply run scli | ||
| 128 | scli | ||
| 129 | #+end_src | ||
| 130 | |||
| 131 | * Configuration | ||
| 132 | |||
| 133 | Lastly, there are a number of configuration options that you can pass | ||
| 134 | via the command or in the =~/.config/sclirc= file. See the Github README | ||
| 135 | for more information on configuration options. | ||
| 136 | |||
| 137 | #+begin_src sh | ||
| 138 | nano ~/.config/sclirc | ||
| 139 | #+end_src | ||
| 140 | |||
| 141 | #+begin_src conf | ||
| 142 | # ~/.config/sclirc | ||
| 143 | |||
| 144 | wrap-at = 80 | ||
| 145 | enable-notifications = true | ||
| 146 | #+end_src | ||
| 147 | |||
| 148 | That's it! Following this guide, I have a functional =scli= program that | ||
| 149 | successfully sends messages to my contacts and myself! | ||
content/blog/2023-12-03-unifi-nextdns.md → content/blog/2023-12-03-unifi-nextdns.org renamed +80 −79
| @@ -1,102 +1,103 @@ | |||
| 1 | +++ | 1 | #+date: <2023-12-03> |
| 2 | date = 2023-12-03 | 2 | #+title: How to Install NextDNS on the Unifi Dream Machine |
| 3 | title = "How to Install NextDNS on the Unifi Dream Machine" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # Overview | 5 | |
| 6 | * Overview | ||
| 9 | 7 | ||
| 10 | I recently installed NextDNS on my Unifi Dream Machine router using the | 8 | I recently installed NextDNS on my Unifi Dream Machine router using the |
| 11 | [UnifiOS](https://github.com/nextdns/nextdns/wiki/UnifiOS) wiki page on | 9 | [[https://github.com/nextdns/nextdns/wiki/UnifiOS][UnifiOS]] wiki page |
| 12 | NextDNS's GitHub repository. | 10 | on NextDNS's GitHub repository. |
| 13 | 11 | ||
| 14 | As a result of this, I wanted to write down the process in case the wiki or | 12 | As a result of this, I wanted to write down the process in case the wiki |
| 15 | installer ever gets lost. | 13 | or installer ever gets lost. |
| 16 | 14 | ||
| 17 | # Wiki | 15 | * Wiki |
| 18 | 16 | ||
| 19 | The following is copied from the wiki page linked above, with one difference in | 17 | The following is copied from the wiki page linked above, with one |
| 20 | the `ssh` command. | 18 | difference in the =ssh= command. |
| 21 | 19 | ||
| 22 | Install instructions for Unifi Dream Machine (UDM) standard and pro routers. | 20 | Install instructions for Unifi Dream Machine (UDM) standard and pro |
| 21 | routers. | ||
| 23 | 22 | ||
| 24 | ## Install | 23 | ** Install |
| 25 | 24 | ||
| 26 | Enable SSH: | 25 | Enable SSH: |
| 27 | 26 | ||
| 28 | - Go to your unifi admin interface and select your device (not the controller | 27 | - Go to your unifi admin interface and select your device (not the |
| 29 | settings, but the Dream Machine settings) | 28 | controller settings, but the Dream Machine settings) |
| 30 | - Click on "Settings" at the bottom of the page | 29 | - Click on "Settings" at the bottom of the page |
| 31 | - Go to the "Advanced" section on the left pan | 30 | - Go to the "Advanced" section on the left pan |
| 32 | - Enable SSH | 31 | - Enable SSH |
| 33 | - Set a SSH password | 32 | - Set a SSH password |
| 34 | 33 | ||
| 35 | Connect to your router using `ssh root@xxx.xxx.xxx.xxx` with the password you | 34 | Connect to your router using =ssh root@xxx.xxx.xxx.xxx= with the |
| 36 | configured. | 35 | password you configured. |
| 37 | 36 | ||
| 38 | Run the following command and follow the instructions: | 37 | Run the following command and follow the instructions: |
| 39 | 38 | ||
| 40 | ```sh | 39 | #+begin_src sh |
| 41 | sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"' | 40 | sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"' |
| 42 | ``` | 41 | #+end_src |
| 43 | 42 | ||
| 44 | Note: Queries from the UDM itself won't be routed to NextDNS nor encrypted due | 43 | Note: Queries from the UDM itself won't be routed to NextDNS nor |
| 45 | to current system limitation. All traffic from other devices on then network | 44 | encrypted due to current system limitation. All traffic from other |
| 46 | will. | 45 | devices on then network will. |
| 47 | 46 | ||
| 48 | ## Upgrade | 47 | ** Upgrade |
| 49 | 48 | ||
| 50 | To upgrade to the last version, simply re-run the installer above. If a new | 49 | To upgrade to the last version, simply re-run the installer above. If a |
| 51 | version is available, the upgrade action will added to the list of possible | 50 | new version is available, the upgrade action will added to the list of |
| 52 | actions. | 51 | possible actions. |
| 53 | 52 | ||
| 54 | ## Uninstall | 53 | ** Uninstall |
| 55 | 54 | ||
| 56 | To uninstall, re-run the installer above and select "Remove" in the menu. | 55 | To uninstall, re-run the installer above and select "Remove" in the |
| 56 | menu. | ||
| 57 | 57 | ||
| 58 | ## Troubleshooting | 58 | ** Troubleshooting |
| 59 | 59 | ||
| 60 | If the installation fail, please the installer in debug mode and contact us at | 60 | If the installation fail, please the installer in debug mode and contact |
| 61 | team@nextdns.io with the transcript of the installation: | 61 | us at team@nextdns.io with the transcript of the installation: |
| 62 | 62 | ||
| 63 | ```sh | 63 | #+begin_src sh |
| 64 | sh -c 'DEBUG=1 sh -c "$(curl -sL https://nextdns.io/install)"' | 64 | sh -c 'DEBUG=1 sh -c "$(curl -sL https://nextdns.io/install)"' |
| 65 | ``` | 65 | #+end_src |
| 66 | 66 | ||
| 67 | ### Content Filtering Conflict | 67 | *** Content Filtering Conflict |
| 68 | 68 | ||
| 69 | NextDNS CLI and the UDM Content Filtering or the Ad Blocking features are | 69 | NextDNS CLI and the UDM Content Filtering or the Ad Blocking features |
| 70 | incompatible. If you want to use NextDNS CLI, please make sure they are | 70 | are incompatible. If you want to use NextDNS CLI, please make sure they |
| 71 | disabled. | 71 | are disabled. |
| 72 | 72 | ||
| 73 | To disable Content Filtering, go to Settings > Network, then for each network, | 73 | To disable Content Filtering, go to Settings > Network, then for each |
| 74 | set the Content Filtering feature to None | 74 | network, set the Content Filtering feature to None |
| 75 | 75 | ||
| 76 | To disable Ad Blocking, go to Settings > Application Firewall. In the General | 76 | To disable Ad Blocking, go to Settings > Application Firewall. In the |
| 77 | tab, uncheck the Ad Blocking checkbox. | 77 | General tab, uncheck the Ad Blocking checkbox. |
| 78 | 78 | ||
| 79 | ### APT Error | 79 | *** APT Error |
| 80 | 80 | ||
| 81 | If you get an apt error as follow: | 81 | If you get an apt error as follow: |
| 82 | 82 | ||
| 83 | ```sh | 83 | #+begin_src sh |
| 84 | E: Failed to fetch http://security.debian.org/dists/stretch/updates/main/binary-arm64/Packages 404 Not Found [IP: 151.101.70.132 80] | 84 | E: Failed to fetch http://security.debian.org/dists/stretch/updates/main/binary-arm64/Packages 404 Not Found [IP: 151.101.70.132 80] |
| 85 | ``` | 85 | #+end_src |
| 86 | 86 | ||
| 87 | You may try to following: | 87 | You may try to following: |
| 88 | 88 | ||
| 89 | ```sh | 89 | #+begin_src sh |
| 90 | sed -i -e 's/deb.debian.org/archive.debian.org/g' \ | 90 | sed -i -e 's/deb.debian.org/archive.debian.org/g' \ |
| 91 | -e 's|security.debian.org|archive.debian.org/|g' \ | 91 | -e 's|security.debian.org|archive.debian.org/|g' \ |
| 92 | -e '/stretch-updates/d' /etc/apt/sources.list | 92 | -e '/stretch-updates/d' /etc/apt/sources.list |
| 93 | ``` | 93 | #+end_src |
| 94 | 94 | ||
| 95 | ## install.sh | 95 | ** install.sh |
| 96 | 96 | ||
| 97 | Here are the contents of the `install.sh` file used above, as of 2023-12-03: | 97 | Here are the contents of the =install.sh= file used above, as of |
| 98 | 2023-12-03: | ||
| 98 | 99 | ||
| 99 | ```sh | 100 | #+begin_src sh |
| 100 | #!/bin/sh | 101 | #!/bin/sh |
| 101 | 102 | ||
| 102 | main() { | 103 | main() { |
| @@ -321,13 +322,13 @@ install_bin() { | |||
| 321 | fi | 322 | fi |
| 322 | log_debug "Installing $INSTALL_RELEASE binary for $GOOS/$GOARCH to $bin_path" | 323 | log_debug "Installing $INSTALL_RELEASE binary for $GOOS/$GOARCH to $bin_path" |
| 323 | case "$INSTALL_RELEASE" in | 324 | case "$INSTALL_RELEASE" in |
| 324 | */*) | 325 | ,*/*) |
| 325 | # Snapshot | 326 | # Snapshot |
| 326 | branch=${INSTALL_RELEASE%/*} | 327 | branch=${INSTALL_RELEASE%/*} |
| 327 | hash=${INSTALL_RELEASE#*/} | 328 | hash=${INSTALL_RELEASE#*/} |
| 328 | url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz" | 329 | url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz" |
| 329 | ;; | 330 | ;; |
| 330 | *) | 331 | ,*) |
| 331 | url="https://github.com/nextdns/nextdns/releases/download/v${INSTALL_RELEASE}/nextdns_${INSTALL_RELEASE}_${GOOS}_${GOARCH}.tar.gz" | 332 | url="https://github.com/nextdns/nextdns/releases/download/v${INSTALL_RELEASE}/nextdns_${INSTALL_RELEASE}_${GOOS}_${GOARCH}.tar.gz" |
| 332 | ;; | 333 | ;; |
| 333 | esac | 334 | esac |
| @@ -629,12 +630,12 @@ install_type() { | |||
| 629 | echo "$FORCE_INSTALL_TYPE"; return 0 | 630 | echo "$FORCE_INSTALL_TYPE"; return 0 |
| 630 | fi | 631 | fi |
| 631 | case "$INSTALL_RELEASE" in | 632 | case "$INSTALL_RELEASE" in |
| 632 | */*) | 633 | ,*/*) |
| 633 | case $OS in | 634 | case $OS in |
| 634 | ubios) | 635 | ubios) |
| 635 | echo "ubios_snapshot"; return 0 | 636 | echo "ubios_snapshot"; return 0 |
| 636 | ;; | 637 | ;; |
| 637 | *) | 638 | ,*) |
| 638 | # Snapshot mode always use binary install | 639 | # Snapshot mode always use binary install |
| 639 | echo "bin"; return 0 | 640 | echo "bin"; return 0 |
| 640 | ;; | 641 | ;; |
| @@ -662,7 +663,7 @@ install_type() { | |||
| 662 | . /etc/os-release | 663 | . /etc/os-release |
| 663 | major=$(echo "$VERSION_ID" | cut -d. -f1) | 664 | major=$(echo "$VERSION_ID" | cut -d. -f1) |
| 664 | case $major in | 665 | case $major in |
| 665 | *[!0-9]*) | 666 | ,*[!0-9]*) |
| 666 | if [ "$VERSION_ID" = "19.07.0-rc1" ]; then | 667 | if [ "$VERSION_ID" = "19.07.0-rc1" ]; then |
| 667 | # No opkg support before 19.07.0-rc2 | 668 | # No opkg support before 19.07.0-rc2 |
| 668 | echo "bin" | 669 | echo "bin" |
| @@ -671,7 +672,7 @@ install_type() { | |||
| 671 | echo "openwrt" | 672 | echo "openwrt" |
| 672 | fi | 673 | fi |
| 673 | ;; | 674 | ;; |
| 674 | *) | 675 | ,*) |
| 675 | if [ "$major" -lt 19 ]; then | 676 | if [ "$major" -lt 19 ]; then |
| 676 | # No opkg support before 19.07.0-rc2 | 677 | # No opkg support before 19.07.0-rc2 |
| 677 | echo "bin" | 678 | echo "bin" |
| @@ -717,7 +718,7 @@ install_type() { | |||
| 717 | # TODO: pkg for xbps | 718 | # TODO: pkg for xbps |
| 718 | echo "bin" | 719 | echo "bin" |
| 719 | ;; | 720 | ;; |
| 720 | *) | 721 | ,*) |
| 721 | log_error "Unsupported installation for $(detect_os)" | 722 | log_error "Unsupported installation for $(detect_os)" |
| 722 | return 1 | 723 | return 1 |
| 723 | ;; | 724 | ;; |
| @@ -869,7 +870,7 @@ ask_bool() { | |||
| 869 | false) | 870 | false) |
| 870 | msg="$msg [y|N]: " | 871 | msg="$msg [y|N]: " |
| 871 | ;; | 872 | ;; |
| 872 | *) | 873 | ,*) |
| 873 | msg="$msg (y/n): " | 874 | msg="$msg (y/n): " |
| 874 | esac | 875 | esac |
| 875 | while true; do | 876 | while true; do |
| @@ -887,7 +888,7 @@ ask_bool() { | |||
| 887 | echo "false" | 888 | echo "false" |
| 888 | return 0 | 889 | return 0 |
| 889 | ;; | 890 | ;; |
| 890 | *) | 891 | ,*) |
| 891 | echo "Invalid input, use yes or no" | 892 | echo "Invalid input, use yes or no" |
| 892 | ;; | 893 | ;; |
| 893 | esac | 894 | esac |
| @@ -899,12 +900,12 @@ detect_endiannes() { | |||
| 899 | # Some firmwares do not contain hexdump, for those, try to detect endianness | 900 | # Some firmwares do not contain hexdump, for those, try to detect endianness |
| 900 | # differently. | 901 | # differently. |
| 901 | case $(cat /proc/cpuinfo) in | 902 | case $(cat /proc/cpuinfo) in |
| 902 | *BCM5300*) | 903 | ,*BCM5300*) |
| 903 | # RT-AC66U does not support Merlin version over 380.70 which | 904 | # RT-AC66U does not support Merlin version over 380.70 which |
| 904 | # lacks hexdump command. | 905 | # lacks hexdump command. |
| 905 | echo "le" | 906 | echo "le" |
| 906 | ;; | 907 | ;; |
| 907 | *) | 908 | ,*) |
| 908 | log_error "Cannot determine endianness" | 909 | log_error "Cannot determine endianness" |
| 909 | return 1 | 910 | return 1 |
| 910 | ;; | 911 | ;; |
| @@ -935,10 +936,10 @@ detect_goarch() { | |||
| 935 | arm) | 936 | arm) |
| 936 | # FreeBSD does not include arm version | 937 | # FreeBSD does not include arm version |
| 937 | case "$(sysctl -b hw.model 2>/dev/null)" in | 938 | case "$(sysctl -b hw.model 2>/dev/null)" in |
| 938 | *A9*) | 939 | ,*A9*) |
| 939 | echo "armv7" | 940 | echo "armv7" |
| 940 | ;; | 941 | ;; |
| 941 | *) | 942 | ,*) |
| 942 | # Unknown version, fallback to the lowest | 943 | # Unknown version, fallback to the lowest |
| 943 | echo "armv5" | 944 | echo "armv5" |
| 944 | ;; | 945 | ;; |
| @@ -962,7 +963,7 @@ detect_goarch() { | |||
| 962 | # "out of memory allocating heap arena metadata". | 963 | # "out of memory allocating heap arena metadata". |
| 963 | echo "armv7" | 964 | echo "armv7" |
| 964 | ;; | 965 | ;; |
| 965 | *) | 966 | ,*) |
| 966 | echo "arm64" | 967 | echo "arm64" |
| 967 | ;; | 968 | ;; |
| 968 | esac | 969 | esac |
| @@ -974,7 +975,7 @@ detect_goarch() { | |||
| 974 | # TODO: detect hardfloat | 975 | # TODO: detect hardfloat |
| 975 | echo "$(uname -m)$(detect_endiannes)_softfloat" | 976 | echo "$(uname -m)$(detect_endiannes)_softfloat" |
| 976 | ;; | 977 | ;; |
| 977 | *) | 978 | ,*) |
| 978 | log_error "Unsupported GOARCH: $(uname -m)" | 979 | log_error "Unsupported GOARCH: $(uname -m)" |
| 979 | return 1 | 980 | return 1 |
| 980 | ;; | 981 | ;; |
| @@ -1001,7 +1002,7 @@ detect_goos() { | |||
| 1001 | OpenBSD) | 1002 | OpenBSD) |
| 1002 | echo "openbsd" | 1003 | echo "openbsd" |
| 1003 | ;; | 1004 | ;; |
| 1004 | *) | 1005 | ,*) |
| 1005 | log_error "Unsupported GOOS: $(uname -s)" | 1006 | log_error "Unsupported GOOS: $(uname -s)" |
| 1006 | return 1 | 1007 | return 1 |
| 1007 | esac | 1008 | esac |
| @@ -1078,7 +1079,7 @@ detect_os() { | |||
| 1078 | OpenBSD) | 1079 | OpenBSD) |
| 1079 | echo "openbsd"; return 0 | 1080 | echo "openbsd"; return 0 |
| 1080 | ;; | 1081 | ;; |
| 1081 | *) | 1082 | ,*) |
| 1082 | esac | 1083 | esac |
| 1083 | log_error "Unsupported OS: $(uname -o) $(grep ID "/etc/os-release" 2>/dev/null | xargs)" | 1084 | log_error "Unsupported OS: $(uname -o) $(grep ID "/etc/os-release" 2>/dev/null | xargs)" |
| 1084 | return 1 | 1085 | return 1 |
| @@ -1097,7 +1098,7 @@ guess_host_type() { | |||
| 1097 | darwin|steamos) | 1098 | darwin|steamos) |
| 1098 | echo "workstation" | 1099 | echo "workstation" |
| 1099 | ;; | 1100 | ;; |
| 1100 | *) | 1101 | ,*) |
| 1101 | echo "unsure" | 1102 | echo "unsure" |
| 1102 | ;; | 1103 | ;; |
| 1103 | esac | 1104 | esac |
| @@ -1156,7 +1157,7 @@ bin_location() { | |||
| 1156 | steamos) | 1157 | steamos) |
| 1157 | echo "$HOME/.local/bin/nextdns" | 1158 | echo "$HOME/.local/bin/nextdns" |
| 1158 | ;; | 1159 | ;; |
| 1159 | *) | 1160 | ,*) |
| 1160 | log_error "Unknown bin location for $OS" | 1161 | log_error "Unknown bin location for $OS" |
| 1161 | ;; | 1162 | ;; |
| 1162 | esac | 1163 | esac |
| @@ -1164,12 +1165,12 @@ bin_location() { | |||
| 1164 | 1165 | ||
| 1165 | is_version_current() { | 1166 | is_version_current() { |
| 1166 | case "$INSTALL_RELEASE" in | 1167 | case "$INSTALL_RELEASE" in |
| 1167 | */*) | 1168 | ,*/*) |
| 1168 | # Snapshot | 1169 | # Snapshot |
| 1169 | hash=${INSTALL_RELEASE#*/} | 1170 | hash=${INSTALL_RELEASE#*/} |
| 1170 | test "0.0.0-$hash" = "$CURRENT_RELEASE" | 1171 | test "0.0.0-$hash" = "$CURRENT_RELEASE" |
| 1171 | ;; | 1172 | ;; |
| 1172 | *) | 1173 | ,*) |
| 1173 | test "$INSTALL_RELEASE" = "$CURRENT_RELEASE" | 1174 | test "$INSTALL_RELEASE" = "$CURRENT_RELEASE" |
| 1174 | ;; | 1175 | ;; |
| 1175 | esac | 1176 | esac |
| @@ -1194,7 +1195,7 @@ get_release() { | |||
| 1194 | curl) cmd="curl -A curl -s" ;; | 1195 | curl) cmd="curl -A curl -s" ;; |
| 1195 | wget) cmd="wget -qO- -U curl" ;; | 1196 | wget) cmd="wget -qO- -U curl" ;; |
| 1196 | openssl) cmd="openssl_get" ;; | 1197 | openssl) cmd="openssl_get" ;; |
| 1197 | *) | 1198 | ,*) |
| 1198 | log_error "Cannot retrieve latest version" | 1199 | log_error "Cannot retrieve latest version" |
| 1199 | return | 1200 | return |
| 1200 | ;; | 1201 | ;; |
| @@ -1246,4 +1247,4 @@ openssl_get() { | |||
| 1246 | 1247 | ||
| 1247 | umask 0022 | 1248 | umask 0022 |
| 1248 | main | 1249 | main |
| 1249 | ``` | 1250 | #+end_src |
content/blog/2024-01-08-dont-say-hello.md deleted −29
| @@ -1,29 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-01-08 | ||
| 3 | title = "Don't Say Hello" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | I recently came back from a winter break and have started working again... only | ||
| 9 | to immediately run into the dilemma of people sending me cliffhanger messages | ||
| 10 | again. | ||
| 11 | |||
| 12 | # No Hello | ||
| 13 | |||
| 14 | A year or two ago, I discovered [no hello](https://nohello.net/en/) and have | ||
| 15 | thought about it often since then. I've even sent it to a few people since then | ||
| 16 | (who wouldn't take offense to it). | ||
| 17 | |||
| 18 | I work in a fast-paced environment where efficiency is extremely valuable. | ||
| 19 | Therefore, I have always held a deep displeasure for conversations where people | ||
| 20 | start with "Hello" and then. | ||
| 21 | |||
| 22 | I searched back through my work messages and found that I received ~50 messages | ||
| 23 | from ~10 people last year from people that contained "hi", "hey", or "hello" and | ||
| 24 | did not contain any indication of the purpose of the conversation. I also | ||
| 25 | noticed that a few of the users were responsible for the large majority of the | ||
| 26 | cliffhangers. | ||
| 27 | |||
| 28 | There's no real point to this post, just a desparate request for people to | ||
| 29 | please stop doing this. | ||
content/blog/2024-01-08-dont-say-hello.org added +27
| @@ -0,0 +1,27 @@ | |||
| 1 | #+date: <2024-01-08> | ||
| 2 | #+title: Don't Say Hello | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | I recently came back from a winter break and have started working | ||
| 7 | again... only to immediately run into the dilemma of people sending me | ||
| 8 | cliffhanger messages again. | ||
| 9 | |||
| 10 | * No Hello | ||
| 11 | |||
| 12 | A year or two ago, I discovered [[https://nohello.net/en/][no hello]] | ||
| 13 | and have thought about it often since then. I've even sent it to a few | ||
| 14 | people since then (who wouldn't take offense to it). | ||
| 15 | |||
| 16 | I work in a fast-paced environment where efficiency is extremely | ||
| 17 | valuable. Therefore, I have always held a deep displeasure for | ||
| 18 | conversations where people start with "Hello" and then. | ||
| 19 | |||
| 20 | I searched back through my work messages and found that I received ~50 | ||
| 21 | messages from ~10 people last year from people that contained "hi", | ||
| 22 | "hey", or "hello" and did not contain any indication of the purpose of | ||
| 23 | the conversation. I also noticed that a few of the users were | ||
| 24 | responsible for the large majority of the cliffhangers. | ||
| 25 | |||
| 26 | There's no real point to this post, just a desparate request for people | ||
| 27 | to please stop doing this. | ||
content/blog/2024-01-09-macos-customization.md deleted −147
| @@ -1,147 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-01-09 | ||
| 3 | title = "Customizing macOS" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | I have been using macOS more than Linux lately, so I wrote this post to describe | ||
| 9 | some simple options to customize macOS beyond the normal built-in settings menu. | ||
| 10 | |||
| 11 | While not all-encompassing, the options below should be a good start for anyone | ||
| 12 | looking to dive down the rabbit hole. | ||
| 13 | |||
| 14 | # Basics | ||
| 15 | |||
| 16 | ## Package Management | ||
| 17 | |||
| 18 | To install a lot of software on macOS, you will need | ||
| 19 | [Homebrew](https://brew.sh/). You can use their installation script to get | ||
| 20 | started. Simply open the `Terminal` application and paste the following snippet: | ||
| 21 | |||
| 22 | ```sh | ||
| 23 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | ||
| 24 | ``` | ||
| 25 | |||
| 26 | This will allow you to easily install and manage applications and other software | ||
| 27 | easily through the `brew` command. | ||
| 28 | |||
| 29 | ## Terminal | ||
| 30 | |||
| 31 | If you're serious about customizing your macOS system, I highly recommend | ||
| 32 | installing a terminal emulator that you like and if you're not comfortable on | ||
| 33 | the command line yet, start learning. A lot of customization options require you | ||
| 34 | to edit hidden files, which is easiest in a terminal. | ||
| 35 | |||
| 36 | There are options like iTerm2, Kitty, Alacritty, Hyper, Warp, or the built-in | ||
| 37 | Terminal app. | ||
| 38 | |||
| 39 | I use [iTerm2](https://iterm2.com/), which can be installed with Homebrew: | ||
| 40 | |||
| 41 | ```sh | ||
| 42 | brew install iterm2 | ||
| 43 | ``` | ||
| 44 | |||
| 45 | To install color schemes, such as the Dracula scheme, you visit [iTerm | ||
| 46 | Themes](https://iterm2colorschemes.com/) and follow their installation | ||
| 47 | instructions to install any of the themes. | ||
| 48 | |||
| 49 | # Desktop | ||
| 50 | |||
| 51 | ## Window Management | ||
| 52 | |||
| 53 | [yabai](https://github.com/koekeishiya/yabai) is a tiling window manager for | ||
| 54 | macOS. While other window managers exist, I found that most of them struggled to | ||
| 55 | create logical layouts and to allow me to easily move windows around the screen. | ||
| 56 | |||
| 57 | Some advanced settings for yabai are only available if partially disable System | ||
| 58 | Integrity Protection (SIP). However, I chose not to do this and it hasn't | ||
| 59 | affected my basic usage of yabai at all. | ||
| 60 | |||
| 61 | Refer to the [yabai | ||
| 62 | wiki](<https://github.com/koekeishiya/yabai/wiki/Installing-yabai-(latest-release)>) | ||
| 63 | for installation instructions. You will need to ensure that yabai is allowed to | ||
| 64 | access the accessibility and screen recording APIs. | ||
| 65 | |||
| 66 | ## Keyboard Shortcuts | ||
| 67 | |||
| 68 | [skhd](https://github.com/koekeishiya/skhd) is a simple hotkey daemon that | ||
| 69 | allows you to define hotkeys in a file for usage on your system. | ||
| 70 | |||
| 71 | Installation is simple: | ||
| 72 | |||
| 73 | ```sh | ||
| 74 | brew install koekeishiya/formulae/skhd | ||
| 75 | skhd --start-service | ||
| 76 | ``` | ||
| 77 | |||
| 78 | After installation, be sure to allow `skhd` access to the accessibility API in | ||
| 79 | the macOS privacy settings. | ||
| 80 | |||
| 81 | You can configure your hotkeys in the `~/.config/skhd/skhdrc` file: | ||
| 82 | |||
| 83 | ```sh | ||
| 84 | nano ~/.config/skhd/skhdrc | ||
| 85 | ``` | ||
| 86 | |||
| 87 | For example, I have hotkeys to open my browser and terminal: | ||
| 88 | |||
| 89 | ```conf | ||
| 90 | # Terminal | ||
| 91 | cmd - return : /Applications/iTerm.app/Contents/MacOS/iTerm2 | ||
| 92 | |||
| 93 | # Browser | ||
| 94 | cmd + shift - return : /Applications/LibreWolf.app/Contents/MacOS/librewolf | ||
| 95 | ``` | ||
| 96 | |||
| 97 | ## Widgets | ||
| 98 | |||
| 99 | [uebersicht](https://github.com/felixhageloh/uebersicht/) is a handy | ||
| 100 | desktop-based widget tool with a plethora of community-made widgets available in | ||
| 101 | the [widgets gallery](https://tracesof.net/uebersicht-widgets/). You can also | ||
| 102 | write your own widgets with this tool. | ||
| 103 | |||
| 104 | To install, simply download the latest release from the [uebersicht | ||
| 105 | website](https://tracesof.net/uebersicht/) and copy it to the Applications | ||
| 106 | folder. | ||
| 107 | |||
| 108 | See below for an example of the | ||
| 109 | [Mond](https://tracesof.net/uebersicht-widgets/#Mond) widget in action. | ||
| 110 | |||
| 111 | ## Status Bar | ||
| 112 | |||
| 113 | [SketchyBar](https://github.com/FelixKratz/SketchyBar) is a customizable | ||
| 114 | replacement for the macOS status or menu bar. | ||
| 115 | |||
| 116 | You can browse a discussion where various users shared their | ||
| 117 | [configurations](https://github.com/FelixKratz/SketchyBar/discussions/47?sort=top) | ||
| 118 | for inspiration or to copy their dotfiles. | ||
| 119 | |||
| 120 | ## Dock | ||
| 121 | |||
| 122 | The easiest way to customize the dock is to install | ||
| 123 | [uBar](https://ubarapp.com/), which uses a Windows-like menu bar as the default | ||
| 124 | style. | ||
| 125 | |||
| 126 | However, the built-in macOS dock cannot be disabled and can only be set to | ||
| 127 | "always hidden". This can be annoying as it will pop out any time your mouse | ||
| 128 | cursor passes closely to the dock edge of the screen. Because of this, I simply | ||
| 129 | use the built-in dock instead of customizing it with third-party software. | ||
| 130 | |||
| 131 | Regardless, see below for the default installation style of uBar. | ||
| 132 | |||
| 133 | ## Application Icons | ||
| 134 | |||
| 135 | You can also customize the icon of any application in macOS, which will show up | ||
| 136 | in Finder, the Dock, Launchpad, search results, etc. I recommend using | ||
| 137 | [macOSicons](https://macosicons.com/) to download icons you want, and then apply | ||
| 138 | them by following this process. | ||
| 139 | |||
| 140 | 1. Open the Finder application. | ||
| 141 | 2. Navigate to the `Applications` folder. | ||
| 142 | 3. Right-click an application of your choice, and select `Get Info`. | ||
| 143 | 4. Drag the image you downloaded on top of the application's icon at the top of | ||
| 144 | information window (you will see a green "plus" symbol when you're hovering | ||
| 145 | over it). | ||
| 146 | 5. Release the new icon on top of the old icon and it will update! | ||
| 147 | |||
content/blog/2024-01-09-macos-customization.org added +151
| @@ -0,0 +1,151 @@ | |||
| 1 | #+date: <2024-01-09> | ||
| 2 | #+title: Customizing macOS | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | I have been using macOS more than Linux lately, so I wrote this post to | ||
| 7 | describe some simple options to customize macOS beyond the normal | ||
| 8 | built-in settings menu. | ||
| 9 | |||
| 10 | While not all-encompassing, the options below should be a good start for | ||
| 11 | anyone looking to dive down the rabbit hole. | ||
| 12 | |||
| 13 | * Basics | ||
| 14 | |||
| 15 | ** Package Management | ||
| 16 | |||
| 17 | To install a lot of software on macOS, you will need | ||
| 18 | [[https://brew.sh/][Homebrew]]. You can use their installation script to | ||
| 19 | get started. Simply open the =Terminal= application and paste the | ||
| 20 | following snippet: | ||
| 21 | |||
| 22 | #+begin_src sh | ||
| 23 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | ||
| 24 | #+end_src | ||
| 25 | |||
| 26 | This will allow you to easily install and manage applications and other | ||
| 27 | software easily through the =brew= command. | ||
| 28 | |||
| 29 | ** Terminal | ||
| 30 | |||
| 31 | If you're serious about customizing your macOS system, I highly | ||
| 32 | recommend installing a terminal emulator that you like and if you're not | ||
| 33 | comfortable on the command line yet, start learning. A lot of | ||
| 34 | customization options require you to edit hidden files, which is easiest | ||
| 35 | in a terminal. | ||
| 36 | |||
| 37 | There are options like iTerm2, Kitty, Alacritty, Hyper, Warp, or the | ||
| 38 | built-in Terminal app. | ||
| 39 | |||
| 40 | I use [[https://iterm2.com/][iTerm2]], which can be installed with | ||
| 41 | Homebrew: | ||
| 42 | |||
| 43 | #+begin_src sh | ||
| 44 | brew install iterm2 | ||
| 45 | #+end_src | ||
| 46 | |||
| 47 | To install color schemes, such as the Dracula scheme, you visit | ||
| 48 | [[https://iterm2colorschemes.com/][iTerm Themes]] and follow their | ||
| 49 | installation instructions to install any of the themes. | ||
| 50 | |||
| 51 | * Desktop | ||
| 52 | |||
| 53 | ** Window Management | ||
| 54 | |||
| 55 | [[https://github.com/koekeishiya/yabai][yabai]] is a tiling window | ||
| 56 | manager for macOS. While other window managers exist, I found that most | ||
| 57 | of them struggled to create logical layouts and to allow me to easily | ||
| 58 | move windows around the screen. | ||
| 59 | |||
| 60 | Some advanced settings for yabai are only available if partially disable | ||
| 61 | System Integrity Protection (SIP). However, I chose not to do this and | ||
| 62 | it hasn't affected my basic usage of yabai at all. | ||
| 63 | |||
| 64 | Refer to the | ||
| 65 | [[https://github.com/koekeishiya/yabai/wiki/Installing-yabai-(latest-release)][yabai | ||
| 66 | wiki]] for installation instructions. You will need to ensure that yabai | ||
| 67 | is allowed to access the accessibility and screen recording APIs. | ||
| 68 | |||
| 69 | ** Keyboard Shortcuts | ||
| 70 | |||
| 71 | [[https://github.com/koekeishiya/skhd][skhd]] is a simple hotkey daemon | ||
| 72 | that allows you to define hotkeys in a file for usage on your system. | ||
| 73 | |||
| 74 | Installation is simple: | ||
| 75 | |||
| 76 | #+begin_src sh | ||
| 77 | brew install koekeishiya/formulae/skhd | ||
| 78 | skhd --start-service | ||
| 79 | #+end_src | ||
| 80 | |||
| 81 | After installation, be sure to allow =skhd= access to the accessibility | ||
| 82 | API in the macOS privacy settings. | ||
| 83 | |||
| 84 | You can configure your hotkeys in the =~/.config/skhd/skhdrc= file: | ||
| 85 | |||
| 86 | #+begin_src sh | ||
| 87 | nano ~/.config/skhd/skhdrc | ||
| 88 | #+end_src | ||
| 89 | |||
| 90 | For example, I have hotkeys to open my browser and terminal: | ||
| 91 | |||
| 92 | #+begin_src conf | ||
| 93 | # Terminal | ||
| 94 | cmd - return : /Applications/iTerm.app/Contents/MacOS/iTerm2 | ||
| 95 | |||
| 96 | # Browser | ||
| 97 | cmd + shift - return : /Applications/LibreWolf.app/Contents/MacOS/librewolf | ||
| 98 | #+end_src | ||
| 99 | |||
| 100 | ** Widgets | ||
| 101 | |||
| 102 | [[https://github.com/felixhageloh/uebersicht/][uebersicht]] is a handy | ||
| 103 | desktop-based widget tool with a plethora of community-made widgets | ||
| 104 | available in the [[https://tracesof.net/uebersicht-widgets/][widgets | ||
| 105 | gallery]]. You can also write your own widgets with this tool. | ||
| 106 | |||
| 107 | To install, simply download the latest release from the | ||
| 108 | [[https://tracesof.net/uebersicht/][uebersicht website]] and copy it to | ||
| 109 | the Applications folder. | ||
| 110 | |||
| 111 | See below for an example of the | ||
| 112 | [[https://tracesof.net/uebersicht-widgets/#Mond][Mond]] widget in | ||
| 113 | action. | ||
| 114 | |||
| 115 | ** Status Bar | ||
| 116 | |||
| 117 | [[https://github.com/FelixKratz/SketchyBar][SketchyBar]] is a | ||
| 118 | customizable replacement for the macOS status or menu bar. | ||
| 119 | |||
| 120 | You can browse a discussion where various users shared their | ||
| 121 | [[https://github.com/FelixKratz/SketchyBar/discussions/47?sort=top][configurations]] | ||
| 122 | for inspiration or to copy their dotfiles. | ||
| 123 | |||
| 124 | ** Dock | ||
| 125 | |||
| 126 | The easiest way to customize the dock is to install | ||
| 127 | [[https://ubarapp.com/][uBar]], which uses a Windows-like menu bar as | ||
| 128 | the default style. | ||
| 129 | |||
| 130 | However, the built-in macOS dock cannot be disabled and can only be set | ||
| 131 | to "always hidden". This can be annoying as it will pop out any time | ||
| 132 | your mouse cursor passes closely to the dock edge of the screen. Because | ||
| 133 | of this, I simply use the built-in dock instead of customizing it with | ||
| 134 | third-party software. | ||
| 135 | |||
| 136 | Regardless, see below for the default installation style of uBar. | ||
| 137 | |||
| 138 | ** Application Icons | ||
| 139 | |||
| 140 | You can also customize the icon of any application in macOS, which will | ||
| 141 | show up in Finder, the Dock, Launchpad, search results, etc. I recommend | ||
| 142 | using [[https://macosicons.com/][macOSicons]] to download icons you | ||
| 143 | want, and then apply them by following this process. | ||
| 144 | |||
| 145 | 1. Open the Finder application. | ||
| 146 | 2. Navigate to the =Applications= folder. | ||
| 147 | 3. Right-click an application of your choice, and select =Get Info=. | ||
| 148 | 4. Drag the image you downloaded on top of the application's icon at the | ||
| 149 | top of information window (you will see a green "plus" symbol when | ||
| 150 | you're hovering over it). | ||
| 151 | 5. Release the new icon on top of the old icon and it will update! | ||
content/blog/2024-01-13-local-llm.md deleted −92
| @@ -1,92 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-01-13 | ||
| 3 | title = "Running Local LLMs on macOS and iOS" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # Requirements | ||
| 9 | |||
| 10 | I've recently started playing with large language models (LLMs), mostly in the | ||
| 11 | popular chatbot form, as part of my job and have decided to see if there's a | ||
| 12 | consistent and reliable way to interact with these models on Apple devices | ||
| 13 | without sacrificing privacy or requiring in-depth technical setup. | ||
| 14 | |||
| 15 | My requirements for this test: | ||
| 16 | |||
| 17 | - Open source platform | ||
| 18 | - On-device model files | ||
| 19 | - Minimal required configuration | ||
| 20 | - Preferably pre-built, but a simple build process is acceptable | ||
| 21 | |||
| 22 | I tested a handful of apps and have summarized my favorite (so far) for macOS | ||
| 23 | and iOS below. | ||
| 24 | |||
| 25 | > TL;DR - Here are the two that met my requirements and I have found the easiest | ||
| 26 | > to install and use so far: | ||
| 27 | |||
| 28 | - macOS: [Ollama](https://ollama.ai/) | ||
| 29 | - iOS : [LLM Farm](https://llmfarm.site/) | ||
| 30 | |||
| 31 | # macOS | ||
| 32 | |||
| 33 | [Ollama](https://ollama.ai/) is a simple Go application for macOS and Linux that | ||
| 34 | can run various LLMs locally. | ||
| 35 | |||
| 36 | For macOS, you can download the pplication on the [Ollama download | ||
| 37 | page](https://ollama.ai/download/mac) and install it by unzipping the | ||
| 38 | `Ollama.app` file and moving it to the `Applications` folder. | ||
| 39 | |||
| 40 | If you prefer the command line, you can run these commands after the download | ||
| 41 | finished: | ||
| 42 | |||
| 43 | ```sh | ||
| 44 | cd ~/Downloads && \ | ||
| 45 | unzip Ollama-darwin.zip && \ | ||
| 46 | mv ~/Downloads/Ollama.app /Applications/ | ||
| 47 | ``` | ||
| 48 | |||
| 49 | After running the app, the app will ask you to open a terminal and run the | ||
| 50 | default `llama2` model, which will open an interactive chat session in the | ||
| 51 | terminal. You can startfully using the application at this point. | ||
| 52 | |||
| 53 | If you don't want to use the default `llama2` model, you can download and run | ||
| 54 | additional models found on the [Models](https://ollama.ai/library) page. | ||
| 55 | |||
| 56 | To see the information for the currently-used model, you can run the `/show | ||
| 57 | info` command in the chat. | ||
| 58 | |||
| 59 | ## Community Integrations | ||
| 60 | |||
| 61 | I highly recommend browsing the [Community | ||
| 62 | Integrations](https://github.com/jmorganca/ollama#community-integrations) | ||
| 63 | section of the project to see how you would prefer to extend Ollama beyond a | ||
| 64 | simple command-line interface. There are options for APIs, browser UIs, advanced | ||
| 65 | terminal configurations, and more. | ||
| 66 | |||
| 67 | # iOS | ||
| 68 | |||
| 69 | While there are a handful of decent macOS options, it was quite difficult to | ||
| 70 | find an iOS app that offered an open source platform without an extensive | ||
| 71 | configuration and building process. I found LLM Farm to be decent enough in | ||
| 72 | quality to sit at the top of my list - however, it's definitely not user | ||
| 73 | friendly enough for me to consider using it on a daily basis. | ||
| 74 | |||
| 75 | [LLM Farm](https://llmfarm.site/) is available on TestFlight, so there's no | ||
| 76 | manual build process required. However, you can view the [LLMFarm | ||
| 77 | repository](https://github.com/guinmoon/LLMFarm) if you wish. | ||
| 78 | |||
| 79 | The caveat is that you will have to manually download the model files from the | ||
| 80 | links in the | ||
| 81 | [models.md](https://github.com/guinmoon/LLMFarm/blob/main/models.md) file to | ||
| 82 | your iPhone to use the app - there's currently no option in the app to reach out | ||
| 83 | and grab the latest version of any supported model. | ||
| 84 | |||
| 85 | Once you have a file downloaded, you simply create a new chat and select the | ||
| 86 | downloaded model file and ensure the inference matches the requirement in the | ||
| 87 | `models.md` file. | ||
| 88 | |||
| 89 | [Enchanted](https://github.com/AugustDev/enchanted) is also an iOS for private | ||
| 90 | AI models, but it requires a public-facing Ollama API, which did not meet my "on | ||
| 91 | device requirement." Nonetheless, it's an interesting looking app and I will | ||
| 92 | likely set it up to test soon. | ||
content/blog/2024-01-13-local-llm.org added +96
| @@ -0,0 +1,96 @@ | |||
| 1 | #+date: <2024-01-13> | ||
| 2 | #+title: Running Local LLMs on macOS and iOS | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * Requirements | ||
| 7 | |||
| 8 | I've recently started playing with large language models (LLMs), mostly | ||
| 9 | in the popular chatbot form, as part of my job and have decided to see | ||
| 10 | if there's a consistent and reliable way to interact with these models | ||
| 11 | on Apple devices without sacrificing privacy or requiring in-depth | ||
| 12 | technical setup. | ||
| 13 | |||
| 14 | My requirements for this test: | ||
| 15 | |||
| 16 | - Open source platform | ||
| 17 | - On-device model files | ||
| 18 | - Minimal required configuration | ||
| 19 | - Preferably pre-built, but a simple build process is acceptable | ||
| 20 | |||
| 21 | I tested a handful of apps and have summarized my favorite (so far) for | ||
| 22 | macOS and iOS below. | ||
| 23 | |||
| 24 | #+begin_quote | ||
| 25 | TL;DR - Here are the two that met my requirements and I have found the | ||
| 26 | easiest to install and use so far: | ||
| 27 | #+end_quote | ||
| 28 | |||
| 29 | - macOS: [[https://ollama.ai/][Ollama]] | ||
| 30 | - iOS : [[https://llmfarm.site/][LLM Farm]] | ||
| 31 | |||
| 32 | * macOS | ||
| 33 | |||
| 34 | [[https://ollama.ai/][Ollama]] is a simple Go application for macOS and | ||
| 35 | Linux that can run various LLMs locally. | ||
| 36 | |||
| 37 | For macOS, you can download the pplication on the | ||
| 38 | [[https://ollama.ai/download/mac][Ollama download page]] and install it | ||
| 39 | by unzipping the =Ollama.app= file and moving it to the =Applications= | ||
| 40 | folder. | ||
| 41 | |||
| 42 | If you prefer the command line, you can run these commands after the | ||
| 43 | download finished: | ||
| 44 | |||
| 45 | #+begin_src sh | ||
| 46 | cd ~/Downloads && \ | ||
| 47 | unzip Ollama-darwin.zip && \ | ||
| 48 | mv ~/Downloads/Ollama.app /Applications/ | ||
| 49 | #+end_src | ||
| 50 | |||
| 51 | After running the app, the app will ask you to open a terminal and run | ||
| 52 | the default =llama2= model, which will open an interactive chat session | ||
| 53 | in the terminal. You can startfully using the application at this point. | ||
| 54 | |||
| 55 | If you don't want to use the default =llama2= model, you can download | ||
| 56 | and run additional models found on the | ||
| 57 | [[https://ollama.ai/library][Models]] page. | ||
| 58 | |||
| 59 | To see the information for the currently-used model, you can run the | ||
| 60 | =/show info= command in the chat. | ||
| 61 | |||
| 62 | ** Community Integrations | ||
| 63 | |||
| 64 | I highly recommend browsing the | ||
| 65 | [[https://github.com/jmorganca/ollama#community-integrations][Community | ||
| 66 | Integrations]] section of the project to see how you would prefer to | ||
| 67 | extend Ollama beyond a simple command-line interface. There are options | ||
| 68 | for APIs, browser UIs, advanced terminal configurations, and more. | ||
| 69 | |||
| 70 | * iOS | ||
| 71 | |||
| 72 | While there are a handful of decent macOS options, it was quite | ||
| 73 | difficult to find an iOS app that offered an open source platform | ||
| 74 | without an extensive configuration and building process. I found LLM | ||
| 75 | Farm to be decent enough in quality to sit at the top of my list - | ||
| 76 | however, it's definitely not user friendly enough for me to consider | ||
| 77 | using it on a daily basis. | ||
| 78 | |||
| 79 | [[https://llmfarm.site/][LLM Farm]] is available on TestFlight, so | ||
| 80 | there's no manual build process required. However, you can view the | ||
| 81 | [[https://github.com/guinmoon/LLMFarm][LLMFarm repository]] if you wish. | ||
| 82 | |||
| 83 | The caveat is that you will have to manually download the model files | ||
| 84 | from the links in the | ||
| 85 | [[https://github.com/guinmoon/LLMFarm/blob/main/models.md][models.md]] | ||
| 86 | file to your iPhone to use the app - there's currently no option in the | ||
| 87 | app to reach out and grab the latest version of any supported model. | ||
| 88 | |||
| 89 | Once you have a file downloaded, you simply create a new chat and select | ||
| 90 | the downloaded model file and ensure the inference matches the | ||
| 91 | requirement in the =models.md= file. | ||
| 92 | |||
| 93 | [[https://github.com/AugustDev/enchanted][Enchanted]] is also an iOS for | ||
| 94 | private AI models, but it requires a public-facing Ollama API, which did | ||
| 95 | not meet my "on device requirement." Nonetheless, it's an interesting | ||
| 96 | looking app and I will likely set it up to test soon. | ||
content/blog/2024-01-26-audit-dashboard.md deleted −150
| @@ -1,150 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-01-26 | ||
| 3 | title = "Building an Audit Status Dashboard" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | Alteryx and Power BI are powerful tools that can help turn your old-school audit | ||
| 9 | trackers into interactive tools that provide useful insights and potential | ||
| 10 | action plans. | ||
| 11 | |||
| 12 | With these tools, we are going to build a dashboard that can effectively | ||
| 13 | communicate project status. | ||
| 14 | |||
| 15 | # Requirements | ||
| 16 | |||
| 17 | This project assumes the following: | ||
| 18 | |||
| 19 | - You have access to Alteryx Designer and Power BI Desktop. | ||
| 20 | - If you only have Power BI Desktop, you may need to perform some analysis | ||
| 21 | in Power BI instead of Alteryx. | ||
| 22 | - Your data is in a format that can be imported into Alteryx and/or Power BI. | ||
| 23 | - You have a basic understanding of data types and visualization. | ||
| 24 | |||
| 25 | # Alteryx: Data Preparation & Analysis | ||
| 26 | |||
| 27 | ## Import Data | ||
| 28 | |||
| 29 | With Alteryx, importing data is easy with the use of the `Input Data` tool. | ||
| 30 | Simply drag this tool onto the canvas from the `In/Out` tab in the Ribbon to | ||
| 31 | create it as a node. | ||
| 32 | |||
| 33 | You can choose the File Format manually or simply connect to your file/database | ||
| 34 | and let Alteryx determine the format for you. For this example, we will be | ||
| 35 | importing an Excel file and changing the `Start Data Import on Line` variable to | ||
| 36 | `2`. | ||
| 37 | |||
| 38 | ## Transform Data | ||
| 39 | |||
| 40 | Next, let's replace null data and remove whitespace to clean up our data. We can | ||
| 41 | do this with the `Data Cleansing` tool in the `Preparation` tab in the Ribbon. | ||
| 42 | |||
| 43 | Ensure that the following options are enabled: | ||
| 44 | |||
| 45 | - Replace Nulls | ||
| 46 | - Replace with Blanks (String Fields) | ||
| 47 | - Replace with 0 (Numeric Fields) | ||
| 48 | - Remove Unwanted Characters | ||
| 49 | - Leading and Trailing Whitespace | ||
| 50 | |||
| 51 | For our next step, we will transform the date fields from strings to datetime | ||
| 52 | format. Add a `Datetime` tool for each field you want to transform - in the | ||
| 53 | example below, I am using the tool twice for the "Started On" and "Submitted On" | ||
| 54 | fields. | ||
| 55 | |||
| 56 | Now that the dates are in the correct format, let's perform a calculation based | ||
| 57 | on those fields. Start by adding a `Filter` tool, naming a new Output Column, | ||
| 58 | and pasting the formula below into it (the two fields used in this formula must | ||
| 59 | match the output of the `Datetime` tools above): | ||
| 60 | |||
| 61 | ```txt | ||
| 62 | DateTimeDiff([SubmittedOn_Out],[StartedOn_Out], "days") | ||
| 63 | ` | ||
| 64 | |||
| 65 | ## Export Data | ||
| 66 | |||
| 67 | Finalize the process by exporting the transformed data set to a new file, for | ||
| 68 | use in the following visualization step. | ||
| 69 | |||
| 70 | # Power BI: Data Visualization | ||
| 71 | |||
| 72 | ## Import Data | ||
| 73 | |||
| 74 | To start, open the Power BI Desktop application. Upon first use, Power BI will | ||
| 75 | ask if you want to open an existing dashboard or import new data. | ||
| 76 | |||
| 77 | As we are creating our first dashboard, let's import our data. In my example | ||
| 78 | below, I'm importing data from the "Tracker" sheet of the Excel file I'm using | ||
| 79 | for this project. | ||
| 80 | |||
| 81 | During this process, I also imported the export from the Alteryx workflow above. | ||
| 82 | Therefore, we have two different files available for use in our dashboard. | ||
| 83 | |||
| 84 | ## Add Visuals | ||
| 85 | |||
| 86 | To create the dashboard, you will need to follow the list instructions | ||
| 87 | below and format as needed. | ||
| 88 | |||
| 89 | Instructions to create the visuals above: | ||
| 90 | |||
| 91 | - `Text Box`: Explain the name and purpose of the dashboard. You can also add | ||
| 92 | images and logos at the top of the dashboard. | ||
| 93 | - `Donut Chart`: Overall status of the project. | ||
| 94 | - `Legend`: Status | ||
| 95 | - `Values`: Count of Status | ||
| 96 | - `Stacked Column Chart`: Task count by assignee. | ||
| 97 | - `X-axis`: Preparer | ||
| 98 | - `Y-axis`: Count of Control ID | ||
| 99 | - `Legend`: Status | ||
| 100 | - `Treemap`: Top N client submitters by average days to submit. | ||
| 101 | - `Details`: Preparer | ||
| 102 | - `Values`: Sum of Avg~DaysToSubmit~ | ||
| 103 | - `Line Chart`: Projected vs. actual hours over time. | ||
| 104 | - `Clustered Bar Chart`: Projected vs. actual hours per person. | ||
| 105 | - `Slicer & Table` - Upcoming due dates. | ||
| 106 | - `Slicer`: | ||
| 107 | - `Values`: Date Due | ||
| 108 | - `Table`: | ||
| 109 | - `Columns`: Count of Control ID, Date Due, Preparer, Status | ||
| 110 | |||
| 111 | ## Format the Dashboard | ||
| 112 | |||
| 113 | You can choose a theme in the View tab of the Ribbon. You can even browse for | ||
| 114 | custom JSON files that define themes, such as ones found online or custom ones | ||
| 115 | created by your organization. | ||
| 116 | |||
| 117 | For each visual, you can click the `Format` button in the `Visualizations` side | ||
| 118 | pane and explore the options. You can custom options such as: | ||
| 119 | |||
| 120 | - Visual | ||
| 121 | - Legend | ||
| 122 | - Colors | ||
| 123 | - Data labels | ||
| 124 | - Category labels | ||
| 125 | - General | ||
| 126 | - Properties | ||
| 127 | - Title | ||
| 128 | - Effects | ||
| 129 | - Header icons | ||
| 130 | - Tooltips | ||
| 131 | - Alt text | ||
| 132 | |||
| 133 | You can always look online for inspiration when trying to decide how best to | ||
| 134 | organize and style your dashboard. | ||
| 135 | |||
| 136 | # Sharing the Results | ||
| 137 | |||
| 138 | Generally, you have a few different options for sharing your dashboards with | ||
| 139 | others: | ||
| 140 | |||
| 141 | 1. Export the dashboard as a PDF in the file menu of Power BI. This will export | ||
| 142 | all tabs and visuals as they are set when the export button is pressed. You | ||
| 143 | will lose all interactivity with this option. | ||
| 144 | 2. Send the full Power BI file to those you wish to share the dashboard. This | ||
| 145 | will retain all settings and interactivity. However, you will also need to | ||
| 146 | send the source files if they need to refresh the dashboard and you will need | ||
| 147 | to re-send the files if you make updates. | ||
| 148 | 3. Store the dashboard in a synced location, such as a shared drive or Microsoft | ||
| 149 | Teams. Depending on how a user configures their local Windows paths, the data | ||
| 150 | source paths may not be compatible for all users with such a setup. | ||
content/blog/2024-01-26-audit-dashboard.org added +153
| @@ -0,0 +1,153 @@ | |||
| 1 | #+date: <2024-01-26> | ||
| 2 | #+title: Building an Audit Status Dashboard | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | Alteryx and Power BI are powerful tools that can help turn your | ||
| 7 | old-school audit trackers into interactive tools that provide useful | ||
| 8 | insights and potential action plans. | ||
| 9 | |||
| 10 | With these tools, we are going to build a dashboard that can effectively | ||
| 11 | communicate project status. | ||
| 12 | |||
| 13 | * Requirements | ||
| 14 | |||
| 15 | This project assumes the following: | ||
| 16 | |||
| 17 | - You have access to Alteryx Designer and Power BI Desktop. | ||
| 18 | - If you only have Power BI Desktop, you may need to perform some | ||
| 19 | analysis in Power BI instead of Alteryx. | ||
| 20 | - Your data is in a format that can be imported into Alteryx and/or | ||
| 21 | Power BI. | ||
| 22 | - You have a basic understanding of data types and visualization. | ||
| 23 | |||
| 24 | * Alteryx: Data Preparation & Analysis | ||
| 25 | |||
| 26 | ** Import Data | ||
| 27 | |||
| 28 | With Alteryx, importing data is easy with the use of the =Input Data= | ||
| 29 | tool. Simply drag this tool onto the canvas from the =In/Out= tab in the | ||
| 30 | Ribbon to create it as a node. | ||
| 31 | |||
| 32 | You can choose the File Format manually or simply connect to your | ||
| 33 | file/database and let Alteryx determine the format for you. For this | ||
| 34 | example, we will be importing an Excel file and changing the | ||
| 35 | =Start Data Import on Line= variable to =2=. | ||
| 36 | |||
| 37 | ** Transform Data | ||
| 38 | |||
| 39 | Next, let's replace null data and remove whitespace to clean up our | ||
| 40 | data. We can do this with the =Data Cleansing= tool in the =Preparation= | ||
| 41 | tab in the Ribbon. | ||
| 42 | |||
| 43 | Ensure that the following options are enabled: | ||
| 44 | |||
| 45 | - Replace Nulls | ||
| 46 | - Replace with Blanks (String Fields) | ||
| 47 | - Replace with 0 (Numeric Fields) | ||
| 48 | - Remove Unwanted Characters | ||
| 49 | - Leading and Trailing Whitespace | ||
| 50 | |||
| 51 | For our next step, we will transform the date fields from strings to | ||
| 52 | datetime format. Add a =Datetime= tool for each field you want to | ||
| 53 | transform - in the example below, I am using the tool twice for the | ||
| 54 | "Started On" and "Submitted On" fields. | ||
| 55 | |||
| 56 | Now that the dates are in the correct format, let's perform a | ||
| 57 | calculation based on those fields. Start by adding a =Filter= tool, | ||
| 58 | naming a new Output Column, and pasting the formula below into it (the | ||
| 59 | two fields used in this formula must match the output of the =Datetime= | ||
| 60 | tools above): | ||
| 61 | |||
| 62 | ``=txt DateTimeDiff([SubmittedOn_Out],[StartedOn_Out], "days")= | ||
| 63 | |||
| 64 | ** Export Data | ||
| 65 | |||
| 66 | Finalize the process by exporting the transformed data set to a new | ||
| 67 | file, for use in the following visualization step. | ||
| 68 | |||
| 69 | * Power BI: Data Visualization | ||
| 70 | |||
| 71 | ** Import Data | ||
| 72 | |||
| 73 | To start, open the Power BI Desktop application. Upon first use, Power | ||
| 74 | BI will ask if you want to open an existing dashboard or import new | ||
| 75 | data. | ||
| 76 | |||
| 77 | As we are creating our first dashboard, let's import our data. In my | ||
| 78 | example below, I'm importing data from the "Tracker" sheet of the Excel | ||
| 79 | file I'm using for this project. | ||
| 80 | |||
| 81 | During this process, I also imported the export from the Alteryx | ||
| 82 | workflow above. Therefore, we have two different files available for use | ||
| 83 | in our dashboard. | ||
| 84 | |||
| 85 | ** Add Visuals | ||
| 86 | |||
| 87 | To create the dashboard, you will need to follow the list instructions | ||
| 88 | below and format as needed. | ||
| 89 | |||
| 90 | Instructions to create the visuals above: | ||
| 91 | |||
| 92 | - =Text Box=: Explain the name and purpose of the dashboard. You can | ||
| 93 | also add images and logos at the top of the dashboard. | ||
| 94 | - =Donut Chart=: Overall status of the project. | ||
| 95 | - =Legend=: Status | ||
| 96 | - =Values=: Count of Status | ||
| 97 | - =Stacked Column Chart=: Task count by assignee. | ||
| 98 | - =X-axis=: Preparer | ||
| 99 | - =Y-axis=: Count of Control ID | ||
| 100 | - =Legend=: Status | ||
| 101 | - =Treemap=: Top N client submitters by average days to submit. | ||
| 102 | - =Details=: Preparer | ||
| 103 | - =Values=: Sum of Avg_{DaysToSubmit} | ||
| 104 | - =Line Chart=: Projected vs. actual hours over time. | ||
| 105 | - =Clustered Bar Chart=: Projected vs. actual hours per person. | ||
| 106 | - =Slicer & Table= - Upcoming due dates. | ||
| 107 | - =Slicer=: | ||
| 108 | - =Values=: Date Due | ||
| 109 | - =Table=: | ||
| 110 | - =Columns=: Count of Control ID, Date Due, Preparer, Status | ||
| 111 | |||
| 112 | ** Format the Dashboard | ||
| 113 | |||
| 114 | You can choose a theme in the View tab of the Ribbon. You can even | ||
| 115 | browse for custom JSON files that define themes, such as ones found | ||
| 116 | online or custom ones created by your organization. | ||
| 117 | |||
| 118 | For each visual, you can click the =Format= button in the | ||
| 119 | =Visualizations= side pane and explore the options. You can custom | ||
| 120 | options such as: | ||
| 121 | |||
| 122 | - Visual | ||
| 123 | - Legend | ||
| 124 | - Colors | ||
| 125 | - Data labels | ||
| 126 | - Category labels | ||
| 127 | - General | ||
| 128 | - Properties | ||
| 129 | - Title | ||
| 130 | - Effects | ||
| 131 | - Header icons | ||
| 132 | - Tooltips | ||
| 133 | - Alt text | ||
| 134 | |||
| 135 | You can always look online for inspiration when trying to decide how | ||
| 136 | best to organize and style your dashboard. | ||
| 137 | |||
| 138 | * Sharing the Results | ||
| 139 | |||
| 140 | Generally, you have a few different options for sharing your dashboards | ||
| 141 | with others: | ||
| 142 | |||
| 143 | 1. Export the dashboard as a PDF in the file menu of Power BI. This will | ||
| 144 | export all tabs and visuals as they are set when the export button is | ||
| 145 | pressed. You will lose all interactivity with this option. | ||
| 146 | 2. Send the full Power BI file to those you wish to share the dashboard. | ||
| 147 | This will retain all settings and interactivity. However, you will | ||
| 148 | also need to send the source files if they need to refresh the | ||
| 149 | dashboard and you will need to re-send the files if you make updates. | ||
| 150 | 3. Store the dashboard in a synced location, such as a shared drive or | ||
| 151 | Microsoft Teams. Depending on how a user configures their local | ||
| 152 | Windows paths, the data source paths may not be compatible for all | ||
| 153 | users with such a setup. | ||
content/blog/2024-01-27-tableau-dashboard.md deleted −148
| @@ -1,148 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-01-27 | ||
| 3 | title = "Data Visualization: Mapping Omaha Crime Data with Tableau" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | In this project, I am going to show you how to use Tableau Public for free to | ||
| 9 | create simple dashboards. | ||
| 10 | |||
| 11 | I will be creating simple visuals from an Omaha crime data set and combining | ||
| 12 | them to create the dashboard below. You can view this dashboard interactively | ||
| 13 | online here: [Omaha Crime Data (2015 - | ||
| 14 | 2023)](https://public.tableau.com/app/profile/c.c7042/viz/OmahaCrimeData2015-2023/OmahaCrimeData2015-2023#1). | ||
| 15 | |||
| 16 |  | ||
| 18 | |||
| 19 | # Gather the Data | ||
| 20 | |||
| 21 | You can download incident data from the Omaha Police Department on their | ||
| 22 | [Incident Data | ||
| 23 | Download](https://police.cityofomaha.org/crime-information/incident-data-download) | ||
| 24 | page. They currently have files for the years 2015 through 2023. | ||
| 25 | |||
| 26 | Each file will be downloaded as a CSV file, approximately 3 MB - 8 MB. | ||
| 27 | |||
| 28 | # Clean and Transform the Data | ||
| 29 | |||
| 30 | I have used Python to combine the files into a single CSV file, as well as | ||
| 31 | adding a custom `datetime` column. You could do this step in any software you | ||
| 32 | prefer, but I prefer Python as its free, easy to use, and has a plethora of | ||
| 33 | support resources online. | ||
| 34 | |||
| 35 | Start by opening a terminal, navigating to your Downloads directory, and | ||
| 36 | creating a python script. | ||
| 37 | |||
| 38 | ```sh | ||
| 39 | cd ~/Downloads | ||
| 40 | nano data_processing.py | ||
| 41 | ``` | ||
| 42 | |||
| 43 | Within the Python script, paste the following: | ||
| 44 | |||
| 45 | ```python | ||
| 46 | # Import modules | ||
| 47 | import pandas as pd | ||
| 48 | import glob | ||
| 49 | import os | ||
| 50 | |||
| 51 | # Import the data | ||
| 52 | path = r"~/Downloads/*.csv" | ||
| 53 | files = glob.glob(path) | ||
| 54 | |||
| 55 | list = [] | ||
| 56 | |||
| 57 | for file in files: | ||
| 58 | df_tmp = pd.read_csv(file) | ||
| 59 | li.append(df_tmp) | ||
| 60 | |||
| 61 | df = pd.concat(list, axis=0, ignore_index=True) | ||
| 62 | |||
| 63 | # Create a combined datetime column | ||
| 64 | df["datetime"] = pd.to_datetime( | ||
| 65 | df["date"] + " " + df["time"], | ||
| 66 | format="%m/%d/%Y %H:%M:%S" | ||
| 67 | ) | ||
| 68 | df.head() | ||
| 69 | |||
| 70 | # Export the combined data | ||
| 71 | df.to_csv(r"~/Downloads/combined_incidents.csv") | ||
| 72 | ``` | ||
| 73 | |||
| 74 | Once pasted, save and close the file. You can execute the file like so: | ||
| 75 | |||
| 76 | ```sh | ||
| 77 | python3 data_processing.py | ||
| 78 | ``` | ||
| 79 | |||
| 80 | After this, you should have a combined data file that contains all incidents | ||
| 81 | between 2015 and 2023. Mine was approximately 55 MB. | ||
| 82 | |||
| 83 | # Tableau Public | ||
| 84 | |||
| 85 | [Tableau Public](https://public.tableau.com/) is a free-to-use web application | ||
| 86 | that allows you to create visualizations by uploading data sources. Note that | ||
| 87 | there's no way to keep the data and visualizations private, so don't upload | ||
| 88 | anything private. | ||
| 89 | |||
| 90 | After creating an account, you can click the `Create` > `Web Authoring` link to | ||
| 91 | create your first visualization. | ||
| 92 | |||
| 93 | ## Upload the Data | ||
| 94 | |||
| 95 | Once you've opened your first project, Tableau will ask you to connect to your | ||
| 96 | data. For this project, click the `Upload from computer` button and select the | ||
| 97 | CSV file previously combined in the step above. | ||
| 98 | |||
| 99 | Once connected, you can refresh the preview of the data with the `Refresh Data | ||
| 100 | Source` button in the toolbar. | ||
| 101 | |||
| 102 | If you need to edit any of the data types, column names, etc., you can do so | ||
| 103 | now. Once complete, generate an extract so that you can start creating | ||
| 104 | visualizations. | ||
| 105 | |||
| 106 | ## Create Visualizations | ||
| 107 | |||
| 108 | To start, create a worksheet in the toolbar at the bottom of the screen. | ||
| 109 | |||
| 110 | Within this screen, select a column from the `Data` side bar on the left and | ||
| 111 | drag it into the `Columns` or `Rows` area of the canvas. | ||
| 112 | |||
| 113 | See below for the map visualization. You can recreate this by adding the | ||
| 114 | following fields: | ||
| 115 | |||
| 116 | - `Columns`: Lon | ||
| 117 | - `Rows`: Lat | ||
| 118 | - `Marks`: | ||
| 119 | - Description | ||
| 120 | - Datetime | ||
| 121 | - `Filters`: Datetime | ||
| 122 | |||
| 123 | You can repeat this process for each visualization you want to create. Explore | ||
| 124 | your options by dragging data fields to different areas and by opening the field | ||
| 125 | options to explore what operations can be performed on different data types | ||
| 126 | (e.g., average, count, etc.). | ||
| 127 | |||
| 128 | ## Create Dashboard | ||
| 129 | |||
| 130 | To create a dashboard, click the button on the toolbar at the bottom of the | ||
| 131 | screen. Within the dashboard, drag each sheet from the left side bar onto the | ||
| 132 | dashboard canvas. | ||
| 133 | |||
| 134 | ## Formatting | ||
| 135 | |||
| 136 | You can explore a ton of different formatting options throughout the worksheets | ||
| 137 | and dashboard. Specifically for maps, you can alter the map layers, background, | ||
| 138 | and visible features through the `Map` menu in the top file menu of the editing | ||
| 139 | screen. | ||
| 140 | |||
| 141 | In the finished dashboard below, I opted for a dark mode with a map that showed | ||
| 142 | county lines and city names. | ||
| 143 | |||
| 144 | There's a ton of other options available to be used in a dashboard like this, | ||
| 145 | but this project shows a quick preview of what you can do in Tableau Public. | ||
| 146 | |||
| 147 |  | ||
content/blog/2024-01-27-tableau-dashboard.org added +148
| @@ -0,0 +1,148 @@ | |||
| 1 | #+date: <2024-01-27> | ||
| 2 | #+title: Data Visualization: Mapping Omaha Crime Data with Tableau | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | In this project, I am going to show you how to use Tableau Public for | ||
| 7 | free to create simple dashboards. | ||
| 8 | |||
| 9 | I will be creating simple visuals from an Omaha crime data set and | ||
| 10 | combining them to create the dashboard below. You can view this | ||
| 11 | dashboard interactively online here: | ||
| 12 | [[https://public.tableau.com/app/profile/c.c7042/viz/OmahaCrimeData2015-2023/OmahaCrimeData2015-2023#1][Omaha | ||
| 13 | Crime Data (2015 - 2023)]]. | ||
| 14 | |||
| 15 | #+caption: Tableau Dashboard | ||
| 16 | [[https://img.cleberg.net/blog/20240127-tableau-dashboard/dashboard.png]] | ||
| 17 | |||
| 18 | * Gather the Data | ||
| 19 | |||
| 20 | You can download incident data from the Omaha Police Department on their | ||
| 21 | [[https://police.cityofomaha.org/crime-information/incident-data-download][Incident | ||
| 22 | Data Download]] page. They currently have files for the years 2015 | ||
| 23 | through 2023. | ||
| 24 | |||
| 25 | Each file will be downloaded as a CSV file, approximately 3 MB - 8 MB. | ||
| 26 | |||
| 27 | * Clean and Transform the Data | ||
| 28 | |||
| 29 | I have used Python to combine the files into a single CSV file, as well | ||
| 30 | as adding a custom =datetime= column. You could do this step in any | ||
| 31 | software you prefer, but I prefer Python as its free, easy to use, and | ||
| 32 | has a plethora of support resources online. | ||
| 33 | |||
| 34 | Start by opening a terminal, navigating to your Downloads directory, and | ||
| 35 | creating a python script. | ||
| 36 | |||
| 37 | #+begin_src sh | ||
| 38 | cd ~/Downloads | ||
| 39 | nano data_processing.py | ||
| 40 | #+end_src | ||
| 41 | |||
| 42 | Within the Python script, paste the following: | ||
| 43 | |||
| 44 | #+begin_src python | ||
| 45 | # Import modules | ||
| 46 | import pandas as pd | ||
| 47 | import glob | ||
| 48 | import os | ||
| 49 | |||
| 50 | # Import the data | ||
| 51 | path = r"~/Downloads/*.csv" | ||
| 52 | files = glob.glob(path) | ||
| 53 | |||
| 54 | list = [] | ||
| 55 | |||
| 56 | for file in files: | ||
| 57 | df_tmp = pd.read_csv(file) | ||
| 58 | li.append(df_tmp) | ||
| 59 | |||
| 60 | df = pd.concat(list, axis=0, ignore_index=True) | ||
| 61 | |||
| 62 | # Create a combined datetime column | ||
| 63 | df["datetime"] = pd.to_datetime( | ||
| 64 | df["date"] + " " + df["time"], | ||
| 65 | format="%m/%d/%Y %H:%M:%S" | ||
| 66 | ) | ||
| 67 | df.head() | ||
| 68 | |||
| 69 | # Export the combined data | ||
| 70 | df.to_csv(r"~/Downloads/combined_incidents.csv") | ||
| 71 | #+end_src | ||
| 72 | |||
| 73 | Once pasted, save and close the file. You can execute the file like so: | ||
| 74 | |||
| 75 | #+begin_src sh | ||
| 76 | python3 data_processing.py | ||
| 77 | #+end_src | ||
| 78 | |||
| 79 | After this, you should have a combined data file that contains all | ||
| 80 | incidents between 2015 and 2023. Mine was approximately 55 MB. | ||
| 81 | |||
| 82 | * Tableau Public | ||
| 83 | |||
| 84 | [[https://public.tableau.com/][Tableau Public]] is a free-to-use web | ||
| 85 | application that allows you to create visualizations by uploading data | ||
| 86 | sources. Note that there's no way to keep the data and visualizations | ||
| 87 | private, so don't upload anything private. | ||
| 88 | |||
| 89 | After creating an account, you can click the =Create= > =Web Authoring= | ||
| 90 | link to create your first visualization. | ||
| 91 | |||
| 92 | ** Upload the Data | ||
| 93 | |||
| 94 | Once you've opened your first project, Tableau will ask you to connect | ||
| 95 | to your data. For this project, click the =Upload from computer= button | ||
| 96 | and select the CSV file previously combined in the step above. | ||
| 97 | |||
| 98 | Once connected, you can refresh the preview of the data with the | ||
| 99 | =Refresh Data Source= button in the toolbar. | ||
| 100 | |||
| 101 | If you need to edit any of the data types, column names, etc., you can | ||
| 102 | do so now. Once complete, generate an extract so that you can start | ||
| 103 | creating visualizations. | ||
| 104 | |||
| 105 | ** Create Visualizations | ||
| 106 | |||
| 107 | To start, create a worksheet in the toolbar at the bottom of the screen. | ||
| 108 | |||
| 109 | Within this screen, select a column from the =Data= side bar on the left | ||
| 110 | and drag it into the =Columns= or =Rows= area of the canvas. | ||
| 111 | |||
| 112 | See below for the map visualization. You can recreate this by adding the | ||
| 113 | following fields: | ||
| 114 | |||
| 115 | - =Columns=: Lon | ||
| 116 | - =Rows=: Lat | ||
| 117 | - =Marks=: | ||
| 118 | - Description | ||
| 119 | - Datetime | ||
| 120 | - =Filters=: Datetime | ||
| 121 | |||
| 122 | You can repeat this process for each visualization you want to create. | ||
| 123 | Explore your options by dragging data fields to different areas and by | ||
| 124 | opening the field options to explore what operations can be performed on | ||
| 125 | different data types (e.g., average, count, etc.). | ||
| 126 | |||
| 127 | ** Create Dashboard | ||
| 128 | |||
| 129 | To create a dashboard, click the button on the toolbar at the bottom of | ||
| 130 | the screen. Within the dashboard, drag each sheet from the left side bar | ||
| 131 | onto the dashboard canvas. | ||
| 132 | |||
| 133 | ** Formatting | ||
| 134 | |||
| 135 | You can explore a ton of different formatting options throughout the | ||
| 136 | worksheets and dashboard. Specifically for maps, you can alter the map | ||
| 137 | layers, background, and visible features through the =Map= menu in the | ||
| 138 | top file menu of the editing screen. | ||
| 139 | |||
| 140 | In the finished dashboard below, I opted for a dark mode with a map that | ||
| 141 | showed county lines and city names. | ||
| 142 | |||
| 143 | There's a ton of other options available to be used in a dashboard like | ||
| 144 | this, but this project shows a quick preview of what you can do in | ||
| 145 | Tableau Public. | ||
| 146 | |||
| 147 | #+caption: Tableau Dashboard | ||
| 148 | [[https://img.cleberg.net/blog/20240127-tableau-dashboard/dashboard.png]] | ||
content/blog/2024-02-06-zfs.md → content/blog/2024-02-06-zfs.org renamed +140 −131
| @@ -1,70 +1,70 @@ | |||
| 1 | +++ | 1 | #+date: <2024-02-06> |
| 2 | date = 2024-02-06 | 2 | #+title: How to Create a ZFS Pool on Ubuntu Linux |
| 3 | title = "How to Create a ZFS Pool on Ubuntu Linux" | 3 | #+description: |
| 4 | description = "" | 4 | |
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 5 | ||
| 8 | This post details the process I used to create ZFS pools, datasets, and | 6 | This post details the process I used to create ZFS pools, datasets, and |
| 9 | snapshots on Ubuntu Server. | 7 | snapshots on Ubuntu Server. |
| 10 | 8 | ||
| 11 | I found the following pages very helpful while going through this process: | 9 | I found the following pages very helpful while going through this |
| 10 | process: | ||
| 12 | 11 | ||
| 13 | - [Setup a ZFS storage | 12 | - [[https://ubuntu.com/tutorials/setup-zfs-storage-pool][Setup a ZFS |
| 14 | pool](https://ubuntu.com/tutorials/setup-zfs-storage-pool) | 13 | storage pool]] |
| 15 | - [Kernel/Reference/ZFS](https://wiki.ubuntu.com/Kernel/Reference/ZFS) | 14 | - [[https://wiki.ubuntu.com/Kernel/Reference/ZFS][Kernel/Reference/ZFS]] |
| 16 | - [ZFS for | 15 | - [[https://blog.victormendonca.com/2020/11/03/zfs-for-dummies/][ZFS for |
| 17 | Dummies](https://blog.victormendonca.com/2020/11/03/zfs-for-dummies/) | 16 | Dummies]] |
| 18 | 17 | ||
| 19 | # Installation | 18 | * Installation |
| 20 | 19 | ||
| 21 | To start, I installed the ZFS package with the following command: | 20 | To start, I installed the ZFS package with the following command: |
| 22 | 21 | ||
| 23 | ```sh | 22 | #+begin_src sh |
| 24 | sudo apt install zfsutils-linux | 23 | sudo apt install zfsutils-linux |
| 25 | ``` | 24 | #+end_src |
| 26 | 25 | ||
| 27 | Once installed, you can check the version to see if it installed correctly. | 26 | Once installed, you can check the version to see if it installed |
| 27 | correctly. | ||
| 28 | 28 | ||
| 29 | ```sh | 29 | #+begin_src sh |
| 30 | > zsf --version | 30 | > zsf --version |
| 31 | 31 | ||
| 32 | zfs-2.1.5-1ubuntu6~22.04.2 | 32 | zfs-2.1.5-1ubuntu6~22.04.2 |
| 33 | zfs-kmod-2.1.5-1ubuntu6~22.04.1 | 33 | zfs-kmod-2.1.5-1ubuntu6~22.04.1 |
| 34 | ``` | 34 | #+end_src |
| 35 | 35 | ||
| 36 | # ZFS Configuration | 36 | * ZFS Configuration |
| 37 | 37 | ||
| 38 | Now that ZFS is installed, we can create and configure the pool. | 38 | Now that ZFS is installed, we can create and configure the pool. |
| 39 | 39 | ||
| 40 | You have various options for configuring ZFS pools that all come different pros | 40 | You have various options for configuring ZFS pools that all come |
| 41 | and cons. I suggest visiting the links at the top of this post or searching | 41 | different pros and cons. I suggest visiting the links at the top of this |
| 42 | online for the best configuration for your use-case. | 42 | post or searching online for the best configuration for your use-case. |
| 43 | 43 | ||
| 44 | - Striped VDEVs (Raid0) | 44 | - Striped VDEVs (Raid0) |
| 45 | - Mirrored VDEVs (Raid1) | 45 | - Mirrored VDEVs (Raid1) |
| 46 | - Striped Mirrored VDEVs (Raid10) | 46 | - Striped Mirrored VDEVs (Raid10) |
| 47 | - RAIDz (Raid5) | 47 | - RAIDz (Raid5) |
| 48 | - RAIDz2 (Raidd6) | 48 | - RAIDz2 (Raidd6) |
| 49 | - RAIDz3 | 49 | - RAIDz3 |
| 50 | - Nested RAIDz (Raid50, Raid60) | 50 | - Nested RAIDz (Raid50, Raid60) |
| 51 | 51 | ||
| 52 | I will be using Raid10 in this guide. However, the majority of the steps are the | 52 | I will be using Raid10 in this guide. However, the majority of the steps |
| 53 | same regardless of your chosen pool configuration. | 53 | are the same regardless of your chosen pool configuration. |
| 54 | 54 | ||
| 55 | ## Creating the Pool | 55 | ** Creating the Pool |
| 56 | 56 | ||
| 57 | To start, let's list the disks available to use. You can use `fdisk` command to | 57 | To start, let's list the disks available to use. You can use =fdisk= |
| 58 | see all available disks. | 58 | command to see all available disks. |
| 59 | 59 | ||
| 60 | ```sh | 60 | #+begin_src sh |
| 61 | sudo fdisk -l | 61 | sudo fdisk -l |
| 62 | ``` | 62 | #+end_src |
| 63 | 63 | ||
| 64 | Or, if you currently have them mounted, you can use the `df` command to view | 64 | Or, if you currently have them mounted, you can use the =df= command to |
| 65 | your disks. | 65 | view your disks. |
| 66 | 66 | ||
| 67 | ```sh | 67 | #+begin_src sh |
| 68 | > sudo df -h | 68 | > sudo df -h |
| 69 | 69 | ||
| 70 | Filesystem Size Used Avail Use% Mounted on | 70 | Filesystem Size Used Avail Use% Mounted on |
| @@ -75,34 +75,35 @@ Filesystem Size Used Avail Use% Mounted on | |||
| 75 | /dev/sdd1 9.1T 8.7T 435G 96% /mnt/white-01 | 75 | /dev/sdd1 9.1T 8.7T 435G 96% /mnt/white-01 |
| 76 | /dev/sde1 7.3T 28K 6.9T 1% /mnt/red-03 | 76 | /dev/sde1 7.3T 28K 6.9T 1% /mnt/red-03 |
| 77 | /dev/sdf1 7.3T 28K 6.9T 1% /mnt/red-04 | 77 | /dev/sdf1 7.3T 28K 6.9T 1% /mnt/red-04 |
| 78 | ``` | 78 | #+end_src |
| 79 | 79 | ||
| 80 | If you're going to use mounted disks, make sure to umount them before creating | 80 | If you're going to use mounted disks, make sure to umount them before |
| 81 | the pool. | 81 | creating the pool. |
| 82 | 82 | ||
| 83 | ```sh | 83 | #+begin_src sh |
| 84 | sudo umount /dev/sda1 | 84 | sudo umount /dev/sda1 |
| 85 | sudo umount /dev/sdb1 | 85 | sudo umount /dev/sdb1 |
| 86 | ``` | 86 | #+end_src |
| 87 | 87 | ||
| 88 | Now that I've identified the disks I want to use and have them unmounted, let's | 88 | Now that I've identified the disks I want to use and have them |
| 89 | create the pool. For this example, I will call it `tank`. | 89 | unmounted, let's create the pool. For this example, I will call it |
| 90 | =tank=. | ||
| 90 | 91 | ||
| 91 | ```sh | 92 | #+begin_src sh |
| 92 | sudo zpool create -f -m /mnt/pool tank mirror /dev/sda /dev/sdb | 93 | sudo zpool create -f -m /mnt/pool tank mirror /dev/sda /dev/sdb |
| 93 | ``` | 94 | #+end_src |
| 94 | 95 | ||
| 95 | See below for the results of the new ZFS pool named `tank`, with a vdev | 96 | See below for the results of the new ZFS pool named =tank=, with a vdev |
| 96 | automatically named `mirror-0`. | 97 | automatically named =mirror-0=. |
| 97 | 98 | ||
| 98 | ```sh | 99 | #+begin_src sh |
| 99 | > zfs list | 100 | > zfs list |
| 100 | 101 | ||
| 101 | NAME USED AVAIL REFER MOUNTPOINT | 102 | NAME USED AVAIL REFER MOUNTPOINT |
| 102 | tank 396K 7.14T 96K /tank | 103 | tank 396K 7.14T 96K /tank |
| 103 | ``` | 104 | #+end_src |
| 104 | 105 | ||
| 105 | ```sh | 106 | #+begin_src sh |
| 106 | > zpool status | 107 | > zpool status |
| 107 | 108 | ||
| 108 | pool: tank | 109 | pool: tank |
| @@ -116,75 +117,78 @@ config: | |||
| 116 | sdb ONLINE 0 0 0 | 117 | sdb ONLINE 0 0 0 |
| 117 | 118 | ||
| 118 | errors: No known data errors | 119 | errors: No known data errors |
| 119 | ``` | 120 | #+end_src |
| 120 | 121 | ||
| 121 | We can also look at the mounted filesystem to see where the pool is mounted and | 122 | We can also look at the mounted filesystem to see where the pool is |
| 122 | some quick stats. | 123 | mounted and some quick stats. |
| 123 | 124 | ||
| 124 | ```sh | 125 | #+begin_src sh |
| 125 | > df -h | 126 | > df -h |
| 126 | 127 | ||
| 127 | Filesystem Size Used Avail Use% Mounted on | 128 | Filesystem Size Used Avail Use% Mounted on |
| 128 | ... | 129 | ... |
| 129 | tank 7.2T 128K 7.2T 1% /tank | 130 | tank 7.2T 128K 7.2T 1% /tank |
| 130 | ``` | 131 | #+end_src |
| 131 | 132 | ||
| 132 | ## Expanding the Pool | 133 | ** Expanding the Pool |
| 133 | 134 | ||
| 134 | If you want to expand this pool, you will need to add a new VDEV to the pool. | 135 | If you want to expand this pool, you will need to add a new VDEV to the |
| 135 | Since I am using 2 disks per VDEV, I will need to add a new 2-disk VDEV to the | 136 | pool. Since I am using 2 disks per VDEV, I will need to add a new 2-disk |
| 136 | existing `tank` pool. | 137 | VDEV to the existing =tank= pool. |
| 137 | 138 | ||
| 138 | ```sh | 139 | #+begin_src sh |
| 139 | sudo zpool add tank mirror /dev/sdX /dev/sdY | 140 | sudo zpool add tank mirror /dev/sdX /dev/sdY |
| 140 | ``` | 141 | #+end_src |
| 141 | 142 | ||
| 142 | If you're adding disks of different sizes, you'll need to use the `-f` flag. | 143 | If you're adding disks of different sizes, you'll need to use the =-f= |
| 143 | Keep in mind that the max size will be limited to the smallest disk added. | 144 | flag. Keep in mind that the max size will be limited to the smallest |
| 145 | disk added. | ||
| 144 | 146 | ||
| 145 | ```sh | 147 | #+begin_src sh |
| 146 | sudo zpool add -f tank mirror /dev/sdX /dev/sdY | 148 | sudo zpool add -f tank mirror /dev/sdX /dev/sdY |
| 147 | ``` | 149 | #+end_src |
| 148 | 150 | ||
| 149 | I added two 8TB hard drives and this process took around 10 seconds to complete. | 151 | I added two 8TB hard drives and this process took around 10 seconds to |
| 152 | complete. | ||
| 150 | 153 | ||
| 151 | When viewing the pool again, you can see that the pool has now doubled in size. | 154 | When viewing the pool again, you can see that the pool has now doubled |
| 152 | We have 14.3 TB useable space and the same space used for mirroring. | 155 | in size. We have 14.3 TB useable space and the same space used for |
| 156 | mirroring. | ||
| 153 | 157 | ||
| 154 | ```sh | 158 | #+begin_src sh |
| 155 | > zfs list | 159 | > zfs list |
| 156 | 160 | ||
| 157 | NAME USED AVAIL REFER MOUNTPOINT | 161 | NAME USED AVAIL REFER MOUNTPOINT |
| 158 | tank 145G 14.3T 104K /tank | 162 | tank 145G 14.3T 104K /tank |
| 159 | tank/cloud 145G 14.3T 145G /tank/cloud | 163 | tank/cloud 145G 14.3T 145G /tank/cloud |
| 160 | tank/media 96K 14.3T 96K /tank/media | 164 | tank/media 96K 14.3T 96K /tank/media |
| 161 | ``` | 165 | #+end_src |
| 162 | 166 | ||
| 163 | ### Converting Disks | 167 | *** Converting Disks |
| 164 | 168 | ||
| 165 | Some disks, such as NTFS-formatted drives, will need to be partitioned and | 169 | Some disks, such as NTFS-formatted drives, will need to be partitioned |
| 166 | formatted prior to being added to the pool. | 170 | and formatted prior to being added to the pool. |
| 167 | 171 | ||
| 168 | Start by identifying the disks you want to format and add to the pool. | 172 | Start by identifying the disks you want to format and add to the pool. |
| 169 | 173 | ||
| 170 | ```sh | 174 | #+begin_src sh |
| 171 | sudo fdisk -l | grep /dev | 175 | sudo fdisk -l | grep /dev |
| 172 | ``` | 176 | #+end_src |
| 173 | 177 | ||
| 174 | I am going to format my `/dev/sdc` and `/dev/sdd` disks with the `fdisk` | 178 | I am going to format my =/dev/sdc= and =/dev/sdd= disks with the =fdisk= |
| 175 | command. | 179 | command. |
| 176 | 180 | ||
| 177 | See below for instructions on how to use `fdisk`. Here's what I did to create | 181 | See below for instructions on how to use =fdisk=. Here's what I did to |
| 178 | basic Linux formatted disks: | 182 | create basic Linux formatted disks: |
| 179 | 183 | ||
| 180 | - `g` : Create GPT partition table | 184 | - =g= : Create GPT partition table |
| 181 | - `n` : Create a new partition, hit Enter for all default options | 185 | - =n= : Create a new partition, hit Enter for all default options |
| 182 | - `t` : Change partition type to `20` for `Linux filesystem` | 186 | - =t= : Change partition type to =20= for =Linux filesystem= |
| 183 | - `w` : Write the changes to disk and exit | 187 | - =w= : Write the changes to disk and exit |
| 184 | 188 | ||
| 185 | I repeated this process for both disks. | 189 | I repeated this process for both disks. |
| 186 | 190 | ||
| 187 | ```sh | 191 | #+begin_src sh |
| 188 | > sudo fdisk /dev/sdc | 192 | > sudo fdisk /dev/sdc |
| 189 | 193 | ||
| 190 | Welcome to fdisk (util-linux 2.37.2). | 194 | Welcome to fdisk (util-linux 2.37.2). |
| @@ -230,95 +234,100 @@ Help: | |||
| 230 | G create a new empty SGI (IRIX) partition table | 234 | G create a new empty SGI (IRIX) partition table |
| 231 | o create a new empty DOS partition table | 235 | o create a new empty DOS partition table |
| 232 | s create a new empty Sun partition table | 236 | s create a new empty Sun partition table |
| 233 | ``` | 237 | #+end_src |
| 234 | 238 | ||
| 235 | Once the drives are formatted, we can add these disks to the pool. | 239 | Once the drives are formatted, we can add these disks to the pool. |
| 236 | 240 | ||
| 237 | ```sh | 241 | #+begin_src sh |
| 238 | sudo zpool add tank mirror /dev/sdc /dev/sdd | 242 | sudo zpool add tank mirror /dev/sdc /dev/sdd |
| 239 | ``` | 243 | #+end_src |
| 240 | 244 | ||
| 241 | When we list the pool again, we can see that our size is now updated to | 245 | When we list the pool again, we can see that our size is now updated to |
| 242 | approximately 22TB. This represents my hard drives totalling 45.6TB when shown | 246 | approximately 22TB. This represents my hard drives totalling 45.6TB when |
| 243 | with `fdisk -l`, with a Raid10 configuration using 22TB for mirroring and 22TB | 247 | shown with =fdisk -l=, with a Raid10 configuration using 22TB for |
| 244 | of useable space. | 248 | mirroring and 22TB of useable space. |
| 245 | 249 | ||
| 246 | ```sh | 250 | #+begin_src sh |
| 247 | > zfs list | 251 | > zfs list |
| 248 | 252 | ||
| 249 | NAME USED AVAIL REFER MOUNTPOINT | 253 | NAME USED AVAIL REFER MOUNTPOINT |
| 250 | tank 145G 21.7T 104K /tank | 254 | tank 145G 21.7T 104K /tank |
| 251 | tank/cloud 145G 21.7T 145G /tank/cloud | 255 | tank/cloud 145G 21.7T 145G /tank/cloud |
| 252 | tank/media 145GT 21.7T 96K /tank/media | 256 | tank/media 145GT 21.7T 96K /tank/media |
| 253 | ``` | 257 | #+end_src |
| 254 | 258 | ||
| 255 | ## Creating Datasets | 259 | ** Creating Datasets |
| 256 | 260 | ||
| 257 | According to [ZFS | 261 | According to |
| 258 | Terminology](https://docs.oracle.com/cd/E18752_01/html/819-5461/ftyue.html), a | 262 | [[https://docs.oracle.com/cd/E18752_01/html/819-5461/ftyue.html][ZFS |
| 259 | `dataset` can refer to "clones, file systems, snapshots, and volumes. | 263 | Terminology]], a =dataset= can refer to “clones, file systems, |
| 264 | snapshots, and volumes. | ||
| 260 | 265 | ||
| 261 | For this guide, I will use the `dataset` term to refer to file systems created | 266 | For this guide, I will use the =dataset= term to refer to file systems |
| 262 | under a pool. | 267 | created under a pool. |
| 263 | 268 | ||
| 264 | Within my `tank` pool, I am going to create some datasets to help organize my | 269 | Within my =tank= pool, I am going to create some datasets to help |
| 265 | files. This will give me location to store data rather than simply dumping | 270 | organize my files. This will give me location to store data rather than |
| 266 | everything at the `/tank/` location. | 271 | simply dumping everything at the =/tank/= location. |
| 267 | 272 | ||
| 268 | ```sh | 273 | #+begin_src sh |
| 269 | sudo zfs create tank/cloud | 274 | sudo zfs create tank/cloud |
| 270 | sudo zfs create tank/media | 275 | sudo zfs create tank/media |
| 271 | ``` | 276 | #+end_src |
| 272 | 277 | ||
| 273 | Once created, you can see these datasets in the output of your pool list: | 278 | Once created, you can see these datasets in the output of your pool |
| 279 | list: | ||
| 274 | 280 | ||
| 275 | ```sh | 281 | #+begin_src sh |
| 276 | > zfs list | 282 | > zfs list |
| 277 | NAME USED AVAIL REFER MOUNTPOINT | 283 | NAME USED AVAIL REFER MOUNTPOINT |
| 278 | tank 752K 7.14T 104K /tank | 284 | tank 752K 7.14T 104K /tank |
| 279 | tank/cloud 96K 7.14T 96K /tank/cloud | 285 | tank/cloud 96K 7.14T 96K /tank/cloud |
| 280 | tank/media 96K 7.14T 96K /tank/media | 286 | tank/media 96K 7.14T 96K /tank/media |
| 281 | ``` | 287 | #+end_src |
| 282 | 288 | ||
| 283 | ## Creating Snapshots | 289 | ** Creating Snapshots |
| 284 | 290 | ||
| 285 | Next, let's create our first snapshot. We can do this by calling the `snapshot` | 291 | Next, let's create our first snapshot. We can do this by calling the |
| 286 | command and give it an output name. I will be throwing the current date and time | 292 | =snapshot= command and give it an output name. I will be throwing the |
| 287 | into my example. | 293 | current date and time into my example. |
| 288 | 294 | ||
| 289 | ```sh | 295 | #+begin_src sh |
| 290 | sudo zfs snapshot tank@$(date '+%Y-%m-%d_%H-%M') | 296 | sudo zfs snapshot tank@$(date '+%Y-%m-%d_%H-%M') |
| 291 | ``` | 297 | #+end_src |
| 292 | 298 | ||
| 293 | We can list the snapshots in our pool with the following command: | 299 | We can list the snapshots in our pool with the following command: |
| 294 | 300 | ||
| 295 | ```sh | 301 | #+begin_src sh |
| 296 | > zfs list -t snapshot | 302 | > zfs list -t snapshot |
| 297 | NAME USED AVAIL REFER MOUNTPOINT | 303 | NAME USED AVAIL REFER MOUNTPOINT |
| 298 | tank@2024-02-06_19-41 0B - 104K - | 304 | tank@2024-02-06_19-41 0B - 104K - |
| 299 | ``` | 305 | #+end_src |
| 300 | 306 | ||
| 301 | ## Destroy Snapshots | 307 | ** Destroy Snapshots |
| 302 | 308 | ||
| 303 | You can always destroy snapshots that are no longer needed: | 309 | You can always destroy snapshots that are no longer needed: |
| 304 | 310 | ||
| 305 | ```sh | 311 | #+begin_src sh |
| 306 | sudo zfs destroy tank@2024-02-06_19-41 | 312 | sudo zfs destroy tank@2024-02-06_19-41 |
| 307 | ``` | 313 | #+end_src |
| 308 | 314 | ||
| 309 | Once deleted, they will no longer appear in the list: | 315 | Once deleted, they will no longer appear in the list: |
| 310 | 316 | ||
| 311 | ```sh | 317 | #+begin_src sh |
| 312 | > zfs list -t snapshot | 318 | > zfs list -t snapshot |
| 313 | no datasets available | 319 | no datasets available |
| 314 | ``` | 320 | #+end_src |
| 315 | 321 | ||
| 316 | # My Thoughts on ZFS So Far | 322 | * My Thoughts on ZFS So Far |
| 317 | 323 | ||
| 318 | - I sacrificed 25TB to be able to mirror my data, but I feel more comfortable | 324 | - I sacrificed 25TB to be able to mirror my data, but I feel more |
| 319 | with the potential to save my data by quickly replacing a disk if I need to. | 325 | comfortable with the potential to save my data by quickly replacing a |
| 320 | - The set-up was surprisingly easy and fast. | 326 | disk if I need to. |
| 321 | - Disk I/O is fast as well. I was worried that the data transfer speeds would | 327 | - The set-up was surprisingly easy and fast. |
| 322 | be slower due to the RAID configuration. | 328 | - Disk I/O is fast as well. I was worried that the data transfer speeds |
| 323 | - Media streaming and transcoding has seen no noticeable drop in performance. | 329 | would be slower due to the RAID configuration. |
| 324 | - My only limitation really is the number of HDD bays in my server HDD cage. | 330 | - Media streaming and transcoding has seen no noticeable drop in |
| 331 | performance. | ||
| 332 | - My only limitation really is the number of HDD bays in my server HDD | ||
| 333 | cage. | ||
content/blog/2024-02-13-ubuntu-emergency-mode.md deleted −72
| @@ -1,72 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-02-13 | ||
| 3 | title = "Stuck in Ubuntu's Emergency Mode? Try Fixing the Fstab File" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | # The Problem | ||
| 9 | |||
| 10 | I recently [migrated my hard drives to a ZFS pool](../zfs/) and found myself | ||
| 11 | stuck in Ubuntu's emergency mode after the first reboot I performed after | ||
| 12 | creating the ZFS pool. | ||
| 13 | |||
| 14 | My server was stuck in the boot process and showed the following error on the | ||
| 15 | screen: | ||
| 16 | |||
| 17 | ```txt | ||
| 18 | You are in emergency mode. | ||
| 19 | After logging in, type "journalctl -xb" to view system logs, | ||
| 20 | "systemctl reboot" to reboot, "systemctl default" | ||
| 21 | or ^D to try again to boot into default mode". | ||
| 22 | ``` | ||
| 23 | |||
| 24 | After rebooting the server and watching the logs scroll on a monitor, I noticed | ||
| 25 | the root cause was related to a very long search for certain drives. I kept | ||
| 26 | seeing errors like this: | ||
| 27 | |||
| 28 | ```txt | ||
| 29 | [ TIME ] Timed out waiting of device dev-disk-by/[disk-uuid] | ||
| 30 | ``` | ||
| 31 | |||
| 32 | I realized that I had not removed the `/etc/fstab` references that asked Ubuntu | ||
| 33 | to mount two disks on boot, but I had recently changed those disks to be part of | ||
| 34 | my ZFS pool instead. Therefore, Ubuntu was trying to identify and mount a disk | ||
| 35 | that was not available. | ||
| 36 | |||
| 37 | Now that we have an idea of the issue, let's move to solution. | ||
| 38 | |||
| 39 | # The Solution | ||
| 40 | |||
| 41 | In order to fix the issue, I waited until I was allowed to type the root user's | ||
| 42 | password, and then I executed the following command: | ||
| 43 | |||
| 44 | ```sh | ||
| 45 | nano /etc/fstab | ||
| 46 | ``` | ||
| 47 | |||
| 48 | Within the `fstab` file, I needed to comment/remove the following lines at the | ||
| 49 | bottom of the file. You can comment-out a line by prepending a `#` symbol at the | ||
| 50 | beginning of the line. You can also delete the line entirely. | ||
| 51 | |||
| 52 | ```conf | ||
| 53 | # What it looked like when running into the issue: | ||
| 54 | UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 55 | UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 56 | |||
| 57 | # What I changed it to, in order to fix the issue: | ||
| 58 | # UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 59 | # UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 60 | ``` | ||
| 61 | |||
| 62 | Once removing the lines above from the `/etc/fstab` file, save and exit the file | ||
| 63 | by hitting the `Ctrl` + `x` key combo. | ||
| 64 | |||
| 65 | You can now hit `Ctrl` + `D` to continue, or reboot: | ||
| 66 | |||
| 67 | ```sh | ||
| 68 | systemctl reboot | ||
| 69 | ``` | ||
| 70 | |||
| 71 | Once rebooted, I was able to watch the machine boot properly and launch to the | ||
| 72 | TTY login screen without errors! | ||
content/blog/2024-02-13-ubuntu-emergency-mode.org added +71
| @@ -0,0 +1,71 @@ | |||
| 1 | #+date: <2024-02-13> | ||
| 2 | #+title: Stuck in Ubuntu's Emergency Mode? Try Fixing the Fstab File | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | * The Problem | ||
| 7 | |||
| 8 | I recently [[../zfs/][migrated my hard drives to a ZFS pool]] and found | ||
| 9 | myself stuck in Ubuntu's emergency mode after the first reboot I | ||
| 10 | performed after creating the ZFS pool. | ||
| 11 | |||
| 12 | My server was stuck in the boot process and showed the following error | ||
| 13 | on the screen: | ||
| 14 | |||
| 15 | #+begin_src txt | ||
| 16 | You are in emergency mode. | ||
| 17 | After logging in, type "journalctl -xb" to view system logs, | ||
| 18 | "systemctl reboot" to reboot, "systemctl default" | ||
| 19 | or ^D to try again to boot into default mode". | ||
| 20 | #+end_src | ||
| 21 | |||
| 22 | After rebooting the server and watching the logs scroll on a monitor, I | ||
| 23 | noticed the root cause was related to a very long search for certain | ||
| 24 | drives. I kept seeing errors like this: | ||
| 25 | |||
| 26 | #+begin_src txt | ||
| 27 | [ TIME ] Timed out waiting of device dev-disk-by/[disk-uuid] | ||
| 28 | #+end_src | ||
| 29 | |||
| 30 | I realized that I had not removed the =/etc/fstab= references that asked | ||
| 31 | Ubuntu to mount two disks on boot, but I had recently changed those | ||
| 32 | disks to be part of my ZFS pool instead. Therefore, Ubuntu was trying to | ||
| 33 | identify and mount a disk that was not available. | ||
| 34 | |||
| 35 | Now that we have an idea of the issue, let's move to solution. | ||
| 36 | |||
| 37 | * The Solution | ||
| 38 | |||
| 39 | In order to fix the issue, I waited until I was allowed to type the root | ||
| 40 | user's password, and then I executed the following command: | ||
| 41 | |||
| 42 | #+begin_src sh | ||
| 43 | nano /etc/fstab | ||
| 44 | #+end_src | ||
| 45 | |||
| 46 | Within the =fstab= file, I needed to comment/remove the following lines | ||
| 47 | at the bottom of the file. You can comment-out a line by prepending a | ||
| 48 | =#= symbol at the beginning of the line. You can also delete the line | ||
| 49 | entirely. | ||
| 50 | |||
| 51 | #+begin_src conf | ||
| 52 | # What it looked like when running into the issue: | ||
| 53 | UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 54 | UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 55 | |||
| 56 | # What I changed it to, in order to fix the issue: | ||
| 57 | # UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 58 | # UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0 | ||
| 59 | #+end_src | ||
| 60 | |||
| 61 | Once removing the lines above from the =/etc/fstab= file, save and exit | ||
| 62 | the file by hitting the =Ctrl= + =x= key combo. | ||
| 63 | |||
| 64 | You can now hit =Ctrl= + =D= to continue, or reboot: | ||
| 65 | |||
| 66 | #+begin_src sh | ||
| 67 | systemctl reboot | ||
| 68 | #+end_src | ||
| 69 | |||
| 70 | Once rebooted, I was able to watch the machine boot properly and launch | ||
| 71 | to the TTY login screen without errors! | ||
content/blog/2024-02-21-self-hosting-otter-wiki.md → content/blog/2024-02-21-self-hosting-otter-wiki.org renamed +44 −44
| @@ -1,39 +1,39 @@ | |||
| 1 | +++ | 1 | #+date: <2024-02-21> |
| 2 | date = 2024-02-21 | 2 | #+title: Self-Hosting an Otter Wiki |
| 3 | title = "Self-Hosting an Otter Wiki" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | # An Otter Wiki | ||
| 9 | 5 | ||
| 10 | [An Otter Wiki](https://otterwiki.com/) is an easy to use wiki software that | 6 | * An Otter Wiki |
| 11 | takes almost no effort to set up and maintain. | ||
| 12 | 7 | ||
| 13 | # Installation | 8 | [[https://otterwiki.com/][An Otter Wiki]] is an easy to use wiki |
| 9 | software that takes almost no effort to set up and maintain. | ||
| 10 | |||
| 11 | * Installation | ||
| 14 | 12 | ||
| 15 | To install An Otter Wiki, I'm going to use Docker Compose to create the | 13 | To install An Otter Wiki, I'm going to use Docker Compose to create the |
| 16 | container and then use Nginx as a reverse proxy to allow external access via a | 14 | container and then use Nginx as a reverse proxy to allow external access |
| 17 | subdomain I own. | 15 | via a subdomain I own. |
| 18 | 16 | ||
| 19 | ## Docker Compose | 17 | ** Docker Compose |
| 20 | 18 | ||
| 21 | Start by creating a directory for the container's files. | 19 | Start by creating a directory for the container's files. |
| 22 | 20 | ||
| 23 | ```sh | 21 | #+begin_src sh |
| 24 | mkdir ~/otterwiki | 22 | mkdir ~/otterwiki |
| 25 | ``` | 23 | #+end_src |
| 26 | 24 | ||
| 27 | Next, create the `docker-compose.yml` file to define the container's parameters. | 25 | Next, create the =docker-compose.yml= file to define the container's |
| 26 | parameters. | ||
| 28 | 27 | ||
| 29 | ```sh | 28 | #+begin_src sh |
| 30 | nano ~/otterwiki/docker-compose.yml | 29 | nano ~/otterwiki/docker-compose.yml |
| 31 | ``` | 30 | #+end_src |
| 32 | 31 | ||
| 33 | Within the file, paste the following content. You can read the project's | 32 | Within the file, paste the following content. You can read the project's |
| 34 | documentation if you want to further override or customize the container. | 33 | documentation if you want to further override or customize the |
| 34 | container. | ||
| 35 | 35 | ||
| 36 | ```conf | 36 | #+begin_src conf |
| 37 | version: '3' | 37 | version: '3' |
| 38 | services: | 38 | services: |
| 39 | otterwiki: | 39 | otterwiki: |
| @@ -45,33 +45,33 @@ services: | |||
| 45 | - 8337:80 | 45 | - 8337:80 |
| 46 | volumes: | 46 | volumes: |
| 47 | - ./app-data:/app-data | 47 | - ./app-data:/app-data |
| 48 | ``` | 48 | #+end_src |
| 49 | 49 | ||
| 50 | Once the file is saved and closed, you can launch the container. | 50 | Once the file is saved and closed, you can launch the container. |
| 51 | 51 | ||
| 52 | ```sh | 52 | #+begin_src sh |
| 53 | cd ~/otterwiki | 53 | cd ~/otterwiki |
| 54 | sudo docker-compose up -d | 54 | sudo docker-compose up -d |
| 55 | ``` | 55 | #+end_src |
| 56 | 56 | ||
| 57 | The container is now available at `localhost:8337`. Next, we will use Nginx to | 57 | The container is now available at =localhost:8337=. Next, we will use |
| 58 | serve this app externally. | 58 | Nginx to serve this app externally. |
| 59 | 59 | ||
| 60 | ## Nginx | 60 | ** Nginx |
| 61 | 61 | ||
| 62 | To access the app externally, let's set up a reverse proxy. I'll start by | 62 | To access the app externally, let's set up a reverse proxy. I'll start |
| 63 | creating the Nginx configuration file for my wiki. | 63 | by creating the Nginx configuration file for my wiki. |
| 64 | 64 | ||
| 65 | ```sh | 65 | #+begin_src sh |
| 66 | sudo nano /etc/nginx/sites-available/wiki | 66 | sudo nano /etc/nginx/sites-available/wiki |
| 67 | ``` | 67 | #+end_src |
| 68 | 68 | ||
| 69 | Within the file, I have saved the following content. This assumes you already | 69 | Within the file, I have saved the following content. This assumes you |
| 70 | have a TLS/SSL cert to use with this subdomain. If not, simply remove the | 70 | already have a TLS/SSL cert to use with this subdomain. If not, simply |
| 71 | `ssl_*` variables, remove the `80` server block, and change the `443` server | 71 | remove the =ssl_*= variables, remove the =80= server block, and change |
| 72 | block to `80` to serve the app without SSL. | 72 | the =443= server block to =80= to serve the app without SSL. |
| 73 | 73 | ||
| 74 | ```conf | 74 | #+begin_src conf |
| 75 | server { | 75 | server { |
| 76 | if ($host ~ ^[^.]+\.example\.com$) { | 76 | if ($host ~ ^[^.]+\.example\.com$) { |
| 77 | return 301 https://$host$request_uri; | 77 | return 301 https://$host$request_uri; |
| @@ -105,18 +105,18 @@ server { | |||
| 105 | proxy_set_header X-Forwarded-Host $http_host; | 105 | proxy_set_header X-Forwarded-Host $http_host; |
| 106 | } | 106 | } |
| 107 | } | 107 | } |
| 108 | ``` | 108 | #+end_src |
| 109 | 109 | ||
| 110 | Save and close the configuration file. On Nginx, we need to symlink the file to | 110 | Save and close the configuration file. On Nginx, we need to symlink the |
| 111 | enable it. | 111 | file to enable it. |
| 112 | 112 | ||
| 113 | ```sh | 113 | #+begin_src sh |
| 114 | sudo ln -s /etc/nginx/sites-available/wiki /etc/nginx/sites-enabled/wiki | 114 | sudo ln -s /etc/nginx/sites-available/wiki /etc/nginx/sites-enabled/wiki |
| 115 | ``` | 115 | #+end_src |
| 116 | 116 | ||
| 117 | Once enabled, restart the Nginx server to start serving the app externally. | 117 | Once enabled, restart the Nginx server to start serving the app |
| 118 | externally. | ||
| 118 | 119 | ||
| 119 | ```sh | 120 | #+begin_src sh |
| 120 | sudo systemctl restart nginx.service | 121 | sudo systemctl restart nginx.service |
| 121 | ``` | 122 | #+end_src |
| 122 | |||
content/blog/2024-03-13-doom-emacs.md deleted −343
| @@ -1,343 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-03-13 | ||
| 3 | title = "Doom Emacs & Org-Mode" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | ## Screenshots | ||
| 9 | |||
| 10 | These screenshots are showing a project opened with projectile, a treemacs side | ||
| 11 | pane open with the project contents, multiple buffers tiled next to each other, | ||
| 12 | and the help pane open at the bottomm. | ||
| 13 | |||
| 14 | The themes are `doom-homage-white` and `doom-homage-black`. | ||
| 15 | |||
| 16 |  | ||
| 18 | |||
| 19 |  | ||
| 21 | |||
| 22 | ## Getting Started | ||
| 23 | |||
| 24 | I have been switching back and forth between | ||
| 25 | [markdown](https://en.wikipedia.org/wiki/Markdown) and | ||
| 26 | [org-mode](https://en.wikipedia.org/wiki/Org-mode) recently for my personal note | ||
| 27 | taking, wiki, and even this blog. As a result, I have been stumbling further | ||
| 28 | into the world of Emacs and found myself at a point where I now prefer to do | ||
| 29 | most of my basic editing within Emacs. | ||
| 30 | |||
| 31 | I'll leave the markdown vs. org-mode debate for another post, but I love | ||
| 32 | org-mode's extensibility and interactive nature within Emacs, but it becomes | ||
| 33 | very unwieldy in any other client implementation of org-mode - especially on | ||
| 34 | iOS. On the flip side, markdown is limited in functionality and fractured into | ||
| 35 | different standards, but it's simple and popular enough that there are a | ||
| 36 | plethora of great clients to choose from that will get the job done. | ||
| 37 | |||
| 38 | For now, I want to focus on how I have been using Emacs and some of the things | ||
| 39 | that would have helped me learn it faster had I known where to start. | ||
| 40 | |||
| 41 | ### Installation | ||
| 42 | |||
| 43 | This post focuses on [Doom Emacs](https://github.com/doomemacs/doomemacs), which | ||
| 44 | is an Emacs framework that provides an alternative experience to the vanilla | ||
| 45 | [GNU Emacs](https://www.gnu.org/software/emacs/). | ||
| 46 | |||
| 47 | The [Getting Start | ||
| 48 | Guide](https://github.com/doomemacs/doomemacs/blob/master/docs/getting_started.org) | ||
| 49 | has an extremely detailed walkthrough of installation for all systems, so please | ||
| 50 | refer to that guide for up-to-date instructions. | ||
| 51 | |||
| 52 | I chose to install on macOS, using the Homebrew option with the | ||
| 53 | `railwaycat/emacsmacport` version of Emacs. | ||
| 54 | |||
| 55 | Once the program is installed, you can run the program by typing `emacs` in a | ||
| 56 | terminal. If you installed a version of Emacs that supports both a GUI and TUI, | ||
| 57 | you will have to run `emacs -nw` to get the TUI instead of the default GUI. | ||
| 58 | |||
| 59 | ### Configuration | ||
| 60 | |||
| 61 | Once installed, you can configure Doom by editing the files within the | ||
| 62 | `~/.doom.d/` directory. This directory holds four files: | ||
| 63 | |||
| 64 | 1. `config.el` - Personal configuration file | ||
| 65 | 2. `custom.el` - Custom set variables | ||
| 66 | 3. `init.el` - Doom modules and load order, must run `doom sync` after | ||
| 67 | modifying | ||
| 68 | 4. `packages.el` - Declare packages to install in this file, then run `doom | ||
| 69 | sync` to install | ||
| 70 | |||
| 71 | I only needed a few customizations for my configuration, so I'll list them | ||
| 72 | below. | ||
| 73 | |||
| 74 | ```lisp | ||
| 75 | ;; ~/.doom.d/config.el | ||
| 76 | (setq doom-theme 'doom-homage-black) | ||
| 77 | (setq display-line-numbers-type t) | ||
| 78 | (setq org-directory "~/Documents/Notes/") | ||
| 79 | |||
| 80 | ;; lengthy org-publish directives at the bottom of the file | ||
| 81 | ``` | ||
| 82 | |||
| 83 | ```lisp | ||
| 84 | ;; ~/.doom.d/init.el | ||
| 85 | (doom! :input | ||
| 86 | :completion | ||
| 87 | company ; the ultimate code completion backend | ||
| 88 | vertico ; the search engine of the future | ||
| 89 | |||
| 90 | :ui | ||
| 91 | doom ; what makes DOOM look the way it does | ||
| 92 | doom-dashboard ; a nifty splash screen for Emacs | ||
| 93 | (emoji +unicode) ; 🙂 | ||
| 94 | hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW | ||
| 95 | minimap ; show a map of the code on the side | ||
| 96 | modeline ; snazzy, Atom-inspired modeline, plus API | ||
| 97 | ophints ; highlight the region an operation acts on | ||
| 98 | (popup +defaults) ; tame sudden yet inevitable temporary windows | ||
| 99 | tabs ; a tab bar for Emacs | ||
| 100 | treemacs ; a project drawer, like neotree but cooler | ||
| 101 | (vc-gutter +pretty) ; vcs diff in the fringe | ||
| 102 | vi-tilde-fringe ; fringe tildes to mark beyond EOB | ||
| 103 | workspaces ; tab emulation, persistence & separate workspaces | ||
| 104 | |||
| 105 | :editor | ||
| 106 | (evil +everywhere); come to the dark side, we have cookies | ||
| 107 | file-templates ; auto-snippets for empty files | ||
| 108 | fold ; (nigh) universal code folding | ||
| 109 | snippets ; my elves. They type so I don't have to | ||
| 110 | |||
| 111 | :emacs | ||
| 112 | dired ; making dired pretty [functional] | ||
| 113 | electric ; smarter, keyword-based electric-indent | ||
| 114 | undo ; persistent, smarter undo for your inevitable mistakes | ||
| 115 | vc ; version-control and Emacs, sitting in a tree | ||
| 116 | |||
| 117 | :term | ||
| 118 | term ; basic terminal emulator for Emacs | ||
| 119 | |||
| 120 | :checkers | ||
| 121 | syntax ; tasing you for every semicolon you forget | ||
| 122 | |||
| 123 | :tools | ||
| 124 | (eval +overlay) ; run code, run (also, repls) | ||
| 125 | lookup ; navigate your code and its documentation | ||
| 126 | magit ; a git porcelain for Emacs | ||
| 127 | |||
| 128 | :os | ||
| 129 | (:if (featurep :system 'macos) macos) ; improve compatibility with macOS | ||
| 130 | |||
| 131 | :lang | ||
| 132 | common-lisp ; if you've seen one lisp, you've seen them all | ||
| 133 | emacs-lisp ; drown in parentheses | ||
| 134 | markdown ; writing docs for people to ignore | ||
| 135 | org ; organize your plain life in plain text | ||
| 136 | python ; beautiful is better than ugly | ||
| 137 | sh ; she sells {ba,z,fi}sh shells on the C xor | ||
| 138 | |||
| 139 | :app | ||
| 140 | irc ; how neckbeards socialize | ||
| 141 | (rss +org) ; emacs as an RSS reader | ||
| 142 | |||
| 143 | (default +bindings +smartparens)) | ||
| 144 | ``` | ||
| 145 | |||
| 146 | If you're editing these files within Doom directly, remember to run `SPC h r r` | ||
| 147 | to reload the configuration. Also remember to run `doom sync` for any changes to | ||
| 148 | the `init.el` or `packages.el` files. | ||
| 149 | |||
| 150 | ## Basic Functionality | ||
| 151 | |||
| 152 | I kept a cheat sheet note open at first with all of the basic functions typed | ||
| 153 | out, copied as I went through the tutorial. After a little while, I no longer | ||
| 154 | needed it. I highly recommend writing down the most applicable shortcuts for | ||
| 155 | your preferred functionality and refer back to it until you've memorized it. | ||
| 156 | |||
| 157 | Memorizing the shortcuts will differ based on the type of Emacs framework being | ||
| 158 | used. Personally, migrating from vanilla Emacs to Doom Emacs simplified | ||
| 159 | everything by a large factor and instantly enabled me to start working on my | ||
| 160 | projects, eliminating most of the hurdles I was running into. The vanilla emacs | ||
| 161 | hotkeys became obnoxious and I actually stopped using Emacs entirely for about a | ||
| 162 | month before trying Doom. | ||
| 163 | |||
| 164 | For me, the first logical step is to interact with the local filesystem. To do | ||
| 165 | this, I needed to know how to open directories, open files, save files, discard | ||
| 166 | changes, close files, and switch between open files. Here are some example | ||
| 167 | shortcuts I've written down in order to accomplish file-based actions. | ||
| 168 | |||
| 169 | | Doom Hotkey | Emacs Hotkey | Description | | ||
| 170 | | --------------- | ------------ | -------------------------------------- | | ||
| 171 | | `SPC :` | `C-x` | Run functions | | ||
| 172 | | `SPC f f` | `C-x f` | Open file in buffer | | ||
| 173 | | `SPC f d` | `C-x d` | Open directory with `dired` | | ||
| 174 | | `i` | `C-x C-q` | Edit current buffer (insert mode) | | ||
| 175 | | `q` | `C-x C-q` | Quit out of insert mode | | ||
| 176 | | `SPC f s` | `C-x s` | Save current buffer | | ||
| 177 | | `SPC b k` | `C-x k` | Kill current buffer | | ||
| 178 | | `SPC w h/j/k/l` | `C-x o`[^1] | Move left/down/up/right to next buffer | | ||
| 179 | |||
| 180 | [^1]: Doom's evil-window functionality is a bit different from GNU Emacs, but | ||
| 181 | you can always switch to the "other" buffer with `C-x o` or `C-x b` to get a | ||
| 182 | list of buffers to select. | ||
| 183 | |||
| 184 | In general, when in Doom, you can press `SPC` and wait a second for the help | ||
| 185 | pane to appear with all available hotkey options. For example, you can press | ||
| 186 | `SPC`, wait for the help pane, and then select a key such as `g` to enter the | ||
| 187 | git help pane and explore further command options. | ||
| 188 | |||
| 189 | ## Editing | ||
| 190 | |||
| 191 | Next in my process is to dive into editing for any languages I'm currently | ||
| 192 | using. In this post, I will just cover Markdown and Org-Mode but I have also | ||
| 193 | been slowly adoping some Python and general web dev tools as well. | ||
| 194 | |||
| 195 | ### Markdown | ||
| 196 | |||
| 197 |  | ||
| 199 | |||
| 200 | Markdown is fairly simple as the syntax is limited, so just make sure the | ||
| 201 | `~/.doom.d/init.el` includes the `markdown` declaration in the `:lang` section. | ||
| 202 | |||
| 203 | This package includes the following hotkey menus. The insert and toggle menu | ||
| 204 | expands further, allowing you to insert various markdown elements and toggle | ||
| 205 | things like link hiding. | ||
| 206 | |||
| 207 | | Doom Hotkey | Function | | ||
| 208 | | ---------------------------- | ------------------------ | | ||
| 209 | | `SPC m '` | markdown-edit-code-block | | ||
| 210 | | `SPC m e` | markdown-export | | ||
| 211 | | `SPC m i` | +insert | | ||
| 212 | | `SPC m o` | markdown-open | | ||
| 213 | | `SPC m p` | markdown-preview | | ||
| 214 | | `SPC m t` | +toggle | | ||
| 215 | | `SPC : markdown-table-align` | markdown-table-align | | ||
| 216 | |||
| 217 | ### Org-Mode | ||
| 218 | |||
| 219 |  | ||
| 220 | |||
| 221 | Similar to the markdown section above, ensure that the `~/.doom.d/init.el` | ||
| 222 | includes the `org` declaration in the `:lang` section. | ||
| 223 | |||
| 224 | There are a few hot keys, but a quick search with `SPC : org` shows that there | ||
| 225 | are 865 possible org-related functions you can run. I won't possibly be able to | ||
| 226 | list them all, so I will simply cover a few of the basic commands I use myself. | ||
| 227 | |||
| 228 | | Doom Hotkey | Function | | ||
| 229 | | -------------- | ------------------------------------- | | ||
| 230 | | `SPC m t` | org-todo | | ||
| 231 | | `SPC n t` | org-todo-list | | ||
| 232 | | `SPC o A` | org-agenda | | ||
| 233 | | `SPC X` | org-capture | | ||
| 234 | | `SPC m p p` | org-priority | | ||
| 235 | | `SPC m d s` | org-schedule | | ||
| 236 | | `TAB` | org-cycle | | ||
| 237 | | `SHIFT TAB` | Collapse/open all headings in buffer | | ||
| 238 | | `M-q` | Format/wrap current section | | ||
| 239 | | `M-Left/Right` | Demote/promote current heading | | ||
| 240 | | `M-Down/Up` | Shift current heading section down/up | | ||
| 241 | |||
| 242 | 1. Org-Publish | ||
| 243 | |||
| 244 | Org includes a [publishing management | ||
| 245 | system](https://orgmode.org/manual/Publishing.html) by default that allows | ||
| 246 | you to export org files to Org, iCalendar, HTML, LaTex, Markdown, ODT, and | ||
| 247 | Plain Text. Most of these can be exported into another buffer and opened, or | ||
| 248 | simply to an external file. | ||
| 249 | |||
| 250 | While inside an org file, simply run `SPC m e` or `M-x org-export-dispatch` | ||
| 251 | to open the export menu. This menu will show all options and ask you to | ||
| 252 | select an option. If you want to export to HTML, simply press `h` and then | ||
| 253 | `H` (As HTML buffer), `h` (As HTML file), or `o` (As HTML file and open). | ||
| 254 | |||
| 255 | 2. Projects | ||
| 256 | |||
| 257 | Some publishing options are easier with a defined project in Emacs. To | ||
| 258 | create a project within Emacs, I use two methods: | ||
| 259 | |||
| 260 | 1. Add the project via the projectile command `SPC p a`. Does not always | ||
| 261 | work for me. | ||
| 262 | 2. Add an empty `.projectile` file in the project root. | ||
| 263 | |||
| 264 | Once a project has been created, you can create custom publishing actions | ||
| 265 | within your `~/.doom.d/config.el` file. For example, here's a test project I | ||
| 266 | created to try and convert this blog to org-mode recently. | ||
| 267 | |||
| 268 | ```lisp | ||
| 269 | ;; org-publish | ||
| 270 | (require 'ox-publish) | ||
| 271 | |||
| 272 | (defun my/org-sitemap-date-entry-format (entry style project) "Format ENTRY in | ||
| 273 | org-publish PROJECT Sitemap format ENTRY ENTRY STYLE format that includes | ||
| 274 | date." (let ((filename (org-publish-find-title entry project))) (if (= (length | ||
| 275 | filename) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}} | ||
| 276 | [[file:%s][%s]]" (format-time-string "%Y-%m-%d" (org-publish-find-date entry | ||
| 277 | project)) entry filename)))) | ||
| 278 | |||
| 279 | (setq org-export-global-macros '(("timestamp" . "@@html:<time datetime='[$1]' | ||
| 280 | class='timestamp'>[$1]</time>@@"))) | ||
| 281 | |||
| 282 | (setq org-publish-project-alist | ||
| 283 | `(("blog" | ||
| 284 | :base-directory "~/Source/cleberg.net/" | ||
| 285 | :base-extension "org" | ||
| 286 | :recursive t | ||
| 287 | :publishing-directory "~/Source/cleberg.net/public/" | ||
| 288 | :publishing-function org-html-publish-to-html | ||
| 289 | ;; HTML5 | ||
| 290 | :html-doctype "html5" | ||
| 291 | :html-html5-fancy t | ||
| 292 | ;; Disable some Org's HTML defaults | ||
| 293 | :html-head-include-scripts nil | ||
| 294 | :html-head-include-default-style nil | ||
| 295 | :section-numbers nil | ||
| 296 | :with-title nil | ||
| 297 | ;; Sitemap | ||
| 298 | :auto-sitemap t | ||
| 299 | :sitemap-title: "Sitemap" | ||
| 300 | :sitemap-sort-files anti-chronologically | ||
| 301 | ; :sitemap-function my/org-sitemap-date-entry-format | ||
| 302 | ;; Customize HTML output | ||
| 303 | :html-divs ((preamble "header" "preamble") | ||
| 304 | (content "main" "content") | ||
| 305 | (postamble "footer" "postamble")) | ||
| 306 | :html-head "<meta name='theme-color' content='#111' media='(prefers-color-scheme: dark)'> | ||
| 307 | <meta name='theme-color' content='#fff' media='(prefers-color-scheme: light)'> | ||
| 308 | <link rel='stylesheet' href='/syntax-theme-dark.css' media='(prefers-color-scheme: dark)'> | ||
| 309 | <link rel='stylesheet' href='/syntax-theme-light.css' media='(prefers-color-scheme: light)'> | ||
| 310 | <link rel='stylesheet' href='/styles.css' type='text/css'>" | ||
| 311 | :html-preamble "<nav class='site-nav' aria-label='site-nav' role='navigation'> | ||
| 312 | <ul> | ||
| 313 | <li><a href='/'>Home</a></li> | ||
| 314 | <li><a href='/blog/'>Blog</a></li> | ||
| 315 | <li><a href='/services/'>Services</a></li> | ||
| 316 | <li><a href='/wiki/'>Wiki</a></li> | ||
| 317 | </ul></nav> | ||
| 318 | <h1>%t</h1> | ||
| 319 | <time datetime='%d'>%d</time>" | ||
| 320 | :html-postamble " | ||
| 321 | <p>Last build: %T</p> | ||
| 322 | <p>Created with %c</p>" | ||
| 323 | ) | ||
| 324 | |||
| 325 | ("static" | ||
| 326 | :base-directory "~/Source/cleberg.net/static/" | ||
| 327 | :base-extension "css\\|txt\\|jpg\\|gif\\|png" | ||
| 328 | :recursive t | ||
| 329 | :publishing-directory "~/Source/cleberg.net/public/" | ||
| 330 | :publishing-function org-publish-attachment) | ||
| 331 | |||
| 332 | ("cleberg.net" :components ("blog" "static")))) | ||
| 333 | ``` | ||
| 334 | |||
| 335 | ## General Thoughts | ||
| 336 | |||
| 337 | I have enjoyed Doom Emacs (far more than GNU Emacs) and will likely continue to | ||
| 338 | use it as my main editor for the time being. Org-Mode is certainly the largest | ||
| 339 | factor here, as I far prefer it over Markdown due to its inherent features and | ||
| 340 | detailed markup options. However, working with org-mode on iOS has been a pain | ||
| 341 | and I will have to see if there's an easier way to resolve those issues or if | ||
| 342 | going back to separate Markdown, Reminders, and Calendar apps is easier to work | ||
| 343 | with than an all-in-one org solution. | ||
content/blog/2024-03-13-doom-emacs.org added +360
| @@ -0,0 +1,360 @@ | |||
| 1 | #+date: <2024-03-13> | ||
| 2 | #+title: Doom Emacs & Org-Mode | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | ** Screenshots | ||
| 7 | |||
| 8 | These screenshots are showing a project opened with projectile, a | ||
| 9 | treemacs side pane open with the project contents, multiple buffers | ||
| 10 | tiled next to each other, and the help pane open at the bottomm. | ||
| 11 | |||
| 12 | The themes are =doom-homage-white= and =doom-homage-black=. | ||
| 13 | |||
| 14 | #+caption: Doom Emacs Light Mode | ||
| 15 | [[https://img.cleberg.net/blog/20240314-doom-emacs/light.png]] | ||
| 16 | |||
| 17 | #+caption: Doom Emacs Dark Mode | ||
| 18 | [[https://img.cleberg.net/blog/20240314-doom-emacs/dark.png]] | ||
| 19 | |||
| 20 | ** Getting Started | ||
| 21 | |||
| 22 | I have been switching back and forth between | ||
| 23 | [[https://en.wikipedia.org/wiki/Markdown][markdown]] and | ||
| 24 | [[https://en.wikipedia.org/wiki/Org-mode][org-mode]] recently for my | ||
| 25 | personal note taking, wiki, and even this blog. As a result, I have been | ||
| 26 | stumbling further into the world of Emacs and found myself at a point | ||
| 27 | where I now prefer to do most of my basic editing within Emacs. | ||
| 28 | |||
| 29 | I'll leave the markdown vs. org-mode debate for another post, but I love | ||
| 30 | org-mode's extensibility and interactive nature within Emacs, but it | ||
| 31 | becomes very unwieldy in any other client implementation of org-mode - | ||
| 32 | especially on iOS. On the flip side, markdown is limited in | ||
| 33 | functionality and fractured into different standards, but it's simple | ||
| 34 | and popular enough that there are a plethora of great clients to choose | ||
| 35 | from that will get the job done. | ||
| 36 | |||
| 37 | For now, I want to focus on how I have been using Emacs and some of the | ||
| 38 | things that would have helped me learn it faster had I known where to | ||
| 39 | start. | ||
| 40 | |||
| 41 | *** Installation | ||
| 42 | |||
| 43 | This post focuses on [[https://github.com/doomemacs/doomemacs][Doom | ||
| 44 | Emacs]], which is an Emacs framework that provides an alternative | ||
| 45 | experience to the vanilla [[https://www.gnu.org/software/emacs/][GNU | ||
| 46 | Emacs]]. | ||
| 47 | |||
| 48 | The | ||
| 49 | [[https://github.com/doomemacs/doomemacs/blob/master/docs/getting_started.org][Getting | ||
| 50 | Start Guide]] has an extremely detailed walkthrough of installation for | ||
| 51 | all systems, so please refer to that guide for up-to-date instructions. | ||
| 52 | |||
| 53 | I chose to install on macOS, using the Homebrew option with the | ||
| 54 | =railwaycat/emacsmacport= version of Emacs. | ||
| 55 | |||
| 56 | Once the program is installed, you can run the program by typing =emacs= | ||
| 57 | in a terminal. If you installed a version of Emacs that supports both a | ||
| 58 | GUI and TUI, you will have to run =emacs -nw= to get the TUI instead of | ||
| 59 | the default GUI. | ||
| 60 | |||
| 61 | *** Configuration | ||
| 62 | |||
| 63 | Once installed, you can configure Doom by editing the files within the | ||
| 64 | =~/.doom.d/= directory. This directory holds four files: | ||
| 65 | |||
| 66 | 1. =config.el= - Personal configuration file | ||
| 67 | 2. =custom.el= - Custom set variables | ||
| 68 | 3. =init.el= - Doom modules and load order, must run =doom sync= after | ||
| 69 | modifying | ||
| 70 | 4. =packages.el= - Declare packages to install in this file, then run | ||
| 71 | =doom sync= to install | ||
| 72 | |||
| 73 | I only needed a few customizations for my configuration, so I'll list | ||
| 74 | them below. | ||
| 75 | |||
| 76 | #+begin_src lisp | ||
| 77 | ;; ~/.doom.d/config.el | ||
| 78 | (setq doom-theme 'doom-homage-black) | ||
| 79 | (setq display-line-numbers-type t) | ||
| 80 | (setq org-directory "~/Documents/Notes/") | ||
| 81 | |||
| 82 | ;; lengthy org-publish directives at the bottom of the file | ||
| 83 | #+end_src | ||
| 84 | |||
| 85 | #+begin_src lisp | ||
| 86 | ;; ~/.doom.d/init.el | ||
| 87 | (doom! :input | ||
| 88 | :completion | ||
| 89 | company ; the ultimate code completion backend | ||
| 90 | vertico ; the search engine of the future | ||
| 91 | |||
| 92 | :ui | ||
| 93 | doom ; what makes DOOM look the way it does | ||
| 94 | doom-dashboard ; a nifty splash screen for Emacs | ||
| 95 | (emoji +unicode) ; 🙂 | ||
| 96 | hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW | ||
| 97 | minimap ; show a map of the code on the side | ||
| 98 | modeline ; snazzy, Atom-inspired modeline, plus API | ||
| 99 | ophints ; highlight the region an operation acts on | ||
| 100 | (popup +defaults) ; tame sudden yet inevitable temporary windows | ||
| 101 | tabs ; a tab bar for Emacs | ||
| 102 | treemacs ; a project drawer, like neotree but cooler | ||
| 103 | (vc-gutter +pretty) ; vcs diff in the fringe | ||
| 104 | vi-tilde-fringe ; fringe tildes to mark beyond EOB | ||
| 105 | workspaces ; tab emulation, persistence & separate workspaces | ||
| 106 | |||
| 107 | :editor | ||
| 108 | (evil +everywhere); come to the dark side, we have cookies | ||
| 109 | file-templates ; auto-snippets for empty files | ||
| 110 | fold ; (nigh) universal code folding | ||
| 111 | snippets ; my elves. They type so I don't have to | ||
| 112 | |||
| 113 | :emacs | ||
| 114 | dired ; making dired pretty [functional] | ||
| 115 | electric ; smarter, keyword-based electric-indent | ||
| 116 | undo ; persistent, smarter undo for your inevitable mistakes | ||
| 117 | vc ; version-control and Emacs, sitting in a tree | ||
| 118 | |||
| 119 | :term | ||
| 120 | term ; basic terminal emulator for Emacs | ||
| 121 | |||
| 122 | :checkers | ||
| 123 | syntax ; tasing you for every semicolon you forget | ||
| 124 | |||
| 125 | :tools | ||
| 126 | (eval +overlay) ; run code, run (also, repls) | ||
| 127 | lookup ; navigate your code and its documentation | ||
| 128 | magit ; a git porcelain for Emacs | ||
| 129 | |||
| 130 | :os | ||
| 131 | (:if (featurep :system 'macos) macos) ; improve compatibility with macOS | ||
| 132 | |||
| 133 | :lang | ||
| 134 | common-lisp ; if you've seen one lisp, you've seen them all | ||
| 135 | emacs-lisp ; drown in parentheses | ||
| 136 | markdown ; writing docs for people to ignore | ||
| 137 | org ; organize your plain life in plain text | ||
| 138 | python ; beautiful is better than ugly | ||
| 139 | sh ; she sells {ba,z,fi}sh shells on the C xor | ||
| 140 | |||
| 141 | :app | ||
| 142 | irc ; how neckbeards socialize | ||
| 143 | (rss +org) ; emacs as an RSS reader | ||
| 144 | |||
| 145 | (default +bindings +smartparens)) | ||
| 146 | #+end_src | ||
| 147 | |||
| 148 | If you're editing these files within Doom directly, remember to run | ||
| 149 | =SPC h r r= to reload the configuration. Also remember to run | ||
| 150 | =doom sync= for any changes to the =init.el= or =packages.el= files. | ||
| 151 | |||
| 152 | ** Basic Functionality | ||
| 153 | |||
| 154 | I kept a cheat sheet note open at first with all of the basic functions | ||
| 155 | typed out, copied as I went through the tutorial. After a little while, | ||
| 156 | I no longer needed it. I highly recommend writing down the most | ||
| 157 | applicable shortcuts for your preferred functionality and refer back to | ||
| 158 | it until you've memorized it. | ||
| 159 | |||
| 160 | Memorizing the shortcuts will differ based on the type of Emacs | ||
| 161 | framework being used. Personally, migrating from vanilla Emacs to Doom | ||
| 162 | Emacs simplified everything by a large factor and instantly enabled me | ||
| 163 | to start working on my projects, eliminating most of the hurdles I was | ||
| 164 | running into. The vanilla emacs hotkeys became obnoxious and I actually | ||
| 165 | stopped using Emacs entirely for about a month before trying Doom. | ||
| 166 | |||
| 167 | For me, the first logical step is to interact with the local filesystem. | ||
| 168 | To do this, I needed to know how to open directories, open files, save | ||
| 169 | files, discard changes, close files, and switch between open files. Here | ||
| 170 | are some example shortcuts I've written down in order to accomplish | ||
| 171 | file-based actions. | ||
| 172 | |||
| 173 | | Doom Hotkey | Emacs Hotkey | Description | | ||
| 174 | |-----------------+---------------+----------------------------------------| | ||
| 175 | | =SPC := | =C-x= | Run functions | | ||
| 176 | | =SPC f f= | =C-x f= | Open file in buffer | | ||
| 177 | | =SPC f d= | =C-x d= | Open directory with =dired= | | ||
| 178 | | =i= | =C-x C-q= | Edit current buffer (insert mode) | | ||
| 179 | | =q= | =C-x C-q= | Quit out of insert mode | | ||
| 180 | | =SPC f s= | =C-x s= | Save current buffer | | ||
| 181 | | =SPC b k= | =C-x k= | Kill current buffer | | ||
| 182 | | =SPC w h/j/k/l= | =C-x o=[fn:2] | Move left/down/up/right to next buffer | | ||
| 183 | |||
| 184 | In general, when in Doom, you can press =SPC= and wait a second for the | ||
| 185 | help pane to appear with all available hotkey options. For example, you | ||
| 186 | can press =SPC=, wait for the help pane, and then select a key such as | ||
| 187 | =g= to enter the git help pane and explore further command options. | ||
| 188 | |||
| 189 | ** Editing | ||
| 190 | |||
| 191 | Next in my process is to dive into editing for any languages I'm | ||
| 192 | currently using. In this post, I will just cover Markdown and Org-Mode | ||
| 193 | but I have also been slowly adoping some Python and general web dev | ||
| 194 | tools as well. | ||
| 195 | |||
| 196 | *** Markdown | ||
| 197 | |||
| 198 | #+caption: Markdown Preview | ||
| 199 | [[https://img.cleberg.net/blog/20240314-doom-emacs/markdown.png]] | ||
| 200 | |||
| 201 | Markdown is fairly simple as the syntax is limited, so just make sure | ||
| 202 | the =~/.doom.d/init.el= includes the =markdown= declaration in the | ||
| 203 | =:lang= section. | ||
| 204 | |||
| 205 | This package includes the following hotkey menus. The insert and toggle | ||
| 206 | menu expands further, allowing you to insert various markdown elements | ||
| 207 | and toggle things like link hiding. | ||
| 208 | |||
| 209 | | Doom Hotkey | Function | | ||
| 210 | |------------------------------+--------------------------| | ||
| 211 | | =SPC m '= | markdown-edit-code-block | | ||
| 212 | | =SPC m e= | markdown-export | | ||
| 213 | | =SPC m i= | +insert | | ||
| 214 | | =SPC m o= | markdown-open | | ||
| 215 | | =SPC m p= | markdown-preview | | ||
| 216 | | =SPC m t= | +toggle | | ||
| 217 | | =SPC : markdown-table-align= | markdown-table-align | | ||
| 218 | |||
| 219 | *** Org-Mode | ||
| 220 | |||
| 221 | #+caption: Org-Mode Preview | ||
| 222 | [[https://img.cleberg.net/blog/20240314-doom-emacs/org.png]] | ||
| 223 | |||
| 224 | Similar to the markdown section above, ensure that the | ||
| 225 | =~/.doom.d/init.el= includes the =org= declaration in the =:lang= | ||
| 226 | section. | ||
| 227 | |||
| 228 | There are a few hot keys, but a quick search with =SPC : org= shows that | ||
| 229 | there are 865 possible org-related functions you can run. I won't | ||
| 230 | possibly be able to list them all, so I will simply cover a few of the | ||
| 231 | basic commands I use myself. | ||
| 232 | |||
| 233 | | Doom Hotkey | Function | | ||
| 234 | |----------------+---------------------------------------| | ||
| 235 | | =SPC m t= | org-todo | | ||
| 236 | | =SPC n t= | org-todo-list | | ||
| 237 | | =SPC o A= | org-agenda | | ||
| 238 | | =SPC X= | org-capture | | ||
| 239 | | =SPC m p p= | org-priority | | ||
| 240 | | =SPC m d s= | org-schedule | | ||
| 241 | | =TAB= | org-cycle | | ||
| 242 | | =SHIFT TAB= | Collapse/open all headings in buffer | | ||
| 243 | | =M-q= | Format/wrap current section | | ||
| 244 | | =M-Left/Right= | Demote/promote current heading | | ||
| 245 | | =M-Down/Up= | Shift current heading section down/up | | ||
| 246 | |||
| 247 | 1. Org-Publish | ||
| 248 | |||
| 249 | Org includes a | ||
| 250 | [[https://orgmode.org/manual/Publishing.html][publishing management | ||
| 251 | system]] by default that allows you to export org files to Org, | ||
| 252 | iCalendar, HTML, LaTex, Markdown, ODT, and Plain Text. Most of these | ||
| 253 | can be exported into another buffer and opened, or simply to an | ||
| 254 | external file. | ||
| 255 | |||
| 256 | While inside an org file, simply run =SPC m e= or | ||
| 257 | =M-x org-export-dispatch= to open the export menu. This menu will | ||
| 258 | show all options and ask you to select an option. If you want to | ||
| 259 | export to HTML, simply press =h= and then =H= (As HTML buffer), =h= | ||
| 260 | (As HTML file), or =o= (As HTML file and open). | ||
| 261 | |||
| 262 | 2. Projects | ||
| 263 | |||
| 264 | Some publishing options are easier with a defined project in Emacs. | ||
| 265 | To create a project within Emacs, I use two methods: | ||
| 266 | |||
| 267 | 1. Add the project via the projectile command =SPC p a=. Does not | ||
| 268 | always work for me. | ||
| 269 | 2. Add an empty =.projectile= file in the project root. | ||
| 270 | |||
| 271 | Once a project has been created, you can create custom publishing | ||
| 272 | actions within your =~/.doom.d/config.el= file. For example, here's a | ||
| 273 | test project I created to try and convert this blog to org-mode | ||
| 274 | recently. | ||
| 275 | |||
| 276 | #+begin_src lisp | ||
| 277 | ;; org-publish | ||
| 278 | (require 'ox-publish) | ||
| 279 | |||
| 280 | (defun my/org-sitemap-date-entry-format (entry style project) "Format ENTRY in | ||
| 281 | org-publish PROJECT Sitemap format ENTRY ENTRY STYLE format that includes | ||
| 282 | date." (let ((filename (org-publish-find-title entry project))) (if (= (length | ||
| 283 | filename) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}} | ||
| 284 | [[file:%s][%s]]" (format-time-string "%Y-%m-%d" (org-publish-find-date entry | ||
| 285 | project)) entry filename)))) | ||
| 286 | |||
| 287 | (setq org-export-global-macros '(("timestamp" . "@@html:<time datetime='[$1]' | ||
| 288 | class='timestamp'>[$1]</time>@@"))) | ||
| 289 | |||
| 290 | (setq org-publish-project-alist | ||
| 291 | `(("blog" | ||
| 292 | :base-directory "~/Source/cleberg.net/" | ||
| 293 | :base-extension "org" | ||
| 294 | :recursive t | ||
| 295 | :publishing-directory "~/Source/cleberg.net/public/" | ||
| 296 | :publishing-function org-html-publish-to-html | ||
| 297 | ;; HTML5 | ||
| 298 | :html-doctype "html5" | ||
| 299 | :html-html5-fancy t | ||
| 300 | ;; Disable some Org's HTML defaults | ||
| 301 | :html-head-include-scripts nil | ||
| 302 | :html-head-include-default-style nil | ||
| 303 | :section-numbers nil | ||
| 304 | :with-title nil | ||
| 305 | ;; Sitemap | ||
| 306 | :auto-sitemap t | ||
| 307 | :sitemap-title: "Sitemap" | ||
| 308 | :sitemap-sort-files anti-chronologically | ||
| 309 | ; :sitemap-function my/org-sitemap-date-entry-format | ||
| 310 | ;; Customize HTML output | ||
| 311 | :html-divs ((preamble "header" "preamble") | ||
| 312 | (content "main" "content") | ||
| 313 | (postamble "footer" "postamble")) | ||
| 314 | :html-head "<meta name='theme-color' content='#111' media='(prefers-color-scheme: dark)'> | ||
| 315 | <meta name='theme-color' content='#fff' media='(prefers-color-scheme: light)'> | ||
| 316 | <link rel='stylesheet' href='/syntax-theme-dark.css' media='(prefers-color-scheme: dark)'> | ||
| 317 | <link rel='stylesheet' href='/syntax-theme-light.css' media='(prefers-color-scheme: light)'> | ||
| 318 | <link rel='stylesheet' href='/styles.css' type='text/css'>" | ||
| 319 | :html-preamble "<nav class='site-nav' aria-label='site-nav' role='navigation'> | ||
| 320 | <ul> | ||
| 321 | <li><a href='/'>Home</a></li> | ||
| 322 | <li><a href='/blog/'>Blog</a></li> | ||
| 323 | <li><a href='/services/'>Services</a></li> | ||
| 324 | <li><a href='/wiki/'>Wiki</a></li> | ||
| 325 | </ul></nav> | ||
| 326 | <h1>%t</h1> | ||
| 327 | <time datetime='%d'>%d</time>" | ||
| 328 | :html-postamble " | ||
| 329 | <p>Last build: %T</p> | ||
| 330 | <p>Created with %c</p>" | ||
| 331 | ) | ||
| 332 | |||
| 333 | ("static" | ||
| 334 | :base-directory "~/Source/cleberg.net/static/" | ||
| 335 | :base-extension "css\\|txt\\|jpg\\|gif\\|png" | ||
| 336 | :recursive t | ||
| 337 | :publishing-directory "~/Source/cleberg.net/public/" | ||
| 338 | :publishing-function org-publish-attachment) | ||
| 339 | |||
| 340 | ("cleberg.net" :components ("blog" "static")))) | ||
| 341 | #+end_src | ||
| 342 | |||
| 343 | ** General Thoughts | ||
| 344 | |||
| 345 | I have enjoyed Doom Emacs (far more than GNU Emacs) and will likely | ||
| 346 | continue to use it as my main editor for the time being. Org-Mode is | ||
| 347 | certainly the largest factor here, as I far prefer it over Markdown due | ||
| 348 | to its inherent features and detailed markup options. However, working | ||
| 349 | with org-mode on iOS has been a pain and I will have to see if there's | ||
| 350 | an easier way to resolve those issues or if going back to separate | ||
| 351 | Markdown, Reminders, and Calendar apps is easier to work with than an | ||
| 352 | all-in-one org solution. | ||
| 353 | |||
| 354 | [fn:1] Doom's evil-window functionality is a bit different from GNU | ||
| 355 | Emacs, but you can always switch to the "other" buffer with | ||
| 356 | =C-x o= or =C-x b= to get a list of buffers to select. | ||
| 357 | |||
| 358 | [fn:2] Doom's evil-window functionality is a bit different from GNU | ||
| 359 | Emacs, but you can always switch to the "other" buffer with | ||
| 360 | =C-x o= or =C-x b= to get a list of buffers to select. | ||
content/blog/2024-03-15-self-hosting-ddns-updater.md → content/blog/2024-03-15-self-hosting-ddns-updater.org renamed +91 −86
| @@ -1,73 +1,78 @@ | |||
| 1 | +++ | 1 | #+date: <2024-03-15> |
| 2 | date = 2024-03-15 | 2 | #+title: Self-Hosting DDNS Updater |
| 3 | title = "Self-Hosting DDNS Updater" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 |  | ||
| 10 | 5 | ||
| 11 | [DDNS Updater](https://github.com/qdm12/ddns-updater) is a program to keep DNS A | 6 | #+caption: DDNS Updater Web View |
| 12 | and/or AAAA records updated for multiple DNS providers. | 7 | [[https://img.cleberg.net/blog/20240315-ddns-updater/ddns.png]] |
| 13 | 8 | ||
| 14 | If you've read any of my other posts, you'll notice that I have been searching | 9 | [[https://github.com/qdm12/ddns-updater][DDNS Updater]] is a program to |
| 15 | for and using a few different DDNS updating solutions for years. You'll also | 10 | keep DNS A and/or AAAA records updated for multiple DNS providers. |
| 16 | notice that I love any projects that offer a Docker Compose solution. | 11 | |
| 12 | If you've read any of my other posts, you'll notice that I have been | ||
| 13 | searching for and using a few different DDNS updating solutions for | ||
| 14 | years. You'll also notice that I love any projects that offer a Docker | ||
| 15 | Compose solution. | ||
| 17 | 16 | ||
| 18 | Luckily, DDNS Upater fits both of these preferences. | 17 | Luckily, DDNS Upater fits both of these preferences. |
| 19 | 18 | ||
| 20 | ## Installation | 19 | ** Installation |
| 21 | 20 | ||
| 22 | To get started, always make sure to review the project's | 21 | To get started, always make sure to review the project's |
| 23 | [README](https://github.com/qdm12/ddns-updater/blob/master/README.md). I'll be | 22 | [[https://github.com/qdm12/ddns-updater/blob/master/README.md][README]]. |
| 24 | documenting my steps below, but they may have changed by the time you read this. | 23 | I'll be documenting my steps below, but they may have changed by the |
| 24 | time you read this. | ||
| 25 | 25 | ||
| 26 | The first step is to set up the directories and files required for the project. | 26 | The first step is to set up the directories and files required for the |
| 27 | project. | ||
| 27 | 28 | ||
| 28 | ```sh | 29 | #+begin_src sh |
| 29 | mkdir ~/ddns-updater | 30 | mkdir ~/ddns-updater |
| 30 | mkdir ~/ddns-updater/data | 31 | mkdir ~/ddns-updater/data |
| 31 | touch ~/ddns-updater/data/config.json | 32 | touch ~/ddns-updater/data/config.json |
| 32 | ``` | 33 | #+end_src |
| 33 | 34 | ||
| 34 | ### Configuration | 35 | *** Configuration |
| 35 | 36 | ||
| 36 | The main configuration you need to update is the `data/config.json` file. There | 37 | The main configuration you need to update is the =data/config.json= |
| 37 | is a large list of supported providers in the README, but I'm going to use | 38 | file. There is a large list of supported providers in the README, but |
| 38 | Cloudflare in this example. | 39 | I'm going to use Cloudflare in this example. |
| 39 | 40 | ||
| 40 | ```sh | 41 | #+begin_src sh |
| 41 | nano ~/ddns-updater/data/config.json | 42 | nano ~/ddns-updater/data/config.json |
| 42 | ``` | 43 | #+end_src |
| 43 | 44 | ||
| 44 | When setting up the configuration for Cloudflare, you'll need the following: | 45 | When setting up the configuration for Cloudflare, you'll need the |
| 45 | 46 | following: | |
| 46 | - Required Parameters | 47 | |
| 47 | - `"zone_identifier"` is the Zone ID of your site from the domain overview | 48 | - Required Parameters |
| 48 | page | 49 | - ="zone_identifier"= is the Zone ID of your site from the domain |
| 49 | - `"host"` is your host and can be `"@"`, a subdomain or the wildcard | 50 | overview page |
| 50 | `"*"`. See [this issue comment for | 51 | - ="host"= is your host and can be ="@"=, a subdomain or the wildcard |
| 51 | context](https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949). | 52 | ="*"=. See |
| 52 | - `"ttl"` integer value for record TTL in seconds (specify 1 for | 53 | [[https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949][this |
| 53 | automatic) | 54 | issue comment for context]]. |
| 54 | - One of the following ([how to find API | 55 | - ="ttl"= integer value for record TTL in seconds (specify 1 for |
| 55 | keys](https://developers.cloudflare.com/fundamentals/api/get-started/)): | 56 | automatic) |
| 56 | - Email `"email"` and Global API Key `"key"` | 57 | - One of the following |
| 57 | - User service key `"user_service_key"` | 58 | ([[https://developers.cloudflare.com/fundamentals/api/get-started/][how |
| 58 | - API Token `"token"`, configured with DNS edit permissions for your | 59 | to find API keys]]): |
| 59 | DNS name's zone | 60 | - Email ="email"= and Global API Key ="key"= |
| 60 | - Optional Parameters | 61 | - User service key ="user_service_key"= |
| 61 | - `"proxied"` can be set to `true` to use the proxy services of Cloudflare | 62 | - API Token ="token"=, configured with DNS edit permissions for your |
| 62 | - `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or | 63 | DNS name's zone |
| 63 | `ipv4 or ipv6` (update one of the two, depending on the public ip | 64 | - Optional Parameters |
| 64 | found). It defaults to `ipv4 or ipv6`. | 65 | - ="proxied"= can be set to =true= to use the proxy services of |
| 65 | - `"ipv6_suffix"` is the IPv6 interface identifier suffix to use. It can | 66 | Cloudflare |
| 66 | be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it | 67 | - ="ip_version"= can be =ipv4= (A records), or =ipv6= (AAAA records) |
| 67 | defaults to no suffix and the raw public IPv6 address obtained is used | 68 | or =ipv4 or ipv6= (update one of the two, depending on the public |
| 68 | in the record updating. | 69 | ip found). It defaults to =ipv4 or ipv6=. |
| 69 | 70 | - ="ipv6_suffix"= is the IPv6 interface identifier suffix to use. It | |
| 70 | ```conf | 71 | can be for example =0:0:0:0:72ad:8fbb:a54e:bedd/64=. If left empty, |
| 72 | it defaults to no suffix and the raw public IPv6 address obtained is | ||
| 73 | used in the record updating. | ||
| 74 | |||
| 75 | #+begin_src conf | ||
| 71 | { | 76 | { |
| 72 | "settings": [ | 77 | "settings": [ |
| 73 | { | 78 | { |
| @@ -83,12 +88,12 @@ When setting up the configuration for Cloudflare, you'll need the following: | |||
| 83 | } | 88 | } |
| 84 | ] | 89 | ] |
| 85 | } | 90 | } |
| 86 | ``` | 91 | #+end_src |
| 87 | 92 | ||
| 88 | Once you have configured the provider of your choice, correct the file and | 93 | Once you have configured the provider of your choice, correct the file |
| 89 | directory permissions and ownership. | 94 | and directory permissions and ownership. |
| 90 | 95 | ||
| 91 | ```sh | 96 | #+begin_src sh |
| 92 | cd ~/ddns_updater | 97 | cd ~/ddns_updater |
| 93 | # Owned by user ID of Docker container (1000) | 98 | # Owned by user ID of Docker container (1000) |
| 94 | chown -R 1000 data | 99 | chown -R 1000 data |
| @@ -96,18 +101,18 @@ chown -R 1000 data | |||
| 96 | chmod 700 data | 101 | chmod 700 data |
| 97 | # read access only | 102 | # read access only |
| 98 | chmod 400 data/config.json | 103 | chmod 400 data/config.json |
| 99 | ``` | 104 | #+end_src |
| 100 | 105 | ||
| 101 | ### Docker Compose | 106 | *** Docker Compose |
| 102 | 107 | ||
| 103 | After creating the project structure, let's create the `docker-compose.yml` | 108 | After creating the project structure, let's create the |
| 104 | file. | 109 | =docker-compose.yml= file. |
| 105 | 110 | ||
| 106 | ```sh | 111 | #+begin_src sh |
| 107 | nano ~/ddns_-pdater/docker-compose.yml | 112 | nano ~/ddns_-pdater/docker-compose.yml |
| 108 | ``` | 113 | #+end_src |
| 109 | 114 | ||
| 110 | ```config | 115 | #+begin_src config |
| 111 | version: "3.7" | 116 | version: "3.7" |
| 112 | services: | 117 | services: |
| 113 | ddns-updater: | 118 | ddns-updater: |
| @@ -143,34 +148,34 @@ services: | |||
| 143 | - LOG_CALLER=hidden | 148 | - LOG_CALLER=hidden |
| 144 | - SHOUTRRR_ADDRESSES= | 149 | - SHOUTRRR_ADDRESSES= |
| 145 | restart: always | 150 | restart: always |
| 146 | ``` | 151 | #+end_src |
| 147 | 152 | ||
| 148 | After configuring your preferences in the `docker-compose.yml`, launch the | 153 | After configuring your preferences in the =docker-compose.yml=, launch |
| 149 | container. | 154 | the container. |
| 150 | 155 | ||
| 151 | ```sh | 156 | #+begin_src sh |
| 152 | cd ~/ddns-updater | 157 | cd ~/ddns-updater |
| 153 | sudo docker-compose up -d | 158 | sudo docker-compose up -d |
| 154 | ``` | 159 | #+end_src |
| 155 | 160 | ||
| 156 | If you've launched this on your local machine, you can launch `localhost:8097` | 161 | If you've launched this on your local machine, you can launch |
| 157 | in your browser to see the results. | 162 | =localhost:8097= in your browser to see the results. |
| 158 | 163 | ||
| 159 | ### Nginx Reverse Proxy | 164 | *** Nginx Reverse Proxy |
| 160 | 165 | ||
| 161 | If you launched this service on a server, other machine, or just want to access | 166 | If you launched this service on a server, other machine, or just want to |
| 162 | it remotely via a domain name, you can use Nginx as a reverse proxy to expose | 167 | access it remotely via a domain name, you can use Nginx as a reverse |
| 163 | the service publicly. | 168 | proxy to expose the service publicly. |
| 164 | 169 | ||
| 165 | Start by creating the Nginx configuration file. | 170 | Start by creating the Nginx configuration file. |
| 166 | 171 | ||
| 167 | ```sh | 172 | #+begin_src sh |
| 168 | sudo nano /etc/nginx/sites-available/ddns | 173 | sudo nano /etc/nginx/sites-available/ddns |
| 169 | ``` | 174 | #+end_src |
| 170 | 175 | ||
| 171 | Here's a basic example that should work properly. | 176 | Here's a basic example that should work properly. |
| 172 | 177 | ||
| 173 | ```conf | 178 | #+begin_src conf |
| 174 | server { | 179 | server { |
| 175 | # If using 443, remember to include your ssl_certificate | 180 | # If using 443, remember to include your ssl_certificate |
| 176 | # and ssl_certificate_key | 181 | # and ssl_certificate_key |
| @@ -186,12 +191,12 @@ server { | |||
| 186 | # see the full example below if necessary | 191 | # see the full example below if necessary |
| 187 | } | 192 | } |
| 188 | } | 193 | } |
| 189 | ``` | 194 | #+end_src |
| 190 | 195 | ||
| 191 | Here's a full example that uses my Authelia authentication service to require | 196 | Here's a full example that uses my Authelia authentication service to |
| 192 | authentication before someone can access the web page. | 197 | require authentication before someone can access the web page. |
| 193 | 198 | ||
| 194 | ```conf | 199 | #+begin_src conf |
| 195 | server { | 200 | server { |
| 196 | if ($host ~ ^[^.]+\.example\.com$) { | 201 | if ($host ~ ^[^.]+\.example\.com$) { |
| 197 | return 301 https://$host$request_uri; | 202 | return 301 https://$host$request_uri; |
| @@ -298,13 +303,13 @@ server { | |||
| 298 | # real_ip_recursive on; | 303 | # real_ip_recursive on; |
| 299 | } | 304 | } |
| 300 | } | 305 | } |
| 301 | ``` | 306 | #+end_src |
| 302 | 307 | ||
| 303 | When complete, simply link the file and restart the web server. | 308 | When complete, simply link the file and restart the web server. |
| 304 | 309 | ||
| 305 | ```sh | 310 | #+begin_src sh |
| 306 | sudo ln -s /etc/nginx/sites-available/ddns /etc/nginx/sites-enabled/ddns | 311 | sudo ln -s /etc/nginx/sites-available/ddns /etc/nginx/sites-enabled/ddns |
| 307 | sudo systemctl restart nginx.service | 312 | sudo systemctl restart nginx.service |
| 308 | ``` | 313 | #+end_src |
| 309 | 314 | ||
| 310 | Your ddns-updater service will now be available via `ddns.example.com`! | 315 | Your ddns-updater service will now be available via =ddns.example.com=! |
content/blog/2024-03-29-org-blog.md → content/blog/2024-03-29-org-blog.org renamed +109 −104
| @@ -1,80 +1,82 @@ | |||
| 1 | +++ | 1 | #+date: <2024-03-29> |
| 2 | date = 2024-03-29 | 2 | #+title: Blogging in Org-Mode |
| 3 | title = "Blogging in Org-Mode" | 3 | #+description: |
| 4 | description = "" | 4 | |
| 5 | draft = false | 5 | |
| 6 | +++ | 6 | First and foremost, apologies to those who subscribe via RSS as I know |
| 7 | 7 | that my feed duplicated itself when I moved this blog over to org-mode | |
| 8 | First and foremost, apologies to those who subscribe via RSS as I know that my | 8 | last night. |
| 9 | feed duplicated itself when I moved this blog over to org-mode last night. | 9 | |
| 10 | 10 | This post focuses specifically on the configuration and tools I use to | |
| 11 | This post focuses specifically on the configuration and tools I use to blog from | 11 | blog from Emacs with Org-Mode and does not focus on Emacs or Org-Mode |
| 12 | Emacs with Org-Mode and does not focus on Emacs or Org-Mode themselves. Refer to | 12 | themselves. Refer to the post I wrote about |
| 13 | the post I wrote about [Doom Emacs & | 13 | [[file:///blog/doom-emacs-org-mode.html][Doom Emacs & Org-Mode]] for |
| 14 | Org-Mode](file:///blog/doom-emacs-org-mode.html) for more information about my | 14 | more information about my base Emacs configuration. |
| 15 | base Emacs configuration. | 15 | |
| 16 | 16 | * Weblorg | |
| 17 | # Weblorg | 17 | |
| 18 | 18 | The first step in blogging with Org-Mode is to choose a method to | |
| 19 | The first step in blogging with Org-Mode is to choose a method to convert the | 19 | convert the source files to HTML and publish them. The Worg site |
| 20 | source files to HTML and publish them. The Worg site maintains a nice list of | 20 | maintains a nice list of |
| 21 | [Blogs and Wikis with Org](https://orgmode.org/worg/org-blog-wiki.html), but the | 21 | [[https://orgmode.org/worg/org-blog-wiki.html][Blogs and Wikis with |
| 22 | tools are inevitably different and opinionated, so you'll need to find what | 22 | Org]], but the tools are inevitably different and opinionated, so you'll |
| 23 | works for you. | 23 | need to find what works for you. |
| 24 | 24 | ||
| 25 | I tried using Jekyll, Hugo, ox-hugo, Nikola, Blorg, org-static-blog, and the | 25 | I tried using Jekyll, Hugo, ox-hugo, Nikola, Blorg, org-static-blog, and |
| 26 | native org-publish functions before finally settling on Weblorg. For one reason | 26 | the native org-publish functions before finally settling on Weblorg. For |
| 27 | or another, the other solutions were a drastic step down from my previous | 27 | one reason or another, the other solutions were a drastic step down from |
| 28 | workflow that used [Zola](https://www.getzola.org/) with Markdown content. | 28 | my previous workflow that used [[https://www.getzola.org/][Zola]] with |
| 29 | 29 | Markdown content. | |
| 30 | [Weblorg](https://github.com/emacs-love/weblorg) is a static site generator for | 30 | |
| 31 | [org-mode](https://orgmode.org/), built for use within | 31 | [[https://github.com/emacs-love/weblorg][Weblorg]] is a static site |
| 32 | [Emacs](https://www.gnu.org/software/emacs/). Since it's written in Emacs Lisp, | 32 | generator for [[https://orgmode.org/][org-mode]], built for use within |
| 33 | there's no need to install other languages or frameworks to get started. More | 33 | [[https://www.gnu.org/software/emacs/][Emacs]]. Since it's written in |
| 34 | than that, you can write in any editor you please and simply invoke the Emacs | 34 | Emacs Lisp, there's no need to install other languages or frameworks to |
| 35 | build process with the `--script` parameter instead of requiring you to blog | 35 | get started. More than that, you can write in any editor you please and |
| 36 | inside Emacs. | 36 | simply invoke the Emacs build process with the =--script= parameter |
| 37 | 37 | instead of requiring you to blog inside Emacs. | |
| 38 | ## Installation | 38 | |
| 39 | 39 | ** Installation | |
| 40 | The [Getting Started](https://emacs.love/weblorg/doc/index.html) page details | 40 | |
| 41 | broad installation requirements. I am using Doom Emacs on macOS, which requires | 41 | The [[https://emacs.love/weblorg/doc/index.html][Getting Started]] page |
| 42 | you to add the package to the `~/.doom.d/packages.el` file and configure the | 42 | details broad installation requirements. I am using Doom Emacs on macOS, |
| 43 | `publish.el` file slightly differently. | 43 | which requires you to add the package to the =~/.doom.d/packages.el= |
| 44 | 44 | file and configure the =publish.el= file slightly differently. | |
| 45 | To start, add the `htmlize` and `weblorg` packages to Doom, sync the changes, | 45 | |
| 46 | and reload. | 46 | To start, add the =htmlize= and =weblorg= packages to Doom, sync the |
| 47 | 47 | changes, and reload. | |
| 48 | ```sh | 48 | |
| 49 | #+begin_src sh | ||
| 49 | nano ~/.doom.d/packages.el | 50 | nano ~/.doom.d/packages.el |
| 50 | ``` | 51 | #+end_src |
| 51 | 52 | ||
| 52 | ```lisp | 53 | #+begin_src lisp |
| 53 | (package! htmlize) | 54 | (package! htmlize) |
| 54 | (package! weblorg) | 55 | (package! weblorg) |
| 55 | ``` | 56 | #+end_src |
| 56 | 57 | ||
| 57 | ```sh | 58 | #+begin_src sh |
| 58 | doom sync | 59 | doom sync |
| 59 | ``` | 60 | #+end_src |
| 60 | 61 | ||
| 61 | Either re-open Emacs or hit `SPC h r r` to reload the changes. | 62 | Either re-open Emacs or hit =SPC h r r= to reload the changes. |
| 62 | 63 | ||
| 63 | ## Configuration | 64 | ** Configuration |
| 64 | 65 | ||
| 65 | Now that I've installed weblorg, I need to configure the project. I'll start by | 66 | Now that I've installed weblorg, I need to configure the project. I'll |
| 66 | navigating to my site's source code and creating a `publish.el` file. | 67 | start by navigating to my site's source code and creating a =publish.el= |
| 68 | file. | ||
| 67 | 69 | ||
| 68 | ```sh | 70 | #+begin_src sh |
| 69 | cd ~/Source/cleberg.net && nano publish.el | 71 | cd ~/Source/cleberg.net && nano publish.el |
| 70 | ``` | 72 | #+end_src |
| 71 | 73 | ||
| 72 | Since I'm using Doom, Emacs will not automatically load the packages I need | 74 | Since I'm using Doom, Emacs will not automatically load the packages I |
| 73 | later in the build process. To compensate, my `publish.el` file needs to | 75 | need later in the build process. To compensate, my =publish.el= file |
| 74 | explicitly tell Emacs where Doom stores the `htmlize`, `weblorg`, and | 76 | needs to explicitly tell Emacs where Doom stores the =htmlize=, |
| 75 | `templatel` packages. | 77 | =weblorg=, and =templatel= packages. |
| 76 | 78 | ||
| 77 | ```lisp | 79 | #+begin_src lisp |
| 78 | ;; explicity load packages since I'm using Doom Emacs | 80 | ;; explicity load packages since I'm using Doom Emacs |
| 79 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/emacs-htmlize") | 81 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/emacs-htmlize") |
| 80 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/weblorg") | 82 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/weblorg") |
| @@ -164,9 +166,9 @@ explicitly tell Emacs where Doom stores the `htmlize`, `weblorg`, and | |||
| 164 | :name "rss" | 166 | :name "rss" |
| 165 | :input-pattern "content/blog/*.org" | 167 | :input-pattern "content/blog/*.org" |
| 166 | :input-aggregate #'weblorg-input-aggregate-all-desc | 168 | :input-aggregate #'weblorg-input-aggregate-all-desc |
| 167 | :template "atom.xml" | 169 | :template "feed.xml" |
| 168 | :output ".build/atom.xml" | 170 | :output ".build/feed.xml" |
| 169 | :url "/atom.xml") | 171 | :url "/feed.xml") |
| 170 | 172 | ||
| 171 | ;; route for static assets that also copies files to .build directory | 173 | ;; route for static assets that also copies files to .build directory |
| 172 | (weblorg-copy-static | 174 | (weblorg-copy-static |
| @@ -175,19 +177,21 @@ explicitly tell Emacs where Doom stores the `htmlize`, `weblorg`, and | |||
| 175 | 177 | ||
| 176 | ;; fire the engine and export all the files declared in the routes above | 178 | ;; fire the engine and export all the files declared in the routes above |
| 177 | (weblorg-export) | 179 | (weblorg-export) |
| 178 | ``` | 180 | #+end_src |
| 179 | 181 | ||
| 180 | # Project | 182 | * Project |
| 181 | 183 | ||
| 182 | ## Structure | 184 | ** Structure |
| 183 | 185 | ||
| 184 | The project structure for weblorg is highly customizable and the main | 186 | The project structure for weblorg is highly customizable and the main |
| 185 | restriction is that the `publish.el` file must point to the correct paths. | 187 | restriction is that the =publish.el= file must point to the correct |
| 188 | paths. | ||
| 186 | 189 | ||
| 187 | For my blog, I prefer to keep the blog content out of the top-level directory. | 190 | For my blog, I prefer to keep the blog content out of the top-level |
| 188 | This results in the following structure (shortened for brevity): | 191 | directory. This results in the following structure (shortened for |
| 192 | brevity): | ||
| 189 | 193 | ||
| 190 | ```txt | 194 | #+begin_src txt |
| 191 | .build/ | 195 | .build/ |
| 192 | content/ | 196 | content/ |
| 193 | blog/ | 197 | blog/ |
| @@ -211,55 +215,56 @@ theme/ | |||
| 211 | wiki.html | 215 | wiki.html |
| 212 | build.sh | 216 | build.sh |
| 213 | publish.el | 217 | publish.el |
| 214 | ``` | 218 | #+end_src |
| 215 | 219 | ||
| 216 | This is simply my preferred structure and you can alter it to fit your needs. | 220 | This is simply my preferred structure and you can alter it to fit your |
| 217 | The key here really is that you can customize at will, as long as the | 221 | needs. The key here really is that you can customize at will, as long as |
| 218 | `publish.el` file matches. | 222 | the =publish.el= file matches. |
| 219 | 223 | ||
| 220 | ## Build & Deploy | 224 | ** Build & Deploy |
| 221 | 225 | ||
| 222 | Once you're content with the status of the project, you're ready to build and | 226 | Once you're content with the status of the project, you're ready to |
| 223 | deploy the blog. | 227 | build and deploy the blog. |
| 224 | 228 | ||
| 225 | My process utilizes a `build.sh` script that combines the steps I take every | 229 | My process utilizes a =build.sh= script that combines the steps I take |
| 226 | time. | 230 | every time. |
| 227 | 231 | ||
| 228 | ```sh | 232 | #+begin_src sh |
| 229 | touch build.sh && chmod +x build.sh && nano build.sh | 233 | touch build.sh && chmod +x build.sh && nano build.sh |
| 230 | ``` | 234 | #+end_src |
| 231 | 235 | ||
| 232 | Within this script, I do the following: | 236 | Within this script, I do the following: |
| 233 | 237 | ||
| 234 | 1. Remove any files within the `.build` directory that I use to store published | 238 | 1. Remove any files within the =.build= directory that I use to store |
| 235 | files. | 239 | published files. |
| 236 | 2. Set the environment variable to `prod` to ensure the `base_url` matches my | 240 | 2. Set the environment variable to =prod= to ensure the =base_url= |
| 237 | configuration in `publish.el`. | 241 | matches my configuration in =publish.el=. |
| 238 | 3. Build the site with Emacs & `publish.el`. | 242 | 3. Build the site with Emacs & =publish.el=. |
| 239 | 4. Use `scp` to copy files to my site's public directory on my server. | 243 | 4. Use =scp= to copy files to my site's public directory on my server. |
| 240 | 244 | ||
| 241 | ```sh | 245 | #+begin_src sh |
| 242 | rm -rf .build/* && \ | 246 | rm -rf .build/* && \ |
| 243 | ENV=prod emacs --script publish.el && \ | 247 | ENV=prod emacs --script publish.el && \ |
| 244 | scp -r .build/* ubuntu:/var/www/cleberg.net/ | 248 | scp -r .build/* ubuntu:/var/www/cleberg.net/ |
| 245 | ``` | 249 | #+end_src |
| 246 | 250 | ||
| 247 | ### Time to Build | 251 | *** Time to Build |
| 248 | 252 | ||
| 249 | My only current complaints are: | 253 | My only current complaints are: |
| 250 | 254 | ||
| 251 | 1. Errors messages are not helpful. It takes work to determine what the error is | 255 | 1. Errors messages are not helpful. It takes work to determine what the |
| 252 | and where it's coming from. I generally have to sit and watch the build | 256 | error is and where it's coming from. I generally have to sit and |
| 253 | process to see the file that weblorg pubslishes right before the error | 257 | watch the build process to see the file that weblorg pubslishes right |
| 254 | occurred. | 258 | before the error occurred. |
| 255 | 2. The build process re-builds every single file on each run, which takes a long | 259 | 2. The build process re-builds every single file on each run, which |
| 256 | time for a blog of my size. See below for the last time I measured. | 260 | takes a long time for a blog of my size. See below for the last time |
| 261 | I measured. | ||
| 257 | 262 | ||
| 258 | ```sh | 263 | #+begin_src sh |
| 259 | > time ./build.sh | 264 | > time ./build.sh |
| 260 | 265 | ||
| 261 | ./build.sh 35.46s user 0.59s system 85% cpu 41.965 total | 266 | ./build.sh 35.46s user 0.59s system 85% cpu 41.965 total |
| 262 | ``` | 267 | #+end_src |
| 263 | 268 | ||
| 264 | Overall, I have thoroughly enjoyed using weblog and will continue to use it | 269 | Overall, I have thoroughly enjoyed using weblog and will continue to use |
| 265 | going forward until I find something better. | 270 | it going forward until I find something better. |
content/blog/2024-04-06-convert-onenote-to-markdown.md deleted −95
| @@ -1,95 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-04-06 | ||
| 3 | title = "Convert OneNote to Markdown or Org-Mode" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | If you're looking to convert your OneNote content to another format, such as | ||
| 9 | Markdown or Org-Mode, you're in luck. I use a solution that doesn't require | ||
| 10 | other programs, such as Evernote or Notion. Personally, I used this solution on | ||
| 11 | a managed corporate laptop that doesn't allow installation of other programs | ||
| 12 | like these. | ||
| 13 | |||
| 14 | This solution uses OneNote and Pandoc on Windows 10. | ||
| 15 | |||
| 16 | # Export OneNote Content to Word | ||
| 17 | |||
| 18 | To start, export any pages or tabs from OneNote to the Word format (`.docx`): | ||
| 19 | |||
| 20 | 1. Open OneNote desktop. | ||
| 21 | 2. Select `File` and then `Export`. | ||
| 22 | 3. Select the scope of content to export, such as `Tab` or `Page`. | ||
| 23 | 4. Name and save the file in an easy to remember location. I recommend your | ||
| 24 | Downloads or Desktop folder. | ||
| 25 | |||
| 26 | See below for a visual walkthrough of the export process. | ||
| 27 | |||
| 28 | # Download Pandoc | ||
| 29 | |||
| 30 | Start by downloading Pandoc from their [GitHub | ||
| 31 | releases](https://github.com/jgm/pandoc/releases) page. I cannot install `.msi` | ||
| 32 | files on my corporate laptop, so I downloaded the | ||
| 33 | `pandoc-3.1.12.3-windows-x86_64.zip` file, which contains a simple `.exe` file | ||
| 34 | that you do not need to install - you will simply run it from the command line | ||
| 35 | below. | ||
| 36 | |||
| 37 | Once downloaded, unzip the archive and move the `pandoc.exe` file to the same | ||
| 38 | folder where your Word documents were saved above. If you prefer, you can move | ||
| 39 | this file to an easier location, such as `C:\Users\youruser\Downloads`. | ||
| 40 | |||
| 41 | # Convert Word to Markdown | ||
| 42 | |||
| 43 | In this example, I will be converting the Word documents to Markdown, but Pandoc | ||
| 44 | supports [a ton of different formats for | ||
| 45 | conversion](https://github.com/jgm/pandoc?tab=readme-ov-file#the-universal-markup-converter). | ||
| 46 | Choose the format you prefer and then modify the following commands as needed. | ||
| 47 | |||
| 48 | To perform the conversion, open the Command Prompt. If you can't find it, open | ||
| 49 | the start menu and search for it. | ||
| 50 | |||
| 51 | Within the command prompt, navigate to the directory where you stored the | ||
| 52 | `pandoc.exe` file and the Word documents. | ||
| 53 | |||
| 54 | ```ps1 | ||
| 55 | cd "C:\Users\yourusername\Downloads" | ||
| 56 | ``` | ||
| 57 | |||
| 58 | You can verify that you're in the correct directory with the `dir` command. | ||
| 59 | |||
| 60 | ```ps1 | ||
| 61 | dir | ||
| 62 | ``` | ||
| 63 | |||
| 64 | Once you have verified that you have the command prompt open in the correct | ||
| 65 | directory with the `pandoc.exe` and the Word documents, you can run the | ||
| 66 | following loop to convert all Word documents to Markdown. | ||
| 67 | |||
| 68 | ```ps1 | ||
| 69 | for %f in (*.docx) do (pandoc.exe --extract-media=. --wrap=preserve "%f" -o "%f.md") | ||
| 70 | ``` | ||
| 71 | |||
| 72 | This loop will perform the following actions: | ||
| 73 | |||
| 74 | 1. Find all documents matching the pattern `*.docx`, which means all Word | ||
| 75 | documents ending with that file extension. | ||
| 76 | 2. Iterate through all files found in step 1. | ||
| 77 | 3. For each file, perform the pandoc command. | ||
| 78 | 4. Within the pandoc command, `--extract-media` saves all media found in the | ||
| 79 | files to the current folder, with pandoc automatically creating a `media` | ||
| 80 | subfolder to hold all images. | ||
| 81 | 5. Within the pandoc command, `--wrap=preserve` will attempt to prseerve the | ||
| 82 | wrapping from the source document. | ||
| 83 | 6. Within the pandoc command, the final step is to specify the output path with | ||
| 84 | `-o`. This option adds the `.md` file extension to recognize the output files | ||
| 85 | as Markdown files. | ||
| 86 | |||
| 87 | If you want to export to another format, simply specify the `-f=`/`--from=` and | ||
| 88 | `-t=`/`--to=` options. | ||
| 89 | |||
| 90 | For example, you can convert the Word document to org-mode. You can also convert | ||
| 91 | to one format and subsequently convert to other formats as needed. | ||
| 92 | |||
| 93 | ```ps1 | ||
| 94 | pandoc.exe -f docx -t org file.docx | ||
| 95 | ``` | ||
content/blog/2024-04-06-convert-onenote-to-markdown.org added +98
| @@ -0,0 +1,98 @@ | |||
| 1 | #+date: <2024-04-06> | ||
| 2 | #+title: Convert OneNote to Markdown or Org-Mode | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | If you're looking to convert your OneNote content to another format, | ||
| 7 | such as Markdown or Org-Mode, you're in luck. I use a solution that | ||
| 8 | doesn't require other programs, such as Evernote or Notion. Personally, | ||
| 9 | I used this solution on a managed corporate laptop that doesn't allow | ||
| 10 | installation of other programs like these. | ||
| 11 | |||
| 12 | This solution uses OneNote and Pandoc on Windows 10. | ||
| 13 | |||
| 14 | * Export OneNote Content to Word | ||
| 15 | |||
| 16 | To start, export any pages or tabs from OneNote to the Word format | ||
| 17 | (=.docx=): | ||
| 18 | |||
| 19 | 1. Open OneNote desktop. | ||
| 20 | 2. Select =File= and then =Export=. | ||
| 21 | 3. Select the scope of content to export, such as =Tab= or =Page=. | ||
| 22 | 4. Name and save the file in an easy to remember location. I recommend | ||
| 23 | your Downloads or Desktop folder. | ||
| 24 | |||
| 25 | See below for a visual walkthrough of the export process. | ||
| 26 | |||
| 27 | * Download Pandoc | ||
| 28 | |||
| 29 | Start by downloading Pandoc from their | ||
| 30 | [[https://github.com/jgm/pandoc/releases][GitHub releases]] page. I | ||
| 31 | cannot install =.msi= files on my corporate laptop, so I downloaded the | ||
| 32 | =pandoc-3.1.12.3-windows-x86_64.zip= file, which contains a simple | ||
| 33 | =.exe= file that you do not need to install - you will simply run it | ||
| 34 | from the command line below. | ||
| 35 | |||
| 36 | Once downloaded, unzip the archive and move the =pandoc.exe= file to the | ||
| 37 | same folder where your Word documents were saved above. If you prefer, | ||
| 38 | you can move this file to an easier location, such as | ||
| 39 | =C:\Users\youruser\Downloads=. | ||
| 40 | |||
| 41 | * Convert Word to Markdown | ||
| 42 | |||
| 43 | In this example, I will be converting the Word documents to Markdown, | ||
| 44 | but Pandoc supports | ||
| 45 | [[https://github.com/jgm/pandoc?tab=readme-ov-file#the-universal-markup-converter][a | ||
| 46 | ton of different formats for conversion]]. Choose the format you prefer | ||
| 47 | and then modify the following commands as needed. | ||
| 48 | |||
| 49 | To perform the conversion, open the Command Prompt. If you can't find | ||
| 50 | it, open the start menu and search for it. | ||
| 51 | |||
| 52 | Within the command prompt, navigate to the directory where you stored | ||
| 53 | the =pandoc.exe= file and the Word documents. | ||
| 54 | |||
| 55 | #+begin_src ps1 | ||
| 56 | cd "C:\Users\yourusername\Downloads" | ||
| 57 | #+end_src | ||
| 58 | |||
| 59 | You can verify that you're in the correct directory with the =dir= | ||
| 60 | command. | ||
| 61 | |||
| 62 | #+begin_src ps1 | ||
| 63 | dir | ||
| 64 | #+end_src | ||
| 65 | |||
| 66 | Once you have verified that you have the command prompt open in the | ||
| 67 | correct directory with the =pandoc.exe= and the Word documents, you can | ||
| 68 | run the following loop to convert all Word documents to Markdown. | ||
| 69 | |||
| 70 | #+begin_src ps1 | ||
| 71 | for %f in (*.docx) do (pandoc.exe --extract-media=. --wrap=preserve "%f" -o "%f.md") | ||
| 72 | #+end_src | ||
| 73 | |||
| 74 | This loop will perform the following actions: | ||
| 75 | |||
| 76 | 1. Find all documents matching the pattern =*.docx=, which means all | ||
| 77 | Word documents ending with that file extension. | ||
| 78 | 2. Iterate through all files found in step 1. | ||
| 79 | 3. For each file, perform the pandoc command. | ||
| 80 | 4. Within the pandoc command, =--extract-media= saves all media found in | ||
| 81 | the files to the current folder, with pandoc automatically creating a | ||
| 82 | =media= subfolder to hold all images. | ||
| 83 | 5. Within the pandoc command, =--wrap=preserve= will attempt to prseerve | ||
| 84 | the wrapping from the source document. | ||
| 85 | 6. Within the pandoc command, the final step is to specify the output | ||
| 86 | path with =-o=. This option adds the =.md= file extension to | ||
| 87 | recognize the output files as Markdown files. | ||
| 88 | |||
| 89 | If you want to export to another format, simply specify the | ||
| 90 | =-f==/=--from== and =-t==/=--to== options. | ||
| 91 | |||
| 92 | For example, you can convert the Word document to org-mode. You can also | ||
| 93 | convert to one format and subsequently convert to other formats as | ||
| 94 | needed. | ||
| 95 | |||
| 96 | #+begin_src ps1 | ||
| 97 | pandoc.exe -f docx -t org file.docx | ||
| 98 | #+end_src | ||
content/blog/2024-04-08-docker-local-web-server.md deleted −140
| @@ -1,140 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-04-08 | ||
| 3 | title = "Using Docker Desktop to Create a Development Web Server" | ||
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | When developing websites locally, I often use a simple Python web server to | ||
| 9 | observe the changes. | ||
| 10 | |||
| 11 | ```sh | ||
| 12 | python3 -m http.server | ||
| 13 | ``` | ||
| 14 | |||
| 15 | However, this approach has its limitations. For example, this approach does not | ||
| 16 | enable logging or access controls. You also need to customize `SimpleHTTPServer` | ||
| 17 | if you have advanced needs from your web server. | ||
| 18 | |||
| 19 | So, I went to find an alternative that is almost as easy and far more extensible | ||
| 20 | and found Docker Desktop to be a suitable replacement. | ||
| 21 | |||
| 22 | # Docker Desktop | ||
| 23 | |||
| 24 | ## Installation | ||
| 25 | |||
| 26 | [Docker Desktop](https://www.docker.com/products/docker-desktop/) is a desktop | ||
| 27 | GUI for the phenomenal Docker container software. This allows you to manage | ||
| 28 | containers, images, volumes, environments, and extensions via an easy-to-use | ||
| 29 | GUI. | ||
| 30 | |||
| 31 | To install, open the link above and click the `Download` button for your | ||
| 32 | platform. I'm going through this process on an M2 Macbook, so I downloaded the | ||
| 33 | Mac - Apple Chip version. | ||
| 34 | |||
| 35 | Open the installer and follow the installation process until the application | ||
| 36 | finishes the installation process. | ||
| 37 | |||
| 38 |  | ||
| 40 | |||
| 41 | ## Creating an Nginx Container | ||
| 42 | |||
| 43 | I prefer to use the command line to create containers, so the following commands | ||
| 44 | will be input via the terminal. | ||
| 45 | |||
| 46 | The following command will create a container, using the `nginx` image: | ||
| 47 | |||
| 48 | 1. `-d`: Run this container as a daemon (detach) | ||
| 49 | 2. `-p`: Allocate a port in the format `<external>:<internal>` | ||
| 50 | 3. `-i`: Keep STDIN open even if not attached | ||
| 51 | 4. `-t`: Allocate a pseudo-TTY | ||
| 52 | 5. `-p`: Allocate a port in the format `<external>:<internal>` | ||
| 53 | 6. `--rm`: Remove the container once it's done running | ||
| 54 | |||
| 55 | ```sh | ||
| 56 | docker run -it --rm -d -p 8000:80 --name web nginx | ||
| 57 | ``` | ||
| 58 | |||
| 59 | You can navigate to <http://localhost:8000> to see the resulting page. | ||
| 60 | |||
| 61 |  | ||
| 63 | |||
| 64 | ## Customizing the Nginx Container | ||
| 65 | |||
| 66 | Now that I have a container running the Nginx web server, I need to link some | ||
| 67 | volumes so that I can modify the site configuration and provide the web files to | ||
| 68 | serve. | ||
| 69 | |||
| 70 | Let's start with the new command, which adds two volumes: | ||
| 71 | |||
| 72 | 1. `<your_content>:/usr/share/nginx/html`: This is the directory where you will | ||
| 73 | provide the web pages for the server to serve. | ||
| 74 | 2. `<your_config>:/etc/nginx/conf.d/default.conf`: This is the Nginx | ||
| 75 | configuration file for your site. | ||
| 76 | |||
| 77 | To see the updates, you can delete the previous container in the GUI or run | ||
| 78 | `docker stop web` to stop the container. Once stopped, you can run the new | ||
| 79 | `docker run` command below. | ||
| 80 | |||
| 81 | ```sh | ||
| 82 | docker run -it -d -p 8000:80 --name web -v ~/Source/cleberg.net/.build:/usr/share/nginx/html -v ~/Source/cleberg.net/nginx-config.conf:/etc/nginx/conf.d/default.conf nginx | ||
| 83 | ``` | ||
| 84 | |||
| 85 | Here's an example of my development configuration file. | ||
| 86 | |||
| 87 | ```conf | ||
| 88 | # nginx-config.conf | ||
| 89 | server { | ||
| 90 | server_name cleberg.net www.cleberg.net; | ||
| 91 | |||
| 92 | root /usr/share/nginx/html; | ||
| 93 | index index.html; | ||
| 94 | autoindex on; | ||
| 95 | |||
| 96 | access_log /var/log/nginx/cleberg.net.access.log; | ||
| 97 | error_log /var/log/nginx/cleberg.net.error.log; | ||
| 98 | |||
| 99 | location / { | ||
| 100 | try_files $uri $uri/ =404; | ||
| 101 | } | ||
| 102 | |||
| 103 | listen [::]:80; | ||
| 104 | listen 80; | ||
| 105 | } | ||
| 106 | ``` | ||
| 107 | |||
| 108 |  | ||
| 109 | |||
| 110 | # Customizing Deployment Actions | ||
| 111 | |||
| 112 | I am currently blogging with [weblorg](https://emacs.love/weblorg/), which uses | ||
| 113 | a custom `publish.el` file to build the static site. Within this file, I have | ||
| 114 | configured my deployment process to check for the `ENV` variable in thesh and if | ||
| 115 | it's set to `prod`, the script will set the base URLs to `https://cleberg.net`. | ||
| 116 | If not, it sets the base URLs to `localhost:8000` (which matches the port used | ||
| 117 | in the container above). | ||
| 118 | |||
| 119 | Therefore, I have modified my `build.sh` script to build with `localhost` URLs | ||
| 120 | if `ENV` is not set to `prod`. It also prevents the build process from sending | ||
| 121 | the built files to the production web server. | ||
| 122 | |||
| 123 | ```sh | ||
| 124 | #!/bin/bash | ||
| 125 | |||
| 126 | if [ "$ENV" == "prod" ]; then | ||
| 127 | echo "Environment = Production" && \ | ||
| 128 | rm -rf .build/* && \ | ||
| 129 | emacs --script publish.el && \ | ||
| 130 | scp -r .build/* ubuntu:/var/www/cleberg.net/ | ||
| 131 | else | ||
| 132 | echo "Environment = Development" && \ | ||
| 133 | rm -rf .build/* && \ | ||
| 134 | emacs --script publish.el | ||
| 135 | fi | ||
| 136 | ``` | ||
| 137 | |||
| 138 | You can modify the container in numerous ways and this approach allows you to | ||
| 139 | create complex scenarios for your web development purposes. I highly recommend | ||
| 140 | switching over to a container-based approach for simple, local web development. | ||
content/blog/2024-04-08-docker-local-web-server.org added +139
| @@ -0,0 +1,139 @@ | |||
| 1 | #+date: <2024-04-08> | ||
| 2 | #+title: Using Docker Desktop to Create a Development Web Server | ||
| 3 | #+description: | ||
| 4 | |||
| 5 | |||
| 6 | When developing websites locally, I often use a simple Python web server | ||
| 7 | to observe the changes. | ||
| 8 | |||
| 9 | #+begin_src sh | ||
| 10 | python3 -m http.server | ||
| 11 | #+end_src | ||
| 12 | |||
| 13 | However, this approach has its limitations. For example, this approach | ||
| 14 | does not enable logging or access controls. You also need to customize | ||
| 15 | =SimpleHTTPServer= if you have advanced needs from your web server. | ||
| 16 | |||
| 17 | So, I went to find an alternative that is almost as easy and far more | ||
| 18 | extensible and found Docker Desktop to be a suitable replacement. | ||
| 19 | |||
| 20 | * Docker Desktop | ||
| 21 | |||
| 22 | ** Installation | ||
| 23 | |||
| 24 | [[https://www.docker.com/products/docker-desktop/][Docker Desktop]] is a | ||
| 25 | desktop GUI for the phenomenal Docker container software. This allows | ||
| 26 | you to manage containers, images, volumes, environments, and extensions | ||
| 27 | via an easy-to-use GUI. | ||
| 28 | |||
| 29 | To install, open the link above and click the =Download= button for your | ||
| 30 | platform. I'm going through this process on an M2 Macbook, so I | ||
| 31 | downloaded the Mac - Apple Chip version. | ||
| 32 | |||
| 33 | Open the installer and follow the installation process until the | ||
| 34 | application finishes the installation process. | ||
| 35 | |||
| 36 | #+caption: Docker Desktop on macOS | ||
| 37 | [[https://img.cleberg.net/blog/20240408-docker-local-web-server/docker-desktop.png]] | ||
| 38 | |||
| 39 | ** Creating an Nginx Container | ||
| 40 | |||
| 41 | I prefer to use the command line to create containers, so the following | ||
| 42 | commands will be input via the terminal. | ||
| 43 | |||
| 44 | The following command will create a container, using the =nginx= image: | ||
| 45 | |||
| 46 | 1. =-d=: Run this container as a daemon (detach) | ||
| 47 | 2. =-p=: Allocate a port in the format =<external>:<internal>= | ||
| 48 | 3. =-i=: Keep STDIN open even if not attached | ||
| 49 | 4. =-t=: Allocate a pseudo-TTY | ||
| 50 | 5. =-p=: Allocate a port in the format =<external>:<internal>= | ||
| 51 | 6. =--rm=: Remove the container once it's done running | ||
| 52 | |||
| 53 | #+begin_src sh | ||
| 54 | docker run -it --rm -d -p 8000:80 --name web nginx | ||
| 55 | #+end_src | ||
| 56 | |||
| 57 | You can navigate to [[http://localhost:8000]] to see the resulting page. | ||
| 58 | |||
| 59 | #+caption: Default Nginx Container | ||
| 60 | [[https://img.cleberg.net/blog/20240408-docker-local-web-server/default-container.png]] | ||
| 61 | |||
| 62 | ** Customizing the Nginx Container | ||
| 63 | |||
| 64 | Now that I have a container running the Nginx web server, I need to link | ||
| 65 | some volumes so that I can modify the site configuration and provide the | ||
| 66 | web files to serve. | ||
| 67 | |||
| 68 | Let's start with the new command, which adds two volumes: | ||
| 69 | |||
| 70 | 1. =<your_content>:/usr/share/nginx/html=: This is the directory where | ||
| 71 | you will provide the web pages for the server to serve. | ||
| 72 | 2. =<your_config>:/etc/nginx/conf.d/default.conf=: This is the Nginx | ||
| 73 | configuration file for your site. | ||
| 74 | |||
| 75 | To see the updates, you can delete the previous container in the GUI or | ||
| 76 | run =docker stop web= to stop the container. Once stopped, you can run | ||
| 77 | the new =docker run= command below. | ||
| 78 | |||
| 79 | #+begin_src sh | ||
| 80 | docker run -it -d -p 8000:80 --name web -v ~/Source/cleberg.net/.build:/usr/share/nginx/html -v ~/Source/cleberg.net/nginx-config.conf:/etc/nginx/conf.d/default.conf nginx | ||
| 81 | #+end_src | ||
| 82 | |||
| 83 | Here's an example of my development configuration file. | ||
| 84 | |||
| 85 | #+begin_src conf | ||
| 86 | # nginx-config.conf | ||
| 87 | server { | ||
| 88 | server_name cleberg.net www.cleberg.net; | ||
| 89 | |||
| 90 | root /usr/share/nginx/html; | ||
| 91 | index index.html; | ||
| 92 | autoindex on; | ||
| 93 | |||
| 94 | access_log /var/log/nginx/cleberg.net.access.log; | ||
| 95 | error_log /var/log/nginx/cleberg.net.error.log; | ||
| 96 | |||
| 97 | location / { | ||
| 98 | try_files $uri $uri/ =404; | ||
| 99 | } | ||
| 100 | |||
| 101 | listen [::]:80; | ||
| 102 | listen 80; | ||
| 103 | } | ||
| 104 | #+end_src | ||
| 105 | |||
| 106 | [[https://img.cleberg.net/blog/20240408-docker-local-web-server/custom-container.png]] | ||
| 107 | |||
| 108 | * Customizing Deployment Actions | ||
| 109 | |||
| 110 | I am currently blogging with [[https://emacs.love/weblorg/][weblorg]], | ||
| 111 | which uses a custom =publish.el= file to build the static site. Within | ||
| 112 | this file, I have configured my deployment process to check for the | ||
| 113 | =ENV= variable in thesh and if it's set to =prod=, the script will set | ||
| 114 | the base URLs to =https://cleberg.net=. If not, it sets the base URLs to | ||
| 115 | =localhost:8000= (which matches the port used in the container above). | ||
| 116 | |||
| 117 | Therefore, I have modified my =build.sh= script to build with | ||
| 118 | =localhost= URLs if =ENV= is not set to =prod=. It also prevents the | ||
| 119 | build process from sending the built files to the production web server. | ||
| 120 | |||
| 121 | #+begin_src sh | ||
| 122 | #!/bin/bash | ||
| 123 | |||
| 124 | if [ "$ENV" == "prod" ]; then | ||
| 125 | echo "Environment = Production" && \ | ||
| 126 | rm -rf .build/* && \ | ||
| 127 | emacs --script publish.el && \ | ||
| 128 | scp -r .build/* ubuntu:/var/www/cleberg.net/ | ||
| 129 | else | ||
| 130 | echo "Environment = Development" && \ | ||
| 131 | rm -rf .build/* && \ | ||
| 132 | emacs --script publish.el | ||
| 133 | fi | ||
| 134 | #+end_src | ||
| 135 | |||
| 136 | You can modify the container in numerous ways and this approach allows | ||
| 137 | you to create complex scenarios for your web development purposes. I | ||
| 138 | highly recommend switching over to a container-based approach for | ||
| 139 | simple, local web development. | ||
content/blog/2024-04-18-mu4e.md → content/blog/2024-04-18-mu4e.org renamed +97 −93
| @@ -1,83 +1,82 @@ | |||
| 1 | +++ | 1 | #+date: <2024-04-18> |
| 2 | date = 2024-04-18 | 2 | #+title: Email in Doom Emacs with Mu4e on macOS |
| 3 | title = "Email in Doom Emacs with Mu4e on macOS" | 3 | #+description: |
| 4 | description = "" | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | 4 | ||
| 8 | This post was heavily inspired by [Email setup in Emacs with Mu4e on | ||
| 9 | macOS](https://macowners.club/posts/email-emacs-mu4e-macos/), but with my own | ||
| 10 | tweaks for a single-account configuration and some Doom-specific configurations. | ||
| 11 | 5 | ||
| 12 | # Overview | 6 | This post was heavily inspired by |
| 7 | [[https://macowners.club/posts/email-emacs-mu4e-macos/][Email setup in | ||
| 8 | Emacs with Mu4e on macOS]], but with my own tweaks for a single-account | ||
| 9 | configuration and some Doom-specific configurations. | ||
| 13 | 10 | ||
| 14 | [Mu4e](https://github.com/emacsmirror/mu4e) is an Emacs-based email client based | 11 | * Overview |
| 15 | on [mu](https://www.djcbsoftware.nl/code/mu/), an indexer that stores email in | ||
| 16 | the Maildir format. | ||
| 17 | 12 | ||
| 18 | This blog post covers the installation of Mu4e in Doom Emacs on macOS. This | 13 | [[https://github.com/emacsmirror/mu4e][Mu4e]] is an Emacs-based email |
| 19 | guide should be very similar to GNU Emacs and Linux-based systems, with a few | 14 | client based on [[https://www.djcbsoftware.nl/code/mu/][mu]], an indexer |
| 20 | tweaks required in the various configuration files. | 15 | that stores email in the Maildir format. |
| 21 | 16 | ||
| 22 | # Installation | 17 | This blog post covers the installation of Mu4e in Doom Emacs on macOS. |
| 18 | This guide should be very similar to GNU Emacs and Linux-based systems, | ||
| 19 | with a few tweaks required in the various configuration files. | ||
| 23 | 20 | ||
| 24 | ## Prerequisites | 21 | * Installation |
| 25 | 22 | ||
| 26 | ### Create the Folder Structure | 23 | ** Prerequisites |
| 24 | |||
| 25 | *** Create the Folder Structure | ||
| 27 | 26 | ||
| 28 | Start by creating the base folder structure: | 27 | Start by creating the base folder structure: |
| 29 | 28 | ||
| 30 | ```sh | 29 | #+begin_src sh |
| 31 | mkdir ~/.maildir | 30 | mkdir ~/.maildir |
| 32 | mkdir ~/.maildir/example # use whatever name you want to call your email account | 31 | mkdir ~/.maildir/example # use whatever name you want to call your email account |
| 33 | mkdir ~/.maildir/certificates # used to store system root certificates | 32 | mkdir ~/.maildir/certificates # used to store system root certificates |
| 34 | ``` | 33 | #+end_src |
| 35 | 34 | ||
| 36 | ### Store Email Account Passwords in macOS Keychain | 35 | *** Store Email Account Passwords in macOS Keychain |
| 37 | 36 | ||
| 38 | Next, I will be using the macOS Keychain to store my email account passwords | 37 | Next, I will be using the macOS Keychain to store my email account |
| 39 | using the command below. | 38 | passwords using the command below. |
| 40 | 39 | ||
| 41 | ```sh | 40 | #+begin_src sh |
| 42 | security add-generic-password -s mu4e-example -a you@example.com -w | 41 | security add-generic-password -s mu4e-example -a you@example.com -w |
| 43 | ``` | 42 | #+end_src |
| 44 | 43 | ||
| 45 | This will prompt you to input your password twice to confirm. Keep the | 44 | This will prompt you to input your password twice to confirm. Keep the |
| 46 | `mu4e-example` name in mind, as you will need to reference it later in the IMAP | 45 | =mu4e-example= name in mind, as you will need to reference it later in |
| 47 | and SMTP configuration files. | 46 | the IMAP and SMTP configuration files. |
| 48 | 47 | ||
| 49 | ### Store Root Certificates | 48 | *** Store Root Certificates |
| 50 | 49 | ||
| 51 | In order to use IMAP and SMTP, we need to provide certificates to the local | 50 | In order to use IMAP and SMTP, we need to provide certificates to the |
| 52 | services. We will use the macOS defaults for this. | 51 | local services. We will use the macOS defaults for this. |
| 53 | 52 | ||
| 54 | 1. Open `Keychain Access.app`. | 53 | 1. Open =Keychain Access.app=. |
| 55 | 2. Select `System Roots` in the sidebar. | 54 | 2. Select =System Roots= in the sidebar. |
| 56 | 3. Select all items with `CMD + a`. | 55 | 3. Select all items with =CMD + a=. |
| 57 | 4. Export selected items with `SHIFT + CMD + a` to the file | 56 | 4. Export selected items with =SHIFT + CMD + a= to the file |
| 58 | `~/.maildir/certificates/root-certificates.pem`. | 57 | =~/.maildir/certificates/root-certificates.pem=. |
| 59 | 58 | ||
| 60 | ### Install Dependencies | 59 | *** Install Dependencies |
| 61 | 60 | ||
| 62 | Install `mbsync` (via `isync`) to fetch emails via IMAP, `mu` to index emails, | 61 | Install =mbsync= (via =isync=) to fetch emails via IMAP, =mu= to index |
| 63 | and `msmtp` to send emails via SMTP. | 62 | emails, and =msmtp= to send emails via SMTP. |
| 64 | 63 | ||
| 65 | ```sh | 64 | #+begin_src sh |
| 66 | brew install mu isync msmtp | 65 | brew install mu isync msmtp |
| 67 | ``` | 66 | #+end_src |
| 68 | 67 | ||
| 69 | ## Installing Mu4e | 68 | ** Installing Mu4e |
| 70 | 69 | ||
| 71 | Within Doom Emacs, we can install Mu4e by enabling the package. | 70 | Within Doom Emacs, we can install Mu4e by enabling the package. |
| 72 | 71 | ||
| 73 | ```sh | 72 | #+begin_src sh |
| 74 | nano ~/.doom.d/init.el | 73 | nano ~/.doom.d/init.el |
| 75 | ``` | 74 | #+end_src |
| 76 | 75 | ||
| 77 | In this file, uncomment the `mu4e` line within the `:email` section. You can | 76 | In this file, uncomment the =mu4e= line within the =:email= section. You |
| 78 | also enable the `+org` and `+gmail` options if you prefer. | 77 | can also enable the =+org= and =+gmail= options if you prefer. |
| 79 | 78 | ||
| 80 | ```lisp | 79 | #+begin_src lisp |
| 81 | (doom! :input | 80 | (doom! :input |
| 82 | ... | 81 | ... |
| 83 | 82 | ||
| @@ -85,34 +84,35 @@ also enable the `+org` and `+gmail` options if you prefer. | |||
| 85 | ;;(mu4e +org +gmail) | 84 | ;;(mu4e +org +gmail) |
| 86 | ... | 85 | ... |
| 87 | (default +bindings +smartparens)) | 86 | (default +bindings +smartparens)) |
| 88 | ``` | 87 | #+end_src |
| 89 | 88 | ||
| 90 | # Configuration | 89 | * Configuration |
| 91 | 90 | ||
| 92 | As an overall suggestion, I create the following configuration files in the | 91 | As an overall suggestion, I create the following configuration files in |
| 93 | `~/.maildir` directory and using symlinks to their proper locations so that I | 92 | the =~/.maildir= directory and using symlinks to their proper locations |
| 94 | can backup and restore these files easily. | 93 | so that I can backup and restore these files easily. |
| 95 | 94 | ||
| 96 | ```sh | 95 | #+begin_src sh |
| 97 | touch ~/.maildir/.mbsyncrc && \ | 96 | touch ~/.maildir/.mbsyncrc && \ |
| 98 | touch ~/.maildir/.msmtprc && \ | 97 | touch ~/.maildir/.msmtprc && \ |
| 99 | ln -s /Users/username/.maildir/.mbsyncrc /Users/username/.mbsyncrc && \ | 98 | ln -s /Users/username/.maildir/.mbsyncrc /Users/username/.mbsyncrc && \ |
| 100 | ln -s /Users/username/.maildir/.msmtprc /Users/username/.msmtprc | 99 | ln -s /Users/username/.maildir/.msmtprc /Users/username/.msmtprc |
| 101 | ``` | 100 | #+end_src |
| 102 | 101 | ||
| 103 | You can also create these files in your home directory and skip the symlinking | 102 | You can also create these files in your home directory and skip the |
| 104 | process above. | 103 | symlinking process above. |
| 105 | 104 | ||
| 106 | ## IMAP | 105 | ** IMAP |
| 107 | 106 | ||
| 108 | Next, let's configure `mbsync` in the file created above. Paste the following | 107 | Next, let's configure =mbsync= in the file created above. Paste the |
| 109 | information and customize it to match your mail provider's information. | 108 | following information and customize it to match your mail provider's |
| 109 | information. | ||
| 110 | 110 | ||
| 111 | ```sh | 111 | #+begin_src sh |
| 112 | nano ~/.maildir/.mbsyncrc | 112 | nano ~/.maildir/.mbsyncrc |
| 113 | ``` | 113 | #+end_src |
| 114 | 114 | ||
| 115 | ```conf | 115 | #+begin_src conf |
| 116 | IMAPAccount example | 116 | IMAPAccount example |
| 117 | Host imap.example.com | 117 | Host imap.example.com |
| 118 | User dummy@example.com | 118 | User dummy@example.com |
| @@ -138,18 +138,19 @@ Create Near | |||
| 138 | Sync All | 138 | Sync All |
| 139 | Expunge Both | 139 | Expunge Both |
| 140 | SyncState * | 140 | SyncState * |
| 141 | ``` | 141 | #+end_src |
| 142 | 142 | ||
| 143 | ## SMTP | 143 | ** SMTP |
| 144 | 144 | ||
| 145 | Next, let's configured `msmtprc` in the file created above. Paste the following | 145 | Next, let's configured =msmtprc= in the file created above. Paste the |
| 146 | information and customize it to match your mail provider's information. | 146 | following information and customize it to match your mail provider's |
| 147 | information. | ||
| 147 | 148 | ||
| 148 | ```sh | 149 | #+begin_src sh |
| 149 | nano ~/.maildir/.msmtprc | 150 | nano ~/.maildir/.msmtprc |
| 150 | ``` | 151 | #+end_src |
| 151 | 152 | ||
| 152 | ```conf | 153 | #+begin_src conf |
| 153 | # Set default values for all the accounts. | 154 | # Set default values for all the accounts. |
| 154 | defaults | 155 | defaults |
| 155 | logfile ~/.maildir/msmtp.log | 156 | logfile ~/.maildir/msmtp.log |
| @@ -171,18 +172,18 @@ tls_starttls off | |||
| 171 | # ====================================================================== | 172 | # ====================================================================== |
| 172 | 173 | ||
| 173 | account default : startmail | 174 | account default : startmail |
| 174 | ``` | 175 | #+end_src |
| 175 | 176 | ||
| 176 | ## Doom Emacs | 177 | ** Doom Emacs |
| 177 | 178 | ||
| 178 | Finally, we need to configure Doom Emacs to use the proper packages and set some | 179 | Finally, we need to configure Doom Emacs to use the proper packages and |
| 179 | variables and functions. | 180 | set some variables and functions. |
| 180 | 181 | ||
| 181 | ```sh | 182 | #+begin_src sh |
| 182 | nano ~/.doom.d/config.el | 183 | nano ~/.doom.d/config.el |
| 183 | ``` | 184 | #+end_src |
| 184 | 185 | ||
| 185 | ```lisp | 186 | #+begin_src lisp |
| 186 | ;; load packages and programs | 187 | ;; load packages and programs |
| 187 | (use-package mu4e | 188 | (use-package mu4e |
| 188 | :load-path "/Users/username/.emacs.d/modules/email/mu4e/") | 189 | :load-path "/Users/username/.emacs.d/modules/email/mu4e/") |
| @@ -266,40 +267,43 @@ nano ~/.doom.d/config.el | |||
| 266 | 267 | ||
| 267 | ;; mu4e address completion | 268 | ;; mu4e address completion |
| 268 | (add-hook 'mu4e-compose-mode-hook 'company-mode) | 269 | (add-hook 'mu4e-compose-mode-hook 'company-mode) |
| 269 | ``` | 270 | #+end_src |
| 270 | 271 | ||
| 271 | Be sure to sync Doom to update the current configurations. | 272 | Be sure to sync Doom to update the current configurations. |
| 272 | 273 | ||
| 273 | ```sh | 274 | #+begin_src sh |
| 274 | doom sync | 275 | doom sync |
| 275 | ``` | 276 | #+end_src |
| 276 | 277 | ||
| 277 | If you have Doom open, execute `SPC h r r` to reload the new configurations. | 278 | If you have Doom open, execute =SPC h r r= to reload the new |
| 279 | configurations. | ||
| 278 | 280 | ||
| 279 | # Initial Sync | 281 | * Initial Sync |
| 280 | 282 | ||
| 281 | Once you have configured all of the relevant files, you can perform an initial | 283 | Once you have configured all of the relevant files, you can perform an |
| 282 | sync. Note that you can perform syncing within Mu4e itself after this. | 284 | initial sync. Note that you can perform syncing within Mu4e itself after |
| 285 | this. | ||
| 283 | 286 | ||
| 284 | ```sh | 287 | #+begin_src sh |
| 285 | mbsync -aV | 288 | mbsync -aV |
| 286 | ``` | 289 | #+end_src |
| 287 | 290 | ||
| 288 | Once you sync the data, you can index the emails. | 291 | Once you sync the data, you can index the emails. |
| 289 | 292 | ||
| 290 | ```sh | 293 | #+begin_src sh |
| 291 | mu init -m ~/.maildir --my-address you@example.com && \ | 294 | mu init -m ~/.maildir --my-address you@example.com && \ |
| 292 | mu index | 295 | mu index |
| 293 | ``` | 296 | #+end_src |
| 294 | 297 | ||
| 295 | The emails will now to be ready to use! | 298 | The emails will now to be ready to use! |
| 296 | 299 | ||
| 297 | # Screenshots | 300 | * Screenshots |
| 298 | |||
| 299 | You can now launch Doom and open Mu4e with `SPC o m`. You can also explore the | ||
| 300 | Mu4e options with `SPC : mu4e`. | ||
| 301 | 301 | ||
| 302 | The home page shows various options and metadata about the account you've opened. | 302 | You can now launch Doom and open Mu4e with =SPC o m=. You can also |
| 303 | explore the Mu4e options with =SPC : mu4e=. | ||
| 303 | 304 | ||
| 304 |  | 305 | The home page shows various options and metadata about the account |
| 306 | you've opened. | ||
| 305 | 307 | ||
| 308 | #+caption: Mu4e Home Page | ||
| 309 | [[https://img.cleberg.net/blog/20240418-mu4e/mu4e.png]] | ||
content/blog/2024-05-03-ubuntu-on-macos.md deleted −96
| @@ -1,96 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-05-03 | ||
| 3 | title = "Running Ubuntu Linux on macOS with OrbStack" | ||
| 4 | description = "Learn how to run Linux machines as applications on macOS with minimal hassle." | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | Being a macOS user who previously used Linux for many years, I often find myself | ||
| 9 | searching for alternatives to the Linux-native tools and methods that I had | ||
| 10 | become used to over the years. | ||
| 11 | |||
| 12 | Luckily, there's an option to simply use Linux directly on your macOS device | ||
| 13 | without having to boot the whole device into Linux or having to SSH into another | ||
| 14 | Linux machine. | ||
| 15 | |||
| 16 | In this post, I use [OrbStack](https://orbstack.dev/) to create an Ubuntu Linux | ||
| 17 | machine to take advantage of the Linux system. | ||
| 18 | |||
| 19 | ## Installing OrbStack | ||
| 20 | |||
| 21 | OrbStack has a [download page](https://orbstack.dev/download) if you prefer to | ||
| 22 | use `*.dmg` files to install applications. | ||
| 23 | |||
| 24 | Personally, I prefer to install everything through Homebrew. For OrbStack, run | ||
| 25 | the following to install the application via Homebrew: | ||
| 26 | |||
| 27 | ```sh | ||
| 28 | brew install orbstack | ||
| 29 | ``` | ||
| 30 | |||
| 31 | ## Creating a Linux Machine | ||
| 32 | |||
| 33 | Once installed, you can launch OrbStack and create a Linux machine in a matter | ||
| 34 | of seconds. The creation dialog defaults to Ubuntu. | ||
| 35 | |||
| 36 |  | ||
| 38 | |||
| 39 | ## Launching the Machine | ||
| 40 | |||
| 41 | Once created, the dialog will close and you will be presented with your new list | ||
| 42 | of machines. Simply double-click on a machine to run it. | ||
| 43 | |||
| 44 | When you run the machine for the first time, it may warn you that a program is | ||
| 45 | attempting to run a shell script. You can choose to manually inspect this shell | ||
| 46 | script via the provided path if you wish. When you're comfortable with the | ||
| 47 | script, you can toggle the `Suppress this message permanently` button and click | ||
| 48 | OK to run the machine. | ||
| 49 | |||
| 50 | > **Quick Tip**! | ||
| 51 | > | ||
| 52 | > You will always be able to launch the OrbStack application and | ||
| 53 | > control the machine via the Machines page, but you can also use the OrbStack | ||
| 54 | > icon in the macOS menu bar to quickly control the machine without launching | ||
| 55 | > OrbStack itself. | ||
| 56 | |||
| 57 | ## Working in Linux | ||
| 58 | |||
| 59 | Once successful, OrbStack will launch your default terminal and log you into the | ||
| 60 | Linux machine you created above. | ||
| 61 | |||
| 62 | In the example below, we can see that the machine is using `Linux ubuntu 6.9.6` | ||
| 63 | and operates similar to a fresh Ubuntu install on bare metal. | ||
| 64 | |||
| 65 |  | ||
| 66 | |||
| 67 | You can also click the machine's folder icon in OrbStack to open the machine's | ||
| 68 | file system within macOS's native Files app. Within Files, you can double-click | ||
| 69 | to open any files and they will open in the macOS defaults for those file types | ||
| 70 | (e.g., TextEdit). | ||
| 71 | |||
| 72 |  | ||
| 73 | |||
| 74 | Here's an example of opening the `test.md` file in the screenshot above, which | ||
| 75 | defaults to opening in Xcode on my system. | ||
| 76 | |||
| 77 |  | ||
| 78 | |||
| 79 | ### Test Examples | ||
| 80 | |||
| 81 | As a quick example of the system, I installed `neofetch`, switched to the `zsh` | ||
| 82 | shell, and ran a few common commands. | ||
| 83 | |||
| 84 |  | ||
| 85 | |||
| 86 | ## Thoughts | ||
| 87 | |||
| 88 | The installation, creation, and launch processes are seamless and provide an | ||
| 89 | easy way to test or use Linux on macOS with little to no hassle. | ||
| 90 | |||
| 91 | The shell and file integrations are incredibly snappy and I have not experienced | ||
| 92 | any lag so far. Further, being able to open files in the Files app allows for me | ||
| 93 | to use macOS native applications, such as TextEdit for quick editing without | ||
| 94 | having to resort to other programs that may not be as familiar to macOS users. | ||
| 95 | |||
| 96 | I love this method and will be happily looking for further use cases to apply. | ||
content/blog/2024-05-03-ubuntu-on-macos.org added +105
| @@ -0,0 +1,105 @@ | |||
| 1 | #+date: <2024-05-03> | ||
| 2 | #+title: Running Ubuntu Linux on macOS with OrbStack | ||
| 3 | #+description: Learn how to run Linux machines as applications on macOS with minimal hassle. | ||
| 4 | |||
| 5 | |||
| 6 | Being a macOS user who previously used Linux for many years, I often | ||
| 7 | find myself searching for alternatives to the Linux-native tools and | ||
| 8 | methods that I had become used to over the years. | ||
| 9 | |||
| 10 | Luckily, there's an option to simply use Linux directly on your macOS | ||
| 11 | device without having to boot the whole device into Linux or having to | ||
| 12 | SSH into another Linux machine. | ||
| 13 | |||
| 14 | In this post, I use [[https://orbstack.dev/][OrbStack]] to create an | ||
| 15 | Ubuntu Linux machine to take advantage of the Linux system. | ||
| 16 | |||
| 17 | ** Installing OrbStack | ||
| 18 | |||
| 19 | OrbStack has a [[https://orbstack.dev/download][download page]] if you | ||
| 20 | prefer to use =*.dmg= files to install applications. | ||
| 21 | |||
| 22 | Personally, I prefer to install everything through Homebrew. For | ||
| 23 | OrbStack, run the following to install the application via Homebrew: | ||
| 24 | |||
| 25 | #+begin_src sh | ||
| 26 | brew install orbstack | ||
| 27 | #+end_src | ||
| 28 | |||
| 29 | ** Creating a Linux Machine | ||
| 30 | |||
| 31 | Once installed, you can launch OrbStack and create a Linux machine in a | ||
| 32 | matter of seconds. The creation dialog defaults to Ubuntu. | ||
| 33 | |||
| 34 | #+caption: OrbStack's Linux Machine Creation Page | ||
| 35 | [[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/create.png]] | ||
| 36 | |||
| 37 | ** Launching the Machine | ||
| 38 | |||
| 39 | Once created, the dialog will close and you will be presented with your | ||
| 40 | new list of machines. Simply double-click on a machine to run it. | ||
| 41 | |||
| 42 | When you run the machine for the first time, it may warn you that a | ||
| 43 | program is attempting to run a shell script. You can choose to manually | ||
| 44 | inspect this shell script via the provided path if you wish. When you're | ||
| 45 | comfortable with the script, you can toggle the | ||
| 46 | =Suppress this message permanently= button and click OK to run the | ||
| 47 | machine. | ||
| 48 | |||
| 49 | #+begin_quote | ||
| 50 | *Quick Tip*! | ||
| 51 | |||
| 52 | You will always be able to launch the OrbStack application and control | ||
| 53 | the machine via the Machines page, but you can also use the OrbStack | ||
| 54 | icon in the macOS menu bar to quickly control the machine without | ||
| 55 | launching OrbStack itself. | ||
| 56 | #+end_quote | ||
| 57 | |||
| 58 | ** Working in Linux | ||
| 59 | |||
| 60 | Once successful, OrbStack will launch your default terminal and log you | ||
| 61 | into the Linux machine you created above. | ||
| 62 | |||
| 63 | In the example below, we can see that the machine is using | ||
| 64 | =Linux ubuntu 6.9.6= and operates similar to a fresh Ubuntu install on | ||
| 65 | bare metal. | ||
| 66 | |||
| 67 | #+caption: Ubuntu CLI | ||
| 68 | [[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/cli.png]] | ||
| 69 | |||
| 70 | You can also click the machine's folder icon in OrbStack to open the | ||
| 71 | machine's file system within macOS's native Files app. Within Files, you | ||
| 72 | can double-click to open any files and they will open in the macOS | ||
| 73 | defaults for those file types (e.g., TextEdit). | ||
| 74 | |||
| 75 | #+caption: Ubuntu Files | ||
| 76 | [[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/files.png]] | ||
| 77 | |||
| 78 | Here's an example of opening the =test.md= file in the screenshot above, | ||
| 79 | which defaults to opening in Xcode on my system. | ||
| 80 | |||
| 81 | #+caption: Ubuntu Files | ||
| 82 | [[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/xcode.png]] | ||
| 83 | |||
| 84 | *** Test Examples | ||
| 85 | |||
| 86 | As a quick example of the system, I installed =neofetch=, switched to | ||
| 87 | the =zsh= shell, and ran a few common commands. | ||
| 88 | |||
| 89 | #+caption: Test Examples | ||
| 90 | [[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/test.png]] | ||
| 91 | |||
| 92 | ** Thoughts | ||
| 93 | |||
| 94 | The installation, creation, and launch processes are seamless and | ||
| 95 | provide an easy way to test or use Linux on macOS with little to no | ||
| 96 | hassle. | ||
| 97 | |||
| 98 | The shell and file integrations are incredibly snappy and I have not | ||
| 99 | experienced any lag so far. Further, being able to open files in the | ||
| 100 | Files app allows for me to use macOS native applications, such as | ||
| 101 | TextEdit for quick editing without having to resort to other programs | ||
| 102 | that may not be as familiar to macOS users. | ||
| 103 | |||
| 104 | I love this method and will be happily looking for further use cases to | ||
| 105 | apply. | ||
content/blog/2024-06-19-deprecated-trusted-gpg-fix.md → content/blog/2024-06-19-deprecated-trusted-gpg-fix.org renamed +56 −56
| @@ -1,49 +1,49 @@ | |||
| 1 | +++ | 1 | #+date: <2024-06-19 08:00:00> |
| 2 | date = 2024-06-19 08:00:00 | 2 | #+title: Fixing Ubuntu Error: 'Key is stored in legacy trusted.gpg keyring' |
| 3 | title = "Fixing Ubuntu Error: 'Key is stored in legacy trusted.gpg keyring'" | 3 | #+description: Learn how to update GPG keys from the trusted.gpg keyring in Ubuntu. |
| 4 | description = "Learn how to update GPG keys from the trusted.gpg keyring in Ubuntu." | ||
| 5 | +++ | ||
| 6 | 4 | ||
| 7 | ## System Warning | ||
| 8 | 5 | ||
| 9 | When running an update on an Ubuntu system, you may have run into a system | 6 | ** System Warning |
| 10 | warning that looks like the example below. | ||
| 11 | 7 | ||
| 12 | ```txt | 8 | When running an update on an Ubuntu system, you may have run into a |
| 9 | system warning that looks like the example below. | ||
| 10 | |||
| 11 | #+begin_src txt | ||
| 13 | W: https://dl.yarnpkg.com/debian/dists/stable/InRelease: Key is stored in legacy | 12 | W: https://dl.yarnpkg.com/debian/dists/stable/InRelease: Key is stored in legacy |
| 14 | trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in | 13 | trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in |
| 15 | apt-key(8) for details. | 14 | apt-key(8) for details. |
| 16 | ``` | 15 | #+end_src |
| 17 | 16 | ||
| 18 | While this example references the `yarn` package, the warning message is the | 17 | While this example references the =yarn= package, the warning message is |
| 19 | same for any repository using the deprecated `trusted.gpg` key ring. | 18 | the same for any repository using the deprecated =trusted.gpg= key ring. |
| 20 | 19 | ||
| 21 | The issue arises from managing keys with the `apt-key` command, which utilizes | 20 | The issue arises from managing keys with the =apt-key= command, which |
| 22 | the `/etc/apt/trusted.gpg` file by default. Instead, Ubuntu has moved to | 21 | utilizes the =/etc/apt/trusted.gpg= file by default. Instead, Ubuntu has |
| 23 | managing key rings with individual `.gpg` files in the `/etc/apt/trusted.gpg.d/` | 22 | moved to managing key rings with individual =.gpg= files in the |
| 24 | directory. | 23 | =/etc/apt/trusted.gpg.d/= directory. |
| 25 | 24 | ||
| 26 | To fix this issue, let's check to see which keys are using the `trusted.gpg` key | 25 | To fix this issue, let's check to see which keys are using the |
| 27 | ring and move them into their own dedicated key ring. | 26 | =trusted.gpg= key ring and move them into their own dedicated key ring. |
| 28 | 27 | ||
| 29 | ## Finding All Keys in the Keyring | 28 | ** Finding All Keys in the Keyring |
| 30 | 29 | ||
| 31 | Let's start by simply listing the keys used by the `apt` commands. To do this, | 30 | Let's start by simply listing the keys used by the =apt= commands. To do |
| 32 | run the following command. | 31 | this, run the following command. |
| 33 | 32 | ||
| 34 | ```sh | 33 | #+begin_src sh |
| 35 | sudo apt-key list | 34 | sudo apt-key list |
| 36 | ``` | 35 | #+end_src |
| 37 | 36 | ||
| 38 | This command will show an output similar to the one below. You may see | 37 | This command will show an output similar to the one below. You may see |
| 39 | additional keys in the `/etc/apt/trusted.gpg.d/` directory - this is where we | 38 | additional keys in the =/etc/apt/trusted.gpg.d/= directory - this is |
| 40 | will be moving any keys currently found in the `trusted.gpg` key ring. | 39 | where we will be moving any keys currently found in the =trusted.gpg= |
| 40 | key ring. | ||
| 41 | 41 | ||
| 42 | In the below example, we can see that this system has four different GPG keys | 42 | In the below example, we can see that this system has four different GPG |
| 43 | stored within the `trusted.gpg` key ring. Let's go ahead and move them into | 43 | keys stored within the =trusted.gpg= key ring. Let's go ahead and move |
| 44 | their own files. | 44 | them into their own files. |
| 45 | 45 | ||
| 46 | ```txt | 46 | #+begin_src txt |
| 47 | Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead | 47 | Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead |
| 48 | (see apt-key(8)). | 48 | (see apt-key(8)). |
| 49 | 49 | ||
| @@ -67,32 +67,32 @@ uid [ unknown] nginx signing key <signing-key-2@nginx.com> | |||
| 67 | pub rsa4096 2024-05-29 [SC] | 67 | pub rsa4096 2024-05-29 [SC] |
| 68 | 9E9B E90E ACBC DE69 FE9B 204C BCDC D8A3 8D88 A2B3 | 68 | 9E9B E90E ACBC DE69 FE9B 204C BCDC D8A3 8D88 A2B3 |
| 69 | uid [ unknown] nginx signing key <signing-key-3@nginx.com> | 69 | uid [ unknown] nginx signing key <signing-key-3@nginx.com> |
| 70 | ``` | 70 | #+end_src |
| 71 | 71 | ||
| 72 | ## Moving Keys to the Proper Location | 72 | ** Moving Keys to the Proper Location |
| 73 | 73 | ||
| 74 | ### Exporting Keys to New Files | 74 | *** Exporting Keys to New Files |
| 75 | 75 | ||
| 76 | Now that we know the keys, we will need to move them into their own key ring. We | 76 | Now that we know the keys, we will need to move them into their own key |
| 77 | can do this by copying the last eight (8) characters from the key's signature | 77 | ring. We can do this by copying the last eight (8) characters from the |
| 78 | and exporting it from this key ring into its own. | 78 | key's signature and exporting it from this key ring into its own. |
| 79 | 79 | ||
| 80 | Using the yarn example from the beginning, here's the command to move this key | 80 | Using the yarn example from the beginning, here's the command to move |
| 81 | into its own key ring. | 81 | this key into its own key ring. |
| 82 | 82 | ||
| 83 | ```sh | 83 | #+begin_src sh |
| 84 | sudo apt-key export 86E50310 | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/yarn.gpg | 84 | sudo apt-key export 86E50310 | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/yarn.gpg |
| 85 | ``` | 85 | #+end_src |
| 86 | 86 | ||
| 87 | You can repeat this process for any other keys, such as the `nginx` keys in the | 87 | You can repeat this process for any other keys, such as the =nginx= keys |
| 88 | example above. | 88 | in the example above. |
| 89 | 89 | ||
| 90 | ### Cleaning Up | 90 | *** Cleaning Up |
| 91 | 91 | ||
| 92 | If you run `sudo apt-key list` again, you should see the keys within their own | 92 | If you run =sudo apt-key list= again, you should see the keys within |
| 93 | key rings: | 93 | their own key rings: |
| 94 | 94 | ||
| 95 | ```txt | 95 | #+begin_src txt |
| 96 | /etc/apt/trusted.gpg.d/nginx-archive-keyring.gpg | 96 | /etc/apt/trusted.gpg.d/nginx-archive-keyring.gpg |
| 97 | ------------------------------------------------ | 97 | ------------------------------------------------ |
| 98 | pub rsa4096 2024-05-29 [SC] | 98 | pub rsa4096 2024-05-29 [SC] |
| @@ -115,19 +115,19 @@ uid [ unknown] Yarn Packaging <yarn@dan.cx> | |||
| 115 | sub rsa4096 2016-10-05 [E] | 115 | sub rsa4096 2016-10-05 [E] |
| 116 | sub rsa4096 2019-01-02 [S] [expires: 2026-01-23] | 116 | sub rsa4096 2019-01-02 [S] [expires: 2026-01-23] |
| 117 | sub rsa4096 2019-01-11 [S] [expires: 2026-01-23] | 117 | sub rsa4096 2019-01-11 [S] [expires: 2026-01-23] |
| 118 | ``` | 118 | #+end_src |
| 119 | 119 | ||
| 120 | Once you have verified that the keys are valid and stored in their own key | 120 | Once you have verified that the keys are valid and stored in their own |
| 121 | rings, you can archive the `trusted.gpg` file and run a system update to test | 121 | key rings, you can archive the =trusted.gpg= file and run a system |
| 122 | the new files. | 122 | update to test the new files. |
| 123 | 123 | ||
| 124 | ```sh | 124 | #+begin_src sh |
| 125 | sudo mv /etc/apt/trusted.gpg /etc/apt/trusted.gpg.bkp | 125 | sudo mv /etc/apt/trusted.gpg /etc/apt/trusted.gpg.bkp |
| 126 | sudo apt update | 126 | sudo apt update |
| 127 | ``` | 127 | #+end_src |
| 128 | |||
| 129 | Once you've verified that updates work as expected and that the keys are working | ||
| 130 | as intended, you can delete the `.bkp` file created above. If you're storing | ||
| 131 | keys that are not easily re-attainable, I suggest keeping the `.bkp` file stored | ||
| 132 | in a safe location until you are positive that you no longer need it. | ||
| 133 | 128 | ||
| 129 | Once you've verified that updates work as expected and that the keys are | ||
| 130 | working as intended, you can delete the =.bkp= file created above. If | ||
| 131 | you're storing keys that are not easily re-attainable, I suggest keeping | ||
| 132 | the =.bkp= file stored in a safe location until you are positive that | ||
| 133 | you no longer need it. | ||
content/blog/2024-07-11-emacs-on-ipad.md deleted −65
| @@ -1,65 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | date = 2024-07-11T20:24:02 | ||
| 3 | title = "Emacs on iPadOS" | ||
| 4 | description = "Learn how to install and use Emacs on the Apple Silicon iPad natively." | ||
| 5 | draft = false | ||
| 6 | +++ | ||
| 7 | |||
| 8 | This post describes the process to install and use Emacs on the iPad Air 13-inch | ||
| 9 | (M2). The iPad used in this post is running iPadOS 17.6. | ||
| 10 | |||
| 11 | ## Shell Application | ||
| 12 | |||
| 13 | In order to use Emacs on an iPad, you will need a terminal emulator application. | ||
| 14 | I recommend [iSH](https://apps.apple.com/us/app/ish-shell/id1436902243), since | ||
| 15 | it runs a version of Alpine Linux within the app itself and will allow you to | ||
| 16 | install packages that you need. | ||
| 17 | |||
| 18 |  | ||
| 19 | |||
| 20 | ## Require Packages | ||
| 21 | |||
| 22 | I started by adding the required packages directly within iSH. Emacs should | ||
| 23 | install dependencies by default, but I include a few other packages that I use | ||
| 24 | in my terminal as well. | ||
| 25 | |||
| 26 | ```sh | ||
| 27 | apk add emacs ripgrep fd findutils | ||
| 28 | ``` | ||
| 29 | |||
| 30 |  | ||
| 32 | |||
| 33 | ## Emacs | ||
| 34 | |||
| 35 | Once this is complete, you should be able to run Emacs natively on your iPad. | ||
| 36 | It's effective, but can be slow at times. | ||
| 37 | |||
| 38 | I attempted to also install Doom Emacs, which technically worked, but was so | ||
| 39 | incredibly slow and buggy that I was not even able to take screenshots. Someone | ||
| 40 | smarter than me could likely get it to work with a little tinkering. | ||
| 41 | |||
| 42 |  | ||
| 43 | |||
| 44 | ### MELPA | ||
| 45 | |||
| 46 | You also have to remember to hook up MELPA yourself in the `.emacs` file to be | ||
| 47 | able to search through their 5700+ packages instead of just ELPA packages. If | ||
| 48 | you don't, you will only have access to ELPA packages like the ones below. | ||
| 49 | |||
| 50 |  | ||
| 51 | |||
| 52 | Once you have MELPA, you can install packages like the `dashboard` package shown | ||
| 53 | below. | ||
| 54 | |||
| 55 |  | ||
| 56 | |||
| 57 | ### Speed | ||
| 58 | |||
| 59 | While Emacs will run on my iPad, it's not perfect. The largest issue on my | ||
| 60 | iPad is speed - loading Emacs takes 6-7 seconds and installing the `magit` | ||
| 61 | package took 129 seconds. | ||
| 62 | |||
| 63 | I haven't played around enough to optimize loading times and poke around to see | ||
| 64 | why the network requests take so long, but it's a big enough issue that I | ||
| 65 | wouldn't see casual Emacs users dealing with the lag. | ||
content/blog/2024-07-11-emacs-on-ipad.org added +70
| @@ -0,0 +1,70 @@ | |||
| 1 | #+date: <2024-07-11 Thu 20:24:02> | ||
| 2 | #+title: Emacs on iPadOS | ||
| 3 | #+description: Learn how to install and use Emacs on the Apple Silicon iPad natively. | ||
| 4 | |||
| 5 | |||
| 6 | This post describes the process to install and use Emacs on the iPad Air | ||
| 7 | 13-inch (M2). The iPad used in this post is running iPadOS 17.6. | ||
| 8 | |||
| 9 | ** Shell Application | ||
| 10 | |||
| 11 | In order to use Emacs on an iPad, you will need a terminal emulator | ||
| 12 | application. I recommend | ||
| 13 | [[https://apps.apple.com/us/app/ish-shell/id1436902243][iSH]], since it | ||
| 14 | runs a version of Alpine Linux within the app itself and will allow you | ||
| 15 | to install packages that you need. | ||
| 16 | |||
| 17 | #+caption: iSH Application | ||
| 18 | [[https://img.cleberg.net/blog/20240711-emacs-on-ipad/ish.png]] | ||
| 19 | |||
| 20 | ** Require Packages | ||
| 21 | |||
| 22 | I started by adding the required packages directly within iSH. Emacs | ||
| 23 | should install dependencies by default, but I include a few other | ||
| 24 | packages that I use in my terminal as well. | ||
| 25 | |||
| 26 | #+begin_src sh | ||
| 27 | apk add emacs ripgrep fd findutils | ||
| 28 | #+end_src | ||
| 29 | |||
| 30 | #+caption: Package !Installation | ||
| 31 | [[https://img.cleberg.net/blog/20240711-emacs-on-ipad/dependencies.png]] | ||
| 32 | |||
| 33 | ** Emacs | ||
| 34 | |||
| 35 | Once this is complete, you should be able to run Emacs natively on your | ||
| 36 | iPad. It's effective, but can be slow at times. | ||
| 37 | |||
| 38 | I attempted to also install Doom Emacs, which technically worked, but | ||
| 39 | was so incredibly slow and buggy that I was not even able to take | ||
| 40 | screenshots. Someone smarter than me could likely get it to work with a | ||
| 41 | little tinkering. | ||
| 42 | |||
| 43 | #+caption: Emacs | ||
| 44 | [[https://img.cleberg.net/blog/20240711-emacs-on-ipad/emacs.png]] | ||
| 45 | |||
| 46 | *** MELPA | ||
| 47 | |||
| 48 | You also have to remember to hook up MELPA yourself in the =.emacs= file | ||
| 49 | to be able to search through their 5700+ packages instead of just ELPA | ||
| 50 | packages. If you don't, you will only have access to ELPA packages like | ||
| 51 | the ones below. | ||
| 52 | |||
| 53 | #+caption: package-install | ||
| 54 | [[https://img.cleberg.net/blog/20240711-emacs-on-ipad/melpa.png]] | ||
| 55 | |||
| 56 | Once you have MELPA, you can install packages like the =dashboard= | ||
| 57 | package shown below. | ||
| 58 | |||
| 59 | #+caption: emacs-dashboard | ||
| 60 | [[https://img.cleberg.net/blog/20240711-emacs-on-ipad/dashboard.png]] | ||
| 61 | |||
| 62 | *** Speed | ||
| 63 | |||
| 64 | While Emacs will run on my iPad, it's not perfect. The largest issue on | ||
| 65 | my iPad is speed - loading Emacs takes 6-7 seconds and installing the | ||
| 66 | =magit= package took 129 seconds. | ||
| 67 | |||
| 68 | I haven't played around enough to optimize loading times and poke around | ||
| 69 | to see why the network requests take so long, but it's a big enough | ||
| 70 | issue that I wouldn't see casual Emacs users dealing with the lag. | ||
content/blog/_index.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Blog" | ||
| 3 | sort_by = "date" | ||
| 4 | template = "blog.html" | ||
| 5 | +++\ No newline at end of file | ||
content/index.org added +2
| @@ -0,0 +1,2 @@ | |||
| 1 | #+title: cleberg.net | ||
| 2 | #+description: Just a blip of ones and zeroes. | ||
content/salary.md deleted −62
| @@ -1,62 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Salary" | ||
| 3 | draft = false | ||
| 4 | +++ | ||
| 5 | |||
| 6 | ## Salary Transparency | ||
| 7 | |||
| 8 | The data below details the base salary information for each job I've held. This | ||
| 9 | information is posted publicly to ensure others in my position have a solid | ||
| 10 | reference point when determining if their current or proposed salary is | ||
| 11 | appropriate. | ||
| 12 | |||
| 13 | While sites like Glassdoor are locking salary data behind a paywall, LinkedIn is | ||
| 14 | discontinuing LinkedIn Salary, and helpful websites like Big 4 Transparency are | ||
| 15 | extremely rare, I wanted to provide my personal data publicly and freely to | ||
| 16 | those who need it. | ||
| 17 | |||
| 18 | I have seen what can happen when great employees don't know the market values | ||
| 19 | for their skills and I happily help those in my teams, so I'm happy to extend | ||
| 20 | this information to the online community. | ||
| 21 | |||
| 22 | As a final note, there are numerous reasons that people in the same role are | ||
| 23 | paid differently (expertise, years of experience, certifications, education, | ||
| 24 | etc.) and that the data in this table should only be used as a single point of | ||
| 25 | reference, not the whole story. | ||
| 26 | |||
| 27 | ## Salary Data | ||
| 28 | |||
| 29 | Note: When in a role that gives periodic raises, I will create a new record with | ||
| 30 | the new base salary in the table below. See the KPMG records for an example of a | ||
| 31 | raise while in the same role. | ||
| 32 | |||
| 33 | | Title | Company | Location | Start | End | Salary | | ||
| 34 | | ---------------------------------------------- | ---------------------- | -------------- | ------- | ------- | -------- | | ||
| 35 | | Senior Associate, Technology Assurance - Audit | KPMG | Omaha, NE | 2023-10 | Current | $116,700 | | ||
| 36 | | Senior Associate, Technology Assurance - Audit | KPMG | Omaha, NE | 2022-06 | 2023-10 | $110,000 | | ||
| 37 | | Senior Technology Risk Consultant | Ernst & Young | Des Moines, IA | 2021-09 | 2022-06 | $89,500 | | ||
| 38 | | Senior IT Auditor | Ameritas | Lincoln, NE | 2021-05 | 2021-09 | $72,000 | | ||
| 39 | | IT Auditor | Ameritas | Lincoln, NE | 2020-04 | 2021-05 | $65,000 | | ||
| 40 | | IS Auditor II | Nelnet | Lincoln, NE | 2019-12 | 2020-04 | $58,000 | | ||
| 41 | | IS Auditor I | Nelnet | Lincoln, NE | 2019-06 | 2019-12 | $20/hour | | ||
| 42 | | Internal Audit Intern | Ameritas | Lincoln, NE | 2018-02 | 2019-06 | $16/hour | | ||
| 43 | | Teaching Assistant | University of Nebraska | Lincoln, NE | 2017-08 | 2018-05 | $7/hour | | ||
| 44 | | Community Management Intern | Walgreens | Lincoln, NE | 2017-06 | 2018-02 | $14/hour | | ||
| 45 | |||
| 46 | [[source data](/static/salary.csv)] | ||
| 47 | |||
| 48 | ## Salary Visualization | ||
| 49 | |||
| 50 | See the graph below for a visualization of each job's salary over time, as well | ||
| 51 | as percentage increases or decreases between jobs. | ||
| 52 | |||
| 53 |  | ||
| 54 | |||
| 55 | This page was inspired by [Xe](https://xeiaso.net/salary-transparency/), and I'm | ||
| 56 | quoting the following wording from them as I want to reiterate this piece: | ||
| 57 | |||
| 58 | > Please consider publishing your salary data like this as well. By open, | ||
| 59 | > voluntary transparency we can help to end stigmas around discussing pay and | ||
| 60 | > help ensure that the next generations of people in tech are treated fairly. | ||
| 61 | > Stigmas thrive in darkness but die in the light of day. You can help end the | ||
| 62 | > stigma by playing your cards out in the open like this. | ||
content/salary/index.org added +70
| @@ -0,0 +1,70 @@ | |||
| 1 | #+title: Salary | ||
| 2 | #+slug: index | ||
| 3 | |||
| 4 | ** Salary Transparency | ||
| 5 | :PROPERTIES: | ||
| 6 | :CUSTOM_ID: salary-transparency | ||
| 7 | :END: | ||
| 8 | The data below details the base salary information for each job I've | ||
| 9 | held. This information is posted publicly to ensure others in my | ||
| 10 | position have a solid reference point when determining if their current | ||
| 11 | or proposed salary is appropriate. | ||
| 12 | |||
| 13 | While sites like Glassdoor are locking salary data behind a paywall, | ||
| 14 | LinkedIn is discontinuing LinkedIn Salary, and helpful websites like Big | ||
| 15 | 4 Transparency are extremely rare, I wanted to provide my personal data | ||
| 16 | publicly and freely to those who need it. | ||
| 17 | |||
| 18 | I have seen what can happen when great employees don't know the market | ||
| 19 | values for their skills and I happily help those in my teams, so I'm | ||
| 20 | happy to extend this information to the online community. | ||
| 21 | |||
| 22 | As a final note, there are numerous reasons that people in the same role | ||
| 23 | are paid differently (expertise, years of experience, certifications, | ||
| 24 | education, etc.) and that the data in this table should only be used as | ||
| 25 | a single point of reference, not the whole story. | ||
| 26 | |||
| 27 | ** Salary Data | ||
| 28 | :PROPERTIES: | ||
| 29 | :CUSTOM_ID: salary-data | ||
| 30 | :END: | ||
| 31 | Note: When in a role that gives periodic raises, I will create a new | ||
| 32 | record with the new base salary in the table below. See the KPMG records | ||
| 33 | for an example of a raise while in the same role. | ||
| 34 | |||
| 35 | | Title | Company | Location | Start | End | Salary | | ||
| 36 | |------------------------------------------------+------------------------+----------------+---------+---------+----------| | ||
| 37 | | Senior Associate, Technology Assurance - Audit | KPMG | Omaha, NE | 2023-10 | Current | $116,700 | | ||
| 38 | | Senior Associate, Technology Assurance - Audit | KPMG | Omaha, NE | 2022-06 | 2023-10 | $110,000 | | ||
| 39 | | Senior Technology Risk Consultant | Ernst & Young | Des Moines, IA | 2021-09 | 2022-06 | $89,500 | | ||
| 40 | | Senior IT Auditor | Ameritas | Lincoln, NE | 2021-05 | 2021-09 | $72,000 | | ||
| 41 | | IT Auditor | Ameritas | Lincoln, NE | 2020-04 | 2021-05 | $65,000 | | ||
| 42 | | IS Auditor II | Nelnet | Lincoln, NE | 2019-12 | 2020-04 | $58,000 | | ||
| 43 | | IS Auditor I | Nelnet | Lincoln, NE | 2019-06 | 2019-12 | $20/hour | | ||
| 44 | | Internal Audit Intern | Ameritas | Lincoln, NE | 2018-02 | 2019-06 | $16/hour | | ||
| 45 | | Teaching Assistant | University of Nebraska | Lincoln, NE | 2017-08 | 2018-05 | $7/hour | | ||
| 46 | | Community Management Intern | Walgreens | Lincoln, NE | 2017-06 | 2018-02 | $14/hour | | ||
| 47 | |||
| 48 | [[[/static/salary.csv][source data]]] | ||
| 49 | |||
| 50 | ** Salary Visualization | ||
| 51 | :PROPERTIES: | ||
| 52 | :CUSTOM_ID: salary-visualization | ||
| 53 | :END: | ||
| 54 | See the graph below for a visualization of each job's salary over time, | ||
| 55 | as well as percentage increases or decreases between jobs. | ||
| 56 | |||
| 57 | #+caption: Salary Visualization | ||
| 58 | [[https://img.cleberg.net/blog/salary/salary.png]] | ||
| 59 | |||
| 60 | This page was inspired by | ||
| 61 | [[https://xeiaso.net/salary-transparency/][Xe]], and I'm quoting the | ||
| 62 | following wording from them as I want to reiterate this piece: | ||
| 63 | |||
| 64 | #+begin_quote | ||
| 65 | Please consider publishing your salary data like this as well. By open, | ||
| 66 | voluntary transparency we can help to end stigmas around discussing pay | ||
| 67 | and help ensure that the next generations of people in tech are treated | ||
| 68 | fairly. Stigmas thrive in darkness but die in the light of day. You can | ||
| 69 | help end the stigma by playing your cards out in the open like this. | ||
| 70 | #+end_quote | ||
content/services.md deleted −21
| @@ -1,21 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Services" | ||
| 3 | draft = false | ||
| 4 | +++ | ||
| 5 | |||
| 6 | - [AnonymousOverflow](https://ao.cleberg.net/) - A StackOverflow proxy | ||
| 7 | - [CyberChef](https://cyberchef.cleberg.net/) - The Cyber Swiss Army Knife | ||
| 8 | - [FileArchive](https://files.cleberg.net/) - An interesting file archive | ||
| 9 | - [FlashPaper](https://paste.cleberg.net/) - One-time encrypted | ||
| 10 | password/secret sharing | ||
| 11 | - [Invidious](https://invidious.cleberg.net/) - A YouTube proxy | ||
| 12 | - [Office](https://office.cleberg.net/) - The world's smallest office suite | ||
| 13 | - [Org-Live](https://org.cleberg.net/) - A basic org-mode editor for the web I | ||
| 14 | built | ||
| 15 | - [Rimgo](https://rimgo.cleberg.net/) - An alternative frontend for Imgur | ||
| 16 | - [SearXNG](https://search.cleberg.net/) - A privacy-respecting, open | ||
| 17 | metasearch engine | ||
| 18 | |||
| 19 | See the [git | ||
| 20 | log](https://git.sr.ht/~cyborg/cleberg.net/log/main/item/content/services.md) if | ||
| 21 | you want to see changes that have been made. | ||
content/services/index.org added +23
| @@ -0,0 +1,23 @@ | |||
| 1 | #+title: Services | ||
| 2 | #+slug: index | ||
| 3 | |||
| 4 | - [[https://ao.cleberg.net/][AnonymousOverflow]] - A StackOverflow proxy | ||
| 5 | - [[https://cyberchef.cleberg.net/][CyberChef]] - The Cyber Swiss Army | ||
| 6 | Knife | ||
| 7 | - [[https://files.cleberg.net/][FileArchive]] - An interesting file | ||
| 8 | archive | ||
| 9 | - [[https://paste.cleberg.net/][FlashPaper]] - One-time encrypted | ||
| 10 | password/secret sharing | ||
| 11 | - [[https://invidious.cleberg.net/][Invidious]] - A YouTube proxy | ||
| 12 | - [[https://office.cleberg.net/][Office]] - The world's smallest office | ||
| 13 | suite | ||
| 14 | - [[https://org.cleberg.net/][Org-Live]] - A basic org-mode editor for | ||
| 15 | the web I built | ||
| 16 | - [[https://rimgo.cleberg.net/][Rimgo]] - An alternative frontend for | ||
| 17 | Imgur | ||
| 18 | - [[https://search.cleberg.net/][SearXNG]] - A privacy-respecting, open | ||
| 19 | metasearch engine | ||
| 20 | |||
| 21 | See the | ||
| 22 | [[https://git.sr.ht/~cyborg/cleberg.net/log/main/item/content/services.md][git | ||
| 23 | log]] if you want to see changes that have been made. | ||
content/wiki/_index.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Wiki" | ||
| 3 | sort_by = "title" | ||
| 4 | template = "wiki.html" | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/ai.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "AI" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/audit.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Audit" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/automation.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Automation" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/blogroll.md deleted −40
| @@ -1,40 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Blogroll" | ||
| 3 | updated = 2024-04-12 | ||
| 4 | draft = false | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Aggregators | ||
| 8 | |||
| 9 | - [1MB Club](https://1mb.club/) | ||
| 10 | - [250KB Club](https://250kb.club/) | ||
| 11 | - [512KB Club](https://512kb.club/) | ||
| 12 | - [Darktheme Club](https://darktheme.club/) | ||
| 13 | - [No CSS Club](https://nocss.club/) | ||
| 14 | - [No-JS Club](https://no-js.club/) | ||
| 15 | - [Ye Olde Blogroll](https://blogroll.org/) | ||
| 16 | |||
| 17 | ## Plain Text | ||
| 18 | |||
| 19 | A list of various plaintext websites and lists. | ||
| 20 | |||
| 21 | - [A List Of Text-Only & Minimalist News | ||
| 22 | Sites](https://greycoder.com/a-list-of-text-only-new-sites/) | ||
| 23 | - [Harvard Law Review](https://harvardlawreview.org/) | ||
| 24 | - [Hyperlinked Text](https://sjmulder.nl/en/textonly.html) | ||
| 25 | - [Plain-text web | ||
| 26 | design](https://medium.com/@letsworkshop/plain-text-web-design-a78ccaf9dbc0) | ||
| 27 | - [Plaintext World](https://plaintextworld.com/) | ||
| 28 | - [txt2web](https://github.com/masonarmand/txt2web) | ||
| 29 | - [Words](https://justinjackson.ca/words.html) | ||
| 30 | |||
| 31 | ## Webrings | ||
| 32 | |||
| 33 | Instead of listing my personal favorites, I'm just going to drop a link to | ||
| 34 | [brisay's webring list](https://brisray.com/web/webring-list.htm), which | ||
| 35 | contains 237 webrings for a total of 7078 websites, as of 2024-03-15. | ||
| 36 | |||
| 37 | ## Everything Else | ||
| 38 | |||
| 39 | - [Dead Simple Sites](https://deadsimplesites.com/) | ||
| 40 | - [Planet Emacslife](https://planet.emacslife.com/) | ||
content/wiki/blogroll.org added +46
| @@ -0,0 +1,46 @@ | |||
| 1 | #+title: Blogroll | ||
| 2 | #+date: <2024-04-12> | ||
| 3 | |||
| 4 | ** Aggregators | ||
| 5 | :PROPERTIES: | ||
| 6 | :CUSTOM_ID: aggregators | ||
| 7 | :END: | ||
| 8 | - [[https://1mb.club/][1MB Club]] | ||
| 9 | - [[https://250kb.club/][250KB Club]] | ||
| 10 | - [[https://512kb.club/][512KB Club]] | ||
| 11 | - [[https://darktheme.club/][Darktheme Club]] | ||
| 12 | - [[https://nocss.club/][No CSS Club]] | ||
| 13 | - [[https://no-js.club/][No-JS Club]] | ||
| 14 | - [[https://blogroll.org/][Ye Olde Blogroll]] | ||
| 15 | |||
| 16 | ** Plain Text | ||
| 17 | :PROPERTIES: | ||
| 18 | :CUSTOM_ID: plain-text | ||
| 19 | :END: | ||
| 20 | A list of various plaintext websites and lists. | ||
| 21 | |||
| 22 | - [[https://greycoder.com/a-list-of-text-only-new-sites/][A List Of | ||
| 23 | Text-Only & Minimalist News Sites]] | ||
| 24 | - [[https://harvardlawreview.org/][Harvard Law Review]] | ||
| 25 | - [[https://sjmulder.nl/en/textonly.html][Hyperlinked Text]] | ||
| 26 | - [[https://medium.com/@letsworkshop/plain-text-web-design-a78ccaf9dbc0][Plain-text | ||
| 27 | web design]] | ||
| 28 | - [[https://plaintextworld.com/][Plaintext World]] | ||
| 29 | - [[https://github.com/masonarmand/txt2web][txt2web]] | ||
| 30 | - [[https://justinjackson.ca/words.html][Words]] | ||
| 31 | |||
| 32 | ** Webrings | ||
| 33 | :PROPERTIES: | ||
| 34 | :CUSTOM_ID: webrings | ||
| 35 | :END: | ||
| 36 | Instead of listing my personal favorites, I'm just going to drop a link | ||
| 37 | to [[https://brisray.com/web/webring-list.htm][brisay's webring list]], | ||
| 38 | which contains 237 webrings for a total of 7078 websites, as of | ||
| 39 | 2024-03-15. | ||
| 40 | |||
| 41 | ** Everything Else | ||
| 42 | :PROPERTIES: | ||
| 43 | :CUSTOM_ID: everything-else | ||
| 44 | :END: | ||
| 45 | - [[https://deadsimplesites.com/][Dead Simple Sites]] | ||
| 46 | - [[https://planet.emacslife.com/][Planet Emacslife]] | ||
content/wiki/cli.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "CLI" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/data.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Data" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/editors.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Editors" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/foss.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "FOSS" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/git.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Git" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/hardware.md deleted −117
| @@ -1,117 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Hardware" | ||
| 3 | updated = 2024-03-16 | ||
| 4 | draft = false | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Desktop | ||
| 8 | |||
| 9 | ### macOS | ||
| 10 | |||
| 11 | Probably should have added more RAM but Macbooks are stupid expensive. | ||
| 12 | |||
| 13 | | Category | Details | | ||
| 14 | | -------- | ----------------------------------------------------- | | ||
| 15 | | Model | [Macbook Pro 16"](https://www.apple.com/macbook-pro/) | | ||
| 16 | | CPU | Apple M2 Pro | | ||
| 17 | | RAM | 16GB | | ||
| 18 | | Storage | 512GB SSD | | ||
| 19 | |||
| 20 | ### Linux | ||
| 21 | |||
| 22 | A beauty. | ||
| 23 | |||
| 24 | | Category | Details | | ||
| 25 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| 26 | | Model | [Lenovo ThinkPad E15 Gen 4, model 21ED0048US](<https://www.lenovo.com/us/en/p/laptops/thinkpad/thinkpade/thinkpad--e15-gen-4-(15-inch-amd)/len101t0023>) | | ||
| 27 | | CPU | AMD Ryzen 5 5625U with Radeon Graphics | | ||
| 28 | | RAM | 16 GB | | ||
| 29 | | Storage | 256 GB SSD | | ||
| 30 | |||
| 31 | ## Mobile | ||
| 32 | |||
| 33 | Previously used a Pixel 6 & Pixel 7 with GrapheneOS. | ||
| 34 | |||
| 35 | | Category | Details | | ||
| 36 | | -------- | --------------------------------------------------------- | | ||
| 37 | | Model | [iPhone 15 Pro Max](https://www.apple.com/iphone-15-pro/) | | ||
| 38 | | CPU | A17 Pro | | ||
| 39 | | RAM | 8GB | | ||
| 40 | | Storage | 256GB | | ||
| 41 | |||
| 42 | ## Homelab | ||
| 43 | |||
| 44 | I run a small homelab with a mix of consumer (compute/storage) and enterprise | ||
| 45 | (network) hardware. I try to keep the lab energy efficient and quiet as my top | ||
| 46 | priorities. | ||
| 47 | |||
| 48 | ### IoT | ||
| 49 | |||
| 50 | A collection of mainly smart lights, sensors, and smart appliances. My first | ||
| 51 | preference is to disable all networking for new smart devices or simply not | ||
| 52 | connect internet in the first place (e.g. I never enable internet on my smart | ||
| 53 | TVs). If the smart device requires LAN access, I will connect the device to my | ||
| 54 | guest-restricted IoT network. As a last resort, I will set-up the internet but | ||
| 55 | monitor the DNS lookups via NextDNS and forcibly block any domains I do not want | ||
| 56 | the device to be using. If the device is egregious or shady, I'll just sell it | ||
| 57 | and either replace it or live without it. | ||
| 58 | |||
| 59 | - Other Appliances (washer, dryer, humidifier, fans, etc.) | ||
| 60 | - [Roomba | ||
| 61 | i7+](https://about.irobot.com/sitecore/content/north-america/irobot-us/home/roomba/i7-series) | ||
| 62 | - [Philips Hue A19 | ||
| 63 | Bulbs](https://www.philips-hue.com/en-us/p/hue-white-and-color-ambiance-a19---e26-smart-bulb---60-w--3-pack-/046677562786) | ||
| 64 | x 15 | ||
| 65 | - [Philips Hue Play Light | ||
| 66 | Bars](https://www.philips-hue.com/en-us/p/hue-bundle-play-blk-ext/33001) | ||
| 67 | - [Philips Hue Smart | ||
| 68 | Bridge](https://www.philips-hue.com/en-us/p/hue-bridge/046677458478) + play | ||
| 69 | light bars and a ton of bulbs | ||
| 70 | - [UP | ||
| 71 | Chime](https://store.ui.com/us/en/collections/unifi-camera-security-special-chime) | ||
| 72 | - [UP-Sense](https://store.ui.com/us/en/collections/unifi-camera-security-special-sensor) | ||
| 73 | x 2 | ||
| 74 | - [USP-Plug](https://store.ui.com/us/en/products/unifi-smart-power) | ||
| 75 | - [UVC G4 | ||
| 76 | Instant](https://store.ui.com/us/en/collections/unifi-camera-security-compact-wifi-connected) | ||
| 77 | x 3 | ||
| 78 | - [UVC G4 Doorbell | ||
| 79 | Pro](https://store.ui.com/us/en/collections/unifi-camera-security-special-wifi-doorbell) | ||
| 80 | |||
| 81 | ### Network | ||
| 82 | |||
| 83 | A rack-mounted Dream Machine Pro, connected downstream to an access point, mesh | ||
| 84 | extender, and a couple ethernet switches. | ||
| 85 | |||
| 86 | - [UDM-Pro](https://store.ui.com/us/en/collections/unifi-dream-machine/products/udm-pro) | ||
| 87 | - [USW-24-PoE](https://store.ui.com/us/en/collections/unifi-switching-standard-power-over-ethernet/products/usw-24-poe) | ||
| 88 | - [USW-Lite-8-PoE](https://store.ui.com/us/en/collections/unifi-switching-utility-poe/products/usw-lite-8-poe) | ||
| 89 | - [U6-Pro](https://store.ui.com/us/en/collections/unifi-wifi-flagship-high-capacity/products/u6-pro) | ||
| 90 | - [U6-Extender](https://store.ui.com/us/en/collections/unifi-wifi-inwall-outlet-mesh) | ||
| 91 | - [USW 24-Port Patch | ||
| 92 | Panel](https://store.ui.com/us/en/collections/unifi-accessory-tech-installations-rackmount/products/uacc-rack-panel-patch-blank-24) | ||
| 93 | |||
| 94 | ### Servers | ||
| 95 | |||
| 96 | 1. Rack-Mount Server | ||
| 97 | |||
| 98 | I wasn't happy with using low-powered PCs as servers and I knew I did not | ||
| 99 | want the ear-shattering enterprise rack-mounted servers, so I built my own. | ||
| 100 | |||
| 101 | | Category | Details | | ||
| 102 | | ------------------ | -------------------------------------- | | ||
| 103 | | Case | Rosewill RSV-R4100U 4U | | ||
| 104 | | Motherboard | NZXT B550 | | ||
| 105 | | CPU | AMD Ryzen 7 5700G with Radeon Graphics | | ||
| 106 | | RAM | 64GB RAM (2x32GB) | | ||
| 107 | | Storage (On-board) | Western Digital 500GB M.2 NVME SSD | | ||
| 108 | | Storage (HDD Bay) | 48TB HDD | | ||
| 109 | | PSU | Corsair RM850 PSU | | ||
| 110 | |||
| 111 | 2. Other | ||
| 112 | |||
| 113 | These ran as my main servers before I built the rack-mounted server above. I | ||
| 114 | have shut these down indefinitely for now as I have no use for them. | ||
| 115 | |||
| 116 | - Dell OptiPlex | ||
| 117 | - Raspberry Pi 4 | ||
content/wiki/hardware.org added +130
| @@ -0,0 +1,130 @@ | |||
| 1 | #+title: Hardware | ||
| 2 | #+date: <2024-03-16> | ||
| 3 | |||
| 4 | ** Desktop | ||
| 5 | :PROPERTIES: | ||
| 6 | :CUSTOM_ID: desktop | ||
| 7 | :END: | ||
| 8 | *** macOS | ||
| 9 | :PROPERTIES: | ||
| 10 | :CUSTOM_ID: macos | ||
| 11 | :END: | ||
| 12 | Probably should have added more RAM but Macbooks are stupid expensive. | ||
| 13 | |||
| 14 | | Category | Details | | ||
| 15 | |----------+---------------------------------------------------------| | ||
| 16 | | Model | [[https://www.apple.com/macbook-pro/][Macbook Pro 16”]] | | ||
| 17 | | CPU | Apple M2 Pro | | ||
| 18 | | RAM | 16GB | | ||
| 19 | | Storage | 512GB SSD | | ||
| 20 | |||
| 21 | *** Linux | ||
| 22 | :PROPERTIES: | ||
| 23 | :CUSTOM_ID: linux | ||
| 24 | :END: | ||
| 25 | A beauty. | ||
| 26 | |||
| 27 | | Category | Details | | ||
| 28 | |----------+----------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| 29 | | Model | [[https://www.lenovo.com/us/en/p/laptops/thinkpad/thinkpade/thinkpad--e15-gen-4-(15-inch-amd)/len101t0023][Lenovo ThinkPad E15 Gen 4, model 21ED0048US]] | | ||
| 30 | | CPU | AMD Ryzen 5 5625U with Radeon Graphics | | ||
| 31 | | RAM | 16 GB | | ||
| 32 | | Storage | 256 GB SSD | | ||
| 33 | |||
| 34 | ** Mobile | ||
| 35 | :PROPERTIES: | ||
| 36 | :CUSTOM_ID: mobile | ||
| 37 | :END: | ||
| 38 | Previously used a Pixel 6 & Pixel 7 with GrapheneOS. | ||
| 39 | |||
| 40 | | Category | Details | | ||
| 41 | |----------+-------------------------------------------------------------| | ||
| 42 | | Model | [[https://www.apple.com/iphone-15-pro/][iPhone 15 Pro Max]] | | ||
| 43 | | CPU | A17 Pro | | ||
| 44 | | RAM | 8GB | | ||
| 45 | | Storage | 256GB | | ||
| 46 | |||
| 47 | ** Homelab | ||
| 48 | :PROPERTIES: | ||
| 49 | :CUSTOM_ID: homelab | ||
| 50 | :END: | ||
| 51 | I run a small homelab with a mix of consumer (compute/storage) and | ||
| 52 | enterprise (network) hardware. I try to keep the lab energy efficient | ||
| 53 | and quiet as my top priorities. | ||
| 54 | |||
| 55 | *** IoT | ||
| 56 | :PROPERTIES: | ||
| 57 | :CUSTOM_ID: iot | ||
| 58 | :END: | ||
| 59 | A collection of mainly smart lights, sensors, and smart appliances. My | ||
| 60 | first preference is to disable all networking for new smart devices or | ||
| 61 | simply not connect internet in the first place (e.g. I never enable | ||
| 62 | internet on my smart TVs). If the smart device requires LAN access, I | ||
| 63 | will connect the device to my guest-restricted IoT network. As a last | ||
| 64 | resort, I will set-up the internet but monitor the DNS lookups via | ||
| 65 | NextDNS and forcibly block any domains I do not want the device to be | ||
| 66 | using. If the device is egregious or shady, I'll just sell it and either | ||
| 67 | replace it or live without it. | ||
| 68 | |||
| 69 | - Other Appliances (washer, dryer, humidifier, fans, etc.) | ||
| 70 | - [[https://about.irobot.com/sitecore/content/north-america/irobot-us/home/roomba/i7-series][Roomba | ||
| 71 | i7+]] | ||
| 72 | - [[https://www.philips-hue.com/en-us/p/hue-white-and-color-ambiance-a19---e26-smart-bulb---60-w--3-pack-/046677562786][Philips | ||
| 73 | Hue A19 Bulbs]] x 15 | ||
| 74 | - [[https://www.philips-hue.com/en-us/p/hue-bundle-play-blk-ext/33001][Philips | ||
| 75 | Hue Play Light Bars]] | ||
| 76 | - [[https://www.philips-hue.com/en-us/p/hue-bridge/046677458478][Philips | ||
| 77 | Hue Smart Bridge]] + play light bars and a ton of bulbs | ||
| 78 | - [[https://store.ui.com/us/en/collections/unifi-camera-security-special-chime][UP | ||
| 79 | Chime]] | ||
| 80 | - [[https://store.ui.com/us/en/collections/unifi-camera-security-special-sensor][UP-Sense]] | ||
| 81 | x 2 | ||
| 82 | - [[https://store.ui.com/us/en/products/unifi-smart-power][USP-Plug]] | ||
| 83 | - [[https://store.ui.com/us/en/collections/unifi-camera-security-compact-wifi-connected][UVC | ||
| 84 | G4 Instant]] x 3 | ||
| 85 | - [[https://store.ui.com/us/en/collections/unifi-camera-security-special-wifi-doorbell][UVC | ||
| 86 | G4 Doorbell Pro]] | ||
| 87 | |||
| 88 | *** Network | ||
| 89 | :PROPERTIES: | ||
| 90 | :CUSTOM_ID: network | ||
| 91 | :END: | ||
| 92 | A rack-mounted Dream Machine Pro, connected downstream to an access | ||
| 93 | point, mesh extender, and a couple ethernet switches. | ||
| 94 | |||
| 95 | - [[https://store.ui.com/us/en/collections/unifi-dream-machine/products/udm-pro][UDM-Pro]] | ||
| 96 | - [[https://store.ui.com/us/en/collections/unifi-switching-standard-power-over-ethernet/products/usw-24-poe][USW-24-PoE]] | ||
| 97 | - [[https://store.ui.com/us/en/collections/unifi-switching-utility-poe/products/usw-lite-8-poe][USW-Lite-8-PoE]] | ||
| 98 | - [[https://store.ui.com/us/en/collections/unifi-wifi-flagship-high-capacity/products/u6-pro][U6-Pro]] | ||
| 99 | - [[https://store.ui.com/us/en/collections/unifi-wifi-inwall-outlet-mesh][U6-Extender]] | ||
| 100 | - [[https://store.ui.com/us/en/collections/unifi-accessory-tech-installations-rackmount/products/uacc-rack-panel-patch-blank-24][USW | ||
| 101 | 24-Port Patch Panel]] | ||
| 102 | |||
| 103 | *** Servers | ||
| 104 | :PROPERTIES: | ||
| 105 | :CUSTOM_ID: servers | ||
| 106 | :END: | ||
| 107 | 1. Rack-Mount Server | ||
| 108 | |||
| 109 | I wasn't happy with using low-powered PCs as servers and I knew I did | ||
| 110 | not want the ear-shattering enterprise rack-mounted servers, so I | ||
| 111 | built my own. | ||
| 112 | |||
| 113 | | Category | Details | | ||
| 114 | |--------------------+----------------------------------------| | ||
| 115 | | Case | Rosewill RSV-R4100U 4U | | ||
| 116 | | Motherboard | NZXT B550 | | ||
| 117 | | CPU | AMD Ryzen 7 5700G with Radeon Graphics | | ||
| 118 | | RAM | 64GB RAM (2x32GB) | | ||
| 119 | | Storage (On-board) | Western Digital 500GB M.2 NVME SSD | | ||
| 120 | | Storage (HDD Bay) | 48TB HDD | | ||
| 121 | | PSU | Corsair RM850 PSU | | ||
| 122 | |||
| 123 | 2. Other | ||
| 124 | |||
| 125 | These ran as my main servers before I built the rack-mounted server | ||
| 126 | above. I have shut these down indefinitely for now as I have no use | ||
| 127 | for them. | ||
| 128 | |||
| 129 | - Dell OptiPlex | ||
| 130 | - Raspberry Pi 4 | ||
content/wiki/internet.md → content/wiki/internet.org renamed +50 −44
| @@ -1,38 +1,38 @@ | |||
| 1 | +++ | 1 | #+title: Internet |
| 2 | title = "Internet" | 2 | #+date: <2024-05-02> |
| 3 | updated = 2024-05-02 | 3 | |
| 4 | draft = false | 4 | ** Gemini |
| 5 | +++ | 5 | :PROPERTIES: |
| 6 | 6 | :CUSTOM_ID: gemini | |
| 7 | ## Gemini | 7 | :END: |
| 8 | 8 | The [[https://geminiprotocol.net/][Gemini Protocol]] is a network of | |
| 9 | The [Gemini Protocol](https://geminiprotocol.net/) is a network of | ||
| 10 | interconnected plaintext documents. If you've used | 9 | interconnected plaintext documents. If you've used |
| 11 | [Gopher](https://en.wikipedia.org/wiki/Gopher_(protocol)) before, Gemini will | 10 | [[https://en.wikipedia.org/wiki/Gopher_(protocol)][Gopher]] before, |
| 12 | feel very familiar. | 11 | Gemini will feel very familiar. |
| 13 | |||
| 14 | Instead of web sites, Gemini uses the term "capsules" that can be access via | ||
| 15 | `gemini://` links. | ||
| 16 | |||
| 17 | In order to browse Gemini capsules, you will need a client capable of supporting | ||
| 18 | Gemini. The Gemini Protocol website has a [Gemini | ||
| 19 | software](https://geminiprotocol.net/software/) page with a list of clients for | ||
| 20 | each platform. | ||
| 21 | 12 | ||
| 13 | Instead of web sites, Gemini uses the term "capsules" that can be access | ||
| 14 | via =gemini://= links. | ||
| 22 | 15 | ||
| 23 | ### Gemtext | 16 | In order to browse Gemini capsules, you will need a client capable of |
| 17 | supporting Gemini. The Gemini Protocol website has a | ||
| 18 | [[https://geminiprotocol.net/software/][Gemini software]] page with a | ||
| 19 | list of clients for each platform. | ||
| 24 | 20 | ||
| 25 | [Gemtext](https://geminiprotocol.net/docs/cheatsheet.gmi) is a plaintext syntax, | 21 | *** Gemtext |
| 26 | very similar to Markdown. | 22 | :PROPERTIES: |
| 23 | :CUSTOM_ID: gemtext | ||
| 24 | :END: | ||
| 25 | [[https://geminiprotocol.net/docs/cheatsheet.gmi][Gemtext]] is a | ||
| 26 | plaintext syntax, very similar to Markdown. | ||
| 27 | 27 | ||
| 28 | To quote the Gemini Protocol's documentation: | 28 | To quote the Gemini Protocol's documentation: |
| 29 | 29 | ||
| 30 | - Long lines get wrapped by the client to fit the screen. | 30 | - Long lines get wrapped by the client to fit the screen. |
| 31 | - Short lines _don't_ get joined together. | 31 | - Short lines /don't/ get joined together. |
| 32 | - Write paragraphs as single long lines. | 32 | - Write paragraphs as single long lines. |
| 33 | - Blank lines are rendered verbatim. | 33 | - Blank lines are rendered verbatim. |
| 34 | 34 | ||
| 35 | ```md | 35 | #+begin_src md |
| 36 | [//]: <> (Headings) | 36 | [//]: <> (Headings) |
| 37 | # Heading | 37 | # Heading |
| 38 | ## Sub-heading | 38 | ## Sub-heading |
| @@ -54,16 +54,19 @@ To quote the Gemini Protocol's documentation: | |||
| 54 | Any line starting with three backticks (```) will tell the client to toggle to | 54 | Any line starting with three backticks (```) will tell the client to toggle to |
| 55 | "preformatted mode", which disables the client's logical checks to render links, | 55 | "preformatted mode", which disables the client's logical checks to render links, |
| 56 | headings, etc. and will render the text as-is. | 56 | headings, etc. and will render the text as-is. |
| 57 | ``` | 57 | #+end_src |
| 58 | 58 | ||
| 59 | ## Gopher | 59 | ** Gopher |
| 60 | 60 | :PROPERTIES: | |
| 61 | The [Gopher Protocol](https://en.wikipedia.org/wiki/Gopher_(protocol)) is a | 61 | :CUSTOM_ID: gopher |
| 62 | communication protocol, similar to Gemini, that allows for browsing of any ones | 62 | :END: |
| 63 | of [the 14 supported item | 63 | The [[https://en.wikipedia.org/wiki/Gopher_(protocol)][Gopher Protocol]] |
| 64 | types](https://datatracker.ietf.org/doc/html/rfc1436#section-3.8): | 64 | is a communication protocol, similar to Gemini, that allows for browsing |
| 65 | 65 | of any ones of | |
| 66 | ```txt | 66 | [[https://datatracker.ietf.org/doc/html/rfc1436#section-3.8][the 14 |
| 67 | supported item types]]: | ||
| 68 | |||
| 69 | #+begin_src txt | ||
| 67 | 0 Item is a file | 70 | 0 Item is a file |
| 68 | 1 Item is a directory | 71 | 1 Item is a directory |
| 69 | 2 Item is a CSO phone-book server | 72 | 2 Item is a CSO phone-book server |
| @@ -80,14 +83,17 @@ types](https://datatracker.ietf.org/doc/html/rfc1436#section-3.8): | |||
| 80 | T Item points to a text-based tn3270 session. | 83 | T Item points to a text-based tn3270 session. |
| 81 | g Item is a GIF format graphics file. | 84 | g Item is a GIF format graphics file. |
| 82 | I Item is some kind of image file. Client decides how to display. | 85 | I Item is some kind of image file. Client decides how to display. |
| 83 | ``` | 86 | #+end_src |
| 84 | |||
| 85 | ### Simplicity is Intentional | ||
| 86 | 87 | ||
| 87 | Gopher is [meant to be | 88 | *** Simplicity is Intentional |
| 88 | simplistic](https://datatracker.ietf.org/doc/html/rfc1436#section-4). | 89 | :PROPERTIES: |
| 90 | :CUSTOM_ID: simplicity-is-intentional | ||
| 91 | :END: | ||
| 92 | Gopher is | ||
| 93 | [[https://datatracker.ietf.org/doc/html/rfc1436#section-4][meant to be | ||
| 94 | simplistic]]. | ||
| 89 | 95 | ||
| 90 | ```txt | 96 | #+begin_src txt |
| 91 | As far as possible we desire any new features to be carried as new | 97 | As far as possible we desire any new features to be carried as new |
| 92 | protocols that will be hidden behind new document-types. The | 98 | protocols that will be hidden behind new document-types. The |
| 93 | internet Gopher philosophy is: | 99 | internet Gopher philosophy is: |
| @@ -110,4 +116,4 @@ internet Gopher philosophy is: | |||
| 110 | (c) The well-tempered client should do something reasonable with | 116 | (c) The well-tempered client should do something reasonable with |
| 111 | funny characters received in text; filter them out, leave them in, | 117 | funny characters received in text; filter them out, leave them in, |
| 112 | whatever. | 118 | whatever. |
| 113 | ```\ No newline at end of file | 119 | #+end_src |
content/wiki/ios.md deleted −204
| @@ -1,204 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "iOS" | ||
| 3 | updated = 2024-05-01 | ||
| 4 | draft = false | ||
| 5 | +++ | ||
| 6 | |||
| 7 | Related: | ||
| 8 | |||
| 9 | - [Hardware](/wiki/hardware.html) | ||
| 10 | |||
| 11 | My primary mobile OS. Currently running iOS 17. This wiki page contains most of | ||
| 12 | the apps I have used at one point or another across my different iPhones. | ||
| 13 | |||
| 14 | (`*`) = My favorites | ||
| 15 | |||
| 16 | ## Configuration | ||
| 17 | |||
| 18 | ### Display | ||
| 19 | |||
| 20 | - Light Mode | ||
| 21 | - 10:00 to 16:00 | ||
| 22 | - Dark Mode | ||
| 23 | - 16:00 to 10:00 | ||
| 24 | |||
| 25 | ### Focus Modes | ||
| 26 | |||
| 27 | - Personal Focus | ||
| 28 | - 06:00 to 21:00 | ||
| 29 | - Allow Notifications From: | ||
| 30 | - Alarms | ||
| 31 | - Calendar | ||
| 32 | - Contacts (1 person) | ||
| 33 | - Messages | ||
| 34 | - Phone | ||
| 35 | - Reminders | ||
| 36 | - Signal | ||
| 37 | - UniFi Protect | ||
| 38 | - Sleep Focus | ||
| 39 | - 21:00 to 06:00 | ||
| 40 | - Allow Notifications From: | ||
| 41 | - Alarms | ||
| 42 | - Contacts (1 person) | ||
| 43 | - Reminders | ||
| 44 | - Signal | ||
| 45 | |||
| 46 | ### Privacy & Security | ||
| 47 | |||
| 48 | I generally follow the [principle of least | ||
| 49 | privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) by only | ||
| 50 | permitting the bare minimum privileges and revoking as soon as they are no | ||
| 51 | longer required. | ||
| 52 | |||
| 53 | Here's the baseline I start with: | ||
| 54 | |||
| 55 | - Disable: | ||
| 56 | - Analytics & Improvements | ||
| 57 | - Apple Advertising | ||
| 58 | - Apple ID > Sign-In & Security > Two-Factor Authentication | ||
| 59 | - Location Services > System Services > Product Improvement | ||
| 60 | - Tracking > Allow Apps to Request to Track | ||
| 61 | - Safari > Advanced > Privacy Preserving Ad Measurement | ||
| 62 | - Enable: | ||
| 63 | - Apple ID > iCloud > Advanced Data Protection | ||
| 64 | - Apple ID > Personal Information > Communication Preferences | ||
| 65 | - App Privacy Report | ||
| 66 | - Location Services only for Camera, Find My, UDisc, & WiFiman (`While | ||
| 67 | Using`) | ||
| 68 | - Safari > Prevent Cross-Site Tracking | ||
| 69 | - Safari > Hide IP Address | ||
| 70 | - Safari > Advanced > Advanced Tracking and Fingerprinting Protection | ||
| 71 | |||
| 72 | ## Native Apps | ||
| 73 | |||
| 74 | ### Business | ||
| 75 | |||
| 76 | - [Element](https://apps.apple.com/us/app/element-messenger/id1083446067) - A | ||
| 77 | cross-platform messenger, based on Matrix | ||
| 78 | - [LinkedIn](https://apps.apple.com/us/app/linkedin-network-job-finder/id288429040) | ||
| 79 | - One of the only social media apps I use | ||
| 80 | |||
| 81 | ### Developer Tools | ||
| 82 | |||
| 83 | - [Harbour](https://testflight.apple.com/join/F2vK7xo4) - Easily manage your | ||
| 84 | Portainer service | ||
| 85 | - [iSH](https://apps.apple.com/us/app/ish-shell/id1436902243) - A local shell | ||
| 86 | with SSH functionality | ||
| 87 | |||
| 88 | ### Entertainment | ||
| 89 | |||
| 90 | - [Plex](https://apps.apple.com/us/app/plex-watch-live-tv-and-movies/id383457673) | ||
| 91 | - A client for the Plex Media Server | ||
| 92 | - [Steam](https://apps.apple.com/us/app/steam-mobile/id495369748) - The top | ||
| 93 | gaming marketplace for computers | ||
| 94 | |||
| 95 | ### Lifestyle | ||
| 96 | |||
| 97 | - [Home](https://apps.apple.com/us/app/home/id1110145103) (`*`) - Apple | ||
| 98 | homekit powered smart home manager | ||
| 99 | - [Hue](https://apps.apple.com/us/app/philips-hue/id1055281310) - Philips Hue | ||
| 100 | smart home manager | ||
| 101 | - [iRobot](https://apps.apple.com/us/app/irobot-home/id1012014442) - Manage | ||
| 102 | iRobot Roomba devices | ||
| 103 | - [UniFi Protect](https://apps.apple.com/us/app/unifi-protect/id1392492235) - | ||
| 104 | View and manage most UniFi Protect cameras and settings | ||
| 105 | |||
| 106 | ### Music | ||
| 107 | |||
| 108 | - [Apple Music](https://apps.apple.com/us/app/apple-music/id1108187390) (`*`) | ||
| 109 | - Apple's native music streaming app | ||
| 110 | - [Plexamp](https://apps.apple.com/us/app/plexamp/id1500797510) (`*`) - | ||
| 111 | Top-notch music app for your Plex Media Server, with a neural network that | ||
| 112 | provides excellent radio/shuffle suggestions | ||
| 113 | |||
| 114 | ### Navigation | ||
| 115 | |||
| 116 | - [deedum](https://apps.apple.com/us/app/deedum/id1546810946) - A Gemini | ||
| 117 | Protocol browser | ||
| 118 | |||
| 119 | ### News | ||
| 120 | |||
| 121 | - [NetNewsWire](https://apps.apple.com/us/app/netnewswire-rss-reader/id1480640210) | ||
| 122 | - A free and open source RSS reader for Mac, iPhone, and iPad | ||
| 123 | |||
| 124 | ### Photo & Video | ||
| 125 | |||
| 126 | - [Aislingeach](https://testflight.apple.com/join/Q6WyyEpS) - A quick way to | ||
| 127 | generate and rate images from the Stable Horde | ||
| 128 | - [Unsplash](https://apps.apple.com/us/app/unsplash/id1290631746) - Premium | ||
| 129 | images, mostly free | ||
| 130 | |||
| 131 | ### Productivity | ||
| 132 | |||
| 133 | - [beorg](https://apps.apple.com/us/app/beorg-to-do-list-agenda/id1238649962) | ||
| 134 | - An org-mode editor, outline, and scheduler with paid extensions | ||
| 135 | - [Bitwarden](https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744) | ||
| 136 | (`*`) - An open source password manager | ||
| 137 | - [Bitwarden | ||
| 138 | Authenticator](https://apps.apple.com/us/app/bitwarden-authenticator/id6497335175) | ||
| 139 | (`*`) - Generate 2FA on your device | ||
| 140 | - [Cryptomator](https://apps.apple.com/us/app/cryptomator/id1560822163) - A | ||
| 141 | cross-platform encryption program | ||
| 142 | - [Obsidian](https://apps.apple.com/us/app/obsidian-connected-notes/id1557175442) | ||
| 143 | (`*`) - A nice Markdown-based editor based on a "vault" structure. Offers a | ||
| 144 | paid sync solution and community extensions | ||
| 145 | - [Strongbox](https://apps.apple.com/us/app/strongbox-password-manager/id897283731) | ||
| 146 | - Keepass password manager for iOS & macOS | ||
| 147 | - [UniFi Network](https://apps.apple.com/us/app/unifi/id1057750338) - View and | ||
| 148 | manage most UniFi Network settings | ||
| 149 | |||
| 150 | ### Safari Extensions | ||
| 151 | |||
| 152 | - [AdGuard](https://apps.apple.com/us/app/adguard-adblock-privacy/id1047223162) | ||
| 153 | - Ad blocker | ||
| 154 | - [Dark | ||
| 155 | Reader](https://apps.apple.com/us/app/dark-reader-for-safari/id1438243180) - | ||
| 156 | Dark mode for all the sites | ||
| 157 | - [PiPifier](https://apps.apple.com/us/app/pipifier/id1234771095) - Force | ||
| 158 | videos to support PiP | ||
| 159 | - [Privacy | ||
| 160 | Redirect](https://apps.apple.com/us/app/privacy-redirect/id1578144015) - | ||
| 161 | Redirect select websites to others, usually to privacy-focused alternatives | ||
| 162 | |||
| 163 | ### Social Networking | ||
| 164 | |||
| 165 | - [MultiTab T](https://apps.apple.com/us/app/multitab-for-tumblr/id1071533778) | ||
| 166 | (`*`) - A gallery-based Tumblr client with some unique features, such as tab | ||
| 167 | history and sync | ||
| 168 | - [Signal](https://apps.apple.com/us/app/signal-private-messenger/id874139669) | ||
| 169 | (`*`) - A simple, powerful, and secure messenger | ||
| 170 | - [Three Cheers](https://testflight.apple.com/join/mpVk1qIy) - A client for | ||
| 171 | Tildes.net with a design focus that matches the intent of Tildes | ||
| 172 | - [Voyager](https://apps.apple.com/us/app/voyager-for-lemmy/id6451429762) - A | ||
| 173 | Lemmy client | ||
| 174 | |||
| 175 | ### Sports | ||
| 176 | |||
| 177 | - [Apple Sports](https://apps.apple.com/us/app/apple-sports/id6446788829) - | ||
| 178 | Apple's new sports app - lacks notifications and live events | ||
| 179 | - [UDisc](https://apps.apple.com/us/app/udisc-disc-golf/id1072228953) - Disc | ||
| 180 | golf course maps, score cards, and more | ||
| 181 | |||
| 182 | ### Utilities | ||
| 183 | |||
| 184 | - [Backblaze](https://apps.apple.com/us/app/backblaze/id628638330) - Quickly | ||
| 185 | view and manage Backblaze b2 cloud storage | ||
| 186 | - [Mullvad VPN](https://apps.apple.com/us/app/mullvad-vpn/id1488466513) (`*`) | ||
| 187 | - A private VPN service | ||
| 188 | - [OTP Auth](https://apps.apple.com/us/app/otp-auth/id659877384) (`*`) - A | ||
| 189 | minimalistic OTP app with support for biometrics, custom icons, | ||
| 190 | import/export, and iCloud sync | ||
| 191 | - [Plex Dash](https://apps.apple.com/us/app/plex-dash/id1500797677) - Stats | ||
| 192 | about your Plex Media Server | ||
| 193 | - [Safari](https://apps.apple.com/us/app/safari/id1146562112) - iOS default | ||
| 194 | browser | ||
| 195 | - [Unifi WiFiman](https://apps.apple.com/us/app/ubiquiti-wifiman/id1385561119) | ||
| 196 | - Create visual layouts of WiFi strength and save heat maps to your phone | ||
| 197 | |||
| 198 | ## Web Apps & Shortcuts | ||
| 199 | |||
| 200 | - [Brutalist Report](https://brutalist.report/) - Minimal news aggregator | ||
| 201 | - [\_Cyber.Report](https://cyber.report/) - Cybersecurity news aggregator | ||
| 202 | - [Hacker News](https://news.ycombinator.com/) - Mostly technical news | ||
| 203 | - [NextDNS](https://nextdns.io/) - NextDNS statistics dashboard | ||
| 204 | - [Readspike](https://readspike.com/) - Minimal news aggregator | ||
content/wiki/ios.org added +246
| @@ -0,0 +1,246 @@ | |||
| 1 | #+title: iOS | ||
| 2 | #+date: <2024-05-01> | ||
| 3 | |||
| 4 | Related: | ||
| 5 | |||
| 6 | - [[/wiki/hardware.html][Hardware]] | ||
| 7 | |||
| 8 | My primary mobile OS. Currently running iOS 17. This wiki page contains | ||
| 9 | most of the apps I have used at one point or another across my different | ||
| 10 | iPhones. | ||
| 11 | |||
| 12 | (=*=) = My favorites | ||
| 13 | |||
| 14 | ** Configuration | ||
| 15 | :PROPERTIES: | ||
| 16 | :CUSTOM_ID: configuration | ||
| 17 | :END: | ||
| 18 | *** Display | ||
| 19 | :PROPERTIES: | ||
| 20 | :CUSTOM_ID: display | ||
| 21 | :END: | ||
| 22 | - Light Mode | ||
| 23 | - 10:00 to 16:00 | ||
| 24 | - Dark Mode | ||
| 25 | - 16:00 to 10:00 | ||
| 26 | |||
| 27 | *** Focus Modes | ||
| 28 | :PROPERTIES: | ||
| 29 | :CUSTOM_ID: focus-modes | ||
| 30 | :END: | ||
| 31 | - Personal Focus | ||
| 32 | - 06:00 to 21:00 | ||
| 33 | - Allow Notifications From: | ||
| 34 | - Alarms | ||
| 35 | - Calendar | ||
| 36 | - Contacts (1 person) | ||
| 37 | - Messages | ||
| 38 | - Phone | ||
| 39 | - Reminders | ||
| 40 | - Signal | ||
| 41 | - UniFi Protect | ||
| 42 | - Sleep Focus | ||
| 43 | - 21:00 to 06:00 | ||
| 44 | - Allow Notifications From: | ||
| 45 | - Alarms | ||
| 46 | - Contacts (1 person) | ||
| 47 | - Reminders | ||
| 48 | - Signal | ||
| 49 | |||
| 50 | *** Privacy & Security | ||
| 51 | :PROPERTIES: | ||
| 52 | :CUSTOM_ID: privacy-security | ||
| 53 | :END: | ||
| 54 | I generally follow the | ||
| 55 | [[https://en.wikipedia.org/wiki/Principle_of_least_privilege][principle | ||
| 56 | of least privilege]] by only permitting the bare minimum privileges and | ||
| 57 | revoking as soon as they are no longer required. | ||
| 58 | |||
| 59 | Here's the baseline I start with: | ||
| 60 | |||
| 61 | - Disable: | ||
| 62 | - Analytics & Improvements | ||
| 63 | - Apple Advertising | ||
| 64 | - Apple ID > Sign-In & Security > Two-Factor Authentication | ||
| 65 | - Location Services > System Services > Product Improvement | ||
| 66 | - Tracking > Allow Apps to Request to Track | ||
| 67 | - Safari > Advanced > Privacy Preserving Ad Measurement | ||
| 68 | - Enable: | ||
| 69 | - Apple ID > iCloud > Advanced Data Protection | ||
| 70 | - Apple ID > Personal Information > Communication Preferences | ||
| 71 | - App Privacy Report | ||
| 72 | - Location Services only for Camera, Find My, UDisc, & WiFiman | ||
| 73 | (=While Using=) | ||
| 74 | - Safari > Prevent Cross-Site Tracking | ||
| 75 | - Safari > Hide IP Address | ||
| 76 | - Safari > Advanced > Advanced Tracking and Fingerprinting Protection | ||
| 77 | |||
| 78 | ** Native Apps | ||
| 79 | :PROPERTIES: | ||
| 80 | :CUSTOM_ID: native-apps | ||
| 81 | :END: | ||
| 82 | *** Business | ||
| 83 | :PROPERTIES: | ||
| 84 | :CUSTOM_ID: business | ||
| 85 | :END: | ||
| 86 | - [[https://apps.apple.com/us/app/element-messenger/id1083446067][Element]] - | ||
| 87 | A cross-platform messenger, based on Matrix | ||
| 88 | - [[https://apps.apple.com/us/app/linkedin-network-job-finder/id288429040][LinkedIn]] | ||
| 89 | - One of the only social media apps I use | ||
| 90 | |||
| 91 | *** Developer Tools | ||
| 92 | :PROPERTIES: | ||
| 93 | :CUSTOM_ID: developer-tools | ||
| 94 | :END: | ||
| 95 | - [[https://testflight.apple.com/join/F2vK7xo4][Harbour]] - Easily | ||
| 96 | manage your Portainer service | ||
| 97 | - [[https://apps.apple.com/us/app/ish-shell/id1436902243][iSH]] - A | ||
| 98 | local shell with SSH functionality | ||
| 99 | |||
| 100 | *** Entertainment | ||
| 101 | :PROPERTIES: | ||
| 102 | :CUSTOM_ID: entertainment | ||
| 103 | :END: | ||
| 104 | - [[https://apps.apple.com/us/app/plex-watch-live-tv-and-movies/id383457673][Plex]] | ||
| 105 | - A client for the Plex Media Server | ||
| 106 | - [[https://apps.apple.com/us/app/steam-mobile/id495369748][Steam]] - | ||
| 107 | The top gaming marketplace for computers | ||
| 108 | |||
| 109 | *** Lifestyle | ||
| 110 | :PROPERTIES: | ||
| 111 | :CUSTOM_ID: lifestyle | ||
| 112 | :END: | ||
| 113 | - [[https://apps.apple.com/us/app/home/id1110145103][Home]] (=*=) - | ||
| 114 | Apple homekit powered smart home manager | ||
| 115 | - [[https://apps.apple.com/us/app/philips-hue/id1055281310][Hue]] - | ||
| 116 | Philips Hue smart home manager | ||
| 117 | - [[https://apps.apple.com/us/app/irobot-home/id1012014442][iRobot]] - | ||
| 118 | Manage iRobot Roomba devices | ||
| 119 | - [[https://apps.apple.com/us/app/unifi-protect/id1392492235][UniFi | ||
| 120 | Protect]] - View and manage most UniFi Protect cameras and settings | ||
| 121 | |||
| 122 | *** Music | ||
| 123 | :PROPERTIES: | ||
| 124 | :CUSTOM_ID: music | ||
| 125 | :END: | ||
| 126 | - [[https://apps.apple.com/us/app/apple-music/id1108187390][Apple | ||
| 127 | Music]] (=*=) | ||
| 128 | - Apple's native music streaming app | ||
| 129 | - [[https://apps.apple.com/us/app/plexamp/id1500797510][Plexamp]] | ||
| 130 | (=*=) - Top-notch music app for your Plex Media Server, with a neural | ||
| 131 | network that provides excellent radio/shuffle suggestions | ||
| 132 | |||
| 133 | *** Navigation | ||
| 134 | :PROPERTIES: | ||
| 135 | :CUSTOM_ID: navigation | ||
| 136 | :END: | ||
| 137 | - [[https://apps.apple.com/us/app/deedum/id1546810946][deedum]] - A | ||
| 138 | Gemini Protocol browser | ||
| 139 | |||
| 140 | *** News | ||
| 141 | :PROPERTIES: | ||
| 142 | :CUSTOM_ID: news | ||
| 143 | :END: | ||
| 144 | - [[https://apps.apple.com/us/app/netnewswire-rss-reader/id1480640210][NetNewsWire]] | ||
| 145 | - A free and open source RSS reader for Mac, iPhone, and iPad | ||
| 146 | |||
| 147 | *** Photo & Video | ||
| 148 | :PROPERTIES: | ||
| 149 | :CUSTOM_ID: photo-video | ||
| 150 | :END: | ||
| 151 | - [[https://testflight.apple.com/join/Q6WyyEpS][Aislingeach]] - A quick | ||
| 152 | way to generate and rate images from the Stable Horde | ||
| 153 | - [[https://apps.apple.com/us/app/unsplash/id1290631746][Unsplash]] - | ||
| 154 | Premium images, mostly free | ||
| 155 | |||
| 156 | *** Productivity | ||
| 157 | :PROPERTIES: | ||
| 158 | :CUSTOM_ID: productivity | ||
| 159 | :END: | ||
| 160 | - [[https://apps.apple.com/us/app/beorg-to-do-list-agenda/id1238649962][beorg]] | ||
| 161 | - An org-mode editor, outline, and scheduler with paid extensions | ||
| 162 | - [[https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744][Bitwarden]] | ||
| 163 | (=*=) - An open source password manager | ||
| 164 | - [[https://apps.apple.com/us/app/bitwarden-authenticator/id6497335175][Bitwarden | ||
| 165 | Authenticator]] (=*=) - Generate 2FA on your device | ||
| 166 | - [[https://apps.apple.com/us/app/cryptomator/id1560822163][Cryptomator]] - | ||
| 167 | A cross-platform encryption program | ||
| 168 | - [[https://apps.apple.com/us/app/obsidian-connected-notes/id1557175442][Obsidian]] | ||
| 169 | (=*=) - A nice Markdown-based editor based on a "vault" structure. | ||
| 170 | Offers a paid sync solution and community extensions | ||
| 171 | - [[https://apps.apple.com/us/app/strongbox-password-manager/id897283731][Strongbox]] | ||
| 172 | - Keepass password manager for iOS & macOS | ||
| 173 | - [[https://apps.apple.com/us/app/unifi/id1057750338][UniFi Network]] - | ||
| 174 | View and manage most UniFi Network settings | ||
| 175 | |||
| 176 | *** Safari Extensions | ||
| 177 | :PROPERTIES: | ||
| 178 | :CUSTOM_ID: safari-extensions | ||
| 179 | :END: | ||
| 180 | - [[https://apps.apple.com/us/app/adguard-adblock-privacy/id1047223162][AdGuard]] | ||
| 181 | - Ad blocker | ||
| 182 | - [[https://apps.apple.com/us/app/dark-reader-for-safari/id1438243180][Dark | ||
| 183 | Reader]] - Dark mode for all the sites | ||
| 184 | - [[https://apps.apple.com/us/app/pipifier/id1234771095][PiPifier]] - | ||
| 185 | Force videos to support PiP | ||
| 186 | - [[https://apps.apple.com/us/app/privacy-redirect/id1578144015][Privacy | ||
| 187 | Redirect]] - Redirect select websites to others, usually to | ||
| 188 | privacy-focused alternatives | ||
| 189 | |||
| 190 | *** Social Networking | ||
| 191 | :PROPERTIES: | ||
| 192 | :CUSTOM_ID: social-networking | ||
| 193 | :END: | ||
| 194 | - [[https://apps.apple.com/us/app/multitab-for-tumblr/id1071533778][MultiTab | ||
| 195 | T]] (=*=) - A gallery-based Tumblr client with some unique features, | ||
| 196 | such as tab history and sync | ||
| 197 | - [[https://apps.apple.com/us/app/signal-private-messenger/id874139669][Signal]] | ||
| 198 | (=*=) - A simple, powerful, and secure messenger | ||
| 199 | - [[https://testflight.apple.com/join/mpVk1qIy][Three Cheers]] - A | ||
| 200 | client for Tildes.net with a design focus that matches the intent of | ||
| 201 | Tildes | ||
| 202 | - [[https://apps.apple.com/us/app/voyager-for-lemmy/id6451429762][Voyager]] - | ||
| 203 | A Lemmy client | ||
| 204 | |||
| 205 | *** Sports | ||
| 206 | :PROPERTIES: | ||
| 207 | :CUSTOM_ID: sports | ||
| 208 | :END: | ||
| 209 | - [[https://apps.apple.com/us/app/apple-sports/id6446788829][Apple | ||
| 210 | Sports]] - Apple's new sports app - lacks notifications and live | ||
| 211 | events | ||
| 212 | - [[https://apps.apple.com/us/app/udisc-disc-golf/id1072228953][UDisc]] - | ||
| 213 | Disc golf course maps, score cards, and more | ||
| 214 | |||
| 215 | *** Utilities | ||
| 216 | :PROPERTIES: | ||
| 217 | :CUSTOM_ID: utilities | ||
| 218 | :END: | ||
| 219 | - [[https://apps.apple.com/us/app/backblaze/id628638330][Backblaze]] - | ||
| 220 | Quickly view and manage Backblaze b2 cloud storage | ||
| 221 | - [[https://apps.apple.com/us/app/mullvad-vpn/id1488466513][Mullvad | ||
| 222 | VPN]] (=*=) | ||
| 223 | - A private VPN service | ||
| 224 | - [[https://apps.apple.com/us/app/otp-auth/id659877384][OTP Auth]] | ||
| 225 | (=*=) - A minimalistic OTP app with support for biometrics, custom | ||
| 226 | icons, import/export, and iCloud sync | ||
| 227 | - [[https://apps.apple.com/us/app/plex-dash/id1500797677][Plex Dash]] - | ||
| 228 | Stats about your Plex Media Server | ||
| 229 | - [[https://apps.apple.com/us/app/safari/id1146562112][Safari]] - iOS | ||
| 230 | default browser | ||
| 231 | - [[https://apps.apple.com/us/app/ubiquiti-wifiman/id1385561119][Unifi | ||
| 232 | WiFiman]] | ||
| 233 | - Create visual layouts of WiFi strength and save heat maps to your | ||
| 234 | phone | ||
| 235 | |||
| 236 | ** Web Apps & Shortcuts | ||
| 237 | :PROPERTIES: | ||
| 238 | :CUSTOM_ID: web-apps-shortcuts | ||
| 239 | :END: | ||
| 240 | - [[https://brutalist.report/][Brutalist Report]] - Minimal news | ||
| 241 | aggregator | ||
| 242 | - [[https://cyber.report/][_Cyber.Report]] - Cybersecurity news | ||
| 243 | aggregator | ||
| 244 | - [[https://news.ycombinator.com/][Hacker News]] - Mostly technical news | ||
| 245 | - [[https://nextdns.io/][NextDNS]] - NextDNS statistics dashboard | ||
| 246 | - [[https://readspike.com/][Readspike]] - Minimal news aggregator | ||
content/wiki/linux.md deleted −51
| @@ -1,51 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Linux" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++ | ||
| 6 | |||
| 7 | Related: | ||
| 8 | |||
| 9 | - [Hardware](/wiki/hardware/) | ||
| 10 | |||
| 11 | ## Alpine Linux | ||
| 12 | |||
| 13 | My secondary laptop OS. | ||
| 14 | |||
| 15 | ## Ubuntu Server | ||
| 16 | |||
| 17 | My primary server OS. | ||
| 18 | |||
| 19 | - Applications | ||
| 20 | - CrowdSec | ||
| 21 | - Netdata | ||
| 22 | - Nginx | ||
| 23 | - Plex Media Server | ||
| 24 | - Docker Containers | ||
| 25 | - AnonymousOverflow | ||
| 26 | - Authelia | ||
| 27 | - Convos | ||
| 28 | - CyberChef | ||
| 29 | - FlashPaper | ||
| 30 | - FreshRSS | ||
| 31 | - GotHub | ||
| 32 | - Gramps | ||
| 33 | - Invidious | ||
| 34 | - Libreddit | ||
| 35 | - NocoDB | ||
| 36 | - OtterWiki | ||
| 37 | - Portainer | ||
| 38 | - rgit | ||
| 39 | - Scrutiny | ||
| 40 | - SearXNG | ||
| 41 | - Stash | ||
| 42 | - Tautulli | ||
| 43 | - Teddit | ||
| 44 | - Watchtower | ||
| 45 | - Utilities | ||
| 46 | - byobu | ||
| 47 | - Certbot | ||
| 48 | - exiftool | ||
| 49 | - fail2ban | ||
| 50 | - ufw | ||
| 51 | - zfs | ||
content/wiki/macos.md deleted −206
| @@ -1,206 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "macOS" | ||
| 3 | updated = 2024-05-01 | ||
| 4 | draft = false | ||
| 5 | +++ | ||
| 6 | |||
| 7 | Related: | ||
| 8 | |||
| 9 | - [Hardware](/wiki/hardware/) | ||
| 10 | |||
| 11 | My primary OS. Currently running macOS Sonoma 14. This wiki page contains most | ||
| 12 | of the apps I have used at one point or another across my different Macbooks. | ||
| 13 | |||
| 14 | (`*`) = My favorites | ||
| 15 | |||
| 16 | ## Configuration | ||
| 17 | |||
| 18 | ### Disable System Services | ||
| 19 | |||
| 20 | - [Disabling and Enabling System Integrity | ||
| 21 | Protection](https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection) | ||
| 22 | - Disable Gatekeeper: `sudo spctl --master-disable` | ||
| 23 | |||
| 24 | ### Dotfiles | ||
| 25 | |||
| 26 | These are probably out of date, but they give a general idea of how I configure | ||
| 27 | my machine. | ||
| 28 | |||
| 29 | ```conf | ||
| 30 | # ~/.zshrc | ||
| 31 | export PATH="/opt/homebrew/bin:$PATH" | ||
| 32 | export PATH="$HOME/.emacs.d/bin:$PATH" | ||
| 33 | export EDITOR="/opt/homebrew/bin/emacs -nw" | ||
| 34 | ZSH_THEME="bureau" | ||
| 35 | plugins=(git zsh-autosuggestions) | ||
| 36 | source $ZSH/oh-my-zsh.sh | ||
| 37 | ``` | ||
| 38 | |||
| 39 | ```conf | ||
| 40 | # ~/.zprofile | ||
| 41 | eval "$(/opt/homebrew/bin/brew shellenv)" | ||
| 42 | ``` | ||
| 43 | |||
| 44 | ```conf | ||
| 45 | # ~/.config/skhd/skhdrc | ||
| 46 | cmd - return : /Applications/iTerm.app/Contents/MacOS/iTerm2 | ||
| 47 | cmd + shift - return : /Applications/LibreWolf.app/Contents/MacOS/librewolf | ||
| 48 | ``` | ||
| 49 | |||
| 50 | ```conf | ||
| 51 | # ~/.config/yabai/yabairc | ||
| 52 | yabai -m config \ | ||
| 53 | mouse_follows_focus off \ | ||
| 54 | focus_follows_mouse off \ | ||
| 55 | window_origin_display default \ | ||
| 56 | window_placement second_child \ | ||
| 57 | window_zoom_persist on \ | ||
| 58 | window_shadow on \ | ||
| 59 | window_animation_duration 0.0 \ | ||
| 60 | window_animation_frame_rate 120 \ | ||
| 61 | window_opacity_duration 0.0 \ | ||
| 62 | active_window_opacity 1.0 \ | ||
| 63 | normal_window_opacity 0.90 \ | ||
| 64 | window_opacity off \ | ||
| 65 | insert_feedback_color 0xffd75f5f \ | ||
| 66 | split_ratio 0.50 \ | ||
| 67 | split_type auto \ | ||
| 68 | auto_balance off \ | ||
| 69 | top_padding 15 \ | ||
| 70 | bottom_padding 15 \ | ||
| 71 | left_padding 15 \ | ||
| 72 | right_padding 15 \ | ||
| 73 | window_gap 10 \ | ||
| 74 | layout bsp \ | ||
| 75 | mouse_modifier fn \ | ||
| 76 | mouse_action1 move \ | ||
| 77 | mouse_action2 resize \ | ||
| 78 | mouse_drop_action swap | ||
| 79 | echo "yabai configuration loaded.." | ||
| 80 | ``` | ||
| 81 | |||
| 82 | ## Software | ||
| 83 | |||
| 84 | ### Browsers | ||
| 85 | |||
| 86 | - [Librewolf](https://librewolf.net/) (`*`) - Custom version of Firefox, | ||
| 87 | focused on privacy and security | ||
| 88 | - [Bitwarden](https://bitwarden.com/) - An open source password manager | ||
| 89 | - [Dark Reader](https://darkreader.org/) - Dark mode for all the websites | ||
| 90 | - [Libredirect](https://libredirect.github.io/) - Automatic web | ||
| 91 | redirections | ||
| 92 | - [Strongbox](https://strongboxsafe.com/) - Keepass password manager for | ||
| 93 | iOS & macOS | ||
| 94 | - [uBlock Origin](https://ublockorigin.com/) - Free, open-source ad | ||
| 95 | content blocker | ||
| 96 | - [Ungoogled | ||
| 97 | Chromium](https://github.com/ungoogled-software/ungoogled-chromium) - Google | ||
| 98 | Chromium, sans integration with Google | ||
| 99 | - [eww](https://www.gnu.org/software/emacs/manual/html_mono/eww.html) - Emacs | ||
| 100 | Web Wowser, for TUI browsing | ||
| 101 | |||
| 102 | ### Command Line Tools | ||
| 103 | |||
| 104 | - [doggo](https://github.com/mr-karan/doggo) - Command-line DNS Client for | ||
| 105 | Humans | ||
| 106 | - [duf](https://github.com/muesli/duf) - Disk Usage/Free Utility - a better | ||
| 107 | 'df' alternative | ||
| 108 | - [dust](https://github.com/bootandy/dust) - A more intuitive version of du in | ||
| 109 | rust | ||
| 110 | - [eza](https://github.com/eza-community/eza) - A modern alternative to ls | ||
| 111 | - [macchina](https://github.com/Macchina-CLI/macchina) - A system information | ||
| 112 | frontend with an emphasis on performance. | ||
| 113 | - [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A youtube-dl fork with | ||
| 114 | additional features and fixes | ||
| 115 | |||
| 116 | ### Communications | ||
| 117 | |||
| 118 | - [Element](https://element.io/) (`*`) - Matrix's default GUI client | ||
| 119 | - [gomuks](https://github.com/tulir/gomuks) - A terminal based Matrix client | ||
| 120 | - [Thunderbird](https://www.thunderbird.net/) (`*`) - An open source email | ||
| 121 | client by Mozilla | ||
| 122 | - [Signal](https://signal.org/) (`*`) - A simple, powerful, and secure | ||
| 123 | messenger | ||
| 124 | |||
| 125 | ### Development | ||
| 126 | |||
| 127 | - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - Docker | ||
| 128 | containers for your desktop | ||
| 129 | - [open-webui](https://github.com/open-webui/open-webui) - User-friendly | ||
| 130 | WebUI for LLMs | ||
| 131 | - [iTerm2](https://iterm2.com/) (`*`) - The best terminal for macOS, hands | ||
| 132 | down | ||
| 133 | - [Podman Desktop](https://podman-desktop.io/) (`*`) - Open source tool for | ||
| 134 | containers and Kubernetes | ||
| 135 | - [Xcode](https://developer.apple.com/xcode/) - Apple's IDE | ||
| 136 | - [zsh](https://en.wikipedia.org/wiki/Z_shell) (`*`) - My shell preference due | ||
| 137 | to its plugin and theme community | ||
| 138 | - [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) | ||
| 139 | - Fish-like autosuggestions for zsh | ||
| 140 | - [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting) | ||
| 141 | - Fish shell like syntax highlighting for Zsh | ||
| 142 | |||
| 143 | ### Editors | ||
| 144 | |||
| 145 | - [Doom Emacs](https://github.com/doomemacs/doomemacs) (`*`) - An Emacs | ||
| 146 | framework, great for working in org-mode | ||
| 147 | - [Obsidian](https://obsidian.md/) - A nice Markdown-based editor based on a | ||
| 148 | "vault" structure. Offers a paid sync solution and community extensions | ||
| 149 | - [Standard Notes](https://standardnotes.com/) - A simple text editor focused | ||
| 150 | on privacy and security. Offers a paid sync solution and community | ||
| 151 | extensions | ||
| 152 | - [VSCodium](https://vscodium.com/) - VS Code without proprietary blobs | ||
| 153 | |||
| 154 | ### Media | ||
| 155 | |||
| 156 | - [Luminar](https://skylum.com/luminar) - Luminar offers top-notch photo | ||
| 157 | editing features | ||
| 158 | - [Minecraft](https://www.minecraft.net/) - Block mining simulator | ||
| 159 | - [NetNewsWire](https://netnewswire.com/) - A free and open source RSS reader | ||
| 160 | for Mac, iPhone, and iPad | ||
| 161 | - [Plex](https://www.plex.tv/) (`*`) - Desktop client for the Plex Media | ||
| 162 | Server | ||
| 163 | - [Steam](https://store.steampowered.com/) - The top gaming marketplace for | ||
| 164 | computers | ||
| 165 | - [Transmission](https://transmissionbt.com/) (`*`) - A Fast, Easy and Free | ||
| 166 | Bittorrent Client | ||
| 167 | - [VLC](https://www.videolan.org/vlc/) - A free and open source cross-platform | ||
| 168 | multimedia player | ||
| 169 | |||
| 170 | ### Package Management | ||
| 171 | |||
| 172 | - [Homebrew](https://brew.sh/) (`*`) - The Missing Package Manager for macOS | ||
| 173 | (or Linux) | ||
| 174 | - [MacPorts](https://www.macports.org/) - A system to compile, install, and | ||
| 175 | manage open source software | ||
| 176 | |||
| 177 | ### Utilities | ||
| 178 | |||
| 179 | - [BetterDisplay](https://betterdisplay.pro/) - Allows you to tweak a ton of | ||
| 180 | features of built-in and external screens, such as scaling, configuration | ||
| 181 | overrides, and color/brightness upscaling | ||
| 182 | - [Bitwarden](https://bitwarden.com/) - An open source password manager | ||
| 183 | - [Ice](https://icemenubar.app/) (`*`)- A powerful menu bar management tool | ||
| 184 | - [LittleSnitch](https://obdev.at/products/littlesnitch/index.html) - Shows | ||
| 185 | all network connections on your Macbook, including system and privileged | ||
| 186 | services | ||
| 187 | - [MicroSnitch](https://obdev.at/products/microsnitch/index.html) - Camera & | ||
| 188 | microphone monitoring and alterting service | ||
| 189 | - [Mullvad](https://mullvad.net/) (`*`) - A private VPN service | ||
| 190 | - [Ollama](https://ollama.com/) - Run Llama 2, Code Llama, and other models | ||
| 191 | locally on your machine | ||
| 192 | - [Ollama Swift](https://github.com/kghandour/Ollama-SwiftUI) - User | ||
| 193 | Interface made for Ollama.ai using Swift | ||
| 194 | - [OrbStack](https://orbstack.dev/) - A fast and convenient GUI to manage | ||
| 195 | Docker contains and Linux VMs | ||
| 196 | - [Raycast](https://www.raycast.com/) - A collection of tools and shortcuts, | ||
| 197 | an alternative to Spotlight | ||
| 198 | - [skhd](https://github.com/koekeishiya/skhd) (`*`) - Simple hotkey daemon for | ||
| 199 | macOS | ||
| 200 | - [Strongbox](https://strongboxsafe.com/) - Keepass password manager for iOS & | ||
| 201 | macOS | ||
| 202 | - [Syncthing](https://syncthing.net/) (`*`) - Continuous file synchronization | ||
| 203 | - [TinkerTool](https://www.bresink.com/osx/TinkerTool.html) - Unlock hidden | ||
| 204 | configuration options for macOS | ||
| 205 | - [yabai](https://github.com/koekeishiya/yabai) (`*`) - Automatic window | ||
| 206 | tiling | ||
content/wiki/macos.org added +236
| @@ -0,0 +1,236 @@ | |||
| 1 | #+title: macOS | ||
| 2 | #+date: <2024-05-01> | ||
| 3 | |||
| 4 | Related: | ||
| 5 | |||
| 6 | - [[/wiki/hardware/][Hardware]] | ||
| 7 | |||
| 8 | My primary OS. Currently running macOS Sonoma 14. This wiki page | ||
| 9 | contains most of the apps I have used at one point or another across my | ||
| 10 | different Macbooks. | ||
| 11 | |||
| 12 | (=*=) = My favorites | ||
| 13 | |||
| 14 | ** Configuration | ||
| 15 | :PROPERTIES: | ||
| 16 | :CUSTOM_ID: configuration | ||
| 17 | :END: | ||
| 18 | *** Disable System Services | ||
| 19 | :PROPERTIES: | ||
| 20 | :CUSTOM_ID: disable-system-services | ||
| 21 | :END: | ||
| 22 | - [[https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection][Disabling | ||
| 23 | and Enabling System Integrity Protection]] | ||
| 24 | - Disable Gatekeeper: =sudo spctl --master-disable= | ||
| 25 | |||
| 26 | *** Dotfiles | ||
| 27 | :PROPERTIES: | ||
| 28 | :CUSTOM_ID: dotfiles | ||
| 29 | :END: | ||
| 30 | These are probably out of date, but they give a general idea of how I | ||
| 31 | configure my machine. | ||
| 32 | |||
| 33 | #+begin_src conf | ||
| 34 | # ~/.zshrc | ||
| 35 | export PATH="/opt/homebrew/bin:$PATH" | ||
| 36 | export PATH="$HOME/.emacs.d/bin:$PATH" | ||
| 37 | export EDITOR="/opt/homebrew/bin/emacs -nw" | ||
| 38 | ZSH_THEME="bureau" | ||
| 39 | plugins=(git zsh-autosuggestions) | ||
| 40 | source $ZSH/oh-my-zsh.sh | ||
| 41 | #+end_src | ||
| 42 | |||
| 43 | #+begin_src conf | ||
| 44 | # ~/.zprofile | ||
| 45 | eval "$(/opt/homebrew/bin/brew shellenv)" | ||
| 46 | #+end_src | ||
| 47 | |||
| 48 | #+begin_src conf | ||
| 49 | # ~/.config/skhd/skhdrc | ||
| 50 | cmd - return : /Applications/iTerm.app/Contents/MacOS/iTerm2 | ||
| 51 | cmd + shift - return : /Applications/LibreWolf.app/Contents/MacOS/librewolf | ||
| 52 | #+end_src | ||
| 53 | |||
| 54 | #+begin_src conf | ||
| 55 | # ~/.config/yabai/yabairc | ||
| 56 | yabai -m config \ | ||
| 57 | mouse_follows_focus off \ | ||
| 58 | focus_follows_mouse off \ | ||
| 59 | window_origin_display default \ | ||
| 60 | window_placement second_child \ | ||
| 61 | window_zoom_persist on \ | ||
| 62 | window_shadow on \ | ||
| 63 | window_animation_duration 0.0 \ | ||
| 64 | window_animation_frame_rate 120 \ | ||
| 65 | window_opacity_duration 0.0 \ | ||
| 66 | active_window_opacity 1.0 \ | ||
| 67 | normal_window_opacity 0.90 \ | ||
| 68 | window_opacity off \ | ||
| 69 | insert_feedback_color 0xffd75f5f \ | ||
| 70 | split_ratio 0.50 \ | ||
| 71 | split_type auto \ | ||
| 72 | auto_balance off \ | ||
| 73 | top_padding 15 \ | ||
| 74 | bottom_padding 15 \ | ||
| 75 | left_padding 15 \ | ||
| 76 | right_padding 15 \ | ||
| 77 | window_gap 10 \ | ||
| 78 | layout bsp \ | ||
| 79 | mouse_modifier fn \ | ||
| 80 | mouse_action1 move \ | ||
| 81 | mouse_action2 resize \ | ||
| 82 | mouse_drop_action swap | ||
| 83 | echo "yabai configuration loaded.." | ||
| 84 | #+end_src | ||
| 85 | |||
| 86 | ** Software | ||
| 87 | :PROPERTIES: | ||
| 88 | :CUSTOM_ID: software | ||
| 89 | :END: | ||
| 90 | *** Browsers | ||
| 91 | :PROPERTIES: | ||
| 92 | :CUSTOM_ID: browsers | ||
| 93 | :END: | ||
| 94 | - [[https://librewolf.net/][Librewolf]] (=*=) - Custom version of | ||
| 95 | Firefox, focused on privacy and security | ||
| 96 | - [[https://bitwarden.com/][Bitwarden]] - An open source password | ||
| 97 | manager | ||
| 98 | - [[https://darkreader.org/][Dark Reader]] - Dark mode for all the | ||
| 99 | websites | ||
| 100 | - [[https://libredirect.github.io/][Libredirect]] - Automatic web | ||
| 101 | redirections | ||
| 102 | - [[https://strongboxsafe.com/][Strongbox]] - Keepass password manager | ||
| 103 | for iOS & macOS | ||
| 104 | - [[https://ublockorigin.com/][uBlock Origin]] - Free, open-source ad | ||
| 105 | content blocker | ||
| 106 | - [[https://github.com/ungoogled-software/ungoogled-chromium][Ungoogled | ||
| 107 | Chromium]] - Google Chromium, sans integration with Google | ||
| 108 | - [[https://www.gnu.org/software/emacs/manual/html_mono/eww.html][eww]] - | ||
| 109 | Emacs Web Wowser, for TUI browsing | ||
| 110 | |||
| 111 | *** Command Line Tools | ||
| 112 | :PROPERTIES: | ||
| 113 | :CUSTOM_ID: command-line-tools | ||
| 114 | :END: | ||
| 115 | - [[https://github.com/mr-karan/doggo][doggo]] - Command-line DNS Client | ||
| 116 | for Humans | ||
| 117 | - [[https://github.com/muesli/duf][duf]] - Disk Usage/Free Utility - a | ||
| 118 | better 'df' alternative | ||
| 119 | - [[https://github.com/bootandy/dust][dust]] - A more intuitive version | ||
| 120 | of du in rust | ||
| 121 | - [[https://github.com/eza-community/eza][eza]] - A modern alternative | ||
| 122 | to ls | ||
| 123 | - [[https://github.com/Macchina-CLI/macchina][macchina]] - A system | ||
| 124 | information frontend with an emphasis on performance. | ||
| 125 | - [[https://github.com/yt-dlp/yt-dlp][yt-dlp]] - A youtube-dl fork with | ||
| 126 | additional features and fixes | ||
| 127 | |||
| 128 | *** Communications | ||
| 129 | :PROPERTIES: | ||
| 130 | :CUSTOM_ID: communications | ||
| 131 | :END: | ||
| 132 | - [[https://element.io/][Element]] (=*=) - Matrix's default GUI client | ||
| 133 | - [[https://github.com/tulir/gomuks][gomuks]] - A terminal based Matrix | ||
| 134 | client | ||
| 135 | - [[https://www.thunderbird.net/][Thunderbird]] (=*=) - An open source | ||
| 136 | email client by Mozilla | ||
| 137 | - [[https://signal.org/][Signal]] (=*=) - A simple, powerful, and secure | ||
| 138 | messenger | ||
| 139 | |||
| 140 | *** Development | ||
| 141 | :PROPERTIES: | ||
| 142 | :CUSTOM_ID: development | ||
| 143 | :END: | ||
| 144 | - [[https://www.docker.com/products/docker-desktop/][Docker Desktop]] - | ||
| 145 | Docker containers for your desktop | ||
| 146 | - [[https://github.com/open-webui/open-webui][open-webui]] - | ||
| 147 | User-friendly WebUI for LLMs | ||
| 148 | - [[https://iterm2.com/][iTerm2]] (=*=) - The best terminal for macOS, | ||
| 149 | hands down | ||
| 150 | - [[https://podman-desktop.io/][Podman Desktop]] (=*=) - Open source | ||
| 151 | tool for containers and Kubernetes | ||
| 152 | - [[https://developer.apple.com/xcode/][Xcode]] - Apple's IDE | ||
| 153 | - [[https://en.wikipedia.org/wiki/Z_shell][zsh]] (=*=) - My shell | ||
| 154 | preference due to its plugin and theme community | ||
| 155 | - [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]] | ||
| 156 | - Fish-like autosuggestions for zsh | ||
| 157 | - [[https://github.com/zsh-users/zsh-syntax-highlighting][zsh-syntax-highlighting]] | ||
| 158 | - Fish shell like syntax highlighting for Zsh | ||
| 159 | |||
| 160 | *** Editors | ||
| 161 | :PROPERTIES: | ||
| 162 | :CUSTOM_ID: editors | ||
| 163 | :END: | ||
| 164 | - [[https://github.com/doomemacs/doomemacs][Doom Emacs]] (=*=) - An | ||
| 165 | Emacs framework, great for working in org-mode | ||
| 166 | - [[https://obsidian.md/][Obsidian]] - A nice Markdown-based editor | ||
| 167 | based on a "vault" structure. Offers a paid sync solution and | ||
| 168 | community extensions | ||
| 169 | - [[https://standardnotes.com/][Standard Notes]] - A simple text editor | ||
| 170 | focused on privacy and security. Offers a paid sync solution and | ||
| 171 | community extensions | ||
| 172 | - [[https://vscodium.com/][VSCodium]] - VS Code without proprietary | ||
| 173 | blobs | ||
| 174 | |||
| 175 | *** Media | ||
| 176 | :PROPERTIES: | ||
| 177 | :CUSTOM_ID: media | ||
| 178 | :END: | ||
| 179 | - [[https://skylum.com/luminar][Luminar]] - Luminar offers top-notch | ||
| 180 | photo editing features | ||
| 181 | - [[https://www.minecraft.net/][Minecraft]] - Block mining simulator | ||
| 182 | - [[https://netnewswire.com/][NetNewsWire]] - A free and open source RSS | ||
| 183 | reader for Mac, iPhone, and iPad | ||
| 184 | - [[https://www.plex.tv/][Plex]] (=*=) - Desktop client for the Plex | ||
| 185 | Media Server | ||
| 186 | - [[https://store.steampowered.com/][Steam]] - The top gaming | ||
| 187 | marketplace for computers | ||
| 188 | - [[https://transmissionbt.com/][Transmission]] (=*=) - A Fast, Easy and | ||
| 189 | Free Bittorrent Client | ||
| 190 | - [[https://www.videolan.org/vlc/][VLC]] - A free and open source | ||
| 191 | cross-platform multimedia player | ||
| 192 | |||
| 193 | *** Package Management | ||
| 194 | :PROPERTIES: | ||
| 195 | :CUSTOM_ID: package-management | ||
| 196 | :END: | ||
| 197 | - [[https://brew.sh/][Homebrew]] (=*=) - The Missing Package Manager for | ||
| 198 | macOS (or Linux) | ||
| 199 | - [[https://www.macports.org/][MacPorts]] - A system to compile, | ||
| 200 | install, and manage open source software | ||
| 201 | |||
| 202 | *** Utilities | ||
| 203 | :PROPERTIES: | ||
| 204 | :CUSTOM_ID: utilities | ||
| 205 | :END: | ||
| 206 | - [[https://betterdisplay.pro/][BetterDisplay]] - Allows you to tweak a | ||
| 207 | ton of features of built-in and external screens, such as scaling, | ||
| 208 | configuration overrides, and color/brightness upscaling | ||
| 209 | - [[https://bitwarden.com/][Bitwarden]] - An open source password | ||
| 210 | manager | ||
| 211 | - [[https://icemenubar.app/][Ice]] (=*=)- A powerful menu bar management | ||
| 212 | tool | ||
| 213 | - [[https://obdev.at/products/littlesnitch/index.html][LittleSnitch]] - | ||
| 214 | Shows all network connections on your Macbook, including system and | ||
| 215 | privileged services | ||
| 216 | - [[https://obdev.at/products/microsnitch/index.html][MicroSnitch]] - | ||
| 217 | Camera & microphone monitoring and alterting service | ||
| 218 | - [[https://mullvad.net/][Mullvad]] (=*=) - A private VPN service | ||
| 219 | - [[https://ollama.com/][Ollama]] - Run Llama 2, Code Llama, and other | ||
| 220 | models locally on your machine | ||
| 221 | - [[https://github.com/kghandour/Ollama-SwiftUI][Ollama Swift]] - User | ||
| 222 | Interface made for Ollama.ai using Swift | ||
| 223 | - [[https://orbstack.dev/][OrbStack]] - A fast and convenient GUI to | ||
| 224 | manage Docker contains and Linux VMs | ||
| 225 | - [[https://www.raycast.com/][Raycast]] - A collection of tools and | ||
| 226 | shortcuts, an alternative to Spotlight | ||
| 227 | - [[https://github.com/koekeishiya/skhd][skhd]] (=*=) - Simple hotkey | ||
| 228 | daemon for macOS | ||
| 229 | - [[https://strongboxsafe.com/][Strongbox]] - Keepass password manager | ||
| 230 | for iOS & macOS | ||
| 231 | - [[https://syncthing.net/][Syncthing]] (=*=) - Continuous file | ||
| 232 | synchronization | ||
| 233 | - [[https://www.bresink.com/osx/TinkerTool.html][TinkerTool]] - Unlock | ||
| 234 | hidden configuration options for macOS | ||
| 235 | - [[https://github.com/koekeishiya/yabai][yabai]] (=*=) - Automatic | ||
| 236 | window tiling | ||
content/wiki/management.md deleted −11
| @@ -1,11 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Management" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Time Management | ||
| 8 | |||
| 9 | ## People Management | ||
| 10 | |||
| 11 | ## Project Management | ||
content/wiki/minimalism.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Minimalism" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/networking.md deleted −27
| @@ -1,27 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Networking" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Bluetooth | ||
| 8 | |||
| 9 | ## DNS | ||
| 10 | |||
| 11 | ## Gemini | ||
| 12 | |||
| 13 | ## Gopher | ||
| 14 | |||
| 15 | ## HTTP | ||
| 16 | |||
| 17 | ## IoT | ||
| 18 | |||
| 19 | ## Nginx | ||
| 20 | |||
| 21 | ## SSH | ||
| 22 | |||
| 23 | ## SSL | ||
| 24 | |||
| 25 | ## VPN | ||
| 26 | |||
| 27 | ## Wi-Fi | ||
content/wiki/plaintext.md deleted −11
| @@ -1,11 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Plaintext" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Markdown | ||
| 8 | |||
| 9 | ## Org-Mode | ||
| 10 | |||
| 11 | ## AsciiDoc | ||
content/wiki/privacy.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Privacy" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/python.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Python" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/security.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Security" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/selfhosted.md deleted −5
| @@ -1,5 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Self-Hosted" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++\ No newline at end of file | ||
content/wiki/windows.md deleted −18
| @@ -1,18 +0,0 @@ | |||
| 1 | +++ | ||
| 2 | title = "Windows" | ||
| 3 | updated = 2024-03-07 | ||
| 4 | draft = true | ||
| 5 | +++ | ||
| 6 | |||
| 7 | ## Windows (Work) | ||
| 8 | |||
| 9 | I am required to use a Windows 10 laptop for work, but I have some | ||
| 10 | limited customization options. | ||
| 11 | |||
| 12 | - Alteryx | ||
| 13 | - Notepad++ | ||
| 14 | - Obsidian | ||
| 15 | - Power BI | ||
| 16 | - Task Scheduler | ||
| 17 | - Auto Dark Mode | ||
| 18 | - Auto Light Mode | ||
publish.el added +98
| @@ -0,0 +1,98 @@ | |||
| 1 | ;; explicity load packages since I'm using Doom Emacs | ||
| 2 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/emacs-htmlize") | ||
| 3 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/weblorg") | ||
| 4 | (add-to-list 'load-path "~/.emacs.d/.local/straight/repos/templatel") | ||
| 5 | (require 'htmlize) | ||
| 6 | (require 'weblorg) | ||
| 7 | |||
| 8 | ;; defaults to http://localhost:8000 | ||
| 9 | ;; ENV=prod emacs --script publish.el | ||
| 10 | (if (string= (getenv "ENV") "prod") | ||
| 11 | (setq weblorg-default-url "https://cleberg.net")) | ||
| 12 | |||
| 13 | (weblorg-site | ||
| 14 | :theme nil | ||
| 15 | :template-vars '(("site_name" . "cleberg.net") | ||
| 16 | ("site_owner" . "Christian Cleberg <hello@cleberg.net>") | ||
| 17 | ("site_description" . "Just a blip of ones and zeroes."))) | ||
| 18 | |||
| 19 | ;; route for rendering the index page of the website | ||
| 20 | (weblorg-route | ||
| 21 | :name "index" | ||
| 22 | :input-pattern "content/index.org" | ||
| 23 | :template "index.html" | ||
| 24 | :output ".build/index.html" | ||
| 25 | :url "/") | ||
| 26 | |||
| 27 | ;; route for rendering each post | ||
| 28 | (weblorg-route | ||
| 29 | :name "blog" | ||
| 30 | :input-pattern "content/blog/*.org" | ||
| 31 | :template "post.html" | ||
| 32 | :output ".build/blog/{{ slug }}.html" | ||
| 33 | :url "/blog/{{ slug }}.html") | ||
| 34 | |||
| 35 | ;; route for rendering the index page of the blog | ||
| 36 | (weblorg-route | ||
| 37 | :name "blog-index" | ||
| 38 | :input-pattern "content/blog/*.org" | ||
| 39 | :input-aggregate #'weblorg-input-aggregate-all-desc | ||
| 40 | :template "blog.html" | ||
| 41 | :output ".build/blog/index.html" | ||
| 42 | :url "/blog/") | ||
| 43 | |||
| 44 | ;; route for rendering each wiki post | ||
| 45 | (weblorg-route | ||
| 46 | :name "wiki" | ||
| 47 | :input-pattern "content/wiki/*.org" | ||
| 48 | :template "post.html" | ||
| 49 | :output ".build/wiki/{{ slug }}.html" | ||
| 50 | :url "/wiki/{{ slug }}.html") | ||
| 51 | |||
| 52 | ;; route for rendering the index page of the wiki | ||
| 53 | (weblorg-route | ||
| 54 | :name "wiki-index" | ||
| 55 | :input-pattern "content/wiki/*.org" | ||
| 56 | :input-aggregate #'weblorg-input-aggregate-all | ||
| 57 | :template "wiki.html" | ||
| 58 | :output ".build/wiki/index.html" | ||
| 59 | :url "/wiki/") | ||
| 60 | |||
| 61 | ;; route for rendering each page | ||
| 62 | (weblorg-route | ||
| 63 | :name "pages" | ||
| 64 | :input-pattern "content/*.org" | ||
| 65 | :template "page.html" | ||
| 66 | :output ".build/{{ slug }}.html" | ||
| 67 | :url "/{{ slug }}.html") | ||
| 68 | |||
| 69 | (weblorg-route | ||
| 70 | :name "salary" | ||
| 71 | :input-pattern "content/salary/*.org" | ||
| 72 | :template "page.html" | ||
| 73 | :output ".build/salary/{{ slug }}.html" | ||
| 74 | :url "/salary/{{ slug }}.html") | ||
| 75 | |||
| 76 | (weblorg-route | ||
| 77 | :name "services" | ||
| 78 | :input-pattern "content/services/*.org" | ||
| 79 | :template "page.html" | ||
| 80 | :output ".build/services/{{ slug }}.html" | ||
| 81 | :url "/services/{{ slug }}.html") | ||
| 82 | |||
| 83 | ;; RSS Feed | ||
| 84 | (weblorg-route | ||
| 85 | :name "rss" | ||
| 86 | :input-pattern "content/blog/*.org" | ||
| 87 | :input-aggregate #'weblorg-input-aggregate-all-desc | ||
| 88 | :template "feed.xml" | ||
| 89 | :output ".build/feed.xml" | ||
| 90 | :url "/feed.xml") | ||
| 91 | |||
| 92 | ;; route for static assets that also copies files to .build directory | ||
| 93 | (weblorg-copy-static | ||
| 94 | :output ".build/{{ file }}" | ||
| 95 | :url "/{{ file }}") | ||
| 96 | |||
| 97 | ;; fire the engine and export all the files declared in the routes above | ||
| 98 | (weblorg-export) | ||
static/syntax-theme-dark.css deleted −280
| @@ -1,280 +0,0 @@ | |||
| 1 | /* | ||
| 2 | * theme "ayu" generated by syntect | ||
| 3 | */ | ||
| 4 | |||
| 5 | .z-code { | ||
| 6 | color: #bfbab0; | ||
| 7 | background-color: #0f1419; | ||
| 8 | } | ||
| 9 | |||
| 10 | .z-comment, .z-punctuation.z-definition.z-comment { | ||
| 11 | color: #5c6773; | ||
| 12 | font-style: italic; | ||
| 13 | } | ||
| 14 | .z-variable { | ||
| 15 | color: #bfbab0; | ||
| 16 | } | ||
| 17 | .z-string, .z-constant.z-other.z-symbol { | ||
| 18 | color: #c2d94c; | ||
| 19 | } | ||
| 20 | .z-constant.z-numeric { | ||
| 21 | color: #f29718; | ||
| 22 | } | ||
| 23 | .z-string.z-regexp, .z-constant.z-character.z-escape { | ||
| 24 | color: #95e6cb; | ||
| 25 | } | ||
| 26 | .z-constant.z-language { | ||
| 27 | color: #f29718; | ||
| 28 | } | ||
| 29 | .z-constant.z-character, .z-constant.z-other { | ||
| 30 | color: #f29718; | ||
| 31 | } | ||
| 32 | .z-variable.z-member { | ||
| 33 | color: #ec5f67; | ||
| 34 | } | ||
| 35 | .z-keyword, .z-keyword.z-operator.z-word { | ||
| 36 | color: #ff7733; | ||
| 37 | } | ||
| 38 | .z-keyword.z-operator { | ||
| 39 | color: #f29668; | ||
| 40 | } | ||
| 41 | .z-punctuation.z-separator, .z-punctuation.z-terminator { | ||
| 42 | color: #bfbab0; | ||
| 43 | } | ||
| 44 | .z-punctuation.z-section { | ||
| 45 | color: #bfbab0; | ||
| 46 | } | ||
| 47 | .z-punctuation.z-accessor { | ||
| 48 | color: #f29668; | ||
| 49 | } | ||
| 50 | .z-punctuation.z-definition.z-annotation { | ||
| 51 | color: #bfbab0; | ||
| 52 | } | ||
| 53 | .z-variable.z-other.z-dollar.z-only.z-js, .z-variable.z-other.z-object.z-dollar.z-only.z-js, .z-variable.z-type.z-dollar.z-only.z-js, .z-support.z-class.z-dollar.z-only.z-js { | ||
| 54 | color: #e6b673; | ||
| 55 | } | ||
| 56 | .z-storage { | ||
| 57 | color: #ff7733; | ||
| 58 | } | ||
| 59 | .z-storage.z-type { | ||
| 60 | color: #ff7733; | ||
| 61 | } | ||
| 62 | .z-entity.z-name.z-function { | ||
| 63 | color: #ffb454; | ||
| 64 | } | ||
| 65 | .z-entity.z-name, .z-entity.z-name.z-tag, .z-entity.z-name.z-label) { | ||
| 66 | color: #59c2ff; | ||
| 67 | } | ||
| 68 | .z-entity.z-other.z-inherited-class { | ||
| 69 | color: #59c2ff; | ||
| 70 | text-decoration: underline; | ||
| 71 | } | ||
| 72 | .z-variable.z-parameter { | ||
| 73 | color: #f29718; | ||
| 74 | } | ||
| 75 | .z-variable.z-language { | ||
| 76 | color: #39bae6; | ||
| 77 | font-style: italic; | ||
| 78 | } | ||
| 79 | .z-entity.z-name.z-tag, .z-meta.z-tag.z-sgml { | ||
| 80 | color: #39bae6; | ||
| 81 | } | ||
| 82 | .z-punctuation.z-definition.z-tag.z-end, .z-punctuation.z-definition.z-tag.z-begin, .z-punctuation.z-definition.z-tag { | ||
| 83 | color: #39bae6; | ||
| 84 | } | ||
| 85 | .z-entity.z-other.z-attribute-name { | ||
| 86 | color: #ffb454; | ||
| 87 | } | ||
| 88 | .z-variable.z-function, .z-variable.z-annotation { | ||
| 89 | color: #ffb454; | ||
| 90 | } | ||
| 91 | .z-support.z-function, .z-support.z-macro { | ||
| 92 | color: #f07178; | ||
| 93 | } | ||
| 94 | .z-support.z-constant { | ||
| 95 | color: #f29668; | ||
| 96 | font-style: italic; | ||
| 97 | } | ||
| 98 | .z-support.z-type, .z-support.z-class { | ||
| 99 | color: #39bae6; | ||
| 100 | font-style: italic; | ||
| 101 | } | ||
| 102 | .z-invalid { | ||
| 103 | color: #ff3333; | ||
| 104 | } | ||
| 105 | .z-invalid.z-deprecated { | ||
| 106 | color: #ffffff; | ||
| 107 | background-color: #ff7733; | ||
| 108 | } | ||
| 109 | .z-meta.z-diff, .z-meta.z-diff.z-header { | ||
| 110 | color: #c594c5; | ||
| 111 | } | ||
| 112 | .z-source.z-ruby .z-variable.z-other.z-readwrite { | ||
| 113 | color: #ffb454; | ||
| 114 | } | ||
| 115 | .z-source.z-css .z-entity.z-name.z-tag, .z-source.z-sass .z-entity.z-name.z-tag, .z-source.z-scss .z-entity.z-name.z-tag, .z-source.z-less .z-entity.z-name.z-tag, .z-source.z-stylus .z-entity.z-name.z-tag { | ||
| 116 | color: #59c2ff; | ||
| 117 | } | ||
| 118 | .z-source.z-css .z-support.z-type, .z-source.z-sass .z-support.z-type, .z-source.z-scss .z-support.z-type, .z-source.z-less .z-support.z-type, .z-source.z-stylus .z-support.z-type { | ||
| 119 | color: #5c6773; | ||
| 120 | } | ||
| 121 | .z-support.z-type.z-property-name { | ||
| 122 | color: #39bae6; | ||
| 123 | } | ||
| 124 | .z-constant.z-numeric.z-line-number.z-find-in-files { | ||
| 125 | color: #5c6773; | ||
| 126 | } | ||
| 127 | .z-constant.z-numeric.z-line-number.z-match { | ||
| 128 | color: #ff7733; | ||
| 129 | } | ||
| 130 | .z-entity.z-name.z-filename.z-find-in-files { | ||
| 131 | color: #c2d94c; | ||
| 132 | } | ||
| 133 | .z-message.z-error { | ||
| 134 | color: #ff3333; | ||
| 135 | } | ||
| 136 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 137 | color: #39bae6; | ||
| 138 | } | ||
| 139 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 140 | color: #39bae6; | ||
| 141 | } | ||
| 142 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 143 | color: #39bae6; | ||
| 144 | } | ||
| 145 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 146 | color: #39bae6; | ||
| 147 | } | ||
| 148 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 149 | color: #39bae6; | ||
| 150 | } | ||
| 151 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 152 | color: #39bae6; | ||
| 153 | } | ||
| 154 | .z-source.z-json .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 155 | color: #39bae6; | ||
| 156 | } | ||
| 157 | .z-source.z-json .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 158 | color: #39bae6; | ||
| 159 | } | ||
| 160 | .z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string { | ||
| 161 | color: #39bae6; | ||
| 162 | } | ||
| 163 | .z-markup.z-heading { | ||
| 164 | color: #ff7733; | ||
| 165 | font-weight: bold; | ||
| 166 | } | ||
| 167 | .z-string.z-other.z-link, .z-markup.z-underline.z-link { | ||
| 168 | color: #95e6cb; | ||
| 169 | text-decoration: underline; | ||
| 170 | font-style: italic; | ||
| 171 | } | ||
| 172 | .z-punctuation.z-definition.z-image { | ||
| 173 | color: #ffb454; | ||
| 174 | } | ||
| 175 | .z-markup.z-italic { | ||
| 176 | color: #f07178; | ||
| 177 | font-style: italic; | ||
| 178 | } | ||
| 179 | .z-markup.z-bold { | ||
| 180 | color: #f07178; | ||
| 181 | font-weight: bold; | ||
| 182 | } | ||
| 183 | .z-markup.z-italic .z-markup.z-bold, .z-markup.z-bold .z-markup.z-italic { | ||
| 184 | font-weight: bold; | ||
| 185 | font-style: italic; | ||
| 186 | } | ||
| 187 | .z-markup.z-raw { | ||
| 188 | background-color: #bfbab0; | ||
| 189 | } | ||
| 190 | .z-markup.z-raw.z-inline { | ||
| 191 | background-color: #bfbab0; | ||
| 192 | } | ||
| 193 | .z-meta.z-separator { | ||
| 194 | color: #5c6773; | ||
| 195 | background-color: #bfbab0; | ||
| 196 | font-weight: bold; | ||
| 197 | } | ||
| 198 | .z-markup.z-quote { | ||
| 199 | color: #f29718; | ||
| 200 | font-style: italic; | ||
| 201 | } | ||
| 202 | .z-markup.z-list.z-numbered.z-bullet, .z-markup.z-list .z-punctuation.z-definition.z-list_item { | ||
| 203 | color: #95e6cb; | ||
| 204 | } | ||
| 205 | .z-markup.z-inserted { | ||
| 206 | color: #c2d94c; | ||
| 207 | } | ||
| 208 | .z-markup.z-changed { | ||
| 209 | color: #39bae6; | ||
| 210 | } | ||
| 211 | .z-markup.z-deleted { | ||
| 212 | color: #f07178; | ||
| 213 | } | ||
| 214 | .z-markup.z-strike { | ||
| 215 | color: #e6b673; | ||
| 216 | } | ||
| 217 | .z-markup.z-table { | ||
| 218 | color: #39bae6; | ||
| 219 | background-color: #bfbab0; | ||
| 220 | } | ||
| 221 | .z-text.z-html.z-markdown .z-markup.z-raw.z-inline { | ||
| 222 | color: #f29668; | ||
| 223 | } | ||
| 224 | .z-text.z-html.z-markdown .z-meta.z-dummy.z-line-break { | ||
| 225 | color: #5c6773; | ||
| 226 | } | ||
| 227 | .z-markup.z-raw.z-block.z-fenced.z-markdown { | ||
| 228 | color: #bfbab0; | ||
| 229 | background-color: #bfbab0; | ||
| 230 | } | ||
| 231 | .z-punctuation.z-definition.z-fenced.z-markdown, .z-variable.z-language.z-fenced.z-markdown { | ||
| 232 | color: #5c6773; | ||
| 233 | background-color: #bfbab0; | ||
| 234 | } | ||
| 235 | .z-variable.z-language.z-fenced.z-markdown { | ||
| 236 | color: #5c6773; | ||
| 237 | } | ||
| 238 | .z-markup.z-inserted.z-git_gutter { | ||
| 239 | color: #c2d94c; | ||
| 240 | } | ||
| 241 | .z-markup.z-changed.z-git_gutter { | ||
| 242 | color: #39bae6; | ||
| 243 | } | ||
| 244 | .z-markup.z-deleted.z-git_gutter { | ||
| 245 | color: #ff3333; | ||
| 246 | } | ||
| 247 | .z-markup.z-ignored.z-git_gutter { | ||
| 248 | color: #20272c; | ||
| 249 | } | ||
| 250 | .z-markup.z-untracked.z-git_gutter { | ||
| 251 | color: #20272c; | ||
| 252 | } | ||
| 253 | .z-gutter_color { | ||
| 254 | color: #ffffff; | ||
| 255 | } | ||
| 256 | .z-acejump.z-label.z-blue { | ||
| 257 | color: #ffffff; | ||
| 258 | background-color: #39bae6; | ||
| 259 | } | ||
| 260 | .z-acejump.z-label.z-green { | ||
| 261 | color: #ffffff; | ||
| 262 | background-color: #c2d94c; | ||
| 263 | } | ||
| 264 | .z-acejump.z-label.z-orange { | ||
| 265 | color: #ffffff; | ||
| 266 | background-color: #ff7733; | ||
| 267 | } | ||
| 268 | .z-acejump.z-label.z-purple { | ||
| 269 | color: #ffffff; | ||
| 270 | background-color: #f07178; | ||
| 271 | } | ||
| 272 | .z-sublimelinter.z-mark.z-warning { | ||
| 273 | color: #39bae6; | ||
| 274 | } | ||
| 275 | .z-sublimelinter.z-gutter-mark { | ||
| 276 | color: #ffffff; | ||
| 277 | } | ||
| 278 | .z-sublimelinter.z-mark.z-error { | ||
| 279 | color: #ff3333; | ||
| 280 | } | ||
static/syntax-theme-light.css deleted −407
| @@ -1,407 +0,0 @@ | |||
| 1 | /* | ||
| 2 | * theme "GitHub" generated by syntect | ||
| 3 | */ | ||
| 4 | |||
| 5 | .z-code { | ||
| 6 | color: #323232; | ||
| 7 | background-color: #ffffff; | ||
| 8 | } | ||
| 9 | |||
| 10 | .z-comment { | ||
| 11 | color: #969896; | ||
| 12 | font-style: italic; | ||
| 13 | } | ||
| 14 | .z-string { | ||
| 15 | color: #183691; | ||
| 16 | } | ||
| 17 | .z-regexp-operator { | ||
| 18 | color: #a71d5d; | ||
| 19 | } | ||
| 20 | .z-string.z-regexp.z-characterclass .z-punctuation.z-definition.z-string.z-begin, .z-string.z-regexp.z-characterclass .z-punctuation.z-definition.z-string.z-end { | ||
| 21 | color: #a71d5d; | ||
| 22 | } | ||
| 23 | .z-constant.z-numeric { | ||
| 24 | color: #0086b3; | ||
| 25 | } | ||
| 26 | .z-constant.z-language { | ||
| 27 | color: #0086b3; | ||
| 28 | } | ||
| 29 | .z-constant.z-character, .z-constant.z-other, .z-variable.z-other.z-constant { | ||
| 30 | color: #0086b3; | ||
| 31 | } | ||
| 32 | .z-variable { | ||
| 33 | color: #323232; | ||
| 34 | } | ||
| 35 | .z-keyword { | ||
| 36 | color: #a71d5d; | ||
| 37 | font-weight: bold; | ||
| 38 | } | ||
| 39 | .z-bitwise-operator { | ||
| 40 | color: #a71d5d; | ||
| 41 | font-weight: bold; | ||
| 42 | } | ||
| 43 | .z-storage { | ||
| 44 | color: #a71d5d; | ||
| 45 | font-weight: bold; | ||
| 46 | } | ||
| 47 | .z-storage.z-type { | ||
| 48 | color: #a71d5d; | ||
| 49 | font-weight: bold; | ||
| 50 | } | ||
| 51 | .z-entity.z-name.z-class { | ||
| 52 | color: #0086b3; | ||
| 53 | } | ||
| 54 | .z-entity.z-other.z-inherited-class { | ||
| 55 | color: #0086b3; | ||
| 56 | } | ||
| 57 | .z-entity.z-name.z-function { | ||
| 58 | color: #795da3; | ||
| 59 | font-weight: bold; | ||
| 60 | } | ||
| 61 | .z-variable.z-parameter { | ||
| 62 | color: #323232; | ||
| 63 | } | ||
| 64 | .z-entity.z-name.z-tag { | ||
| 65 | color: #63a35c; | ||
| 66 | } | ||
| 67 | .z-entity.z-other.z-attribute-name { | ||
| 68 | color: #795da3; | ||
| 69 | } | ||
| 70 | .z-support.z-function { | ||
| 71 | color: #62a35c; | ||
| 72 | } | ||
| 73 | .z-support.z-constant { | ||
| 74 | color: #0086b3; | ||
| 75 | } | ||
| 76 | .z-support.z-type, .z-support.z-class { | ||
| 77 | color: #0086b3; | ||
| 78 | } | ||
| 79 | .z-support.z-other.z-variable { | ||
| 80 | color: #323232; | ||
| 81 | } | ||
| 82 | .z-invalid, .z-invalid.z-illegal, .z-invalid.z-deprecated { | ||
| 83 | color: #b52a1d; | ||
| 84 | background-color: #f5f5f5; | ||
| 85 | font-weight: bold; | ||
| 86 | } | ||
| 87 | .z-entity.z-name.z-filename.z-find-in-files { | ||
| 88 | color: #323232; | ||
| 89 | font-weight: bold; | ||
| 90 | } | ||
| 91 | .z-constant.z-numeric.z-line-number.z-find-in-files, .z-constant.z-numeric.z-line-number.z-match.z-find-in-files { | ||
| 92 | color: #b3b3b3; | ||
| 93 | } | ||
| 94 | .z-meta.z-diff.z-header { | ||
| 95 | color: #969896; | ||
| 96 | background-color: #ffffff; | ||
| 97 | font-style: italic; | ||
| 98 | } | ||
| 99 | .z-meta.z-diff.z-header .z-punctuation.z-definition.z-from-file.z-diff { | ||
| 100 | color: #bd2c00; | ||
| 101 | background-color: #ffecec; | ||
| 102 | font-weight: bold; | ||
| 103 | font-style: italic; | ||
| 104 | } | ||
| 105 | .z-meta.z-diff.z-header .z-punctuation.z-definition.z-to-file.z-diff { | ||
| 106 | color: #55a532; | ||
| 107 | background-color: #eaffea; | ||
| 108 | font-weight: bold; | ||
| 109 | font-style: italic; | ||
| 110 | } | ||
| 111 | .z-meta.z-diff.z-range { | ||
| 112 | color: #969896; | ||
| 113 | font-weight: bold; | ||
| 114 | font-style: italic; | ||
| 115 | } | ||
| 116 | .z-markup.z-deleted { | ||
| 117 | background-color: #ffecec; | ||
| 118 | } | ||
| 119 | .z-markup.z-deleted .z-punctuation.z-definition.z-inserted { | ||
| 120 | color: #bd2c00; | ||
| 121 | font-weight: bold; | ||
| 122 | } | ||
| 123 | .z-markup.z-inserted { | ||
| 124 | background-color: #eaffea; | ||
| 125 | } | ||
| 126 | .z-markup.z-inserted .z-punctuation.z-definition.z-inserted { | ||
| 127 | color: #55a532; | ||
| 128 | font-weight: bold; | ||
| 129 | } | ||
| 130 | .z-markup.z-deleted.z-git_gutter { | ||
| 131 | color: #bd2c00; | ||
| 132 | } | ||
| 133 | .z-markup.z-inserted.z-git_gutter { | ||
| 134 | color: #55a532; | ||
| 135 | } | ||
| 136 | .z-markup.z-changed.z-git_gutter { | ||
| 137 | color: #0086b3; | ||
| 138 | } | ||
| 139 | .z-markup.z-ignored.z-git_gutter { | ||
| 140 | color: #b3b3b3; | ||
| 141 | } | ||
| 142 | .z-markup.z-untracked.z-git_gutter { | ||
| 143 | color: #b3b3b3; | ||
| 144 | } | ||
| 145 | .z-source.z-css .z-punctuation.z-definition.z-entity { | ||
| 146 | color: #323232; | ||
| 147 | } | ||
| 148 | .z-source.z-css .z-entity.z-other.z-attribute-name.z-pseudo-class, .z-source.z-css .z-entity.z-other.z-attribute-name.z-pseudo-element { | ||
| 149 | color: #a71d5d; | ||
| 150 | } | ||
| 151 | .z-source.z-css .z-meta.z-value, .z-source.z-css .z-support.z-constant, .z-source.z-css .z-support.z-function { | ||
| 152 | color: #323232; | ||
| 153 | } | ||
| 154 | .z-source.z-css .z-constant.z-other.z-color { | ||
| 155 | color: #ed6a43; | ||
| 156 | } | ||
| 157 | .z-source.z-scss .z-punctuation.z-definition.z-entity { | ||
| 158 | color: #323232; | ||
| 159 | } | ||
| 160 | .z-source.z-scss .z-entity.z-other.z-attribute-name.z-pseudo-class, .z-source.z-scss .z-entity.z-other.z-attribute-name.z-pseudo-element { | ||
| 161 | color: #a71d5d; | ||
| 162 | } | ||
| 163 | .z-source.z-scss .z-support.z-constant.z-property-value, .z-source.z-scss .z-support.z-function { | ||
| 164 | color: #323232; | ||
| 165 | } | ||
| 166 | .z-source.z-scss .z-variable { | ||
| 167 | color: #a71d5d; | ||
| 168 | } | ||
| 169 | .z-variable.z-language.z-this.z-js { | ||
| 170 | color: #ed6a43; | ||
| 171 | } | ||
| 172 | .z-source.z-js .z-entity.z-name.z-function { | ||
| 173 | color: #323232; | ||
| 174 | } | ||
| 175 | .z-source.z-js .z-meta.z-function .z-entity.z-name.z-function, .z-source.z-js .z-entity.z-name.z-function .z-meta.z-function { | ||
| 176 | color: #795da3; | ||
| 177 | font-weight: bold; | ||
| 178 | } | ||
| 179 | .z-entity.z-name.z-type.z-new.z-js { | ||
| 180 | color: #795da3; | ||
| 181 | } | ||
| 182 | .z-variable.z-language.z-prototype.z-js { | ||
| 183 | color: #0086b3; | ||
| 184 | } | ||
| 185 | .z-source.z-js .z-support.z-function { | ||
| 186 | color: #0086b3; | ||
| 187 | } | ||
| 188 | .z-support.z-type.z-object.z-console.z-js { | ||
| 189 | color: #795da3; | ||
| 190 | } | ||
| 191 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 192 | color: #183691; | ||
| 193 | font-weight: bold; | ||
| 194 | } | ||
| 195 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 196 | color: #323232; | ||
| 197 | } | ||
| 198 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 199 | color: #183691; | ||
| 200 | font-weight: bold; | ||
| 201 | } | ||
| 202 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 203 | color: #323232; | ||
| 204 | } | ||
| 205 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 206 | color: #183691; | ||
| 207 | font-weight: bold; | ||
| 208 | } | ||
| 209 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 210 | color: #323232; | ||
| 211 | } | ||
| 212 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 213 | color: #183691; | ||
| 214 | font-weight: bold; | ||
| 215 | } | ||
| 216 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 217 | color: #323232; | ||
| 218 | } | ||
| 219 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 220 | color: #183691; | ||
| 221 | font-weight: bold; | ||
| 222 | } | ||
| 223 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 224 | color: #323232; | ||
| 225 | } | ||
| 226 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 227 | color: #183691; | ||
| 228 | font-weight: bold; | ||
| 229 | } | ||
| 230 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 231 | color: #323232; | ||
| 232 | } | ||
| 233 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 234 | color: #183691; | ||
| 235 | font-weight: bold; | ||
| 236 | } | ||
| 237 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 238 | color: #323232; | ||
| 239 | } | ||
| 240 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 241 | color: #183691; | ||
| 242 | font-weight: bold; | ||
| 243 | } | ||
| 244 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 245 | color: #323232; | ||
| 246 | } | ||
| 247 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 248 | color: #183691; | ||
| 249 | font-weight: bold; | ||
| 250 | } | ||
| 251 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 252 | color: #323232; | ||
| 253 | } | ||
| 254 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 255 | color: #183691; | ||
| 256 | font-weight: bold; | ||
| 257 | } | ||
| 258 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 259 | color: #323232; | ||
| 260 | } | ||
| 261 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 262 | color: #183691; | ||
| 263 | font-weight: bold; | ||
| 264 | } | ||
| 265 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 266 | color: #323232; | ||
| 267 | } | ||
| 268 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 269 | color: #183691; | ||
| 270 | font-weight: bold; | ||
| 271 | } | ||
| 272 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 273 | color: #323232; | ||
| 274 | } | ||
| 275 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 276 | color: #183691; | ||
| 277 | font-weight: bold; | ||
| 278 | } | ||
| 279 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 280 | color: #323232; | ||
| 281 | } | ||
| 282 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 283 | color: #183691; | ||
| 284 | font-weight: bold; | ||
| 285 | } | ||
| 286 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 287 | color: #323232; | ||
| 288 | } | ||
| 289 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 290 | color: #183691; | ||
| 291 | font-weight: bold; | ||
| 292 | } | ||
| 293 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 294 | color: #323232; | ||
| 295 | } | ||
| 296 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 297 | color: #183691; | ||
| 298 | font-weight: bold; | ||
| 299 | } | ||
| 300 | .z-meta .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 301 | color: #323232; | ||
| 302 | } | ||
| 303 | .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 304 | color: #183691; | ||
| 305 | font-weight: bold; | ||
| 306 | } | ||
| 307 | .z-meta .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 308 | color: #323232; | ||
| 309 | } | ||
| 310 | .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 311 | color: #183691; | ||
| 312 | font-weight: bold; | ||
| 313 | } | ||
| 314 | .z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 315 | color: #323232; | ||
| 316 | } | ||
| 317 | .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 318 | color: #183691; | ||
| 319 | font-weight: bold; | ||
| 320 | } | ||
| 321 | .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 322 | color: #323232; | ||
| 323 | } | ||
| 324 | .z-meta .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 325 | color: #183691; | ||
| 326 | font-weight: bold; | ||
| 327 | } | ||
| 328 | .z-meta .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 329 | color: #323232; | ||
| 330 | } | ||
| 331 | .z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { | ||
| 332 | color: #183691; | ||
| 333 | font-weight: bold; | ||
| 334 | } | ||
| 335 | .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-value.z-json .z-string.z-quoted.z-double.z-json { | ||
| 336 | color: #323232; | ||
| 337 | } | ||
| 338 | .z-source.z-python .z-keyword { | ||
| 339 | font-weight: bold; | ||
| 340 | } | ||
| 341 | .z-source.z-python .z-storage { | ||
| 342 | font-weight: bold; | ||
| 343 | } | ||
| 344 | .z-source.z-python .z-storage.z-type { | ||
| 345 | font-weight: bold; | ||
| 346 | } | ||
| 347 | .z-source.z-python .z-entity.z-name.z-function { | ||
| 348 | color: #323232; | ||
| 349 | font-weight: bold; | ||
| 350 | } | ||
| 351 | .z-source.z-php .z-entity.z-name.z-type.z-class { | ||
| 352 | color: #323232; | ||
| 353 | font-weight: bold; | ||
| 354 | } | ||
| 355 | .z-variable.z-language.z-ruby { | ||
| 356 | color: #ed6a43; | ||
| 357 | } | ||
| 358 | .z-entity.z-name.z-type.z-module.z-ruby { | ||
| 359 | color: #795da3; | ||
| 360 | font-weight: bold; | ||
| 361 | } | ||
| 362 | .z-entity.z-name.z-type.z-class.z-ruby { | ||
| 363 | color: #795da3; | ||
| 364 | font-weight: bold; | ||
| 365 | } | ||
| 366 | .z-entity.z-other.z-inherited-class.z-ruby { | ||
| 367 | color: #795da3; | ||
| 368 | font-weight: bold; | ||
| 369 | } | ||
| 370 | .z-punctuation.z-definition.z-heading.z-markdown, .z-punctuation.z-definition.z-italic.z-markdown, .z-punctuation.z-definition.z-bold.z-markdown, .z-punctuation.z-definition.z-string.z-begin.z-markdown, .z-punctuation.z-definition.z-string.z-end.z-markdown, .z-punctuation.z-definition.z-metadata.z-markdown, .z-punctuation.z-definition.z-link.z-markdown, .z-punctuation.z-definition.z-blockquote.z-markdown, .z-punctuation.z-definition.z-raw.z-markdown { | ||
| 371 | color: #a71d5d; | ||
| 372 | } | ||
| 373 | .z-text.z-html.z-markdown .z-meta.z-separator { | ||
| 374 | color: #b3b3b3; | ||
| 375 | } | ||
| 376 | .z-text.z-html.z-markdown .z-markup.z-heading { | ||
| 377 | font-weight: bold; | ||
| 378 | } | ||
| 379 | .z-text.z-html.z-markdown .z-markup.z-raw.z-block { | ||
| 380 | color: #323232; | ||
| 381 | } | ||
| 382 | .z-text.z-html.z-markdown .z-markup.z-raw.z-inline { | ||
| 383 | color: #323232; | ||
| 384 | } | ||
| 385 | .z-text.z-html.z-markdown .z-meta.z-link, .z-text.z-html.z-markdown .z-meta.z-image { | ||
| 386 | color: #4183c4; | ||
| 387 | } | ||
| 388 | .z-text.z-html.z-markdown .z-markup.z-underline.z-link, .z-text.z-html.z-markdown .z-constant.z-other.z-reference { | ||
| 389 | font-style: italic; | ||
| 390 | } | ||
| 391 | .z-text.z-html.z-markdown .z-markup.z-list { | ||
| 392 | color: #ed6a43; | ||
| 393 | } | ||
| 394 | .z-text.z-html.z-markdown .z-markup.z-bold { | ||
| 395 | font-weight: bold; | ||
| 396 | } | ||
| 397 | .z-text.z-html.z-markdown .z-markup.z-italic { | ||
| 398 | font-style: italic; | ||
| 399 | } | ||
| 400 | .z-text.z-html.z-markdown .z-markup.z-bold .z-markup.z-italic { | ||
| 401 | font-weight: bold; | ||
| 402 | font-style: italic; | ||
| 403 | } | ||
| 404 | .z-text.z-html.z-markdown .z-markup.z-italic .z-markup.z-bold { | ||
| 405 | font-weight: bold; | ||
| 406 | font-style: italic; | ||
| 407 | } | ||
templates/base.html deleted −53
| @@ -1,53 +0,0 @@ | |||
| 1 | <!doctype html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | {% block title %} | ||
| 5 | <title>cleberg.net</title> | ||
| 6 | {% endblock %} | ||
| 7 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 8 | <meta name="theme-color" content="#111" media="(prefers-color-scheme: | ||
| 9 | dark)"> | ||
| 10 | <meta name="theme-color" content="#fff" media="(prefers-color-scheme: | ||
| 11 | light)"> | ||
| 12 | {% if page %} {% if page.description %} | ||
| 13 | <meta name="description" content="{{ page.description }}"> | ||
| 14 | {% elif config.description %} | ||
| 15 | <meta name="description" content="{{ config.description }}"> | ||
| 16 | {% endif %} {% elif config.description %} | ||
| 17 | <meta name="description" content="{{ config.description }}" /> {% endif | ||
| 18 | %} | ||
| 19 | |||
| 20 | {% block rss %} | ||
| 21 | <link rel="alternate" type="application/atom+xml" title="RSS" href="{{ | ||
| 22 | get_url(path="atom.xml", trailing_slash=false) }}"> | ||
| 23 | {% endblock %} | ||
| 24 | |||
| 25 | {% block css %} | ||
| 26 | <link rel="stylesheet" href="/styles.min.css"> | ||
| 27 | {% endblock css %} | ||
| 28 | |||
| 29 | {% block extra_head %}{% endblock extra_head %} | ||
| 30 | </head> | ||
| 31 | <body> | ||
| 32 | <nav class="site-nav" aria-label="site-nav" role="navigation"> | ||
| 33 | <ul> | ||
| 34 | <li><a href="/">Home</a></li> | ||
| 35 | <li><a href="/blog/">Blog</a></li> | ||
| 36 | <li><a href="/services/">Services</a></li> | ||
| 37 | <li><a href="/wiki/">Wiki</a></li> | ||
| 38 | </ul> | ||
| 39 | </nav> | ||
| 40 | |||
| 41 | <main>{% block content %}{% endblock content %}</main> | ||
| 42 | |||
| 43 | <footer> | ||
| 44 | <p>Last build: <{{ now() | date(format='%Y-%m-%d %a %H:%M:%S') }}></p> | ||
| 45 | <p> | ||
| 46 | <a href="https://git.sr.ht/~cyborg/cleberg.net" | ||
| 47 | target="_blank" rel="noopener">Source Code</a> | ||
| 48 | · | ||
| 49 | <a href="/atom.xml">Atom RSS Feed</a> | ||
| 50 | </p> | ||
| 51 | </footer> | ||
| 52 | </body> | ||
| 53 | </html> | ||
templates/blog.html deleted −20
| @@ -1,20 +0,0 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <h1>{{ section.title }}</h1> | ||
| 5 | <p>Use <code>Ctrl + F</code> to search blog post titles for keywords.</p> | ||
| 6 | <p>You can also add the <a href="/atom.xml">Atom RSS Feed</a> to your feed | ||
| 7 | reader.</p> | ||
| 8 | {% set post_date = "1970-01-01" | date(format="%Y") %} | ||
| 9 | {% for page in section.pages %} | ||
| 10 | {% if post_date != page.date | date(format="%Y") %} | ||
| 11 | {% set_global post_date = page.date | date(format="%Y") %} | ||
| 12 | <h2>{{ post_date }}</h2> | ||
| 13 | {% endif %} | ||
| 14 | <div class="post"> | ||
| 15 | <time datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') }}">{{ | ||
| 16 | page.date | date(format='%Y-%m-%d') }}</time> | ||
| 17 | <a href="/blog/{{ page.slug }}/">{{ page.title }}</a> | ||
| 18 | </div> | ||
| 19 | {% endfor %} | ||
| 20 | {% endblock content %} | ||
templates/index.html deleted −58
| @@ -1,58 +0,0 @@ | |||
| 1 | {% extends "base.html" %} {% block content %} | ||
| 2 | <section> | ||
| 3 | <h1>cleberg.net</h1> | ||
| 4 | <pre>pub rsa4096 2022-11-16 [SC] | ||
| 5 | <a href="/gpg.txt">3917 973F B159 BBB8 6194 5385 6945 1A51 7AC0 CB37</a> | ||
| 6 | uid [ultimate] Christian Cleberg <hello@cleberg.net></pre> | ||
| 7 | </section> | ||
| 8 | <section> | ||
| 9 | <h2>Recent Blog Posts</h2> | ||
| 10 | {% set section = get_section(path="blog/_index.md") %} {% set i = 0 %} {% for | ||
| 11 | page in section.pages %} {% if i < 3%} {% set_global i = i + 1%} | ||
| 12 | <div class="post"> | ||
| 13 | <time | ||
| 14 | datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') | ||
| 15 | }}" | ||
| 16 | >{{ page.date | date(format='%Y-%m-%d') }}</time | ||
| 17 | > | ||
| 18 | <a href="/blog/{{ page.slug }}/">{{ page.title }}</a> | ||
| 19 | </div> | ||
| 20 | {% endif %} {% endfor %} | ||
| 21 | <br /> | ||
| 22 | <a href="/blog/">All Posts →</a> | ||
| 23 | </section> | ||
| 24 | <section> | ||
| 25 | <h2>Everything Else</h2> | ||
| 26 | <ul> | ||
| 27 | <li><a href="https://cv.cleberg.net">Curriculum Vitae</a></li> | ||
| 28 | <li><a href="/salary/">Salary</a></li> | ||
| 29 | <li><a href="/services/">Services</a></li> | ||
| 30 | <li><a href="/wiki/">Wiki</a></li> | ||
| 31 | </ul> | ||
| 32 | </section> | ||
| 33 | <section> | ||
| 34 | <h2>Contact</h2> | ||
| 35 | <ul> | ||
| 36 | <li>hello [at] cleberg.net (<a href="/gpg.txt">GPG</a>)</li> | ||
| 37 | <li> | ||
| 38 | cyb0rg on | ||
| 39 | <a href="https://libera.chat" target="_blank" rel="noopener" | ||
| 40 | >libera.chat</a | ||
| 41 | > | ||
| 42 | </li> | ||
| 43 | <li> | ||
| 44 | <a | ||
| 45 | href="https://matrix.to/#/@cyborg:matrix.tchncs.de" | ||
| 46 | target="_blank" | ||
| 47 | rel="noopener" | ||
| 48 | >@cyborg:@matrix.tchncs.de</a | ||
| 49 | > | ||
| 50 | </li> | ||
| 51 | <li> | ||
| 52 | <a href="https://git.sr.ht/~cyborg/" target="_blank" rel="noopener" | ||
| 53 | >Sourcehut</a | ||
| 54 | > | ||
| 55 | </li> | ||
| 56 | </ul> | ||
| 57 | </section> | ||
| 58 | {% endblock content %} | ||
templates/page.html deleted −55
| @@ -1,55 +0,0 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | |||
| 3 | {% block title %} | ||
| 4 | <title>{{ page.title }} | cleberg.net</title> | ||
| 5 | {% endblock %} | ||
| 6 | |||
| 7 | {% block css %} | ||
| 8 | <link rel="stylesheet" href="/syntax-theme-dark.css" | ||
| 9 | media="(prefers-color-scheme: dark)"> | ||
| 10 | <link rel="stylesheet" href="/syntax-theme-light.css" | ||
| 11 | media="(prefers-color-scheme: light)"> | ||
| 12 | <link rel="stylesheet" href="/styles.css"> | ||
| 13 | {% endblock css %} | ||
| 14 | |||
| 15 | {% block content %} | ||
| 16 | <section class="post-metadata"> | ||
| 17 | <h1>{{ page.title }}</h1> | ||
| 18 | {% if page.date %} | ||
| 19 | <p><time datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') }}">{{ | ||
| 20 | page.date }}</time></p> | ||
| 21 | {% endif %} {% if page.updated %} | ||
| 22 | <p>Last Updated: <time datetime="{{ page.updated | date(format='%Y-%m-%d | ||
| 23 | %H:%M:%S') }}">{{ page.updated }}</time></p> | ||
| 24 | {% endif %} | ||
| 25 | <p>{{ page.word_count }} words; {{ page.reading_time }} minute(s)</p> | ||
| 26 | {% if page.toc %} | ||
| 27 | <details open> | ||
| 28 | <summary>Table of Contents</summary> | ||
| 29 | <nav class="page-nav" aria-label="page-nav" role="navigation"> | ||
| 30 | <ul> | ||
| 31 | {% for h1 in page.toc %} | ||
| 32 | <li> | ||
| 33 | <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> | ||
| 34 | {% if h1.children %} | ||
| 35 | <ul> | ||
| 36 | {% for h2 in h1.children %} | ||
| 37 | <li> | ||
| 38 | <a href="{{ h2.permalink | safe }}">{{ | ||
| 39 | h2.title }}</a> | ||
| 40 | </li> | ||
| 41 | {% endfor %} | ||
| 42 | </ul> | ||
| 43 | {% endif %} | ||
| 44 | </li> | ||
| 45 | {% endfor %} | ||
| 46 | </ul> | ||
| 47 | </nav> | ||
| 48 | </details> | ||
| 49 | {% endif %} | ||
| 50 | </section> | ||
| 51 | |||
| 52 | <article role="article"> | ||
| 53 | {{ page.content | safe }} | ||
| 54 | </article> | ||
| 55 | {% endblock content %} | ||
templates/wiki.html deleted −16
| @@ -1,16 +0,0 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <h1>{{ section.title }}</h1> | ||
| 5 | <p>Use <code>Ctrl + F</code> to search wiki pages for keywords.</p> | ||
| 6 | <div class="wiki-post"> | ||
| 7 | <p>Wiki Page</p> | ||
| 8 | <p>Last Updated</p> | ||
| 9 | </div> | ||
| 10 | {% for page in section.pages %} | ||
| 11 | <div class="wiki-post"> | ||
| 12 | <p><a href="/wiki/{{ page.slug }}/">{{ page.title }}</a></p> | ||
| 13 | <time datetime="{{ page.updated | date(format='%Y-%m-%d %H:%M:%S') | ||
| 14 | }}">{{ page.updated | date(format='%Y-%m-%d')}}</p> | ||
| 15 | </div> | ||
| 16 | {% endfor %} {% endblock content %} | ||
static/.well-known/security.txt → theme/static/.well-known/security.txt renamed
static/gpg.txt → theme/static/gpg.txt renamed
static/robots.txt → theme/static/robots.txt renamed
static/salary.csv → theme/static/salary.csv renamed
static/styles.css → theme/static/styles.css renamed
static/styles.min.css → theme/static/styles.min.css renamed
theme/templates/base.html added +39
| @@ -0,0 +1,39 @@ | |||
| 1 | <!doctype html> | ||
| 2 | <html lang="en-us"> | ||
| 3 | <head> | ||
| 4 | {% block head %} | ||
| 5 | <meta charset="utf-8"> | ||
| 6 | <title> | ||
| 7 | {% block subtitle %}{% endblock %} | ||
| 8 | {% block title %}{{ site_name | default("weblorg website") }}{% endblock %} | ||
| 9 | </title> | ||
| 10 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 11 | {% if site_owner is defined %}<meta name="author" content="{{ site_owner }}" >{% endif %} | ||
| 12 | {% if site_description is defined %}<meta name="description" content="{{ site_description }}" >{% endif %} | ||
| 13 | {% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}" >{% endif %} | ||
| 14 | <link rel="stylesheet" href="{{ url_for("static", file="styles.min.css") }}" type="text/css"> | ||
| 15 | <link rel="icon" href="data:,"> | ||
| 16 | {% block meta %}{% endblock %} | ||
| 17 | {% endblock %} | ||
| 18 | </head> | ||
| 19 | <body> | ||
| 20 | <nav class="site-nav" aria-label="site-nav" role="navigation"> | ||
| 21 | <ul> | ||
| 22 | <li><a href="/">Home</a></li> | ||
| 23 | <li><a href="/blog/">Blog</a></li> | ||
| 24 | <li><a href="/services/">Services</a></li> | ||
| 25 | <li><a href="/wiki/">Wiki</a></li> | ||
| 26 | </ul> | ||
| 27 | </nav> | ||
| 28 | <main>{% block main %}{% endblock %}</main> | ||
| 29 | <footer> | ||
| 30 | <p>Last build: <{{ now() | strftime("%Y-%m-%d %a %H:%M:%S") }}></p> | ||
| 31 | <p> | ||
| 32 | <a href="https://git.sr.ht/~cyborg/cleberg.net" | ||
| 33 | target="_blank" rel="noopener">Source Code</a> | ||
| 34 | · | ||
| 35 | <a href="/feed.xml">RSS Feed</a> | ||
| 36 | </p> | ||
| 37 | </footer> | ||
| 38 | </body> | ||
| 39 | </html> | ||
theme/templates/blog.html added +15
| @@ -0,0 +1,15 @@ | |||
| 1 | {% extends "base.html" %} {% block subtitle %}Blog | {% endblock %} {% block | ||
| 2 | main %} | ||
| 3 | <h1>Blog</h1> | ||
| 4 | <p>Use <code>Ctrl + F</code> to search blog post titles for keywords.</p> | ||
| 5 | <p> | ||
| 6 | You can also add the <a href="/feed.xml">RSS Feed</a> to your feed reader. | ||
| 7 | </p> | ||
| 8 | {% for post in posts %} | ||
| 9 | <div class="post"> | ||
| 10 | <time datetime='{{ post.date | strftime("%Y-%m-%d") }}' | ||
| 11 | >{{ post.date|strftime("%Y-%m-%d") }}</time | ||
| 12 | > | ||
| 13 | <a href='{{ url_for("blog", slug=post.slug) }}'>{{ post.title }}</a> | ||
| 14 | </div> | ||
| 15 | {% endfor %} {% endblock %} | ||
theme/templates/feed.xml added +37
| @@ -0,0 +1,37 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8" ?> | ||
| 2 | <rss version="2.0" | ||
| 3 | xmlns:content="http://purl.org/rss/1.0/modules/content/" | ||
| 4 | xmlns:wfw="http://wellformedweb.org/CommentAPI/" | ||
| 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" | ||
| 6 | xmlns:atom="http://www.w3.org/2005/Atom" | ||
| 7 | xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" | ||
| 8 | xmlns:slash="http://purl.org/rss/1.0/modules/slash/" | ||
| 9 | xmlns:georss="http://www.georss.org/georss" | ||
| 10 | xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" | ||
| 11 | xmlns:media="http://search.yahoo.com/mrss/"> | ||
| 12 | <channel> | ||
| 13 | <title>{{ site_name | default("cleberg.net") }}</title> | ||
| 14 | <atom:link | ||
| 15 | href="{{ url_for("rss") }}" | ||
| 16 | rel="self" type="application/rss+xml" /> | ||
| 17 | <link>{{ url_for("index") }}</link> | ||
| 18 | <description><![CDATA[]]></description> | ||
| 19 | <language>en</language> | ||
| 20 | <pubDate>{{ posts | first | getattr("date") | strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate> | ||
| 21 | <lastBuildDate>{{ now() | strftime("%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate> | ||
| 22 | <generator>{{ meta.generator }}</generator> | ||
| 23 | <webMaster>{{ site_owner | default("hello@cleberg.net") }}</webMaster> | ||
| 24 | |||
| 25 | {% for post in posts %} | ||
| 26 | <item> | ||
| 27 | <title>{{ post.title }}</title> | ||
| 28 | <link>{{ url_for("blog", slug=post.slug) }}</link> | ||
| 29 | <author>{{ post.author|default("hello@cleberg.net") }}</author> | ||
| 30 | <guid isPermaLink="false">{{ url_for("blog", slug=post.slug) }}</guid> | ||
| 31 | <pubDate>{{ post.date|strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate> | ||
| 32 | <description><![CDATA[{{ post.html|safe }}]]></description> | ||
| 33 | </item> | ||
| 34 | {% endfor %} | ||
| 35 | |||
| 36 | </channel> | ||
| 37 | </rss> | ||
theme/templates/index.html added +60
| @@ -0,0 +1,60 @@ | |||
| 1 | {% extends "base.html" %} {% block main %} | ||
| 2 | <section> | ||
| 3 | <h1>cleberg.net</h1> | ||
| 4 | <pre>pub rsa4096 2022-11-16 [SC] | ||
| 5 | <a href="/gpg.txt">3917 973F B159 BBB8 6194 5385 6945 1A51 7AC0 CB37</a> | ||
| 6 | uid [ultimate] Christian Cleberg <hello@cleberg.net></pre> | ||
| 7 | </section> | ||
| 8 | <section> | ||
| 9 | <h2>Recent Blog Posts</h2> | ||
| 10 | |||
| 11 | <div class="post"> | ||
| 12 | <time datetime="2024-07-11 20:24:02">2024-07-11</time> | ||
| 13 | <a href="/blog/emacs-on-ipad/">Emacs on iPadOS</a> | ||
| 14 | </div> | ||
| 15 | |||
| 16 | <div class="post"> | ||
| 17 | <time datetime="2024-06-19 08:00:00">2024-06-19</time> | ||
| 18 | <a href="/blog/deprecated-trusted-gpg-fix/" | ||
| 19 | >Fixing Ubuntu Error: 'Key is stored in legacy trusted.gpg keyring'</a | ||
| 20 | > | ||
| 21 | </div> | ||
| 22 | |||
| 23 | <div class="post"> | ||
| 24 | <time datetime="2024-05-03 00:00:00">2024-05-03</time> | ||
| 25 | <a href="/blog/ubuntu-on-macos/" | ||
| 26 | >Running Ubuntu Linux on macOS with OrbStack</a | ||
| 27 | > | ||
| 28 | </div> | ||
| 29 | |||
| 30 | <br /> | ||
| 31 | <a href="/blog/">All Posts →</a> | ||
| 32 | </section> | ||
| 33 | |||
| 34 | <section> | ||
| 35 | <h2>Everything Else</h2> | ||
| 36 | <ul> | ||
| 37 | <li><a href="https://cv.cleberg.net">Curriculum Vitae</a></li> | ||
| 38 | <li><a href="/salary/">Salary</a></li> | ||
| 39 | <li><a href="/services/">Services</a></li> | ||
| 40 | <li><a href="/wiki/">Wiki</a></li> | ||
| 41 | </ul> | ||
| 42 | </section> | ||
| 43 | |||
| 44 | <section> | ||
| 45 | <h2>Contact</h2> | ||
| 46 | <ul> | ||
| 47 | <li> | ||
| 48 | hello [at] cleberg.net (<a href="https://cleberg.net/gpg.txt">GPG</a>) | ||
| 49 | </li> | ||
| 50 | <li>cyb0rg on <a href="https://libera.chat/">libera.chat</a></li> | ||
| 51 | <li> | ||
| 52 | <a href="https://matrix.to/#/@cyborg:matrix.tchncs.de" | ||
| 53 | >@cyborg:@matrix.tchncs.de</a | ||
| 54 | > | ||
| 55 | </li> | ||
| 56 | <li><a href="https://git.sr.ht/~cyborg/">Sourcehut</a></li> | ||
| 57 | </ul> | ||
| 58 | </section> | ||
| 59 | |||
| 60 | {% endblock %} | ||
theme/templates/page.html added +11
| @@ -0,0 +1,11 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | |||
| 3 | {% block subtitle %} | ||
| 4 | {{ post.title }} | ||
| 5 | {% endblock %} | ||
| 6 | {% block main %} | ||
| 7 | <article role="article"> | ||
| 8 | <h1>{{ post.title }}</h1> | ||
| 9 | <section>{{ post.html | safe }}</section> | ||
| 10 | </article> | ||
| 11 | {% endblock %} | ||
theme/templates/post.html added +21
| @@ -0,0 +1,21 @@ | |||
| 1 | {% extends "base.html" %} {% block subtitle %} {{ post.title }} | {% endblock %} | ||
| 2 | {% block meta %} | ||
| 3 | <meta property="og:title" content="{{ post.title }}" /> | ||
| 4 | <meta property="og:url" content="{{ post.url }}" /> | ||
| 5 | {% if post.description is defined %} | ||
| 6 | <meta property="og:description" content="{{ post.description }}" /> | ||
| 7 | {% elif project_description is defined %} | ||
| 8 | <meta property="og:description" content="{{ project_description }}" /> | ||
| 9 | {% endif %} {% endblock %} {% block main %} | ||
| 10 | <article role="article"> | ||
| 11 | <section class="post-metadata"> | ||
| 12 | <h1 class="post__title">{{ post.title }}</h1> | ||
| 13 | {% if post.date is defined %} | ||
| 14 | <time datetime='{{ post.date|strftime("%Y-%m-%d") }}' | ||
| 15 | ><{{ post.date|strftime("%Y-%m-%d %a %H:%M:%S") }}></time | ||
| 16 | > | ||
| 17 | {% endif %} | ||
| 18 | </section> | ||
| 19 | <section>{{ post.html | safe }}</section> | ||
| 20 | </article> | ||
| 21 | {% endblock %} | ||
theme/templates/wiki.html added +12
| @@ -0,0 +1,12 @@ | |||
| 1 | {% extends "base.html" %} {% block subtitle %}Wiki | {% endblock %} {% block | ||
| 2 | main %} | ||
| 3 | <h1>Wiki</h1> | ||
| 4 | <p>Use <code>Ctrl + F</code> to search wiki pages for keywords.</p> | ||
| 5 | {% for post in posts %} | ||
| 6 | <div class="post"> | ||
| 7 | <time datetime='{{ post.date | strftime("%Y-%m-%d") }}' | ||
| 8 | >{{ post.date|strftime("%Y-%m-%d") }}</time | ||
| 9 | > | ||
| 10 | <a href='{{ url_for("wiki", slug=post.slug) }}'>{{ post.title }}</a> | ||
| 11 | </div> | ||
| 12 | {% endfor %} {% endblock %} | ||
utils/nginx-dev.conf added +17
| @@ -0,0 +1,17 @@ | |||
| 1 | server { | ||
| 2 | server_name cleberg.net www.cleberg.net; | ||
| 3 | |||
| 4 | root /usr/share/nginx/html; | ||
| 5 | index index.html; | ||
| 6 | autoindex on; | ||
| 7 | |||
| 8 | access_log /var/log/nginx/cleberg.net.access.log; | ||
| 9 | error_log /var/log/nginx/cleberg.net.error.log; | ||
| 10 | |||
| 11 | location / { | ||
| 12 | try_files $uri $uri/ =404; | ||
| 13 | } | ||
| 14 | |||
| 15 | listen [::]:80; | ||
| 16 | listen 80; | ||
| 17 | } | ||