Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
31

Radio Install in a Porsche Boxster

from noahbuscher.com [alt+shift+b] in technology

The 987.2 Porsche Boxster is one of the best values in the used sports car market today. There is so much to love about the vehicle, but the OEM headunit is not one of them. It's very dated, clunky, and doesn't support CarPlay (!), so one of the first things I did after picking up my car was purchase and install a new headunit. I selected the Sony XAV-AX1000 headunit because: It's one of the only aftermarket headunits with a physical volume knob The matte black plastic finish very closely matches my Porsche's interior I didn't need wireless CarPlay It's relatively cheap I also picked up this dash kit from Crutchfield as well to have the install look as factory as possible. I will say that the fit is very tight (it's a friction fit) so triple check wiring before sliding this into the dash. If you do happen to get the kit stuck with the headunit in it, I've had luck freeing it with the help of a metal putty knife. My 987.2 has the Sound Package Plus (SPP), which is the middle tier between the basic speakers and the Bose speakers (which are much more difficult to connect). The connector I needed as a result was the Metra 70-9003 harness. The connector may come with a fuse tap, but I opted to just use a separately purcahsed unit that fit well in the fuse box without needing to drill any holes. Note that the Metra 70-9003 harness may not work with the base package. Other supplies needed: Electrical tape Assorted t-taps Assorted spade connectors Low-profile fuse tap Metra 70-1787 harness (seperate from the Metra 70-9003 harness, this is just a donor for the RCA cables) Stranded red wire (used to connect power from fuse tap to headunit) Banana plug adapter for connecing SiriusXM Tools needed: Heat gun/heat shrink Screwdriver set Soldering iron/solder Wire strippers Scissors Also of note that my Porsche did not come equipped with steering wheel controls. If you vehicle does have steering wheel controls, you may need to purchase extra materials in order to get those to work,...
28th Jun 2023

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from noahbuscher.com

Create a Markdown Portfolio with Next.js

So you've set out to create a new portfolio for yourself. You start gathering inspiration from platforms like Godly and Minimal Gallery, draw some rectangles in Figma, and open your text editor. You pause. There's thousands of ways to build your website, how do you decide which to go with? You want your website to be beautiful for the users, but you also want to be able to quickly add new posts and case studies. You decide you want to have a dynamic website (good choice!), but the dozens of CMS options weigh on you. Do you pick headless or full-stack? What if you just make a theme? So many choices. I was faced with the same dilemma a few months ago. I finally had the motivation to create a new personal website, but wasn't sure where to start. I decided to keep things simple, I'd write it with a library I knew very well: React. That only solved half of the equation, however. When it came time to decide how to power the dynamic content on the site, I knew I wanted it to be free, easy to use, and open source. A static site powered by Next.js and markdown was the obvious choice. View the source on GitHub. To get started on creating your portfolio, run the following commands. I'm using pnpm, but feel free to use yarn or npm! pnpm dlx create-next-app@latest --typescript cd into your new project and run the following commands to install the necessary packages and bootstrap Tailwind: pnpm install dayjs pnpm install gray-matter react-markdown tailwindcss postcss autoprefixer -D pnpm dlx tailwindcss init -p That's a lot of packages. Let's break them down... gray-matter will be used to parse "front-matter" from our markdown files (we'll read more about this later!) react-markdown is a React component to render markdown content tailwindcss postcss autoprefixer are requirements for installing Tailwind for Next.js Phew! Let's continue our setup by finishing the Tailwind install... Add the following to the module.exports in the generated tailwind.config.js file: content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}" ], To bring the styles into your project, add the following to the styles/globals.css file: @tailwind base; @tailwind components; @tailwind utilities; Great! Finally, let's create our new directories by running the following in the root: mkdir _projects mkdir public/projects mkdir layouts mkdir components mkdir utils We're finally ready to start coding! To get started, create a new layout file, ProjectCard.tsx in the components directory. This component will be displayed for each project on the home page's grid. In components/ProjectCard.tsx add: import React from "react"; import Image from "next/image"; import Link from "next/link"; const ProjectCard = ({ title, description, slug, photo, }: { title: string; description: string; slug: string; photo: string; }) => ( <Link href={`/project/${slug}`}> <div className="flex-1 flex flex-col gap-2"> <Image src={photo} alt={title} width="0" height="0" sizes="100vw" className="w-full h-full aspect-square object-cover" priority /> <h3 className="text-black font-bold text-xs">{title}</h3> <p className="text-gray-500 text-xs">{description}</p> </div> </Link> ); export default ProjectCard; Now we need a place to display all of our project cards. Create a new layout called Grid.tsx in the layouts directory. In layouts/Grid.tsx, add the following: import React from "react"; import ProjectCard from "../components/ProjectCard"; import type { Project } from "../layouts/Project"; const Grid = ({ projects }: { projects: Project[] }) => ( <div className="flex flex-col gap-12 max-w-screen-md mx-auto"> <h2 className="text-black font-bold text-lg">Projects</h2> <div className="grid grid-cols-2 md:grid-cols-4 gap-8"> {projects.map(({ meta }) => ( <ProjectCard key={meta.slug} title={meta.title} description={meta.description} slug={meta.slug} photo={meta.photo} /> ))} </div> </div> ); export default Grid; Let's add a demo project now! In the _projects at your root, create a new file. The name doesn't matter, but I find it helpful to make it the same as the URL slug. In that file, add soemthing similar to the following: --- title: Example Project slug: example-project date: February 1, 2023 description: Just an example project! photo: /projects/example-project.jpg --- Something about Example Project... The area between the ---'s is called "front-matter", and it's what we brought gray-matter in to parse for us. Everything under the second seperator is the content of the case study. It can be any valid markdown. Save the file and let's move on to tying things together! In your pages/index.tsx file that Next.js generated, replace the content with the following. Feel free to ignore the missing Project type for now. We'll add that soon! import React from "react"; import path from "path"; import { promises as fs } from "fs"; import matter from "gray-matter"; import dayjs from "dayjs"; import Grid from "../layouts/Grid"; import type { Project } from "../layouts/Project"; const PROJECTS_DIR = "_projects"; export async function getStaticProps() { const projectsDir = path.join(process.cwd(), PROJECTS_DIR); const files = await fs.readdir(projectsDir); const postPaths = files.filter((file) => { const ext = path.extname(file); return ext === ".md"; }); const projects = await Promise.all( postPaths.map(async (file: string) => { const contents = await fs.readFile(path.join(projectsDir, file), "utf8"); const parsed = matter(contents); const project = { content: parsed.content, meta: parsed.data, }; return project; }) ); const sortedProjects = projects.sort((a, b) => dayjs(a.meta.date).isAfter(dayjs(b.meta.date)) ? -1 : 1 ); return { props: { projects: sortedProjects, }, }; } export default function Home({ projects }: { projects: Project[] }) { return ( <main className="mx-10 sm:mx-0 my-10"> <Grid projects={projects} /> </main> ); } This may look a little confusing if you've never seen getStaticProps before, but you can read more about it in the Next.js docs. All it's doing here is getting an array of Projects that we then use dayjs to sort from a human-readable date in the metadata and pass that array as a prop to our Grid. Now just run pnpm dev and you should see your project on the home page! Feel free to add more projects and see how it sorts by date and adds them to the grid. Now, if you try and click on a project, you'll notice it takes you to a 404 page. Let's go ahead and add a single project layout by creating a file called Project.tsx in layouts. In Project.tsx, add the following: import React from "react"; import Image from "next/image"; export type ProjectMeta = { title: string; description: string; slug: string; date: string; photo: string; }; export type Project = { meta: ProjectMeta; content: string; }; const Project = ({ project, renderedProjectContent, }: { project: Project; renderedProjectContent: string; }) => ( <div className="flex flex-col gap-8 max-w-screen-md mx-auto"> <h2 className="text-black font-bold text-lg">{project.meta.title}</h2> <h3 className="text-gray-500 text-base">{project.meta.description}</h3> <Image src={project.meta.photo} alt={project.meta.title} width="0" height="0" sizes="100vw" className="w-full h-full" priority /> <div className="flex flex-col gap-8" dangerouslySetInnerHTML={{ __html: renderedProjectContent }} /> </div> ); export default Project; ⚠️ Note the dangerouslySetInnerHTML. It's ok to use here as the only user's content that will be rendered there is your's. This is not a great practice, however, and you should not do this on a platform that other's are able to post content to. In the next step, we are going to be writing a utility to generate that HTML. To do so, create a new file called markdown.tsx in the utils directory and add the following: import React from "react"; import Link from "next/link"; import * as ReactDOMServer from "react-dom/server"; import ReactMarkdown from "react-markdown"; const Img = ({ ...props }: any) => ( <img className="rounded-[8px] max-w-screen-lg mx-auto w-full" {...props} /> ); const Text = ({ children, node }: { children: React.ReactNode; node: any }) => { if (node.children[0].tagName === "img") { const image: any = node.children[0]; return <Img src={image.properties.src} />; } return ( <p className="flex-1 flex-grow w-full text-xs leading-6 max-w-screen-md mx-auto"> {children} </p> ); }; const Anchor = ({ children, href }: any) => ( <Link href={href} className="inline underline"> {children} </Link> ); export const renderMarkdownToHTML = (markup: string) => { return ReactDOMServer.renderToStaticMarkup( <ReactMarkdown components={{ p: Text, img: Img, a: Anchor, }} > {markup.trim()!} </ReactMarkdown> ); }; This component uses react-markdown to enable us to define JSX component mappings for markdown elements! Feel free to get creative here and expand upon what's already in there. So far so good! We're getting close. Now we just need to create a new file called [slug].tsx in pages/project that will pull that new component in. In [slug.tsx], add the following: import React from "react"; import { promises as fs } from "fs"; import path from "path"; import matter from "gray-matter"; import ProjectLayout from "../../layouts/Project"; import { renderMarkdownToHTML } from "../../utils/markdown"; import type { Project } from "../../layouts/Project"; const PROJECTS_DIR = "_projects"; export async function getStaticProps({ params }: { params: { slug: string } }) { const projectsDir = path.join(process.cwd(), PROJECTS_DIR); const files = await fs.readdir(projectsDir); const projectPaths = files.filter((file) => { const ext = path.extname(file); return ext === ".md"; }); const projects = await Promise.all( projectPaths.map(async (file: string) => { const contents = await fs.readFile(path.join(projectsDir, file), "utf8"); const parsed = matter(contents); return { content: parsed.content, meta: parsed.data, }; }) ); const project = projects.find((p) => p?.meta?.slug === params.slug); const renderedProjectContent = renderMarkdownToHTML(project?.content!); return { props: { project, renderedProjectContent, }, }; } export async function getStaticPaths() { const projectsDir = path.join(process.cwd(), PROJECTS_DIR); const files = await fs.readdir(projectsDir); const projectPaths = files.filter((file) => { const ext = path.extname(file); return ext === ".md"; }); const projects = await Promise.all( projectPaths.map(async (file: string) => { const contents = await fs.readFile(path.join(projectsDir, file), "utf8"); const parsed = matter(contents); return { content: parsed.content, meta: parsed.data, }; }) ); const paths = projects.map((project) => ({ params: { slug: project?.meta?.slug }, })); return { paths, fallback: false, }; } const Project = ({ project, renderedProjectContent, }: { project: Project; renderedProjectContent: string; }) => ( <main className="mx-10 sm:mx-0 my-10"> <ProjectLayout project={project} renderedProjectContent={renderedProjectContent} /> </main> ); export default Project; Similar to the index.tsx page, we are grabbing the list of projects again, however this time we are filtering to find the single project whose slug matches that in the dynamic page's URL. Then, we are defining a function called getStaticPaths that is another special Next.js feature to dynamically create the static paths for generated content (in this case, our Projects!). You can read more about that in the Next.js docs. Great! If you go back to the home page and click a project, you should now see it takes you to a single project page! That's really all you need to get started! You now have a simple markdown-powered portfolio. You can use this as a jumping-off point to continue building out the (easy-to-update) portfolio of your dreams! Here's some ideas to get you started: Use next-sitemap to generate a sitemap to all of your dynamic routes to make it easier for search engines to index your site As you probably noted, some of the code we wrote isn't very DRY; maybe extract some of the repeated code into more utils Add a navbar and footer Add more pages to tell visitors about yourself or how to get in touch with you Looking foward to see what y'all create! View the source on GitHub.

