e81f338
Branches > main > e81f338 > files
Don't read/write bad values
Commit: e81f338aeeca921a0901e9f5c328603475add5a0 latest
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)]}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."+ Throws when the string exceeds the maximum length in (UTF-16) characters,+ 32767 by default." ([max-length] (fn [buf s] (write-string buf s max-length))) ([buf s]- (write-string buf s ##Inf)))+ (write-string buf s readers/maximum-string-characters))) (defn identifier> "Writes an identifier as an encoded string."@@ -206,16 +223,26 @@ (defn enum> "Returns a writer that writes one of a set of values via 'writer'.- Takes a map of encoded values to decoded values (values that will appear in- the packet map), same as the enum reader. For example, the map {1 :foo}- will write 1 (via 'writer') when given :foo, so the map is 'backwards'.- When the value is not in the enum map, calls 'writer' with nil."- [writer m]- (apply> (set/map-invert m) writer))+ Takes a map of {encoded-value decoded-value}, the same map as the enum+ reader. When given a decoded value, writes its encoded value via 'writer'.+ Throws if the value is not in the map, or if a decoded value has multiple+ encoded values associated. Use {value nil} to encode nils/missing map keys,+ as they throw by default"+ [writer enum-values]+ (let [m (set/map-invert enum-values)]+ (if-not (= (count enum-values) (count m))+ (throw (ex-info "Duplicate value when creating enum> writer."+ {:enum-values enum-values}))+ (fn [buf x]+ (if (contains? m x)+ (writer buf (get m x))+ (throw (ex-info "Failed to write enum, value is not a member."+ {:value x+ :enum-map m}))))))) (defn block-location> "Writes a block location as a 64-bit integer made of 3 signed integers: 26- bits for the x, 26 bits for the y, and 12 bits for the z. Throws if any+ bits for the x, 26 bits for the z, and 12 bits for the y. Throws if any of the coordinates do not fit within their respective integer sizes." [buf {::block/keys! [x y z]}] (if (and (<= (Math/pow -2 25) x (- (Math/pow 2 25) 1))@@ -246,20 +273,33 @@ (declare nbt-writer) +(defn- compound-keys+ "Returns a map of encoded keys to values. Throws if the map contains+ duplicate keys, such as \"foo\" and :foo, which both encode to \"foo\"."+ [m]+ (reduce-kv (fn [acc k v]+ (let [nbt-key (name* k)]+ (if (contains? acc nbt-key)+ (throw (ex-info "Duplicate keys in NBT compound."+ {:key k+ :in-map m}))+ (assoc acc nbt-key v))))+ {} m))+ (defn nbt-compound> "Writes an NBT compound from a map. Compounds are written as 0 or more entries of the map, where each entry consists of the value's NBT type ID, the NBT string name of the key, and the encoded value.- The encoded compound ends with the 'tag end' byte (0x0A)."+ The encoded compound ends with the 'tag end' byte (0x00)." [buf m] (-> (reduce (fn [acc [k v]] (let [t (nbt/type v)] (-> acc (byte> (req! nbt/nbt-type->id t))- (nbt-string> (name k))+ (nbt-string> k) ((nbt-writer t) v))))- buf m)+ buf (compound-keys m)) (byte> (nbt/nbt-type->id ::nbt/tag-end)))) (defn nbt-list>@@ -268,10 +308,7 @@ [buf xs] (let [nbt-type (nbt/list-type xs) id (nbt/nbt-type->id nbt-type)- writer (array>- (if (= nbt-type ::nbt/list)- (nbt-writer xs)- (nbt-writer nbt-type)))]+ writer (array> (nbt-writer nbt-type))] (-> buf (byte> id) (int> (count xs))