Skip to content
Draft
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
82 changes: 82 additions & 0 deletions sip-codec/src/main/java/com/sip/codec/typed/CSeqParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.sip.codec.typed;

import com.sip.codec.SipCodecException;
import com.sip.message.SipMethod;
import com.sip.message.header.HeaderName;
import com.sip.message.header.Headers;
import com.sip.message.header.RawHeader;
import com.sip.message.header.typed.CSeqHeader;

import java.util.Optional;

/**
* Parses the CSeq header (RFC 3261 §20.16):
* <pre>
* CSeq = 1*DIGIT LWS Method
* </pre>
*/
public final class CSeqParser {

private CSeqParser() { }

public static Optional<CSeqHeader> parse(Headers headers) {
return headers.first(HeaderName.CSEQ).map(CSeqParser::parse);
}

public static CSeqHeader parse(RawHeader raw) {
return parse(raw.value());
}

public static CSeqHeader parse(String text) {
if (text == null) {
throw malformed("CSeq is null");
}
String s = text.trim();
int sp = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == ' ' || c == '\t') {
sp = i;
break;
}
}
if (sp <= 0) {
throw malformed("CSeq missing whitespace between sequence and method: '" + s + "'");
}
String seqText = s.substring(0, sp);
long sequence;
try {
sequence = Long.parseLong(seqText);
} catch (NumberFormatException e) {
throw malformed("CSeq sequence is not numeric: '" + seqText + "'", e);
}
if (sequence < 0 || sequence > CSeqHeader.MAX_SEQUENCE) {
throw malformed("CSeq sequence out of range [0," + CSeqHeader.MAX_SEQUENCE
+ "]: " + sequence);
}
int methodStart = sp;
while (methodStart < s.length()
&& (s.charAt(methodStart) == ' ' || s.charAt(methodStart) == '\t')) {
methodStart++;
}
if (methodStart == s.length()) {
throw malformed("CSeq missing Method token");
}
String methodText = s.substring(methodStart).trim();
try {
return new CSeqHeader(sequence, SipMethod.of(methodText));
} catch (IllegalArgumentException e) {
throw malformed("CSeq Method invalid: " + e.getMessage(), e);
}
}

private static SipCodecException malformed(String message) {
return new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1, message);
}

private static SipCodecException malformed(String message, Throwable cause) {
return new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1, message, cause);
}
}
59 changes: 59 additions & 0 deletions sip-codec/src/main/java/com/sip/codec/typed/CallIdValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.sip.codec.typed;

import com.sip.codec.SipCodecException;
import com.sip.message.header.HeaderName;
import com.sip.message.header.Headers;

import java.util.Optional;

/**
* Validates Call-ID per RFC 3261 §20.8 / §25:
* <pre>
* Call-ID = word [ "@" word ]
* word = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+"
* / "`" / "'" / "~" / "(" / ")" / "<" / ">"
* / ":" / "\" / DQUOTE / "/" / "[" / "]" / "?" / "{" / "}")
* </pre>
*
* <p>Returns the validated string (after trimming surrounding LWS) for use
* as a dialog identity component.</p>
*/
public final class CallIdValidator {

private CallIdValidator() { }

public static Optional<String> get(Headers headers) {
return headers.first(HeaderName.CALL_ID).map(r -> validate(r.value()));
}

public static String validate(String text) {
String s = text == null ? "" : text.trim();
if (s.isEmpty()) {
throw malformed("Call-ID is empty");
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!isWordChar(c) && c != '@') {
throw malformed("Call-ID contains invalid character at offset " + i
+ ": '" + c + "' (0x" + Integer.toHexString(c) + ")");
}
}
return s;
}

private static boolean isWordChar(char c) {
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
return true;
}
return switch (c) {
case '-', '.', '!', '%', '*', '_', '+', '`', '\'', '~',
'(', ')', '<', '>', ':', '\\', '"', '/', '[', ']', '?', '{', '}' -> true;
default -> false;
};
}

private static SipCodecException malformed(String message) {
return new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1, message);
}
}
53 changes: 53 additions & 0 deletions sip-codec/src/main/java/com/sip/codec/typed/ContactParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.sip.codec.typed;

import com.sip.codec.SipCodecException;
import com.sip.message.header.HeaderName;
import com.sip.message.header.Headers;
import com.sip.message.header.RawHeader;
import com.sip.message.header.typed.ContactValue;

import java.util.ArrayList;
import java.util.List;

/**
* Parses Contact header values (RFC 3261 §20.10):
*
* <pre>
* Contact = ("Contact" / "m") HCOLON
* ( STAR / (contact-param *(COMMA contact-param)) )
* contact-param = (name-addr / addr-spec) *(SEMI contact-params)
* </pre>
*
* <p>Returns either a single wildcard entry or a list of named contacts.</p>
*/
public final class ContactParser {

private ContactParser() { }

public static List<ContactValue> parseAll(Headers headers) {
List<ContactValue> out = new ArrayList<>();
boolean sawWildcard = false;
for (RawHeader raw : headers.all(HeaderName.CONTACT)) {
String v = raw.value().trim();
if ("*".equals(v)) {
if (!out.isEmpty()) {
throw new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1,
"Contact: '*' cannot be combined with other contact values");
}
sawWildcard = true;
out.add(ContactValue.wildcard());
continue;
}
if (sawWildcard) {
throw new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1,
"additional Contact values after wildcard '*'");
}
for (var na : NameAddrParser.parseList(v)) {
out.add(ContactValue.of(na));
}
}
return out;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.sip.codec.typed;

import com.sip.codec.SipCodecException;
import com.sip.message.header.HeaderName;
import com.sip.message.header.Headers;

import java.util.Optional;

/**
* Helpers for simple integer headers (Max-Forwards, Expires, Content-Length).
*/
public final class IntegerHeaderParser {

private IntegerHeaderParser() { }

public static Optional<Integer> maxForwards(Headers h) {
return h.first(HeaderName.MAX_FORWARDS).map(r -> parseInt(r.value(), 0, 255,
"Max-Forwards"));
}

public static Optional<Integer> expires(Headers h) {
return h.first(HeaderName.EXPIRES).map(r -> parseInt(r.value(), 0, Integer.MAX_VALUE,
"Expires"));
}

public static Optional<Integer> contentLength(Headers h) {
return h.first(HeaderName.CONTENT_LENGTH).map(r -> parseInt(r.value(), 0,
Integer.MAX_VALUE, "Content-Length"));
}

static int parseInt(String text, int min, int max, String headerName) {
String s = text.trim();
int v;
try {
v = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1,
headerName + " is not an integer: '" + s + "'", e);
}
if (v < min || v > max) {
throw new SipCodecException(
SipCodecException.Category.MALFORMED_HEADER, -1,
headerName + " out of range [" + min + "," + max + "]: " + v);
}
return v;
}
}
Loading
Loading