Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active November 15, 2021 17:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariankordi/8e80e2ab21ac9a76165b0c7f773c40a9 to your computer and use it in GitHub Desktop.
Save ariankordi/8e80e2ab21ac9a76165b0c7f773c40a9 to your computer and use it in GitHub Desktop.
rewrite of ForwardNotifierServer for this iphone tweak https://github.com/Greg0109/ForwardNotifier
package main
import (
"github.com/valyala/fasthttp"
"github.com/gen2brain/beeep"
"encoding/json"
"encoding/base64"
// to create a reader for the base64 image
"strings"
// for catching os signals and deleting the temporary directory on exit
"os/signal"
"syscall"
// for creating and removing temporary icon files
"io/ioutil"
"io"
"os"
"flag"
"log"
"fmt"
)
var (
addr = flag.String("addr", "0.0.0.0:8000", "address to run the server on")
logEnabled = flag.Bool("log", false, "log title and truncated message of incoming notifications")
dumpEnabled = flag.Bool("dump", false, "dump requests that failed in the temporary directory")
// imageDir is the temporary directory where images are stored and it is defined in main
imageDir string
)
// deleteImageDir is called when terminated and when the server fails to listen
func deleteImageDir() {
log.Println("recursively deleting temporary directory:", imageDir)
// i'm scared that the imageDir might be blank and then everything will be deleted
if strings.Contains(imageDir, "ForwardNotifierServer") {
if err := os.RemoveAll(imageDir); err != nil {
log.Println("os.RemoveAll failed:", err)
}
log.Println("done")
} else {
log.Println("never mind - the temporary directory does not contain \"ForwardNotifierServer\" - THIS WOULD'VE DELETED EVERYTHING OTHERWISE! WHAT")
}
}
// sometimes the message or image will be set to this for some inexplicable reason
const objcNull = "(null)"
func main() {
// parse command line flags, such as addr
flag.Parse()
// create temporary image directory in the directory returned by os.TempDir()
var err error
imageDir, err = os.MkdirTemp("", "ForwardNotifierServer")
if err != nil {
log.Fatalln("could not create temporary directory:", err)
}
// listen for signal to exit then delete temporary directory
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGTERM)
signal.Notify(signalChannel, syscall.SIGINT)
go func() {
signalReceived := <-signalChannel
log.Println("received signal:", signalReceived)
// delete image directory
deleteImageDir()
// exit with code 0
os.Exit(0)
}()
// addr is a pointer
log.Println("running server on", *addr, "temporary image directory is at:", imageDir)
if *logEnabled {
log.Println("log is enabled - notifications will be printed with log.Println (usually in stderr)")
}
if *dumpEnabled {
log.Println("dump is also enabled so requests will be dumped at the image directory on certain errors")
}
// create a custom server mostly for the request body limit
server := &fasthttp.Server{
Handler: requestHandler,
// limit max request body to 1 mb
MaxRequestBodySize: 1000000,
// keepalive is not necessary
//DisableKeepalive: true,
// worth limiting
MaxConnsPerIP: 10,
// save bandwidth????
NoDefaultContentType: true,
NoDefaultDate: true,
NoDefaultServerHeader: true,
}
if err = server.ListenAndServe(*addr); err != nil {
deleteImageDir()
log.Fatalln("error in fasthttp.ListenAndServe: ", err)
}
}
type forwardNotifierNotification struct {
// Title and Message are originally encoded in Base64
Title string `json:"Title"`
Message string `json:"Message"`
// Image is encoded in Base64 too
Image string `json:"img"`
AppName string `json:"appname"`
}
// DecodeTitleAndMessage decodes the fields Title and Message from Base64 encoding.
func (r *forwardNotifierNotification) DecodeTitleAndMessage() error {
data, err := base64.StdEncoding.DecodeString(r.Title)
if err != nil {
return fmt.Errorf("could not decode title: %v", err)
}
r.Title = string(data)
data, err = base64.StdEncoding.DecodeString(r.Message)
if err != nil {
return fmt.Errorf("could not decode message: %v", err)
}
r.Message = string(data)
return nil
}
// WriteImageToFile decodes the base64 encoded Image string and writes it to a file.
func (r *forwardNotifierNotification) WriteImageToFile(imageFile *os.File) error {
// make a string reader for the base64 image because base64 decoder wants a reader
imageReader := strings.NewReader(r.Image)
// make new base64 decoder to decode the image
imageDecoder := base64.NewDecoder(base64.StdEncoding, imageReader)
// now copy the base64 image decoder to the file!!!
_, err := io.Copy(imageFile, imageDecoder)
if err != nil {
return fmt.Errorf("could not decode and write image: %v", err)
}
return nil
}
func dumpRequestBodyToFile(body []byte/**fasthttp.RequestCtx*/) {
// ioutil.TempFile just makes a temporary file in the specified directory
dumpFile, err := ioutil.TempFile(imageDir, "dump")
if err != nil {
log.Println("could not create dump file", err)
return
}
/*if _, err = io.Copy(dumpFile, ctx.RequestBodyStream()); err != nil {
log.Println("writing dump to file failed:", err)
return
}*/
if _, err = dumpFile.Write(body); err != nil {
log.Println("writing dump to file failed:", err)
return
}
if err = dumpFile.Close(); err != nil {
log.Println("dump file could not be closed", err)
return
}
log.Println("created request dump at", dumpFile.Name())
}
// just sends notifications from any request received
func requestHandler(ctx *fasthttp.RequestCtx) {
//fmt.Fprintln(ctx, "hi ><><><")
// ignore requests that are not post at /, so basically that are random
if !ctx.IsPost() || string(ctx.RequestURI()) != "/" {
ctx.Response.Header.SetNoDefaultContentType(false)
fmt.Fprintln(ctx, "👋 🥺")
return
}
// make an empty struct for the notification
notificationRequest := &forwardNotifierNotification{}
// try json decoding request to the struct
// this cannot actually be done because the dump function
// needs to read the request as well as the decode functions
// the dump function needs to run at any given time so it cannot just be
// replaced in the case that dump is enabled
/*
jsonDecoder := json.NewDecoder(ctx.RequestBodyStream())
if err := jsonDecoder.Decode(&notificationRequest); err != nil {
*/
postBody := ctx.PostBody()
if err := json.Unmarshal(postBody, &notificationRequest); err != nil {
if *logEnabled {
log.Println("non-json request from", ctx.RemoteAddr())
}
if *dumpEnabled {
dumpRequestBodyToFile(postBody)
}
ctx.Error("cannot decode request (it should be in json format!): " + err.Error(), fasthttp.StatusBadRequest)
return
}
// ignore "(null)" as a title because this is usually for non-notifications such as Do Not Disturb
if notificationRequest.Title == objcNull {
if *logEnabled {
log.Println("\"(null)\" as title from", ctx.RemoteAddr())
}
if *dumpEnabled {
dumpRequestBodyToFile(postBody)
}
ctx.Error("ignoring this null title", fasthttp.StatusBadRequest)
return
}
// ignore notifications where the message is "(null)" this started happening when i updated to ios 14 for some reason
if notificationRequest.Message == objcNull {
if *logEnabled {
log.Println("\"(null)\" as message from", ctx.RemoteAddr())
}
if *dumpEnabled {
dumpRequestBodyToFile(postBody)
}
ctx.Error("null? why is the message an objective-c null? why did you send this? ignoring", fasthttp.StatusBadRequest)
return
}
// try decoding base64 title and message from the notification
if err := notificationRequest.DecodeTitleAndMessage(); err != nil {
if *logEnabled {
log.Println("couldn't decode title and message from", ctx.RemoteAddr())
}
if *dumpEnabled {
dumpRequestBodyToFile(postBody)
}
ctx.Error(err.Error(), fasthttp.StatusBadRequest)
return
}
// a file is created from AppName so if there is an attempt to descend the directory here then don't do this request
if strings.Contains(notificationRequest.AppName, "./") {
if *logEnabled {
log.Println("\"./\" in appname from", ctx.RemoteAddr())
}
ctx.Error("an app name that has ../ in it? weird..........", fasthttp.StatusBadRequest)
return
}
var imageFileName string
// check if the image is not null because sometimes it is for some reason
if notificationRequest.Image == objcNull {
if *logEnabled {
log.Println("\"(null)\" as image, ignoring image from", ctx.RemoteAddr())
}
if *dumpEnabled {
dumpRequestBodyToFile(postBody)
}
// just do not process icon at this point, leave it blank
imageFileName = ""
} else {
// save the image to a temporary file so that beeeeeeeeeeeeeeeeep can access it
// appname is the filename of the image, so that if it exists then it doesn't have to be created again
imageFileName = imageDir + "/" + notificationRequest.AppName + ".png"
// os.OpenFile will open a file or create it if it doesn't exist
imageFile, err := os.OpenFile(imageFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
// panic if the temporary file could not be created
log.Panicln("could not create or open temporary file!!!!!!!!!", err)
return
}
// now that the file is open, decode and write the image to it
// assuming that the image is formatted correctly
if err = notificationRequest.WriteImageToFile(imageFile); err != nil {
log.Println("writing image to file failed:", err)
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
return
}
// close the file when it is finished
if err = imageFile.Close(); err != nil {
log.Panicln("temporary file could not be closed????????", err)
}
}
// now an actual desktop notification can be sent
if err := beeep.Notify(notificationRequest.Title, notificationRequest.Message, imageFileName); err != nil {
log.Println("beeep.Notify:", err)
}
// print a log of the message if *logEnabled is enabled, at the very end
if *logEnabled {
log.Printf("notification from %s: title \"%.20s\", message \"%.20s\", icon path %s\n", ctx.RemoteAddr().String(), notificationRequest.Title, notificationRequest.Message, imageFileName)
}
// idk if this is truly necessary
//fmt.Fprintln(ctx, "Sent!")
}
# designed to be a systemd user service! systemctl --user
[Unit]
Description=ForwardNotifierServer
# network online??? lol??????
#Requires=NetworkManager-wait-online.service
# check if connected to ethernet? but this is active even when it's not plugged
#Requires=sys-subsystem-net-devices-enp0s31f6.device
After=gnome-session-manager@gnome.service
[Service]
Type=simple
# check if ip is correct? need to make that forwardnotifier bridge......
# doesn't work because this service runs before the system comes online
#ExecCondition=/bin/sh -c 'ip addr | grep 192.168.2.20'
#ExecStart=python3 /usr/local/bin/ForwardNotifierServer.py
# new version in go
ExecStart=/usr/local/bin/ForwardNotifierServer
Environment="DISPLAY=:0"
RemainAfterExit=yes
# sandboxing
CapabilityBoundingSet=
DevicePolicy=strict
KeyringMode=private
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
# complains about needing setup or something
#PrivateIPC=yes
PrivateMounts=yes
# breaks ip addr
#PrivateNetwork=yes
# when this is set, the wrong tmp is specified
#PrivateTmp=yes
# gives full access to the real tmp
ReadWritePaths=/tmp
PrivateUsers=yes
ProtectClock=yes
ProtectControlGroups=yes
# notification sending won't work without this
#ProtectHome=yes
InaccessiblePaths=-/home -/root
# actually just needs to connect to socket /run/user/x/bus
ProtectHostname=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
ProtectProc=invisible
ProcSubset=pid
ProtectSystem=strict
# af_netlink is needed for ip addr.. af_unix might be necessary to talk to dbus
#RestrictAddressFamilies=AF_INET AF_NETLINK AF_UNIX
RestrictAddressFamilies=AF_INET AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
#SystemCallFilter=@system-service
SystemCallFilter=@basic-io
SystemCallErrorNumber=EPERM
#IPAddressAllow=192.168.2.0/24
#IPAddressDeny=any
UMask=0066
[Install]
WantedBy=graphical-session.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment