Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

require (
github.com/danielpaulus/go-ios v1.0.182
github.com/gorilla/websocket v1.5.3
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func StartServer(addr string, enableCORS bool) error {

mux.HandleFunc("/", sendBanner)
mux.HandleFunc("/rpc", handleJSONRPC)
mux.HandleFunc("/ws", NewWebSocketHandler(enableCORS))

// if host is missing, default to localhost
if !strings.Contains(addr, ":") {
Expand Down
255 changes: 255 additions & 0 deletions server/websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package server

import (
"encoding/json"
"log"
"net/http"
"net/url"
"sync"
"time"

"github.com/gorilla/websocket"
"github.com/mobile-next/mobilecli/utils"
)

type wsConnection struct {
conn *websocket.Conn
writeMu sync.Mutex
}

type validationError struct {
code int
message string
data interface{}
}

const (
wsMaxMessageSize = 64 * 1024
wsWriteWait = 10 * time.Second
wsPongWait = 60 * time.Second
wsPingPeriod = (wsPongWait * 9) / 10

jsonRPCVersion = "2.0"
errMsgParseError = "expecting jsonrpc payload"
errMsgInvalidJSONRPC = "'jsonrpc' must be '2.0'"
errMsgIDRequired = "'id' field is required"
errMsgMethodRequired = "'method' is required"
errMsgTextOnly = "only text messages accepted for requests"
errMsgScreencapture = "screencapture not supported over WebSocket, use HTTP /rpc endpoint"
errTitleParseError = "Parse error"
errTitleInvalidReq = "Invalid Request"
errTitleMethodNotSupp = "Method not supported"
)

func newUpgrader(enableCORS bool) *websocket.Upgrader {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}

if enableCORS {
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
} else {
upgrader.CheckOrigin = isSameOrigin
}

return &upgrader
}

func upgradeConnection(w http.ResponseWriter, r *http.Request, upgrader *websocket.Upgrader) (*websocket.Conn, error) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}
return conn, nil
}

func configureConnection(conn *websocket.Conn) {
conn.SetReadLimit(wsMaxMessageSize)
if err := conn.SetReadDeadline(time.Now().Add(wsPongWait)); err != nil {
utils.Verbose("failed to set read deadline: %v", err)
}
conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(wsPongWait))
})
}

func startPingRoutine(wsConn *wsConnection) func() {
pingDone := make(chan struct{})
go pingLoop(wsConn, pingDone)
return func() { close(pingDone) }
}

func pingLoop(wsConn *wsConnection, done <-chan struct{}) {
ticker := time.NewTicker(wsPingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wsConn.writeMu.Lock()
if err := wsConn.conn.SetWriteDeadline(time.Now().Add(wsWriteWait)); err != nil {
utils.Verbose("failed to set write deadline: %v", err)
wsConn.writeMu.Unlock()
return
}
if err := wsConn.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
wsConn.writeMu.Unlock()
return
}
wsConn.writeMu.Unlock()
case <-done:
return
}
}
}

