├── .gitignore ├── .github ├── dependabot.yaml └── workflows │ ├── pr.yaml │ └── push.yaml ├── events ├── conn.go ├── unsub.go ├── pubrec.go ├── pubrel.go ├── sub.go ├── suback.go ├── puback.go ├── connack.go ├── pubcomp.go ├── unsuback.go ├── pub.go └── fast.go ├── config └── config.go ├── storage ├── provider.go └── file_provider.go ├── go.mod ├── SECURITY.md ├── docs ├── about.md ├── configuration.md ├── source-build.md ├── index.md └── docker-build.md ├── conn ├── tcp.go └── ws.go ├── go.sum ├── Dockerfile ├── handlers ├── conn.go ├── base.go ├── pubrel.go ├── sub.go ├── unsub.go └── pub.go ├── README.md ├── interfaces ├── tcp.go └── ws.go ├── broker ├── client_test.go ├── subscription.go ├── subscription_test.go ├── broker.go └── client.go ├── mkdocs.yml ├── LICENSE.md ├── main.go ├── .devcontainer └── devcontainer.json └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /events/conn.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const Conn = "conn" 4 | 5 | type ConnEvent struct { 6 | Kind string `json:"kind"` 7 | ClientId string `json:"client_id"` 8 | } 9 | -------------------------------------------------------------------------------- /events/unsub.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const Unsub = "unsub" 4 | 5 | type UnsubEvent struct { 6 | Kind string `json:"kind"` 7 | Pattern string `json:"pattern"` 8 | } 9 | -------------------------------------------------------------------------------- /events/pubrec.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const PubRec = "pubrec" 4 | 5 | type PubRecEvent struct { 6 | Kind string `json:"kind"` 7 | PacketId string `json:"packet_id"` 8 | } 9 | -------------------------------------------------------------------------------- /events/pubrel.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const PubRel = "pubrel" 4 | 5 | type PubRelEvent struct { 6 | Kind string `json:"kind"` 7 | PacketId string `json:"packet_id"` 8 | } 9 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Storage *Storage `json:"storage"` 5 | } 6 | 7 | type Storage struct { 8 | RootDir string `json:"root_dir"` 9 | } 10 | -------------------------------------------------------------------------------- /storage/provider.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "github.com/c16a/microq/events" 4 | 5 | type Provider interface { 6 | SaveMessage(event *events.PubEvent) error 7 | Close() error 8 | } 9 | -------------------------------------------------------------------------------- /events/sub.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const Sub = "sub" 4 | 5 | type SubEvent struct { 6 | Kind string `json:"kind"` 7 | Group string `json:"group"` 8 | Pattern string `json:"pattern"` 9 | } 10 | -------------------------------------------------------------------------------- /events/suback.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const SubAck = "suback" 4 | 5 | type SubAckEvent struct { 6 | Kind string `json:"kind"` 7 | Success bool `json:"success"` 8 | Pattern string `json:"pattern"` 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/c16a/microq 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/gorilla/websocket v1.5.1 8 | ) 9 | 10 | require golang.org/x/net v0.25.0 // indirect 11 | -------------------------------------------------------------------------------- /events/puback.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const PubAck = "puback" 4 | 5 | type PubAckEvent struct { 6 | Kind string `json:"kind"` 7 | PacketId string `json:"packet_id"` 8 | Success bool `json:"success"` 9 | } 10 | -------------------------------------------------------------------------------- /events/connack.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const ConnAck = "connack" 4 | 5 | type ConnAckEvent struct { 6 | Kind string `json:"kind"` 7 | ClientId string `json:"client_id"` 8 | Success bool `json:"success"` 9 | } 10 | -------------------------------------------------------------------------------- /events/pubcomp.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const PubComp = "pubcomp" 4 | 5 | type PubCompEvent struct { 6 | Kind string `json:"kind"` 7 | PacketId string `json:"packet_id"` 8 | Success bool `json:"success"` 9 | } 10 | -------------------------------------------------------------------------------- /events/unsuback.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const UnSubAck = "unsuback" 4 | 5 | type UnsubAckEvent struct { 6 | Kind string `json:"kind"` 7 | Success bool `json:"success"` 8 | Pattern string `json:"pattern"` 9 | } 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Pending the release of 1.x version 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please send an email to [chaitanya.m61292@gmail.com](mailto:chaitanya.m61292@gmail.com) 10 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | microq is a tiny MQTT broker written in Go with a focus on minimalism. 2 | 3 | Hence, the minimal content here, too. 4 | 5 | [![GitHub](https://img.shields.io/github/license/c16a/microq)](https://github.com/c16a/microq/blob/master/LICENSE) -------------------------------------------------------------------------------- /events/pub.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const Pub = "pub" 4 | 5 | type PubEvent struct { 6 | Kind string `json:"kind"` 7 | PacketId string `json:"packet_id"` 8 | Message string `json:"message"` 9 | Topic string `json:"topic"` 10 | QoS int `json:"qos"` 11 | Retain bool `json:"retain"` 12 | } 13 | -------------------------------------------------------------------------------- /conn/tcp.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import "net" 4 | 5 | type TCPConnection struct { 6 | conn *net.TCPConn 7 | } 8 | 9 | func NewTCPConnection(conn *net.TCPConn) *TCPConnection { 10 | return &TCPConnection{conn: conn} 11 | } 12 | 13 | func (tc *TCPConnection) WriteMessage(data []byte) error { 14 | data = append(data, '\n') 15 | _, err := tc.conn.Write(data) 16 | return err 17 | } 18 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | microq can be configured via a custom JSON configuration, and the path can be passed over via the `CONFIG_FILE_PATH` environment variable. 2 | The current JSON schema to be adhered to, can be found at [**c16a/microq:/config/config.go**](https://github.com/c16a/microq/blob/master/config/config.go) 3 | 4 | When running on Docker or Kubernetes, this file should be mounted as a volume. -------------------------------------------------------------------------------- /events/fast.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type kindOnly struct { 8 | Kind string `json:"kind"` 9 | } 10 | 11 | // This doesn't parse the entire JSON, so it's (probably) fast 12 | func GetKindFromJson(data []byte) string { 13 | var k kindOnly 14 | err := json.Unmarshal(data, &k) 15 | if err != nil { 16 | return "" 17 | } 18 | return k.Kind 19 | } 20 | -------------------------------------------------------------------------------- /conn/ws.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import "github.com/gorilla/websocket" 4 | 5 | type WebsocketConnection struct { 6 | conn *websocket.Conn 7 | } 8 | 9 | func NewWebsocketConnection(conn *websocket.Conn) *WebsocketConnection { 10 | return &WebsocketConnection{conn: conn} 11 | } 12 | 13 | func (wc *WebsocketConnection) WriteMessage(data []byte) error { 14 | return wc.conn.WriteMessage(websocket.TextMessage, data) 15 | } 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 4 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 5 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 6 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/golang:1.22.3 AS builder 2 | 3 | WORKDIR /app 4 | COPY go.mod ./ 5 | COPY go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | ENV CGO_ENABLED=0 10 | RUN go build -ldflags="-s -w" -o microq github.com/c16a/microq 11 | 12 | FROM scratch 13 | 14 | LABEL org.opencontainers.image.source=https://github.com/c16a/microq 15 | LABEL org.opencontainers.image.description="A tiny event broker" 16 | LABEL org.opencontainers.image.licenses=MIT 17 | 18 | WORKDIR /app 19 | COPY --from=builder /app/microq ./ 20 | CMD ["/app/microq"] 21 | -------------------------------------------------------------------------------- /handlers/conn.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/c16a/microq/broker" 6 | "github.com/c16a/microq/events" 7 | ) 8 | 9 | func handleConn(message []byte, client *broker.ConnectedClient, b *broker.Broker) error { 10 | var event events.ConnEvent 11 | err := json.Unmarshal(message, &event) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | client.SetId(event.ClientId) 17 | b.Connect(event.ClientId, client) 18 | connackEvent := &events.ConnAckEvent{ 19 | Kind: events.ConnAck, 20 | Success: true, 21 | ClientId: event.ClientId, 22 | } 23 | return client.WriteInterface(connackEvent) 24 | } 25 | -------------------------------------------------------------------------------- /handlers/base.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/c16a/microq/broker" 5 | "github.com/c16a/microq/events" 6 | "github.com/c16a/microq/storage" 7 | ) 8 | 9 | func HandleMessage(client *broker.ConnectedClient, broker *broker.Broker, sp storage.Provider, message []byte) error { 10 | kind := events.GetKindFromJson(message) 11 | switch kind { 12 | case events.Pub: 13 | return handlePublish(message, client, broker, sp) 14 | case events.PubRel: 15 | return handlePubrel(message, client) 16 | case events.Sub: 17 | return handleSubscribe(message, client) 18 | case events.Unsub: 19 | return handleUnsubscribe(message, client) 20 | case events.Conn: 21 | return handleConn(message, client, broker) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /docs/source-build.md: -------------------------------------------------------------------------------- 1 | microq can be built from source on Linux, Windows, or macOS. 2 | 3 | ## Prerequisites 4 | - Git 5 | - Golang 1.15 or newer 6 | 7 | ## Building 8 | ```shell 9 | git clone https://github.com/c16a/microq.git 10 | cd microq 11 | go build -ldflags="-s -w" -o binary github.com/c16a/microq/app 12 | ``` 13 | 14 | ### Cross compiling 15 | To cross compile the microq binary to a different architecture or operating system, 16 | the `GOOS` and `GOARCH` environment variables can be used. 17 | ```shell 18 | # List all available os/arch combinations for cross compiling 19 | go tool dist list 20 | 21 | # To compile the binary for Linux ARM 64-bit, use the below 22 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o binary_amd64 github.com/c16a/microq/app 23 | ``` -------------------------------------------------------------------------------- /handlers/pubrel.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/c16a/microq/broker" 7 | "github.com/c16a/microq/events" 8 | ) 9 | 10 | func handlePubrel(message []byte, client *broker.ConnectedClient) error { 11 | if !client.IsIdentified() { 12 | client.WriteInterface(&events.PubCompEvent{ 13 | Kind: events.PubComp, 14 | Success: false, 15 | }) 16 | return errors.New("unidentified client") 17 | } 18 | 19 | var event events.PubRelEvent 20 | err := json.Unmarshal(message, &event) 21 | if err != nil { 22 | return err 23 | } 24 | pubCompEvent := &events.PubCompEvent{ 25 | Kind: events.PubComp, 26 | PacketId: event.PacketId, 27 | } 28 | client.WriteInterface(pubCompEvent) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microq 2 | A tiny event broker. 3 | 4 | ## Features 5 | - [x] Websocket support 6 | - [x] TCP support 7 | - [x] Grouped subscriptions 8 | - [ ] Offline messages 9 | - [ ] Clustering 10 | - [x] QoS (0, 1, 2) 11 | - [x] Topics & Patterns 12 | 13 | ### Subscriptions 14 | Topics and patterns are heavily inspired by [NATS's subject-based messaging](https://docs.nats.io/nats-concepts/subjects). 15 | 16 | `Unsub` and `Sub` actions set on clients are applied in that order. 17 | 18 | ```markdown 19 | # Sample algorithm only 20 | SUB us.* 21 | UNSUB us.payments 22 | PUB us.accounts -> this will still be received by client 23 | PUB us.payments -> this will not be received 24 | SUB us.payments 25 | PUB us.payments -> this will now be received 26 | ``` 27 | ## Why does this exist? 28 | Because. -------------------------------------------------------------------------------- /handlers/sub.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/c16a/microq/broker" 7 | "github.com/c16a/microq/events" 8 | ) 9 | 10 | func handleSubscribe(message []byte, client *broker.ConnectedClient) error { 11 | if !client.IsIdentified() { 12 | client.WriteInterface(&events.SubAckEvent{ 13 | Kind: events.SubAck, 14 | Success: false, 15 | }) 16 | return errors.New("unidentified client") 17 | } 18 | 19 | var event events.SubEvent 20 | err := json.Unmarshal(message, &event) 21 | if err != nil { 22 | return err 23 | } 24 | client.SubscribeToPattern(event.Pattern, event.Group) 25 | subackEvent := &events.SubAckEvent{ 26 | Kind: events.SubAck, 27 | Success: true, 28 | Pattern: event.Pattern, 29 | } 30 | return client.WriteInterface(subackEvent) 31 | } 32 | -------------------------------------------------------------------------------- /handlers/unsub.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/c16a/microq/broker" 7 | "github.com/c16a/microq/events" 8 | ) 9 | 10 | func handleUnsubscribe(message []byte, client *broker.ConnectedClient) error { 11 | if !client.IsIdentified() { 12 | client.WriteInterface(&events.UnsubAckEvent{ 13 | Kind: events.PubAck, 14 | Success: false, 15 | }) 16 | return errors.New("unidentified client") 17 | } 18 | 19 | var event events.UnsubEvent 20 | err := json.Unmarshal(message, &event) 21 | if err != nil { 22 | return err 23 | } 24 | client.UnsubscribeFromPattern(event.Pattern) 25 | unsubackEvent := &events.UnsubAckEvent{ 26 | Kind: events.UnSubAck, 27 | Success: true, 28 | Pattern: event.Pattern, 29 | } 30 | return client.WriteInterface(unsubackEvent) 31 | } 32 | -------------------------------------------------------------------------------- /interfaces/tcp.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "bufio" 5 | "github.com/c16a/microq/broker" 6 | "github.com/c16a/microq/conn" 7 | "github.com/c16a/microq/handlers" 8 | "github.com/c16a/microq/storage" 9 | "net" 10 | ) 11 | 12 | func RunTcp(b *broker.Broker, storageProvider storage.Provider) error { 13 | listener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 8081}) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | for { 19 | c, err := listener.AcceptTCP() 20 | if err != nil { 21 | continue 22 | } 23 | 24 | scanner := bufio.NewScanner(c) 25 | 26 | tcpConn := conn.NewTCPConnection(c) 27 | client := broker.NewUnidentifiedClient(tcpConn) 28 | 29 | for scanner.Scan() { 30 | line := scanner.Text() 31 | 32 | err = handlers.HandleMessage(client, b, storageProvider, []byte(line)) 33 | if err != nil { 34 | continue 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /broker/client_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type TestingWebSocketConnection struct { 8 | } 9 | 10 | func (t *TestingWebSocketConnection) WriteMessage(data []byte) error { 11 | return nil 12 | } 13 | 14 | func TestConnectedClient_Subscribe_Unsubscribe(t *testing.T) { 15 | client := NewConnectedClient(&TestingWebSocketConnection{}, "client-1") 16 | 17 | client.SubscribeToPattern("t1.*", "g1") 18 | client.UnsubscribeFromPattern("t1.accounts") 19 | 20 | s1 := client.GetEligibility("t1.payments") 21 | if s1 == nil { 22 | t.Fatal("GetEligibility returned nil") 23 | } 24 | 25 | s2 := client.GetEligibility("t1.accounts") 26 | if s2 != nil { 27 | t.Fatal("GetEligibility returned non-nil eligibility") 28 | } 29 | 30 | client.UnsubscribeFromPattern("t1.payments") 31 | 32 | s3 := client.GetEligibility("t1.payments") 33 | if s3 != nil { 34 | t.Fatal("GetEligibility returned non-nil eligibility") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # This, is microq 2 | 3 | microq is a tiny MQTT broker written in Go. 4 | It is inspired by several mature messaging systems such as NATS, Kafka, ActiveMQ etc. 5 | 6 | ## Vision 7 | 8 | #### Messaging should be easy 9 | Developers should be able to onboard microq and start writing code without the 10 | need to unlearn their current messaging experience. 11 | microq will be cloud compatible, but you don't need to have Kubernetes running. 12 | You will be able to run microq anywhere from a Raspberry Pi, to a traditional Linux Desktop, 13 | a gigantic public cloud machine, an IBM-Z server, and of course, Kubernetes. 14 | 15 | #### Messaging should be based on open standards 16 | When messaging systems are built on open standards, it grows the ecosystem instead of dividing it. microq will always be based on multiple open standards and transports. 17 | 18 | 19 | #### Knowledge is best when shared 20 | There will never be an *enterprise*, or *premium* flavor of microq. 21 | It is MIT Licensed - you are free to use it however you wish to. -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: microq 2 | repo_name: c16a/microq 3 | repo_url: https://github.com/c16a/microq 4 | theme: 5 | name: material 6 | features: 7 | - navigation.instant 8 | - navigation.tabs 9 | - navigation.sections 10 | - navigation.expand 11 | icon: 12 | repo: fontawesome/brands/github 13 | font: 14 | text: IBM Plex Sans 15 | code: JetBrains Mono 16 | 17 | nav: 18 | - Home: index.md 19 | - Developer Guide: 20 | - 'Running Locally': 21 | - 'Using Docker': docker-build.md 22 | - 'Building from source': source-build.md 23 | - 'Configuration': configuration.md 24 | - About: about.md 25 | 26 | markdown_extensions: 27 | - pymdownx.highlight 28 | - pymdownx.inlinehilite 29 | - pymdownx.superfences 30 | - attr_list 31 | 32 | extra: 33 | social: 34 | - icon: fontawesome/brands/twitter 35 | link: https://twitter.com/iamunukutla 36 | - icon: fontawesome/brands/github 37 | link: https://github.com/c16a 38 | 39 | copyright: Copyright © 2024 Chaitanya Munukutla -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Chaitanya Munukutla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "github.com/c16a/microq/broker" 7 | "github.com/c16a/microq/config" 8 | "github.com/c16a/microq/interfaces" 9 | "github.com/c16a/microq/storage" 10 | "log" 11 | "os" 12 | ) 13 | 14 | var configFileName string 15 | 16 | func init() { 17 | flag.StringVar(&configFileName, "config", "config.json", "configuration file") 18 | } 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | c, err := ParseJsonFile[config.Config](configFileName) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | b := broker.NewBroker() 29 | 30 | var storageProvider = storage.NewFileStorageProvider(c) 31 | defer storageProvider.Close() 32 | 33 | go interfaces.RunWs(b, storageProvider) 34 | log.Fatal(interfaces.RunTcp(b, storageProvider)) 35 | } 36 | 37 | func ParseJsonFile[T any](path string) (*T, error) { 38 | var config T 39 | file, err := os.Open(path) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer file.Close() 44 | decoder := json.NewDecoder(file) 45 | err = decoder.Decode(&config) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &config, nil 50 | } 51 | -------------------------------------------------------------------------------- /interfaces/ws.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/c16a/microq/broker" 5 | "github.com/c16a/microq/conn" 6 | "github.com/c16a/microq/handlers" 7 | "github.com/c16a/microq/storage" 8 | "github.com/gorilla/websocket" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func RunWs(b *broker.Broker, storageProvider storage.Provider) { 14 | var upgrader = websocket.Upgrader{} 15 | http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { 16 | c, err := upgrader.Upgrade(w, r, nil) 17 | if err != nil { 18 | return 19 | } 20 | defer c.Close() 21 | 22 | wsConn := conn.NewWebsocketConnection(c) 23 | client := broker.NewUnidentifiedClient(wsConn) 24 | 25 | for { 26 | _, message, err := c.ReadMessage() 27 | if err != nil { 28 | if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseAbnormalClosure) { 29 | b.Disconnect(client) 30 | break 31 | } else { 32 | continue 33 | } 34 | } 35 | err = handlers.HandleMessage(client, b, storageProvider, message) 36 | if err != nil { 37 | continue 38 | } 39 | } 40 | }) 41 | 42 | log.Fatal(http.ListenAndServe(":8080", nil)) 43 | } 44 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "docker.io/golang:1.22.2-bookworm", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | "forwardPorts": [ 13 | 8080 14 | ], 15 | 16 | // Use 'postCreateCommand' to run commands after the container is created. 17 | "postCreateCommand": "go mod tidy", 18 | 19 | "customizations": { 20 | "vscode": { 21 | "extensions": [ 22 | "golang.Go" 23 | ] 24 | } 25 | }, 26 | "containerEnv": { 27 | "GOROOT": "/usr/local/go", 28 | "GOPATH": "/go", 29 | "PATH": "/usr/local/go/bin:/go/bin:${PATH}" 30 | }, 31 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 32 | // "remoteUser": "root" 33 | } -------------------------------------------------------------------------------- /broker/subscription.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import "strings" 4 | 5 | const ( 6 | PatternSeparator = "." 7 | SingleTokenMatcher = "*" 8 | MultiTokenMatcher = ">" 9 | ) 10 | 11 | type Subscription struct { 12 | active bool 13 | group string 14 | pattern string 15 | } 16 | 17 | func (s *Subscription) IsActive() bool { 18 | return s.active 19 | } 20 | 21 | func (s *Subscription) GetGroup() string { 22 | return s.group 23 | } 24 | 25 | func (s *Subscription) GetPattern() string { 26 | return s.pattern 27 | } 28 | 29 | func (s *Subscription) Matches(topic string) bool { 30 | 31 | patternTokens := strings.Split(s.pattern, PatternSeparator) 32 | topicTokens := strings.Split(topic, PatternSeparator) 33 | 34 | if len(patternTokens) > len(topicTokens) { 35 | return false 36 | } 37 | 38 | for tIdx, topicToken := range topicTokens { 39 | if tIdx > len(patternTokens)-1 { 40 | return false 41 | } 42 | if patternTokens[tIdx] == MultiTokenMatcher { 43 | break 44 | } 45 | if patternTokens[tIdx] == topicToken || patternTokens[tIdx] == SingleTokenMatcher { 46 | continue 47 | } else { 48 | return false 49 | } 50 | } 51 | 52 | return true 53 | } 54 | -------------------------------------------------------------------------------- /broker/subscription_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import "testing" 4 | 5 | var matchTests = []struct { 6 | pattern string 7 | topic string 8 | expected bool 9 | }{ 10 | { 11 | pattern: "time.us.*", 12 | topic: "time.us.east", 13 | expected: true, 14 | }, 15 | { 16 | pattern: "time.us.*", 17 | topic: "time.us.east.atlanta", 18 | expected: false, 19 | }, 20 | { 21 | pattern: "time.us.>", 22 | topic: "time.us.east.atlanta", 23 | expected: true, 24 | }, 25 | { 26 | pattern: "time.*.east", 27 | topic: "time.us.east", 28 | expected: true, 29 | }, 30 | { 31 | pattern: "time.*.east", 32 | topic: "time.eu.east", 33 | expected: true, 34 | }, 35 | { 36 | pattern: ">", 37 | topic: "time.eu.east", 38 | expected: true, 39 | }, 40 | } 41 | 42 | func TestSubscription_Matches(t *testing.T) { 43 | for _, tt := range matchTests { 44 | t.Run(tt.pattern, func(_t *testing.T) { 45 | subscription := &Subscription{ 46 | active: true, 47 | group: "", 48 | pattern: tt.pattern, 49 | } 50 | ok := subscription.Matches(tt.topic) 51 | if ok != tt.expected { 52 | _t.Fatal("doesn't match") 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /storage/file_provider.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/c16a/microq/config" 7 | "github.com/c16a/microq/events" 8 | "io/fs" 9 | "os" 10 | ) 11 | 12 | type FileProvider struct { 13 | rootDir string 14 | } 15 | 16 | func (f *FileProvider) SaveMessage(event *events.PubEvent) error { 17 | fSys := os.DirFS(f.rootDir) 18 | fInfo, err := fs.Stat(fSys, "msg.txt") 19 | 20 | var offlineFile *os.File 21 | if err != nil { 22 | // Couldn't stat the file 23 | if errors.Is(err, fs.ErrNotExist) { 24 | // If file doesn't exist, create it. 25 | if offlineFile, err = os.Create(f.rootDir + "/msg.txt"); err != nil { 26 | return err 27 | } 28 | } else { 29 | // Any other error, throw 30 | return err 31 | } 32 | } 33 | if fInfo.IsDir() { 34 | return errors.New("found a directory instead of a file") 35 | } 36 | eventBytes, err := json.Marshal(event) 37 | if err != nil { 38 | return err 39 | } 40 | _, err = offlineFile.Write(eventBytes) 41 | return err 42 | } 43 | 44 | func (f *FileProvider) Close() error { 45 | return nil 46 | } 47 | 48 | func NewFileStorageProvider(c *config.Config) *FileProvider { 49 | return &FileProvider{rootDir: c.Storage.RootDir} 50 | } 51 | -------------------------------------------------------------------------------- /handlers/pub.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/c16a/microq/broker" 7 | "github.com/c16a/microq/events" 8 | "github.com/c16a/microq/storage" 9 | "github.com/google/uuid" 10 | "strings" 11 | ) 12 | 13 | func handlePublish(message []byte, client *broker.ConnectedClient, broker *broker.Broker, sp storage.Provider) error { 14 | 15 | if !client.IsIdentified() { 16 | client.WriteInterface(&events.PubAckEvent{ 17 | Kind: events.PubAck, 18 | Success: false, 19 | }) 20 | return errors.New("unidentified client") 21 | } 22 | 23 | var event events.PubEvent 24 | err := json.Unmarshal(message, &event) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | topic := event.Topic 30 | if strings.Contains(topic, "/") || strings.Contains(topic, "\\") || strings.Contains(topic, "..") { 31 | client.WriteInterface(&events.PubAckEvent{ 32 | Kind: events.PubAck, 33 | Success: false, 34 | }) 35 | return errors.New("topic not allowed") 36 | } 37 | 38 | event.PacketId = uuid.New().String() 39 | 40 | if event.Retain { 41 | if err := sp.SaveMessage(&event); err != nil { 42 | return err 43 | } 44 | } 45 | go broker.Broadcast(event) 46 | 47 | switch event.QoS { 48 | case 1: 49 | pubAckEvent := &events.PubAckEvent{ 50 | Kind: events.PubAck, 51 | PacketId: event.PacketId, 52 | } 53 | client.WriteInterface(pubAckEvent) 54 | break 55 | case 2: 56 | pubRecEvent := &events.PubRecEvent{ 57 | Kind: events.PubRec, 58 | PacketId: event.PacketId, 59 | } 60 | client.WriteInterface(pubRecEvent) 61 | break 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /broker/broker.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/c16a/microq/events" 6 | "math/rand" 7 | "sync" 8 | ) 9 | 10 | type GenericConnection interface { 11 | WriteMessage(data []byte) error 12 | } 13 | 14 | type Broker struct { 15 | clients map[string]*ConnectedClient 16 | mutex sync.RWMutex 17 | } 18 | 19 | func NewBroker() *Broker { 20 | return &Broker{ 21 | clients: make(map[string]*ConnectedClient), 22 | } 23 | } 24 | 25 | func (broker *Broker) Connect(clientId string, client *ConnectedClient) { 26 | broker.mutex.Lock() 27 | defer broker.mutex.Unlock() 28 | 29 | broker.clients[clientId] = client 30 | } 31 | 32 | func (broker *Broker) Disconnect(client *ConnectedClient) { 33 | broker.mutex.Lock() 34 | defer broker.mutex.Unlock() 35 | 36 | for _, c := range broker.clients { 37 | if client.GetId() == c.GetId() { 38 | delete(broker.clients, client.GetId()) 39 | } 40 | } 41 | } 42 | 43 | func (broker *Broker) Broadcast(event events.PubEvent) error { 44 | broker.mutex.RLock() 45 | defer broker.mutex.RUnlock() 46 | 47 | data, err := json.Marshal(event) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | var groupedClients = make(map[string][]*ConnectedClient, 0) 53 | for _, client := range broker.clients { 54 | subscription := client.GetEligibility(event.Topic) 55 | if subscription != nil { 56 | if subscription.group == "" { 57 | client.WriteDataMessage(data) 58 | } else { 59 | if _, ok := groupedClients[subscription.group]; !ok { 60 | groupedClients[subscription.group] = make([]*ConnectedClient, 0) 61 | } 62 | groupedClients[subscription.group] = append(groupedClients[subscription.group], client) 63 | } 64 | } 65 | } 66 | 67 | // Go to every group, and write the data to just one of them 68 | for _, clients := range groupedClients { 69 | idx := 0 70 | if len(clients) > 1 { 71 | idx = pickRandomNumber(0, len(clients)-1) 72 | } 73 | client := clients[idx] 74 | client.WriteDataMessage(data) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func pickRandomNumber(min int, max int) int { 81 | return min + rand.Intn(max-min) 82 | } 83 | -------------------------------------------------------------------------------- /docs/docker-build.md: -------------------------------------------------------------------------------- 1 | microq uses a multi stage docker build for hermetic builds, while creating a minimal image. Hence, please ensure you use 2 | Docker v17.05 or newer. 3 | 4 | ```shell 5 | git clone https://github.com/c16a/microq.git 6 | cd microq 7 | docker build -t microq . 8 | ``` 9 | 10 | ### Running the image 11 | 12 | ```shell 13 | docker run -p 8080:8080 -v $pwd/config.json:/app/config.json microq 14 | ``` 15 | 16 | The above example assumes that the TCP server has been configured to listen on port 8080. In case that is configured to 17 | another port, please configure the docker exposed port accordingly. 18 | 19 | #### SELinux policies 20 | 21 | When using Docker on a host with SELinux enabled, the container is denied access to certain parts of host file system 22 | unless it is run in privileged mode. To resolve this, you can use a named volume 23 | 24 | ```shell 25 | # Create a docker volume and map it to /tmp/microq on the host 26 | docker volume create --driver local --opt type=none --opt device=/tmp/microq --opt o=bind microq_volume 27 | 28 | # Ensure /tmp/microq/config.json has the required broker configuration 29 | # Use the above created microq_volume to mount the config file into the container 30 | docker run -p 8080:8080 -e CONFIG_FILE_PATH=/tmp/microq/config.json --mount source=microq_volume,target=/tmp/microq microq 31 | ``` 32 | 33 | Please note that however, you place your `config.json` in the `/tmp` directory, SELinux does not restrict you access 34 | when you use a direct volume mapping. 35 | 36 | ```shell 37 | # This won't work with SELinux enabled 38 | docker run -p 8080:8080 -e CONFIG_FILE_PATH=/tmp/microq/config.json -v /home/user/config.json:/tmp/microq/config.json microq 39 | 40 | # This will work 41 | docker run -p 8080:8080 -e CONFIG_FILE_PATH=/tmp/microq/config.json -v /tmp/microq/config.json:/tmp/microq/config.json microq 42 | ``` 43 | 44 | The [Configuration](configuration.md) section has more details on which attributes of the broker can be configured. 45 | 46 | ### Running in Compose mode 47 | 48 | Create the named volume `microq_volume`. 49 | 50 | ```shell 51 | # Create a docker volume and map it to /tmp/microq on the host 52 | docker volume create --driver local --opt type=none --opt device=/tmp/microq --opt o=bind microq_volume 53 | ``` 54 | 55 | Reference the named volume for the service 56 | 57 | ```yaml 58 | version: "3.9" 59 | services: 60 | broker: 61 | build: 62 | context: . 63 | environment: 64 | CONFIG_FILE_PATH: "/tmp/microq/config.json" 65 | volumes: 66 | - microq_volume:/tmp/microq 67 | ports: 68 | - 8080:8080 69 | - 5000:5000 70 | volumes: 71 | microq_volume: 72 | external: true 73 | ``` -------------------------------------------------------------------------------- /broker/client.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | ) 7 | 8 | type ConnectedClient struct { 9 | id string 10 | subscriptions []*Subscription 11 | unsubscriptions []*Subscription 12 | conn GenericConnection 13 | mutex sync.RWMutex 14 | } 15 | 16 | func NewConnectedClient(conn GenericConnection, id string) *ConnectedClient { 17 | return &ConnectedClient{ 18 | id: id, 19 | conn: conn, 20 | mutex: sync.RWMutex{}, 21 | subscriptions: make([]*Subscription, 0), 22 | unsubscriptions: make([]*Subscription, 0), 23 | } 24 | } 25 | 26 | func NewUnidentifiedClient(conn GenericConnection) *ConnectedClient { 27 | return NewConnectedClient(conn, "") 28 | } 29 | 30 | func (client *ConnectedClient) IsIdentified() bool { 31 | return client.id != "" 32 | } 33 | 34 | func (client *ConnectedClient) SetId(id string) { 35 | client.id = id 36 | } 37 | 38 | func (client *ConnectedClient) GetId() string { 39 | return client.id 40 | } 41 | 42 | func (client *ConnectedClient) WriteDataMessage(data []byte) error { 43 | client.mutex.Lock() 44 | defer client.mutex.Unlock() 45 | 46 | return client.conn.WriteMessage(data) 47 | } 48 | 49 | func (client *ConnectedClient) WriteInterface(v any) error { 50 | data, err := json.Marshal(v) 51 | if err != nil { 52 | return err 53 | } 54 | return client.WriteDataMessage(data) 55 | } 56 | 57 | func (client *ConnectedClient) GetEligibility(topic string) *Subscription { 58 | client.mutex.RLock() 59 | defer client.mutex.RUnlock() 60 | 61 | // If topic matches any unsubscribed pattern, 62 | // it's not eligible 63 | for _, unsub := range client.unsubscriptions { 64 | if unsub.Matches(topic) { 65 | return nil 66 | } 67 | } 68 | 69 | // If topic is not unsubscribed as a part of any pattern, 70 | // check if it matches the subscribed ones 71 | for _, sub := range client.subscriptions { 72 | if sub.Matches(topic) { 73 | return sub 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (client *ConnectedClient) SubscribeToPattern(pattern string, group string) { 81 | client.mutex.Lock() 82 | defer client.mutex.Unlock() 83 | 84 | subscription := &Subscription{active: true, pattern: pattern} 85 | if len(group) > 0 { 86 | subscription.group = group 87 | } 88 | 89 | client.subscriptions = append(client.subscriptions, subscription) 90 | 91 | // If this exact pattern has been previously unsubscribed from, remove that entry 92 | for i, unsub := range client.unsubscriptions { 93 | if unsub.pattern == pattern { 94 | client.unsubscriptions = append(client.unsubscriptions[:i], client.unsubscriptions[i+1:]...) 95 | } 96 | } 97 | } 98 | 99 | func (client *ConnectedClient) UnsubscribeFromPattern(pattern string) { 100 | client.mutex.Lock() 101 | defer client.mutex.Unlock() 102 | 103 | unsubscription := &Subscription{active: true, pattern: pattern} 104 | client.unsubscriptions = append(client.unsubscriptions, unsubscription) 105 | 106 | // If this exact pattern has been previously subscribed to, remove that entry 107 | for i, sub := range client.subscriptions { 108 | if sub.pattern == pattern { 109 | client.subscriptions = append(client.subscriptions[:i], client.subscriptions[i+1:]...) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [chaitanya.m61292@gmail.com](mailto:chaitanya.m61292@gmail.com). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | codeql: 9 | name: Security Scan 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 360 12 | permissions: 13 | security-events: write 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Initialize CodeQL 19 | uses: github/codeql-action/init@v3 20 | with: 21 | languages: go 22 | build-mode: autobuild 23 | 24 | - name: Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v3 26 | with: 27 | category: "/language:go" 28 | 29 | devskim: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Run DevSkim scanner 41 | uses: microsoft/DevSkim-Action@v1 42 | 43 | - name: Upload DevSkim scan results to GitHub Security tab 44 | uses: github/codeql-action/upload-sarif@v3 45 | with: 46 | sarif_file: devskim-results.sarif 47 | 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | needs: 52 | - devskim 53 | - codeql 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Setup Go 59 | uses: actions/setup-go@v5 60 | with: 61 | go-version: '1.22.3' 62 | 63 | - name: Test 64 | run: go test -v ./... 65 | 66 | build-linux-amd64: 67 | env: 68 | GOOS: linux 69 | GOARCH: amd64 70 | CGO_ENABLED: 0 71 | name: Build (linux/amd64) 72 | needs: 73 | - test 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | 79 | - name: Setup Go 80 | uses: actions/setup-go@v5 81 | with: 82 | go-version: '1.22.3' 83 | 84 | - name: Build 85 | run: go build -o microq main.go 86 | 87 | build-linux-arm64: 88 | env: 89 | GOOS: linux 90 | GOARCH: arm64 91 | CGO_ENABLED: 0 92 | name: Build (linux/arm64) 93 | needs: 94 | - test 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | 100 | - name: Setup Go 101 | uses: actions/setup-go@v5 102 | with: 103 | go-version: '1.22.3' 104 | 105 | - name: Build 106 | run: go build -o microq main.go 107 | 108 | build-linux-riscv64: 109 | env: 110 | GOOS: linux 111 | GOARCH: riscv64 112 | CGO_ENABLED: 0 113 | name: Build (linux/riscv64) 114 | needs: 115 | - test 116 | runs-on: ubuntu-latest 117 | steps: 118 | - name: Checkout 119 | uses: actions/checkout@v4 120 | 121 | - name: Setup Go 122 | uses: actions/setup-go@v5 123 | with: 124 | go-version: '1.22.3' 125 | 126 | - name: Build 127 | run: go build -o microq main.go 128 | 129 | build-linux-ppc64le: 130 | env: 131 | GOOS: linux 132 | GOARCH: ppc64le 133 | CGO_ENABLED: 0 134 | name: Build (linux/ppc64le) 135 | needs: 136 | - test 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Checkout 140 | uses: actions/checkout@v4 141 | 142 | - name: Setup Go 143 | uses: actions/setup-go@v5 144 | with: 145 | go-version: '1.22.3' 146 | 147 | - name: Build 148 | run: go build -o microq main.go 149 | 150 | build-darwin-arm64: 151 | env: 152 | GOOS: darwin 153 | GOARCH: arm64 154 | CGO_ENABLED: 0 155 | name: Build (darwin/arm64) 156 | needs: 157 | - test 158 | runs-on: ubuntu-latest 159 | steps: 160 | - name: Checkout 161 | uses: actions/checkout@v4 162 | 163 | - name: Setup Go 164 | uses: actions/setup-go@v5 165 | with: 166 | go-version: '1.22.3' 167 | 168 | - name: Build 169 | run: go build -o microq main.go 170 | 171 | build-windows-amd64: 172 | env: 173 | GOOS: windows 174 | GOARCH: amd64 175 | CGO_ENABLED: 0 176 | name: Build (windows/amd64) 177 | needs: 178 | - test 179 | runs-on: ubuntu-latest 180 | steps: 181 | - name: Checkout 182 | uses: actions/checkout@v4 183 | 184 | - name: Setup Go 185 | uses: actions/setup-go@v5 186 | with: 187 | go-version: '1.22.3' 188 | 189 | - name: Build 190 | run: go build -o microq main.go 191 | 192 | build-windows-arm64: 193 | env: 194 | GOOS: windows 195 | GOARCH: arm64 196 | CGO_ENABLED: 0 197 | name: Build (windows/arm64) 198 | needs: 199 | - test 200 | runs-on: ubuntu-latest 201 | steps: 202 | - name: Checkout 203 | uses: actions/checkout@v4 204 | 205 | - name: Setup Go 206 | uses: actions/setup-go@v5 207 | with: 208 | go-version: '1.22.3' 209 | 210 | - name: Build 211 | run: go build -o microq main.go 212 | 213 | image: 214 | name: Build Image 215 | needs: 216 | - build-linux-amd64 217 | - build-linux-arm64 218 | runs-on: ubuntu-latest 219 | steps: 220 | - name: Checkout 221 | uses: actions/checkout@v4 222 | 223 | - name: Set up QEMU 224 | uses: docker/setup-qemu-action@v3 225 | 226 | - name: Set up Docker Buildx 227 | uses: docker/setup-buildx-action@v3 228 | 229 | - name: Login to GitHub Container Registry 230 | uses: docker/login-action@v3 231 | with: 232 | registry: ghcr.io 233 | username: ${{ github.repository_owner }} 234 | password: ${{ secrets.GITHUB_TOKEN }} 235 | 236 | - name: Build 237 | uses: docker/build-push-action@v5 238 | with: 239 | context: . 240 | platforms: linux/amd64,linux/arm64 241 | push: false 242 | tags: | 243 | ghcr.io/c16a/microq:rolling 244 | 245 | docs: 246 | name: Build docs site 247 | runs-on: ubuntu-latest 248 | permissions: 249 | contents: write 250 | needs: 251 | - image 252 | - build-linux-amd64 253 | - build-linux-arm64 254 | - build-linux-riscv64 255 | - build-linux-ppc64le 256 | - build-darwin-arm64 257 | - build-windows-amd64 258 | - build-windows-arm64 259 | steps: 260 | - uses: actions/checkout@v4 261 | - uses: actions/setup-python@v5 262 | with: 263 | python-version: 3.12 264 | - run: pip install mkdocs-material 265 | - run: mkdocs build -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Push 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | codeql: 9 | name: Security Scan 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 360 12 | permissions: 13 | security-events: write 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Initialize CodeQL 19 | uses: github/codeql-action/init@v3 20 | with: 21 | languages: go 22 | build-mode: autobuild 23 | 24 | - name: Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v3 26 | with: 27 | category: "/language:go" 28 | 29 | devskim: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Run DevSkim scanner 41 | uses: microsoft/DevSkim-Action@v1 42 | 43 | - name: Upload DevSkim scan results to GitHub Security tab 44 | uses: github/codeql-action/upload-sarif@v3 45 | with: 46 | sarif_file: devskim-results.sarif 47 | 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | needs: 52 | - devskim 53 | - codeql 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Setup Go 59 | uses: actions/setup-go@v5 60 | with: 61 | go-version: '1.22.3' 62 | 63 | - name: Test 64 | run: go test -v ./... 65 | 66 | build-linux-amd64: 67 | env: 68 | GOOS: linux 69 | GOARCH: amd64 70 | CGO_ENABLED: 0 71 | name: Build (linux/amd64) 72 | needs: 73 | - test 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | 79 | - name: Setup Go 80 | uses: actions/setup-go@v5 81 | with: 82 | go-version: '1.22.3' 83 | 84 | - name: Build 85 | run: go build -o microq main.go 86 | 87 | build-linux-arm64: 88 | env: 89 | GOOS: linux 90 | GOARCH: arm64 91 | CGO_ENABLED: 0 92 | name: Build (linux/arm64) 93 | needs: 94 | - test 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | 100 | - name: Setup Go 101 | uses: actions/setup-go@v5 102 | with: 103 | go-version: '1.22.3' 104 | 105 | - name: Build 106 | run: go build -o microq main.go 107 | 108 | build-linux-riscv64: 109 | env: 110 | GOOS: linux 111 | GOARCH: riscv64 112 | CGO_ENABLED: 0 113 | name: Build (linux/riscv64) 114 | needs: 115 | - test 116 | runs-on: ubuntu-latest 117 | steps: 118 | - name: Checkout 119 | uses: actions/checkout@v4 120 | 121 | - name: Setup Go 122 | uses: actions/setup-go@v5 123 | with: 124 | go-version: '1.22.3' 125 | 126 | - name: Build 127 | run: go build -o microq main.go 128 | 129 | build-linux-ppc64le: 130 | env: 131 | GOOS: linux 132 | GOARCH: ppc64le 133 | CGO_ENABLED: 0 134 | name: Build (linux/ppc64le) 135 | needs: 136 | - test 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Checkout 140 | uses: actions/checkout@v4 141 | 142 | - name: Setup Go 143 | uses: actions/setup-go@v5 144 | with: 145 | go-version: '1.22.3' 146 | 147 | - name: Build 148 | run: go build -o microq main.go 149 | 150 | build-darwin-arm64: 151 | env: 152 | GOOS: darwin 153 | GOARCH: arm64 154 | CGO_ENABLED: 0 155 | name: Build (darwin/arm64) 156 | needs: 157 | - test 158 | runs-on: ubuntu-latest 159 | steps: 160 | - name: Checkout 161 | uses: actions/checkout@v4 162 | 163 | - name: Setup Go 164 | uses: actions/setup-go@v5 165 | with: 166 | go-version: '1.22.3' 167 | 168 | - name: Build 169 | run: go build -o microq main.go 170 | 171 | build-windows-amd64: 172 | env: 173 | GOOS: windows 174 | GOARCH: amd64 175 | CGO_ENABLED: 0 176 | name: Build (windows/amd64) 177 | needs: 178 | - test 179 | runs-on: ubuntu-latest 180 | steps: 181 | - name: Checkout 182 | uses: actions/checkout@v4 183 | 184 | - name: Setup Go 185 | uses: actions/setup-go@v5 186 | with: 187 | go-version: '1.22.3' 188 | 189 | - name: Build 190 | run: go build -o microq main.go 191 | 192 | build-windows-arm64: 193 | env: 194 | GOOS: windows 195 | GOARCH: arm64 196 | CGO_ENABLED: 0 197 | name: Build (windows/arm64) 198 | needs: 199 | - test 200 | runs-on: ubuntu-latest 201 | steps: 202 | - name: Checkout 203 | uses: actions/checkout@v4 204 | 205 | - name: Setup Go 206 | uses: actions/setup-go@v5 207 | with: 208 | go-version: '1.22.3' 209 | 210 | - name: Build 211 | run: go build -o microq main.go 212 | 213 | image: 214 | name: Build Image 215 | needs: 216 | - build-linux-amd64 217 | - build-linux-arm64 218 | runs-on: ubuntu-latest 219 | permissions: 220 | packages: write 221 | steps: 222 | - name: Checkout 223 | uses: actions/checkout@v4 224 | 225 | - name: Set up QEMU 226 | uses: docker/setup-qemu-action@v3 227 | 228 | - name: Set up Docker Buildx 229 | uses: docker/setup-buildx-action@v3 230 | 231 | - name: Login to GitHub Container Registry 232 | uses: docker/login-action@v3 233 | with: 234 | registry: ghcr.io 235 | username: ${{ github.repository_owner }} 236 | password: ${{ secrets.GITHUB_TOKEN }} 237 | 238 | - name: Build 239 | uses: docker/build-push-action@v5 240 | with: 241 | context: . 242 | platforms: linux/amd64,linux/arm64 243 | push: true 244 | provenance: mode=max 245 | sbom: true 246 | tags: | 247 | ghcr.io/c16a/microq:rolling 248 | 249 | docs: 250 | name: Update docs site 251 | runs-on: ubuntu-latest 252 | permissions: 253 | contents: write 254 | needs: 255 | - image 256 | - build-linux-amd64 257 | - build-linux-arm64 258 | - build-linux-riscv64 259 | - build-linux-ppc64le 260 | - build-darwin-arm64 261 | - build-windows-amd64 262 | - build-windows-arm64 263 | steps: 264 | - uses: actions/checkout@v4 265 | - uses: actions/setup-python@v5 266 | with: 267 | python-version: 3.12 268 | - run: pip install mkdocs-material 269 | - run: mkdocs gh-deploy --force --------------------------------------------------------------------------------