7th Mar 2023 • 30 votes

More in technology

FLIP Fluid on Flip Dots

[Hardware] Electromechanical Fluid Simulation

2 days ago • 1 votes
Is This A Joke? In The Auth Header? (F5 BIG-IP UnAuth Heap-Overflow to RCE CVE-2026-94127)

Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry. We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’

3 days ago • 1 votes
The Reason You Prohibit Things

You want less of them. That’s the reason. You may find that it’s too hard to stop people from doing the thing, literally blood, sweat, and tears trying to prosecute people, but that’s a different thing.

3 days ago
Solitaire Alone Together

Solitaire Alone Together I made a new game. It's called Solitaire Alone Together. It's Windows 98 solitaire, but you can play with everyone else on the internet. Read the full post on my blog! Here's a raw link, if you need it: https://eieio.games/blog/solitaire-alone-together

5 days ago • 1 votes
All The Ways I Broke My Website

This post is a living diary of all the times I messed up something with my website in a funny way. I value those who have the confidence to own their mistakes and share the learning with others, and so this is me doing just that! That Time I Accidentally Made a Tarpit That Time I Accidentally Made Really Large Headers That Time I Accidentally Made a Tarpit Back to Top A "tarpit" is an unofficial term used in computing to describe an intentionally slow response to a request. In these modern times many people are using tarpits as a way to combat the relentless theft of data by AI companies, although there's little to no evidence of that actually being in any way effective. I don't use tarpits, at least not intentionally, but there was that one time when I accidentally created a tarpit and trapped all visitors in it. As I've shared previously, I refuse connections from IP addresses that are blocked or belong to a blocked subnet, and I enforce this firewall during the TCP handshake. The logic here is straightforward: there's no reason to waste resources doing a TLS handshake, accepting an HTTP request, and then rejecting the connection if I already know I'm going to reject it at the earliest step. At the time, the code worked like this: the HTTP server would repeatedly call the Accept() function below expecting a new connection. I've added some comments to help explain the logic. func (l *firewallListener) Accept() (net.Conn, error) { // Accept the connection from the TCP listener. This blocks until there is a connection to accept or the listner was closed. conn, err := l.l.AcceptTCP() if err != nil { return conn, err } // Separate the IP address out from the remote address (which includes the port) ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } // Check if it's blocked, if so close the connection and return a refuseError if IsBlocked(ip, true) { conn.Close() return nil, &refuseError{} } // Otherwise return the connection on to the HTTP server return conn, nil } If the incoming connection was from a blocked IP then I'd return a refuseError. I need to use a specific error interface because the HTTP server will halt if it encounters a non-temporary error from the call to Accept(), so I need to return an error that satisfies the definition of a temporary error. I defined refuseError like this: type refuseError struct{} func (e *refuseError) Error() string { return "." } func (e *refuseError) Timeout() bool { return true } func (e *refuseError) Temporary() bool { return true } func (e *refuseError) Is(err error) bool { return err == context.DeadlineExceeded } This did accomplish the goal of rejecting connections before the TLS handshake for blocked addresses, but it had one really unintended and difficult to track down side-effect. Accepting connections is done serially, after which servers typically then process that request on a dedicated thread (or in Go's case a goroutine). This means that any delays during the accept loop will block all incoming connection. What I had missed while reviewing the code for Go's HTTP server is that when it receives a temporary error from Accept() is that while it doesn't abort, it does sleep for up to a maximum of 1 second. This sleep blocks the entire server for all incoming connections. You can see a trimmed copy of the code that does this below, with some marks I've added which I will explain. // src/net/http/server.go // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. for { // (1) rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } // (2) if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } s.logf("http: Accept error: %v; retrying in %v", err, tempDelay) // (3) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := s.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return // (4) go c.serve(connCtx) } At mark 1 the server calls the Accept() function, this is the exact function that I defined above where I might return a temporary error. At mark 2 it checks if an error was returned, and if so if that error is temporary. If there was a temporary error, at mark 3 it sleeps for an increasing amount of time up-to 1 second, otherwise, at mark 4 it processes the connection on a dedicated goroutine, which allows the server to accept the next connection. I'm not entirely sure why the Go developers added this sleep delay and the change when it was introduced doesn't provide any meaningful insight. Regardless, it caused significant latency connecting to my website when a flood of rejected requests was coming in. It just goes to show how important it is to write meaningful commit messages, because you never know when somebody might come back years later wondering "why was this done?". I sure home I don't come to eat those words later. Coincidentally, you can actually see this happening if you look carefully at one of the metric graphs I shared in my first post about my server's security model: Securing My Web Infrastructure. This is the graph I shared in that blog post and while I didn't know it at the time, the fact that these request spikes all cap-out at around 60 requests per minute was not a coincidence. These requests were not being made with a limit in mind, attackers rarely ever care about things like that, instead it the accidental tarpit I had created. The downside to this was that while the malicious requests were being rate-limited, all requests were being rate-limited, up to a point of taking so long they timed out. The Fix Fixing the issue was relatively straightforward enough. Instead of returning a temporary error to the HTTP server during the accept loop, just don't return anything at all and wait for the next valid connection. func (l *firewallListener) Accept() (net.Conn, error) { for { conn, err := l.l.AcceptTCP() if err != nil { return conn, err } ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } if IsBlocked(ip, true) { conn.SetLinger(0) conn.Close() continue } return conn, nil } } Now, when the HTTP server calls Accept(), the only time it returns is with a connection from an IP that isn't blocked, or if there genuinely is an error. No more sleep delays, no more excessive timeouts. That Time I Accidentally Made Really Large Headers Back to Top For about 10 years now all major browsers have support for a security feature known as a Content Security Policy or CSP. A CSP is an HTTP header provided by the server that instructs the browser on where it can load assets from, this could be scripts, images, stylesheets, fonts, etc. The objective of using a CSP is to prevent against injected HTML that tries to load assets, such as a malicious Javascript file, from a remote source. With so much user-provided content being available online, it's very possible for this to happen without an attacker compromising the entire web server. CSP protects against that by saying "scripts can only be loaded from these domains". That's a really simplified way of looking at it, anyways. My web server supports injecting the CSP header automatically, but before I go on I need to explain a little bit about the structure of my web server. When an incoming HTTP request is accepted (having passed all firewall checks and assertions), we look at the destination host for the request. This can either be the value of the Host header or as specified during the TLS handshake. We then look at a map of hosts to apps. Apps are just an interface that accept a few methods: type App interface { Cleanup() ReloadConfig() ServeHTTP(rw http.ResponseWriter, r *http.Request) Setup(dataDir string) error Shutdown() } One of the apps is the Proxy app, which is a reverse proxy - it accepts the incoming HTTP request and then proxies it on to another host. This is a very common design, especially with increasingly complex TLS setups. Because each app is unique to a host, and different hosts have different requirements for CSP rules, the proxy app includes a CSP preset that we use to build the header value, or skip it entirely. When the proxy app was going to copy an HTTP request to the downstream host, it would build the CSP header, however there was a slight bug... func (a *App) ServeHTTP(rw http.ResponseWriter, inRequest *ht2.Request) { // --snip -- if a.CSP != nil { a.CSP.ConnectSrc += " " + inRequest.Origin } CopyHttpRequest(inRequest, outRequest, rw, CopyHttpRequestOptions{ Origin: inRequest.Origin, Csp: a.CSP, Cors: a.CORS, AddHeaders: !a.SkipHeaders, UseHTTP3: a.UseHTTP3, InsecureTLS: a.InsecureTLS, }) } I'm really unsure as to what I was doing with the line to append to the ConnectSrc, but the impact is that I'm appending to a variable that lives on the App, rather than a variable that is per-request. This meant that every time there was a request to the app, any request at all, the origin would be appended to the header value. This went on for quite a long time unnoticed and unresolved, largely because I am constantly tweaking and tinkering with my web server, after all, it's how I made having a website fun again. Each time I restarted the server process, the header value would be reset, but only for it to continue to grow and grow. Eventually, after a period of being busy with other matters, the server process stayed running for long enough that the header value grew too large and HTTP clients began to reject it. There is no defined maximum for an HTTP header value, however most HTTP clients use 100KiB, which is perfectly reasonable, and this header value would continue to grow well beyond that. Diagnosing this issue turned out to be difficult as tools like Curl would fail with errors relating to entities being too large, but stopped short of saying what specifically. I eventually used openssl s_client to send an HTTP request by hand and observed my terminal window being filled with a domain name repeated thousands of times. Looking at the commit history, it was really unclear why I added the culprit lines of code. The commit message just says "Improved CSP support". It just goes to show how important it is to write - hey look it's those words I'm now having to eat! The Fix The fix was to just delete those three lines of code. Yup, it really was that simple, and fixing this bug actually made a larger positive impact than I had expected, as it was immediately clear when I fixed the bug by looking at outbound network bytes: So much traffic was being wasted on excessive header sizes. You might look at these mistakes I've made and think "wow, Ian, these are some obvious mistakes, I never would have made them!" to which I say "good for you!" with the utmost sarcasm and disdain. I enjoy making and refining software, and making anything means making mistakes along the way. Each time I make mistakes such as the ones above, I improve my skills of investigation, diagnosing, and repair. Skills that, judging by my peers in the industry, seemingly everyone is quickly willing to throw away because a robot does it "better" than you. Header Image: "Car accident on the Ffestiniog to Bala road. Nobody was hurt" by Geoff Charles, CC BY-SA 4.0, via Wikimedia Commons.

a week ago
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in