diff --git a/packages/insomnia-smoke-test/server/websocket.ts b/packages/insomnia-smoke-test/server/websocket.ts index edbdecfd2de..de19084886a 100644 --- a/packages/insomnia-smoke-test/server/websocket.ts +++ b/packages/insomnia-smoke-test/server/websocket.ts @@ -45,13 +45,26 @@ Location: ws://localhost:4010 `); return; }; +const return401withBody = (socket: Socket) => { + socket.end(`HTTP/1.1 401 Unauthorized + + + + +
+

401 Unauthorized

+
+ + `); + return; +}; const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, head: Buffer) => { if (request.url === '/redirect') { return redirectOnSuccess(socket); } if (request.url === '/bearer') { if (request.headers.authorization !== 'Bearer insomnia-cool-token-!!!1112113243111') { - socket.end('HTTP/1.1 401 Unauthorized\n\n'); + return401withBody(socket); return; } return redirectOnSuccess(socket); @@ -59,7 +72,7 @@ const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, if (request.url === '/basic-auth') { // login with user:password if (request.headers.authorization !== 'Basic dXNlcjpwYXNzd29yZA==') { - socket.end('HTTP/1.1 401 Unauthorized\n\n'); + return401withBody(socket); return; } return redirectOnSuccess(socket); diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 118fa82b73c..cbb9e238428 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -562,6 +562,7 @@ export const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__'; export const EXPORT_TYPE_REQUEST = 'request'; export const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request'; export const EXPORT_TYPE_WEBSOCKET_REQUEST = 'websocket_request'; +export const EXPORT_TYPE_WEBSOCKET_PAYLOAD = 'websocket_payload'; export const EXPORT_TYPE_REQUEST_GROUP = 'request_group'; export const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite'; export const EXPORT_TYPE_UNIT_TEST = 'unit_test'; diff --git a/packages/insomnia/src/common/export.ts b/packages/insomnia/src/common/export.ts index c7403f1d925..073740ea559 100644 --- a/packages/insomnia/src/common/export.ts +++ b/packages/insomnia/src/common/export.ts @@ -14,6 +14,7 @@ import { isRequest } from '../models/request'; import { isRequestGroup } from '../models/request-group'; import { isUnitTest } from '../models/unit-test'; import { isUnitTestSuite } from '../models/unit-test-suite'; +import { isWebSocketPayload } from '../models/websocket-payload'; import { isWebSocketRequest } from '../models/websocket-request'; import { isWorkspace, Workspace } from '../models/workspace'; import { resetKeys } from '../sync/ignore-keys'; @@ -29,6 +30,7 @@ import { EXPORT_TYPE_REQUEST_GROUP, EXPORT_TYPE_UNIT_TEST, EXPORT_TYPE_UNIT_TEST_SUITE, + EXPORT_TYPE_WEBSOCKET_PAYLOAD, EXPORT_TYPE_WEBSOCKET_REQUEST, EXPORT_TYPE_WORKSPACE, getAppVersion, @@ -176,7 +178,8 @@ export async function exportRequestsData( isUnitTestSuite(d) || isUnitTest(d) || isProtoFile(d) || - isProtoDirectory(d) + isProtoDirectory(d) || + isWebSocketPayload(d) ); }); docs.push(...descendants); @@ -190,6 +193,7 @@ export async function exportRequestsData( isUnitTestSuite(d) || isUnitTest(d) || isRequest(d) || + isWebSocketPayload(d) || isWebSocketRequest(d) || isGrpcRequest(d) || isRequestGroup(d) || @@ -234,6 +238,9 @@ export async function exportRequestsData( } else if (isGrpcRequest(d)) { // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_GRPC_REQUEST; + } else if (isWebSocketPayload(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? + d._type = EXPORT_TYPE_WEBSOCKET_PAYLOAD; } else if (isWebSocketRequest(d)) { // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_WEBSOCKET_REQUEST; diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index 32f874e96e4..8247706a815 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -238,6 +238,9 @@ const createWebSocketConnection = async ( models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null }); }); ws.on('unexpected-response', async (clientRequest, incomingMessage) => { + incomingMessage.on('data', chunk => { + timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: chunk.toString(), name: 'DataOut', timestamp: Date.now() }) + '\n'); + }); // @ts-expect-error -- private property const internalRequestHeader = clientRequest._header; const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader); diff --git a/packages/insomnia/src/models/index.ts b/packages/insomnia/src/models/index.ts index e8b674f925b..f3249af3e85 100644 --- a/packages/insomnia/src/models/index.ts +++ b/packages/insomnia/src/models/index.ts @@ -9,6 +9,7 @@ import { EXPORT_TYPE_REQUEST_GROUP, EXPORT_TYPE_UNIT_TEST, EXPORT_TYPE_UNIT_TEST_SUITE, + EXPORT_TYPE_WEBSOCKET_PAYLOAD, EXPORT_TYPE_WEBSOCKET_REQUEST, EXPORT_TYPE_WORKSPACE, } from '../common/constants'; @@ -36,6 +37,7 @@ import * as _stats from './stats'; import * as _unitTest from './unit-test'; import * as _unitTestResult from './unit-test-result'; import * as _unitTestSuite from './unit-test-suite'; +import * as _webSocketPayload from './websocket-payload'; import * as _webSocketRequest from './websocket-request'; import * as _webSocketResponse from './websocket-response'; import * as _workspace from './workspace'; @@ -78,6 +80,7 @@ export const protoFile = _protoFile; export const protoDirectory = _protoDirectory; export const grpcRequest = _grpcRequest; export const grpcRequestMeta = _grpcRequestMeta; +export const webSocketPayload = _webSocketPayload; export const webSocketRequest = _webSocketRequest; export const webSocketResponse = _webSocketResponse; export const workspace = _workspace; @@ -113,6 +116,7 @@ export function all() { protoDirectory, grpcRequest, grpcRequestMeta, + webSocketPayload, webSocketRequest, webSocketResponse, ] as const; @@ -214,6 +218,7 @@ export async function initModel(type: string, ...sources: R export const MODELS_BY_EXPORT_TYPE: Record = { [EXPORT_TYPE_REQUEST]: request, + [EXPORT_TYPE_WEBSOCKET_PAYLOAD]: webSocketPayload, [EXPORT_TYPE_WEBSOCKET_REQUEST]: webSocketRequest, [EXPORT_TYPE_GRPC_REQUEST]: grpcRequest, [EXPORT_TYPE_REQUEST_GROUP]: requestGroup, diff --git a/packages/insomnia/src/models/websocket-payload.ts b/packages/insomnia/src/models/websocket-payload.ts new file mode 100644 index 00000000000..80d28a17c7b --- /dev/null +++ b/packages/insomnia/src/models/websocket-payload.ts @@ -0,0 +1,70 @@ +import { database } from '../common/database'; +import type { BaseModel } from '.'; + +export const name = 'WebSocket Payload'; + +export const type = 'WebSocketPayload'; + +export const prefix = 'ws-payload'; + +export const canDuplicate = true; + +// @TODO: enable this at some point +export const canSync = false; + +export interface BaseWebSocketPayload { + value: string; + mode: string; +} + +export type WebSocketPayload = BaseModel & BaseWebSocketPayload & { type: typeof type }; + +export const isWebSocketPayload = (model: Pick): model is WebSocketPayload => ( + model.type === type +); + +export const isWebSocketPayloadId = (id: string | null) => ( + id?.startsWith(`${prefix}_`) +); + +export const init = (): BaseWebSocketPayload => ({ + value: '', + mode: 'application/json', +}); + +export const migrate = (doc: WebSocketPayload) => doc; + +export const create = (patch: Partial = {}) => { + if (!patch.parentId) { + throw new Error(`New WebSocketPayload missing \`parentId\`: ${JSON.stringify(patch)}`); + } + + return database.docCreate(type, patch); +}; + +export const remove = (obj: WebSocketPayload) => database.remove(obj); + +export const update = ( + obj: WebSocketPayload, + patch: Partial = {} +) => database.docUpdate(obj, patch); + +export async function duplicate(request: WebSocketPayload, patch: Partial = {}) { + // Only set name and "(Copy)" if the patch does + // not define it and the request itself has a name. + // Otherwise leave it blank so the request URL can + // fill it in automatically. + if (!patch.name && request.name) { + patch.name = `${request.name} (Copy)`; + } + + return database.duplicate(request, { + name, + ...patch, + }); +} + +export const getById = (_id: string) => database.getWhere(type, { _id }); +export const getByParentId = (parentId: string) => database.getWhere(type, { parentId }); + +export const all = () => database.all(type); diff --git a/packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx similarity index 81% rename from packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx rename to packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx index c0685a125c0..b8466b391c1 100644 --- a/packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx @@ -5,17 +5,17 @@ import { Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownItem } from '../base/dropdown/dropdown-item'; interface Props { - payloadType: string; - onClick: (payloadType: string) => void; + previewMode: string; + onClick: (previewMode: string) => void; } -export const PayloadTypeDropdown: FC = ({ payloadType, onClick }) => { +export const WebSocketPreviewModeDropdown: FC = ({ previewMode, onClick }) => { return ( {{ [CONTENT_TYPE_JSON]: 'JSON', [CONTENT_TYPE_PLAINTEXT]: 'Raw', - }[payloadType]} + }[previewMode]} diff --git a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx index 544917624df..6b5112323a8 100644 --- a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx @@ -1,4 +1,4 @@ -import React, { FC, FormEvent, useRef, useState } from 'react'; +import React, { FC, FormEvent, useEffect, useRef, useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import styled from 'styled-components'; @@ -9,7 +9,7 @@ import { WebSocketRequest } from '../../../models/websocket-request'; import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state'; import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor'; import { AuthDropdown } from '../dropdowns/auth-dropdown'; -import { PayloadTypeDropdown } from '../dropdowns/payload-type-dropdown'; +import { WebSocketPreviewModeDropdown } from '../dropdowns/websocket-preview-mode'; import { AuthWrapper } from '../editors/auth/auth-wrapper'; import { RequestHeadersEditor } from '../editors/request-headers-editor'; import { showAlert, showModal } from '../modals'; @@ -52,16 +52,30 @@ const PaneHeader = styled(OriginalPaneHeader)({ interface FormProps { request: WebSocketRequest; - payloadType: string; + previewMode: string; + initialValue: string; environmentId: string; + createOrUpdatePayload: (payload: string, mode: string) => Promise; } const WebSocketRequestForm: FC = ({ request, - payloadType, + previewMode, + initialValue, + createOrUpdatePayload, environmentId, }) => { const editorRef = useRef(null); + useEffect(() => { + let isMounted = true; + if (isMounted) { + editorRef.current?.codeMirror?.setValue(initialValue); + } + return () => { + isMounted = false; + }; + }, [initialValue]); + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const message = editorRef.current?.getValue() || ''; @@ -102,9 +116,10 @@ const WebSocketRequestForm: FC = ({ createOrUpdatePayload(value, previewMode)} enableNunjucks /> @@ -125,13 +140,49 @@ interface Props { // TODO: @gatzjames discuss above assertion in light of request and settings drills export const WebSocketRequestPane: FC = ({ request, workspaceId, environmentId, forceRefreshKey }) => { const readyState = useWSReadyState(request._id); + const disabled = readyState === ReadyState.OPEN || readyState === ReadyState.CLOSING; const handleOnChange = (url: string) => { if (url !== request.url) { models.webSocketRequest.update(request, { url }); } }; - const [payloadType, setPayloadType] = useState(CONTENT_TYPE_JSON); + const [previewMode, setPreviewMode] = useState(CONTENT_TYPE_JSON); + const [initialValue, setInitialValue] = useState(''); + + useEffect(() => { + let isMounted = true; + const fn = async () => { + const payload = await models.webSocketPayload.getByParentId(request._id); + if (isMounted && payload) { + setInitialValue(payload?.value || ''); + setPreviewMode(payload.mode); + } + }; + fn(); + return () => { + isMounted = false; + }; + }, [request._id]); + + const changeMode = (mode: string) => { + setPreviewMode(mode); + createOrUpdatePayload(initialValue, mode); + }; + + const createOrUpdatePayload = async (value: string, mode: string) => { + // @TODO: multiple payloads + const payload = await models.webSocketPayload.getByParentId(request._id); + if (payload) { + await models.webSocketPayload.update(payload, { value, mode }); + return; + } + await models.webSocketPayload.create({ + parentId: request._id, + value, + mode, + }); + }; const uniqueKey = `${forceRefreshKey}::${request._id}`; @@ -151,7 +202,7 @@ export const WebSocketRequestPane: FC = ({ request, workspaceId, environm - + = ({ request, workspaceId, environm diff --git a/packages/insomnia/src/ui/redux/modules/entities.ts b/packages/insomnia/src/ui/redux/modules/entities.ts index cd4c416e631..293f1c00f3b 100644 --- a/packages/insomnia/src/ui/redux/modules/entities.ts +++ b/packages/insomnia/src/ui/redux/modules/entities.ts @@ -27,6 +27,7 @@ import { Stats } from '../../../models/stats'; import { UnitTest } from '../../../models/unit-test'; import { UnitTestResult } from '../../../models/unit-test-result'; import { UnitTestSuite } from '../../../models/unit-test-suite'; +import { WebSocketPayload } from '../../../models/websocket-payload'; import { WebSocketRequest } from '../../../models/websocket-request'; import { WebSocketResponse } from '../../../models/websocket-response'; import { Workspace } from '../../../models/workspace'; @@ -72,6 +73,7 @@ export interface EntitiesState { protoDirectories: EntityRecord; grpcRequests: EntityRecord; grpcRequestMetas: EntityRecord; + webSocketPayloads: EntityRecord; webSocketRequests: EntityRecord; webSocketResponses: EntityRecord; } @@ -102,6 +104,7 @@ export const initialEntitiesState: EntitiesState = { protoDirectories: {}, grpcRequests: {}, grpcRequestMetas: {}, + webSocketPayloads: {}, webSocketRequests: {}, webSocketResponses: {}, }; @@ -203,6 +206,7 @@ export async function allDocs() { ...(await models.protoDirectory.all()), ...(await models.grpcRequest.all()), ...(await models.grpcRequestMeta.all()), + ...(await models.webSocketPayload.all()), ...(await models.webSocketRequest.all()), ...(await models.webSocketResponse.all()), ]; diff --git a/packages/insomnia/src/ui/redux/sidebar-selectors.ts b/packages/insomnia/src/ui/redux/sidebar-selectors.ts index 54587c33796..69f2e1c08eb 100644 --- a/packages/insomnia/src/ui/redux/sidebar-selectors.ts +++ b/packages/insomnia/src/ui/redux/sidebar-selectors.ts @@ -21,7 +21,7 @@ export const shouldShowInSidebar = (model: BaseModel): boolean => isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model) || isRequestGroup(model); export const shouldIgnoreChildrenOf = (model: SidebarModel): boolean => - isRequest(model) || isGrpcRequest(model); + isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model); export const sortByMetaKeyOrId = (a: SidebarModel, b: SidebarModel): number => { if (a.metaSortKey === b.metaSortKey) {