func readMessages(wsConn *wsConnection) {
for {
messageType, message, err := wsConn.conn.ReadMessage()
if err != nil {
utils.Verbose("WebSocket connection closed: %v", err)
break
}

if messageType != websocket.TextMessage {
wsConn.sendError(nil, ErrCodeInvalidRequest, errTitleInvalidReq, errMsgTextOnly)

Check failure on line 117 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendError` is not checked (errcheck)
continue
}

handleWSMessage(wsConn, message)
}
}
Comment on lines +108 to +123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle or acknowledge the error return from sendError.

The static analysis flags that sendError return value is unchecked on line 117. While the loop continuing regardless is reasonable behavior, the error should be logged for observability.

🔧 Suggested fix
 		if messageType != websocket.TextMessage {
-			wsConn.sendError(nil, ErrCodeInvalidRequest, errTitleInvalidReq, errMsgTextOnly)
+			if err := wsConn.sendError(nil, ErrCodeInvalidRequest, errTitleInvalidReq, errMsgTextOnly); err != nil {
+				utils.Verbose("failed to send error response: %v", err)
+			}
 			continue
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func readMessages(wsConn *wsConnection) {
for {
messageType, message, err := wsConn.conn.ReadMessage()
if err != nil {
utils.Verbose("WebSocket connection closed: %v", err)
break
}
if messageType != websocket.TextMessage {
wsConn.sendError(nil, ErrCodeInvalidRequest, errTitleInvalidReq, errMsgTextOnly)
continue
}
handleWSMessage(wsConn, message)
}
}
func readMessages(wsConn *wsConnection) {
for {
messageType, message, err := wsConn.conn.ReadMessage()
if err != nil {
utils.Verbose("WebSocket connection closed: %v", err)
break
}
if messageType != websocket.TextMessage {
if err := wsConn.sendError(nil, ErrCodeInvalidRequest, errTitleInvalidReq, errMsgTextOnly); err != nil {
utils.Verbose("failed to send error response: %v", err)
}
continue
}
handleWSMessage(wsConn, message)
}
}
🧰 Tools
🪛 GitHub Check: lint

[failure] 117-117:
Error return value of wsConn.sendError is not checked (errcheck)

🤖 Prompt for AI Agents
In `@server/websocket.go` around lines 108 - 123, In readMessages, the return
value from wsConnection.sendError is ignored; modify the code to capture the
error returned by sendError (call on wsConn.sendError) and log it (e.g., via
utils.Verbose or a logger) so failures to send error frames are observable;
ensure you still continue the loop after logging so behavior is unchanged but
errors are recorded for wsConnection/sendError failures.


func NewWebSocketHandler(enableCORS bool) http.HandlerFunc {
upgrader := newUpgrader(enableCORS)
return func(w http.ResponseWriter, r *http.Request) {
conn, err := upgradeConnection(w, r, upgrader)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()

Check failure on line 133 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `conn.Close` is not checked (errcheck)

wsConn := &wsConnection{conn: conn}
configureConnection(conn)
stopPing := startPingRoutine(wsConn)
defer stopPing()

readMessages(wsConn)
}
}

func isSameOrigin(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}

originURL, err := url.Parse(origin)
if err != nil {
return false
}

return originURL.Host == r.Host
}

func validateJSONRPCRequest(req JSONRPCRequest) *validationError {
if req.JSONRPC != jsonRPCVersion {
return &validationError{
code: ErrCodeInvalidRequest,
message: errTitleInvalidReq,
data: errMsgInvalidJSONRPC,
}
}

if req.ID == nil {
return &validationError{
code: ErrCodeInvalidRequest,
message: errTitleInvalidReq,
data: errMsgIDRequired,
}
}

if req.Method == "" {
return &validationError{
code: ErrCodeInvalidRequest,
message: errTitleInvalidReq,
data: errMsgMethodRequired,
}
}

if req.Method == "screencapture" {
return &validationError{
code: ErrCodeMethodNotFound,
message: errTitleMethodNotSupp,
data: errMsgScreencapture,
}
}

return nil
}

func handleWSMessage(wsConn *wsConnection, message []byte) {
var req JSONRPCRequest
if err := json.Unmarshal(message, &req); err != nil {
wsConn.sendError(nil, ErrCodeParseError, errTitleParseError, errMsgParseError)

Check failure on line 197 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendError` is not checked (errcheck)
return
}

if validationErr := validateJSONRPCRequest(req); validationErr != nil {
wsConn.sendError(req.ID, validationErr.code, validationErr.message, validationErr.data)

Check failure on line 202 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendError` is not checked (errcheck)
return
}

utils.Info("WebSocket Request ID: %v, Method: %s, Params: %s", req.ID, req.Method, string(req.Params))

handleWSMethodCall(wsConn, req)
}
Comment on lines +194 to +209
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address unchecked error returns and potential sensitive data logging.

  1. Static analysis flags unchecked sendError returns on lines 197 and 202.
  2. Line 206 logs req.Params at Info level, which may contain sensitive user data. Consider using Verbose level or redacting params.
🔧 Suggested fix
 func handleWSMessage(wsConn *wsConnection, message []byte) {
 	var req JSONRPCRequest
 	if err := json.Unmarshal(message, &req); err != nil {
-		wsConn.sendError(nil, ErrCodeParseError, errTitleParseError, errMsgParseError)
+		if sendErr := wsConn.sendError(nil, ErrCodeParseError, errTitleParseError, errMsgParseError); sendErr != nil {
+			utils.Verbose("failed to send parse error: %v", sendErr)
+		}
 		return
 	}
 
 	if validationErr := validateJSONRPCRequest(req); validationErr != nil {
-		wsConn.sendError(req.ID, validationErr.code, validationErr.message, validationErr.data)
+		if sendErr := wsConn.sendError(req.ID, validationErr.code, validationErr.message, validationErr.data); sendErr != nil {
+			utils.Verbose("failed to send validation error: %v", sendErr)
+		}
 		return
 	}
 
-	utils.Info("WebSocket Request ID: %v, Method: %s, Params: %s", req.ID, req.Method, string(req.Params))
+	utils.Verbose("WebSocket Request ID: %v, Method: %s, Params: %s", req.ID, req.Method, string(req.Params))
 
 	handleWSMethodCall(wsConn, req)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func handleWSMessage(wsConn *wsConnection, message []byte) {
var req JSONRPCRequest
if err := json.Unmarshal(message, &req); err != nil {
wsConn.sendError(nil, ErrCodeParseError, errTitleParseError, errMsgParseError)
return
}
if validationErr := validateJSONRPCRequest(req); validationErr != nil {
wsConn.sendError(req.ID, validationErr.code, validationErr.message, validationErr.data)
return
}
utils.Info("WebSocket Request ID: %v, Method: %s, Params: %s", req.ID, req.Method, string(req.Params))
handleWSMethodCall(wsConn, req)
}
func handleWSMessage(wsConn *wsConnection, message []byte) {
var req JSONRPCRequest
if err := json.Unmarshal(message, &req); err != nil {
if sendErr := wsConn.sendError(nil, ErrCodeParseError, errTitleParseError, errMsgParseError); sendErr != nil {
utils.Verbose("failed to send parse error: %v", sendErr)
}
return
}
if validationErr := validateJSONRPCRequest(req); validationErr != nil {
if sendErr := wsConn.sendError(req.ID, validationErr.code, validationErr.message, validationErr.data); sendErr != nil {
utils.Verbose("failed to send validation error: %v", sendErr)
}
return
}
utils.Verbose("WebSocket Request ID: %v, Method: %s, Params: %s", req.ID, req.Method, string(req.Params))
handleWSMethodCall(wsConn, req)
}
🧰 Tools
🪛 GitHub Check: lint

[failure] 202-202:
Error return value of wsConn.sendError is not checked (errcheck)


[failure] 197-197:
Error return value of wsConn.sendError is not checked (errcheck)

🤖 Prompt for AI Agents
In `@server/websocket.go` around lines 194 - 209, The function handleWSMessage
should check and handle the return values from wsConn.sendError calls (called in
the json.Unmarshal error branch and the validateJSONRPCRequest error branch)
instead of ignoring them—capture the error return and log/handle it
appropriately; also avoid logging potentially sensitive request payloads with
utils.Info: change the log to utils.Verbose (or call a redact function on
req.Params before logging) so JSONRPCRequest params are not emitted at Info
level; keep the rest of the flow (validateJSONRPCRequest, handleWSMethodCall)
intact.


func handleWSMethodCall(wsConn *wsConnection, req JSONRPCRequest) {
registry := GetMethodRegistry()
handler, exists := registry[req.Method]
if !exists {
wsConn.sendError(req.ID, ErrCodeMethodNotFound, "Method not found", req.Method+" not found")

Check failure on line 215 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendError` is not checked (errcheck)
return
}

result, err := handler(req.Params)
if err != nil {
log.Printf("Error executing method %s: %v", req.Method, err)
wsConn.sendError(req.ID, ErrCodeServerError, "Server error", err.Error())

Check failure on line 222 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendError` is not checked (errcheck)
return
}

wsConn.sendResponse(req.ID, result)

Check failure on line 226 in server/websocket.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `wsConn.sendResponse` is not checked (errcheck)
}
Comment on lines +211 to +227
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle error returns from sendError and sendResponse.

Static analysis flags unchecked errors on lines 215, 222, and 226. These should be logged for observability.

🔧 Suggested fix
 func handleWSMethodCall(wsConn *wsConnection, req JSONRPCRequest) {
 	registry := GetMethodRegistry()
 	handler, exists := registry[req.Method]
 	if !exists {
-		wsConn.sendError(req.ID, ErrCodeMethodNotFound, "Method not found", req.Method+" not found")
+		if err := wsConn.sendError(req.ID, ErrCodeMethodNotFound, "Method not found", req.Method+" not found"); err != nil {
+			utils.Verbose("failed to send method not found error: %v", err)
+		}
 		return
 	}
 
 	result, err := handler(req.Params)
 	if err != nil {
 		log.Printf("Error executing method %s: %v", req.Method, err)
-		wsConn.sendError(req.ID, ErrCodeServerError, "Server error", err.Error())
+		if sendErr := wsConn.sendError(req.ID, ErrCodeServerError, "Server error", err.Error()); sendErr != nil {
+			utils.Verbose("failed to send server error: %v", sendErr)
+		}
 		return
 	}
 
-	wsConn.sendResponse(req.ID, result)
+	if err := wsConn.sendResponse(req.ID, result); err != nil {
+		utils.Verbose("failed to send response: %v", err)
+	}
 }
🧰 Tools
🪛 GitHub Check: lint

[failure] 226-226:
Error return value of wsConn.sendResponse is not checked (errcheck)


[failure] 222-222:
Error return value of wsConn.sendError is not checked (errcheck)


[failure] 215-215:
Error return value of wsConn.sendError is not checked (errcheck)

🤖 Prompt for AI Agents
In `@server/websocket.go` around lines 211 - 227, The calls to wsConn.sendError
and wsConn.sendResponse in handleWSMethodCall ignore returned errors and static
analysis flags these; update handleWSMethodCall to capture and log any error
returned from wsConn.sendError (both the "Method not found" and server error
paths) and from wsConn.sendResponse after successful execution, e.g., call the
methods, check the error value, and log it with context (mentioning req.Method
and req.ID) so failures in sendError/sendResponse are observable; locate these
calls inside handleWSMethodCall and add error handling/logging around
wsConn.sendError and wsConn.sendResponse.


func (wsc *wsConnection) sendResponse(id interface{}, result interface{}) error {
response := JSONRPCResponse{
JSONRPC: jsonRPCVersion,
Result: result,
ID: id,
}
return wsc.sendJSON(response)
}

func (wsc *wsConnection) sendError(id interface{}, code int, message string, data interface{}) error {
response := JSONRPCResponse{
JSONRPC: jsonRPCVersion,
Error: map[string]interface{}{
"code": code,
"message": message,
"data": data,
},
ID: id,
}
return wsc.sendJSON(response)
}

func (wsc *wsConnection) sendJSON(v interface{}) error {
wsc.writeMu.Lock()
defer wsc.writeMu.Unlock()
return wsc.conn.WriteJSON(v)
}
Loading