latest
Branches > main > latest > files
Don't read/write bad values
Commit: e81f338aeeca921a0901e9f5c328603475add5a0
Parent: af0a324
5 changed files:
src/net/bowmap/server/network/streams.clj view file
@@ -2,7 +2,7 @@ "Functions for reading and writing frames to Input and OutputStreams" (:require [net.bowmap.server.packet.frame :as frame]- [net.bowmap.server.packet.readers :refer [max-varint-size]])+ [net.bowmap.server.packet.varint :as varint]) (:import [java.io IOException InputStream OutputStream])) @@ -15,19 +15,17 @@ (throw (IOException. "EOF while reading from InputStream")) x))) +(def max-frame-bytes 2097151)+ (defn read-varint!! "Blocks and reads a varint from an InputStream.- Throws if EOF is reached, or if the encoded varint is invalid."+ Throws if EOF is reached, if the encoded varint is invalid, or if it is+ not a valid frame length." [input-stream]- (loop [num-read 0- result 0]- (if (>= num-read max-varint-size)- (throw (IOException. "VarInt too long"))- (let [b (read-byte!! input-stream)- result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]- (if (zero? (bit-and b 0x80))- result'- (recur (inc num-read) result'))))))+ (let [n (varint/decode-varint read-byte!! input-stream)]+ (if (<= 0 n max-frame-bytes)+ n+ (throw (IOException. (str "Invalid frame length: " n)))))) (defn read-frame!! "Blocks and reads a frame from an InputStream.@@ -46,20 +44,19 @@ (defn write-varint!! "Blocks and writes a varint to an OutputStream." [output-stream n]- (loop [n n]- (let [b (bit-and n 0x7F)- nxt (unsigned-bit-shift-right n 7)]- (if (zero? nxt)- (OutputStream/.write output-stream b)- (do (OutputStream/.write output-stream (bit-or b 0x80))- (recur nxt))))))+ (varint/encode-varint (fn [output-stream b]+ (doto output-stream+ (^[int] OutputStream/.write b)))+ output-stream n)) (defn write-frame!! "Blocks and writes a frame to an OutputStream. Throws if the frame cannot be written." [frame output-stream]- (let [array (frame/array frame)]- (doto output-stream- (write-varint!! (alength array))- (OutputStream/.write array)- (OutputStream/.flush))))+ (let [^bytes array (frame/array frame)]+ (if (> (alength array) max-frame-bytes)+ (throw (IOException. "Frame exceeds maximum length."))+ (doto output-stream+ (write-varint!! (alength array))+ (OutputStream/.write array)+ (OutputStream/.flush)))))src/net/bowmap/server/packet/readers.clj view file
@@ -7,7 +7,8 @@ (:require [clojure.spec.alpha :as s] [medley.core :refer [assoc-some]]- [net.bowmap.server.packet.identifier :as identifier])+ [net.bowmap.server.packet.identifier :as identifier]+ [net.bowmap.server.packet.varint :as varint]) (:import [io.netty.buffer ByteBuf] [java.nio.charset StandardCharsets]@@ -41,15 +42,6 @@ (read-key acc k reader buf)) {} pairs)))) -(def byte< ByteBuf/.readByte)-(def ubyte< ByteBuf/.readUnsignedByte)-(def short< ByteBuf/.readShort)-(def ushort< ByteBuf/.readUnsignedShort)-(def int< ByteBuf/.readInt)-(def long< ByteBuf/.readLong)-(def float< ByteBuf/.readFloat)-(def double< ByteBuf/.readDouble)- (defn apply< "Returns a reader that calls f with the result of reading with each reader." ([f reader]@@ -68,25 +60,39 @@ (fn [buf] ((f (reader buf)) buf))) -(def max-varint-size 5)+(def byte< ByteBuf/.readByte)+(def ubyte< ByteBuf/.readUnsignedByte)+(def short< ByteBuf/.readShort)+(def ushort< ByteBuf/.readUnsignedShort)+(def int< ByteBuf/.readInt)+(def long< ByteBuf/.readLong)++(defn- finite+ "Returns its argument, or throws if it is not a finite floating point number."+ [x]+ (if (and (float? x) (Double/isFinite x))+ x+ (throw (ex-info "Value is not a valid float (cannot read NaN/Infinity)"+ {:value x}))))++(def float< (apply< finite ByteBuf/.readFloat))+(def double< (apply< finite ByteBuf/.readDouble)) (defn varint< [buf]- (loop [num-read 0- result 0]- (if (>= num-read max-varint-size)- (throw (Exception. "VarInt too long"))- (let [b (byte< buf)- result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]- (if (zero? (bit-and b 0x80))- result'- (recur (inc num-read) result'))))))+ (varint/decode-varint ubyte< buf))++(defn varlong< [buf]+ (varint/decode-varlong ubyte< buf)) (defn array< "Returns a readers that 'length' number of values into a vector from a buffer via 'reader'." [reader length]- (fn [buf]- (into [] (repeatedly length #(reader buf)))))+ (if (nat-int? length)+ (fn [buf]+ (into [] (repeatedly length #(reader buf))))+ (throw (ex-info "Reading array length must be a non-negative integer."+ {:length length})))) (defn list< "Returns a reader that reads a varint length, then reads that many values@@ -97,8 +103,36 @@ ([k v & kvs] (list< (apply map< k v kvs)))) -(defn string< [buf]- (ByteBuf/.readCharSequence buf (varint< buf) StandardCharsets/UTF_8))+(defn- within-valid-length?+ "Returns whether 'byte-count' number of UTF-8 bytes *may* be within+ 'max-length' UTF-16 code points (or string characters) when encoded into a+ string. This can be used to avoid constructing strings that would be+ certainly too long."+ [max-length byte-count]+ (<= 0 byte-count (* 3 max-length)))++(def maximum-string-characters 32767)++(defn string<*+ "Returns a reader that reads a string as a varint byte length followed by its+ UTF-8 bytes. Throws when the string exceeds 'max-length' characters."+ [max-length]+ {:pre [(int? max-length)+ (<= 0 max-length maximum-string-characters)]}+ (fn [buf]+ (let [byte-count (varint< buf)]+ (if-not (within-valid-length? max-length byte-count)+ (throw (ex-info "String cannot fit within maximum length for reading."+ {:max-length max-length+ :number-of-bytes byte-count}))+ (let [s (str (ByteBuf/.readCharSequence buf byte-count StandardCharsets/UTF_8))]+ (if (> (count s) max-length)+ (throw (ex-info "String exceeds maximum length for reading."+ {:max-length max-length+ :actual-length (count s)}))+ s))))))++(def string< (string<* maximum-string-characters)) (def split-identifier< "Reads an identifier from 2 strings: the namespace and the name."@@ -106,5 +140,17 @@ (def uuid< (apply< UUID/new long< long<)) -(defn enum< [reader m]- (apply< m reader))+(defn enum<+ "Returns a reader that reads one of a set of values via 'reader'.+ Takes a map of {encoded-value decoded-value}, the same map as the enum+ writer. Reads an encoded value via 'reader' and returns its decoded value.+ Throws if the read value is not in the map. Use {value nil} to decode a+ value as nil."+ [reader enum-values]+ (fn [buf]+ (let [x (reader buf)]+ (if (contains? enum-values x)+ (get enum-values x)+ (throw (ex-info "Failed to read enum, value is not a member."+ {:value x+ :enum-map enum-values}))))))src/net/bowmap/server/packet/serialize.clj view file
@@ -23,7 +23,8 @@ (defn decode-frame "Decodes a packet from a frame.- Can throw if the frame does not contain a valid packet."+ Can throw if the frame does not contain a valid packet.+ Does not throw when the frame has leftover bytes." [{:keys! [::phase/phase]} frame] (let [buf (frame/->ByteBuf frame) numeric-id (readers/varint< buf)src/net/bowmap/server/packet/varint.clj view file
@@ -0,0 +1,43 @@+(ns net.bowmap.server.packet.varint)++(defn encode-varlong+ "Calls 'rf' with the accumulator and each of the bytes of a varlong.+ When encoding a varint, turn the number into an unsigned long."+ [rf init n]+ (loop [n n+ acc init]+ (let [b (bit-and n 0x7F)+ nxt (unsigned-bit-shift-right n 7)]+ (if (zero? nxt)+ (rf acc b)+ (recur nxt (rf acc (bit-or b 0x80)))))))++(defn encode-varint+ [rf init n]+ (encode-varlong rf init (Integer/toUnsignedLong n)))++(defn- decode-varint*+ [read-byte src max-bytes]+ (loop [num-read 0+ result 0]+ (if (>= num-read max-bytes)+ (throw (ex-info "Cannot decode varint, too many bytes."+ {:max-bytes max-bytes}))+ (let [b (read-byte src)]+ (if (and (= num-read 9) (> b 0x01))+ (throw (ex-info "Varint exceeds 64 bits while decoding"+ {:last-byte b}))+ (let [result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]+ (if (zero? (bit-and b 0x80))+ result'+ (recur (inc num-read) result'))))))))++(defn decode-varint [read-byte src]+ (let [n (decode-varint* read-byte src 5)]+ (if (<= n 0xFFFFFFFF)+ (long (unchecked-int n))+ (throw (ex-info "VarInt exceeds 32 bits."+ {:value n})))))++(defn decode-varlong [read-byte src]+ (decode-varint* read-byte src 10))src/net/bowmap/server/packet/writers.clj view file
@@ -12,7 +12,9 @@ [net.bowmap.server.game.text :as text] [net.bowmap.server.packet.identifier :as identifier] [net.bowmap.server.packet.nbt :as nbt]- [net.bowmap.server.packet.schema :as p])+ [net.bowmap.server.packet.readers :as readers]+ [net.bowmap.server.packet.schema :as p]+ [net.bowmap.server.packet.varint :as varint]) (:import [io.netty.buffer ByteBuf ByteBufOutputStream] [java.nio.charset StandardCharsets]@@ -59,14 +61,34 @@ (catch Exception e (throw (ex-info "Failed to call writer with applied function" {:f f :writer writer} e)))))) -(def byte> ByteBuf/.writeByte)-(def ubyte> ByteBuf/.writeByte)-(def short> ByteBuf/.writeShort)-(def ushort> ByteBuf/.writeShort)-(def int> ByteBuf/.writeInt)-(def long> ByteBuf/.writeLong)-(def float> (apply> (comp Float/floatToRawIntBits float) int>))-(def double> (apply> Double/doubleToRawLongBits long>))+(defn- check-int+ "Returns a function that throws when its argument is not an integer within 'min' to 'max'."+ [min max]+ (fn [x]+ (if (and (int? x) (<= min x max))+ x+ (throw (ex-info "Integer out of size range."+ {:min min+ :max max+ :value x})))))++(def byte> (apply> (check-int -128 127) ByteBuf/.writeByte))+(def ubyte> (apply> (check-int 0 255) ByteBuf/.writeByte))+(def short> (apply> (check-int -32768 32767) ByteBuf/.writeShort))+(def ushort> (apply> (check-int 0 65535) ByteBuf/.writeShort))+(def int> (apply> (check-int Integer/MIN_VALUE Integer/MAX_VALUE) ByteBuf/.writeInt))+(def long> (apply> (check-int Long/MIN_VALUE Long/MAX_VALUE) ByteBuf/.writeLong))++(defn- finite+ "Returns its argument, or throws if it is not a finite floating point number."+ [x]+ (if (and (number? x) (Double/isFinite x))+ x+ (throw (ex-info "Value is not a valid float (cannot write NaN/Infinity)"+ {:value x}))))++(def float> (apply> (comp Float/floatToRawIntBits float finite) int>))+(def double> (apply> (comp Double/doubleToRawLongBits finite) long>)) (defn then> "Returns a writer that writes (f x) to the buffer, then (g x)."@@ -83,15 +105,11 @@ (byte> buf 1) (byte> buf 0))) -(defn varint> [buf n]- (loop [buf buf- n n]- (let [b (bit-and n 0x7F)- nxt (unsigned-bit-shift-right n 7)]- (if (zero? nxt)- (byte> buf b)- (recur (byte> buf (bit-or b 0x80))- nxt)))))+(def varlong> (apply> (check-int Long/MIN_VALUE Long/MAX_VALUE)+ (partial varint/encode-varlong ubyte>)))++(def varint> (apply> (check-int Integer/MIN_VALUE Integer/MAX_VALUE)+ (partial varint/encode-varint ubyte>))) (defn array> "Returns a writer that writes every value in a coll via 'writer'."@@ -112,26 +130,25 @@ (list> (apply map> k v kvs)))) (defn- write-string [buf s max-length]- (let [bytes (String/.getBytes s StandardCharsets/UTF_8)]- (if (> (count bytes) max-length)- (throw (ex-info "String exceeds maximum length for writing."- {:max-length max-length- :actual-count (count bytes)- :string s}))+ (if (> (count s) max-length)+ (throw (ex-info "String exceeds maximum length for writing."+ {:max-length max-length+ :actual-length (count s)+ :string s}))+ (let [bytes (String/.getBytes s StandardCharsets/UTF_8)] (-> buf (varint> (count bytes)) (ByteBuf/.writeBytes bytes))))) (defn string> "Writes a string as a varint byte length followed by its UTF-8 bytes.-- When passed 1 arg, returns a string writer that throws when the string- exceeds the supplied maximum number of UTF-8 bytes."