Skip to content

Commit 4f82138

Browse files
committed
Initial version
1 parent 6027e47 commit 4f82138

File tree

7 files changed

+597
-1
lines changed

7 files changed

+597
-1
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@
1919

2020
# Go workspace file
2121
go.work
22+
23+
# DB file
24+
*.sqlite3
25+
26+
# Compiled binaries
27+
dist

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# PenguinMod-StorageExtAPI
2-
e ExtenThe PenguinMod Storagsion API is responsible for storing and retrieving server data in the Storage extension.
2+
The PenguinMod Storage Extension API is responsible for storing and retrieving server data in the Storage extension.
3+
4+
Maintained and hosted by Tnix.

compile.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
mkdir -p dist
2+
rm -rf dist/*
3+
4+
platforms=("windows" "darwin" "linux")
5+
6+
for platform in "${platforms[@]}"
7+
do
8+
if [ $platform == "windows" ]; then
9+
GOOS="${platform}" GOARCH=amd64 go build -o "dist/PM-StorageExtAPI-${platform}-amd64.exe"
10+
GOOS="${platform}" GOARCH=arm64 go build -o "dist/PM-StorageExtAPI-${platform}-arm64.exe"
11+
else
12+
GOOS="${platform}" GOARCH=amd64 go build -o "dist/PM-StorageExtAPI-${platform}-amd64"
13+
GOOS="${platform}" GOARCH=arm64 go build -o "dist/PM-StorageExtAPI-${platform}-arm64"
14+
fi
15+
done

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/PenguinMod/PenguinMod-StorageExtAPI
2+
3+
go 1.21.3
4+
5+
require (
6+
github.com/go-chi/chi v1.5.5
7+
github.com/go-chi/cors v1.2.1
8+
github.com/mattn/go-sqlite3 v1.14.18
9+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
2+
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
3+
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
4+
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
5+
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
6+
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=

main.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"database/sql"
6+
"encoding/base64"
7+
"encoding/json"
8+
"log"
9+
"net"
10+
"net/http"
11+
"os"
12+
"regexp"
13+
"time"
14+
15+
"github.com/go-chi/chi"
16+
"github.com/go-chi/cors"
17+
_ "github.com/mattn/go-sqlite3"
18+
)
19+
20+
var totalRequests int
21+
22+
// this is for anyone trying to be sneaky and store files by base64 encoding them
23+
// (this was very painful to write)
24+
var b64DataRegex = regexp.MustCompile("^data:.+;base64,")
25+
var fileHeaders = [][]byte{
26+
{0x42, 0x4d}, // .bmp
27+
{0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45}, // .fits
28+
{0x47, 0x49, 0x46, 0x38}, // .gif
29+
{0x47, 0x4b, 0x53, 0x4d}, // .gks
30+
{0x01, 0xda}, // .rgb
31+
{0xf1, 0x00, 0x40, 0xbb}, // .itc
32+
{0xff, 0xd8, 0xff, 0xe0}, // .jpg
33+
{0x49, 0x49, 0x4e, 0x31}, // .nif
34+
{0x56, 0x49, 0x45, 0x57}, // .pm (not PenguinMod lol)
35+
{0x89, 0x50, 0x4e, 0x47}, // .png
36+
{0x25, 0x21}, // .[e]ps
37+
{0x59, 0xa6, 0x6a, 0x95}, // .ras
38+
{0x4d, 0x4d, 0x00, 0x2a}, // .tif (Motorola)
39+
{0x49, 49, 0x2a, 0x00}, // .tif (Intel)
40+
{0x67, 0x69, 0x6d, 0x70, 0x20, 0x78, 0x63, 0x66, 0x20, 0x76}, // .xcf
41+
{0x23, 0x46, 0x49, 0x47}, // .fig
42+
{0x2f, 0x2a, 0x20, 0x58, 0x50, 0x4d, 0x20, 0x2a, 0x2f}, // .xpm
43+
{0x42, 0x5a}, // .bz
44+
{0x1f, 0x9d}, // .Z
45+
{0x1f, 0x8b}, // .gz
46+
{0x50, 0x4b, 0x03, 0x04}, // .zip
47+
{0x75, 0x73, 0x74, 0x61, 0x72}, // .tar
48+
{0x4d, 0x5a}, // .exe
49+
{0x7f, 0x45, 0x4c, 0x46}, // .elf
50+
{0xca, 0xfe, 0xba, 0xbe}, // .class
51+
{0x00, 0x00, 0x01, 0x00}, // .ico
52+
{0x52, 0x49, 0x46, 0x46}, // .avi
53+
{0x46, 0x57, 0x53}, // .swf
54+
{0x46, 0x4c, 0x56}, // .flv
55+
{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32}, // .mp4
56+
{0x6d, 0x6f, 0x6f, 0x76}, // .mov
57+
{0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf}, // .wmv/.wma
58+
{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}, // .msi/.doc/.msg
59+
{0x4c, 0x01}, // .obj
60+
{0x4d, 0x5a}, // .dll
61+
{0x4d, 0x53, 0x43, 0x46}, // .cab
62+
{0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00}, // .rar
63+
{0x25, 0x50, 0x44, 0x46}, // .pdf
64+
{0x50, 0x4b, 0x03, 0x04}, // .docx
65+
{0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x00, 0x08, 0x00}, // .jar
66+
{0x78, 0x9c}, // .zlib
67+
}
68+
var timedOutIPs = map[string]int64{}
69+
70+
func includesFile(input string) bool {
71+
// Remove the data URI prefix if present
72+
b64String := string(b64DataRegex.ReplaceAll([]byte(input), []byte("")))
73+
74+
// Attempt to base64 decode string
75+
decoded, err := base64.StdEncoding.DecodeString(b64String)
76+
if err != nil {
77+
return false
78+
}
79+
80+
// Check against file headers
81+
for i := 0; i < len(fileHeaders); i++ {
82+
if bytes.HasPrefix(decoded, fileHeaders[i]) {
83+
log.Println("File detected with header" + string(fileHeaders[i]))
84+
return true
85+
}
86+
}
87+
88+
return false
89+
}
90+
91+
func jsonError(w http.ResponseWriter, errStr string, status int) {
92+
jsonified, err := json.Marshal(struct {
93+
Error string `json:"error"`
94+
}{
95+
Error: errStr,
96+
})
97+
if err != nil {
98+
log.Println(err)
99+
http.Error(w, "Internal server error", http.StatusInternalServerError)
100+
return
101+
}
102+
103+
w.Header().Set("Content-Type", "application/json")
104+
w.WriteHeader(status)
105+
w.Write(jsonified)
106+
}
107+
108+
func jsonSuccess(w http.ResponseWriter) {
109+
w.Header().Set("Content-Type", "application/json")
110+
w.WriteHeader(http.StatusOK)
111+
w.Write([]byte(`{"success": true}`))
112+
}
113+
114+
func main() {
115+
// Initialise database connection
116+
db, err := sql.Open("sqlite3", "db.sqlite3")
117+
if err != nil {
118+
log.Fatalln(err)
119+
}
120+
121+
// Test database connection
122+
if err := db.Ping(); err != nil {
123+
log.Fatalln(err)
124+
}
125+
126+
// Create database table
127+
if _, err := db.Exec(`
128+
CREATE TABLE IF NOT EXISTS kv (
129+
project STRING,
130+
key STRING NOT NULL,
131+
val STRING NOT NULL,
132+
set_by STRING,
133+
UNIQUE (project, key) /* this should automatically index these fields */
134+
);
135+
`); err != nil {
136+
log.Fatalln(err)
137+
}
138+
139+
// Create HTTP server
140+
r := chi.NewRouter()
141+
r.Use(cors.New(cors.Options{
142+
AllowedOrigins: []string{"*"}, // Allow all origins
143+
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
144+
AllowedHeaders: []string{"*"}, // Allow all headers
145+
AllowCredentials: true,
146+
MaxAge: 300, // Max cache age (seconds)
147+
}).Handler)
148+
r.Use(func(next http.Handler) http.Handler {
149+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150+
totalRequests++
151+
next.ServeHTTP(w, r)
152+
})
153+
})
154+
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
155+
jsonified, err := json.Marshal(struct {
156+
Online bool `json:"online"`
157+
ReqCount int `json:"reqCount"`
158+
}{
159+
Online: true,
160+
ReqCount: totalRequests,
161+
})
162+
if err != nil {
163+
log.Println(err)
164+
jsonError(w, "InternalServerError", http.StatusInternalServerError)
165+
return
166+
} else {
167+
w.Header().Set("Content-Type", "application/json")
168+
w.WriteHeader(http.StatusOK)
169+
w.Write(jsonified)
170+
}
171+
})
172+
r.Get("/get", func(w http.ResponseWriter, r *http.Request) {
173+
if !r.URL.Query().Has("key") {
174+
jsonError(w, "NoKeySpecified", http.StatusBadRequest)
175+
return
176+
}
177+
178+
project := r.URL.Query().Get("project")
179+
key := r.URL.Query().Get("key")
180+
181+
var val string
182+
err := db.QueryRow("SELECT val FROM kv WHERE project=$1 AND key=$2", project, key).Scan(&val)
183+
if err == sql.ErrNoRows {
184+
jsonError(w, "KeyFileNonExistent", http.StatusBadRequest)
185+
return
186+
} else if err != nil {
187+
log.Println(err)
188+
jsonError(w, "InternalServerError", http.StatusInternalServerError)
189+
return
190+
} else {
191+
w.Header().Set("Content-Type", "text/plain")
192+
w.WriteHeader(http.StatusOK)
193+
w.Write([]byte(val))
194+
}
195+
})
196+
r.Post("/set", func(w http.ResponseWriter, r *http.Request) {
197+
ip := r.Header.Get("Cf-Connecting-Ip")
198+
if ip == "" {
199+
ip, _, err = net.SplitHostPort(r.RemoteAddr)
200+
if err != nil {
201+
log.Println(err)
202+
jsonError(w, "InternalServerError", http.StatusForbidden)
203+
return
204+
}
205+
}
206+
207+
timedOutUntil := timedOutIPs[ip]
208+
if timedOutUntil > time.Now().UnixMilli() {
209+
jsonError(w, "TimedOut", http.StatusForbidden)
210+
return
211+
}
212+
213+
if !r.URL.Query().Has("key") {
214+
jsonError(w, "NoKeySpecified", http.StatusBadRequest)
215+
return
216+
}
217+
218+
project := r.URL.Query().Get("project")
219+
key := r.URL.Query().Get("key")
220+
221+
var data struct {
222+
Val string `json:"val"`
223+
}
224+
err = json.NewDecoder(r.Body).Decode(&data)
225+
if err != nil {
226+
jsonError(w, "InvalidBody", http.StatusBadRequest)
227+
return
228+
}
229+
230+
if includesFile(data.Val) {
231+
timedOutIPs[ip] = time.Now().UnixMilli() + 10000 // 10 seconds
232+
jsonError(w, "IncludesFile", http.StatusForbidden)
233+
return
234+
}
235+
236+
_, err = db.Exec("INSERT OR REPLACE INTO kv VALUES ($1, $2, $3, $4)", project, key, data.Val, ip)
237+
if err != nil {
238+
log.Println(err)
239+
jsonError(w, "InternalServerError", http.StatusInternalServerError)
240+
} else {
241+
jsonSuccess(w)
242+
}
243+
})
244+
r.Delete("/delete", func(w http.ResponseWriter, r *http.Request) {
245+
if !r.URL.Query().Has("key") {
246+
jsonError(w, "NoKeySpecified", http.StatusBadRequest)
247+
return
248+
}
249+
250+
project := r.URL.Query().Get("project")
251+
key := r.URL.Query().Get("key")
252+
253+
_, err := db.Exec("DELETE FROM kv WHERE project=$1 AND key=$2", project, key)
254+
if err != nil {
255+
log.Println(err)
256+
jsonError(w, "InternalServerError", http.StatusInternalServerError)
257+
} else {
258+
jsonSuccess(w)
259+
}
260+
})
261+
262+
// Serve HTTP server
263+
port := os.Getenv("PORT")
264+
if port == "" {
265+
port = "3000"
266+
}
267+
log.Println("Serving HTTP server on :" + port)
268+
http.ListenAndServe(":"+port, r)
269+
}

0 commit comments

Comments
 (0)