├── .gitignore ├── .gitmodules ├── README.md ├── __init__.py ├── extender.py ├── images ├── burp-installation-final.png ├── burp-installation.png ├── protoburp-extension-tab.png └── protoburp-transformation.png ├── json-generator.py ├── log.txt ├── protobuf-encoder.py ├── setup.sh ├── tab.py └── test_app ├── __init__.py ├── addressbook.proto ├── app.py ├── complexmessage.proto ├── employee.proto └── userprofile.proto /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/protobuf"] 2 | path = deps/protobuf 3 | url = https://github.com/protocolbuffers/protobuf.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProtoBurp 2 | Check out my blog post detailing the wonderful things you can do with `ProtoBurp`! [https://dillonfrankesecurity.com/posts/protoburp-encode-custom-protobuf-messages-in-burp/](https://dillonfrankesecurity.com/posts/protoburp-encode-custom-protobuf-messages-in-burp/) 3 | 4 | ## Description 5 | `ProtoBurp` is a Burp Suite extension that enables security researchers to encode and fuzz custom Protobuf messages. It allows users to automatically convert JSON data into a Protobuf message based on a provided protobuf definition file. This opens up opportunities for fuzzing inputs using Burp's Repeater and Intruder tools, as well as proxy traffic from other tools (e.g. `sqlmap`). 6 | 7 | ## Installation 8 | 9 | ### 1. Clone the `ProtoBurp` repository and its submodules 10 | ```bash 11 | git clone --recursive https://github.com/dillonfranke/protoburp.git 12 | ``` 13 | > Make sure to add the `--recursive` option 14 | ### 2. Install the `protoc` utility, which you'll need to compile Protobuf defintion (`.proto`) files 15 | Mac: 16 | ```bash 17 | brew install protobuf 18 | ``` 19 | Debian Linux: 20 | ```bash 21 | sudo apt-get update 22 | sudo apt-get install protobuf-compiler 23 | ``` 24 | Windows: 25 | https://github.com/protocolbuffers/protobuf/releases 26 | 27 | ### 3. Run the `setup.sh` script 28 | This will install Google's `protobuf` module so it can be used by the extension 29 | ```bash 30 | sudo ./setup.sh 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### 1. Compile the `.proto` file you want to convert into Python format 36 | > Several example `.proto` files are contained in the `test_app` folder 37 | ```bash 38 | protoc --python_out=. addressbook.proto 39 | ``` 40 | 41 | ### 2. Load the `ProtoBurp` extension and select your compiled `.proto` file 42 | 43 | 44 | 45 | 46 | 47 | - Click 'Enable ProtoBurp' 48 | - Select the Python Protobuf definition file you just compiled 49 | 50 | 51 | 52 | ### 3. Set the `ProtoBurp` header on your requests, and your requests will be transformed from JSON to Protobuf! 53 | 54 | 55 | ### Generating a JSON payload 56 | You might be wondering: "How can I generate a JSON object from a `.proto` file to use with `ProtoBurp`?" 57 | 58 | Easy, I wrote a script that, given a `.proto` file, will fill in placeholder values to generate a JSON payload. You can then use the JSON payload with `ProtoBurp`. Here's how you use the script: 59 | 60 | ```bash 61 | ❯ python3 json-generator.py 62 | Usage: python3 json-generator.py 63 | ``` 64 | ```bash 65 | ❯ python3 json-generator.py test_app/addressbook_pb2.py AddressBook 66 | { 67 | "people": [ 68 | { 69 | "name": "example", 70 | "id": 1, 71 | "email": "example", 72 | "phones": [ 73 | { 74 | "number": "example", 75 | "type": "PHONE_TYPE_UNSPECIFIED" 76 | }, 77 | { 78 | "number": "example", 79 | "type": "PHONE_TYPE_UNSPECIFIED" 80 | } 81 | ] 82 | }, 83 | { 84 | "name": "example", 85 | "id": 1, 86 | "email": "example", 87 | "phones": [ 88 | { 89 | "number": "example", 90 | "type": "PHONE_TYPE_UNSPECIFIED" 91 | }, 92 | { 93 | "number": "example", 94 | "type": "PHONE_TYPE_UNSPECIFIED" 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | ## Use Cases 103 | Please see my [blog post](https://dillonfrankesecurity.com/posts/protoburp-encode-custom-protobuf-messages-in-burp/), where I talk about how you can use `ProtoBurp` with Repeater, Intruder, and external security utilities like `sqlmap`! 104 | 105 | ## Bugs and Feature Requests 106 | Please use the [issues tab](https://github.com/dillonfranke/protoburp/issues) for any bugs or feature requests. 107 | 108 | Happy Hunting! -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import inspect 4 | 5 | _BASE_DIR = os.path.abspath( 6 | os.path.dirname(inspect.getfile(inspect.currentframe())) 7 | ) 8 | 9 | sys.path.insert(0, _BASE_DIR + "/deps/protobuf/python/") 10 | sys.path.insert(0, _BASE_DIR + "/gen") 11 | 12 | -------------------------------------------------------------------------------- /extender.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import inspect 4 | import base64 5 | from struct import pack 6 | 7 | from burp import IBurpExtender 8 | from burp import IHttpListener 9 | from burp import ITab 10 | from javax.swing import JPanel 11 | from javax.swing import JButton 12 | from javax.swing import JFileChooser 13 | from java.awt import FlowLayout 14 | from tab import Tab 15 | 16 | # Add correct directory to sys.path 17 | _BASE_DIR = os.path.abspath( 18 | os.path.dirname(inspect.getfile(inspect.currentframe())) 19 | ) 20 | 21 | sys.path.insert(0, _BASE_DIR + "/deps/protobuf/python/") 22 | 23 | EXTENSION_NAME = "ProtoBurp" 24 | 25 | class BurpExtender(IBurpExtender, IHttpListener): 26 | 27 | # Implement IBurpExtender methods 28 | def registerExtenderCallbacks(self, callbacks): 29 | # keep a reference to our callbacks object (Burp Extensibility Feature) 30 | self._callbacks = callbacks 31 | 32 | # set our extension name that will display in the Extender tool when loaded 33 | self._callbacks.setExtensionName(EXTENSION_NAME) 34 | 35 | # register ourselves as an HTTP listener 36 | self._callbacks.registerHttpListener(self) 37 | 38 | # get burp helper functions 39 | self._helpers = self._callbacks.getHelpers() 40 | 41 | self.suite_tab = Tab(self, callbacks) 42 | 43 | # Add the custom tab 44 | callbacks.addSuiteTab(self.suite_tab) 45 | 46 | def getTabCaption(self): 47 | return "ProtoBurp" 48 | 49 | def getUiComponent(self): 50 | return self._jPanel 51 | 52 | def file_chooser(self, event): 53 | chooser = JFileChooser() 54 | action = chooser.showOpenDialog(self._jPanel) 55 | 56 | if action == JFileChooser.APPROVE_OPTION: 57 | file = chooser.getSelectedFile() 58 | 59 | def json_to_protobuf_in_python3(self, json_body): 60 | # Prepare the command to run in Python 3 61 | cmd = ["python3", os.path.join(_BASE_DIR, "protobuf-encoder.py"), "--json", json.dumps(json_body), "--protobuf_definition", str(self.suite_tab.selectedFilePath)] 62 | 63 | output = "" 64 | # Run the command 65 | try: 66 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 67 | except subprocess.CalledProcessError as e: 68 | print("Subprocess exited with error (status code {}):".format(e.returncode)) 69 | print(e.output.decode()) 70 | output = output.decode("utf-8").strip() 71 | protobuf = base64.b64decode(output) 72 | 73 | return protobuf 74 | 75 | # Implement IHttpListener methods 76 | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): 77 | # Only continue if the extension is enabled 78 | if not self.suite_tab.protoburp_enabled: 79 | return 80 | # Only process requests 81 | if not messageIsRequest: 82 | return 83 | 84 | # Get the HTTP service for the request 85 | httpService = messageInfo.getHttpService() 86 | # Convert the request to a IRequestInfo object 87 | requestInfo = self._helpers.analyzeRequest(httpService, messageInfo.getRequest()) 88 | # requestInfo is an IRequestInfo object 89 | headers = requestInfo.getHeaders() 90 | 91 | # Convert header names to lower case for case-insensitive comparison 92 | header_names = [header.split(":")[0].lower() for header in headers] 93 | 94 | # Only process if the ProtoBurp header exists 95 | if not "protoburp" in header_names: 96 | return 97 | 98 | # Get the body of the request 99 | body = messageInfo.getRequest()[requestInfo.getBodyOffset():] 100 | # Convert the body from bytes to string 101 | body_string = body.tostring().decode() 102 | # Convert the string to a JSON object 103 | json_body = json.loads(body_string) 104 | # Convert the JSON to Protobuf 105 | protobuf = self.json_to_protobuf_in_python3(json_body) 106 | 107 | #For header value that contains application/grpc content type 108 | if any("application/grpc" in header.lower() for header in headers): 109 | # Calculate the length of the serialized message. 110 | message_length = len(protobuf) 111 | 112 | # Create the gRPC message header. 113 | # It consists of a compressed flag (0 for uncompressed) and the message length. 114 | grpc_header = pack('>B', 0) + pack('>I', message_length) 115 | 116 | # Concatenate the header and the serialized message. 117 | protobuf = grpc_header + protobuf 118 | 119 | # Create a new HTTP message with the Protobuf body 120 | new_message = self._helpers.buildHttpMessage(headers, protobuf) 121 | # Update the request in the messageInfo object 122 | messageInfo.setRequest(new_message) 123 | -------------------------------------------------------------------------------- /images/burp-installation-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dillonfranke/protoburp/249b4eaea52ea99b53003b04cd911a6d0125ced5/images/burp-installation-final.png -------------------------------------------------------------------------------- /images/burp-installation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dillonfranke/protoburp/249b4eaea52ea99b53003b04cd911a6d0125ced5/images/burp-installation.png -------------------------------------------------------------------------------- /images/protoburp-extension-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dillonfranke/protoburp/249b4eaea52ea99b53003b04cd911a6d0125ced5/images/protoburp-extension-tab.png -------------------------------------------------------------------------------- /images/protoburp-transformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dillonfranke/protoburp/249b4eaea52ea99b53003b04cd911a6d0125ced5/images/protoburp-transformation.png -------------------------------------------------------------------------------- /json-generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | import sys 4 | # Add correct directory to sys.path 5 | _BASE_DIR = os.path.abspath( 6 | os.path.dirname(inspect.getfile(inspect.currentframe())) 7 | ) 8 | 9 | sys.path.insert(0, _BASE_DIR + "/deps/protobuf/python/") 10 | 11 | from google.protobuf.json_format import MessageToJson 12 | import importlib 13 | import importlib.util 14 | 15 | def set_placeholder_values(message): 16 | for field in message.DESCRIPTOR.fields: 17 | try: 18 | if field.type == field.TYPE_DOUBLE or field.type == field.TYPE_FLOAT: 19 | if field.label == field.LABEL_REPEATED: 20 | getattr(message, field.name).extend([1.1, 2.2]) 21 | else: 22 | setattr(message, field.name, 1.1) 23 | elif field.type == field.TYPE_INT64 or field.type == field.TYPE_UINT64 or \ 24 | field.type == field.TYPE_INT32 or field.type == field.TYPE_FIXED64 or \ 25 | field.type == field.TYPE_FIXED32 or field.type == field.TYPE_UINT32 or \ 26 | field.type == field.TYPE_SFIXED32 or field.type == field.TYPE_SFIXED64 or \ 27 | field.type == field.TYPE_SINT32 or field.type == field.TYPE_SINT64: 28 | if field.label == field.LABEL_REPEATED: 29 | getattr(message, field.name).extend([1, 2]) 30 | else: 31 | setattr(message, field.name, 1) 32 | elif field.type == field.TYPE_BOOL: 33 | if field.label == field.LABEL_REPEATED: 34 | getattr(message, field.name).extend([True, False]) 35 | else: 36 | setattr(message, field.name, True) 37 | elif field.type == field.TYPE_STRING: 38 | if field.label == field.LABEL_REPEATED: 39 | getattr(message, field.name).extend(['example1', 'example2']) 40 | else: 41 | setattr(message, field.name, 'example') 42 | elif field.type == field.TYPE_BYTES: 43 | if field.label == field.LABEL_REPEATED: 44 | getattr(message, field.name).extend([b'example1', b'example2']) 45 | else: 46 | setattr(message, field.name, b'example') 47 | elif field.type == field.TYPE_ENUM: 48 | if field.label == field.LABEL_REPEATED: 49 | getattr(message, field.name).extend([list(field.enum_type.values_by_name.values())[i].number for i in range(2)]) 50 | else: 51 | setattr(message, field.name, list(field.enum_type.values_by_name.values())[0].number) 52 | 53 | elif field.type == field.TYPE_MESSAGE: 54 | if field.label == field.LABEL_REPEATED: 55 | nested_message = getattr(message, field.name).add() 56 | set_placeholder_values(nested_message) 57 | nested_message = getattr(message, field.name).add() 58 | set_placeholder_values(nested_message) 59 | else: 60 | nested_message = getattr(message, field.name) 61 | set_placeholder_values(nested_message) 62 | except Exception as e: 63 | sys.stderr.write(f"Had an issue with the {field.name} field, so keeping it uninitialized...") 64 | 65 | def generate_example_json(protobuf_module, message_name): 66 | # Get the protobuf message class 67 | message_class = getattr(protobuf_module, message_name) 68 | # Instantiate the message class 69 | message = message_class() 70 | 71 | set_placeholder_values(message) 72 | 73 | # Convert the protobuf message to a JSON string and return it 74 | return MessageToJson(message, preserving_proto_field_name=True) 75 | 76 | def main(): 77 | try: 78 | protobuf_definition_path = sys.argv[1] 79 | message_name = sys.argv[2] 80 | except: 81 | print("Usage: python3 json-generator.py ") 82 | exit(1) 83 | # Add the directory of protobuf_definition_path to sys.path 84 | sys.path.insert(0, os.path.dirname(os.path.abspath(protobuf_definition_path))) 85 | # Dynamically import the module 86 | protobuf_module = importlib.import_module(os.path.splitext(os.path.basename(protobuf_definition_path))[0]) 87 | 88 | # Generate and print an example JSON object 89 | json_obj = generate_example_json(protobuf_module, message_name) 90 | print(json_obj) 91 | 92 | if __name__ == '__main__': 93 | main() 94 | 95 | -------------------------------------------------------------------------------- /log.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /protobuf-encoder.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import argparse 4 | import json 5 | import base64 6 | import importlib.util 7 | import inspect 8 | 9 | _BASE_DIR = os.path.abspath( 10 | os.path.dirname(inspect.getfile(inspect.currentframe())) 11 | ) 12 | sys.path.insert(0, _BASE_DIR + "/deps/protobuf/python/") 13 | from google.protobuf.json_format import Parse 14 | from google.protobuf.message import Message 15 | 16 | def main(): 17 | with open('log.txt', 'w') as logfile: 18 | 19 | # Parse arguments 20 | logfile.write("Parsing Args...\n") 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument("--json") 23 | parser.add_argument("--protobuf_definition") 24 | args = parser.parse_args() 25 | 26 | # Load protobuf module from specified file 27 | logfile.write("Loading protobuf module...\n") 28 | sys.path.insert(0, os.path.dirname(args.protobuf_definition)) 29 | 30 | # Get the filename with the extension 31 | base_name = os.path.basename(args.protobuf_definition) 32 | 33 | # Remove the extension 34 | class_name = os.path.splitext(base_name)[0] 35 | proto_module = __import__(class_name) 36 | 37 | proto_class = None 38 | 39 | for name, obj in inspect.getmembers(proto_module): 40 | if inspect.isclass(obj) and issubclass(obj, Message): 41 | proto_class = getattr(proto_module, name) 42 | break 43 | 44 | # Convert JSON to protobuf 45 | proto_msg = proto_class() 46 | logfile.write("Parsing JSON string into protobuf\n") 47 | Parse(args.json, proto_msg) 48 | 49 | logfile.write("Serializing protobuf structure to string\n") 50 | serialized_protobuf = proto_msg.SerializeToString() 51 | 52 | # Print the resulting protobuf 53 | logfile.write("Done, returning base64 encoded string\n") 54 | logfile.write(base64.b64encode(serialized_protobuf).decode()) 55 | print(base64.b64encode(serialized_protobuf).decode()) 56 | 57 | 58 | main() 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | cd deps/protobuf/python 2 | python3 setup.py install 3 | -------------------------------------------------------------------------------- /tab.py: -------------------------------------------------------------------------------- 1 | from burp import ITab 2 | from javax.swing import JSplitPane, JScrollPane, JPanel, JButton, BoxLayout, Box 3 | from javax.swing import JLabel, JCheckBox, JTextArea, JList, ListSelectionModel, JFileChooser 4 | from javax.swing import DefaultListModel 5 | from javax.swing.border import EmptyBorder 6 | from java.awt import Component, Dimension, BorderLayout 7 | 8 | class Tab(ITab): 9 | """Burp tab for selecting a compile Protobuf file and enabling/disabling the extension""" 10 | 11 | def __init__(self, extension, burp_callbacks): 12 | self.protoburp_enabled = False 13 | self._burp_callbacks = burp_callbacks 14 | self._extension = extension 15 | self.selectedFilePath = None 16 | 17 | self._type_list_component = JList(DefaultListModel()) 18 | self._type_list_component.setSelectionMode( 19 | ListSelectionModel.MULTIPLE_INTERVAL_SELECTION 20 | ) 21 | 22 | self._component = JPanel() 23 | self._component.setLayout(BorderLayout()) 24 | self._component.setBorder(EmptyBorder(10, 10, 10, 10)) 25 | 26 | # Add instruction label 27 | instructionText = """ 28 | Welcome to ProtoBurp! ProtoBurp converts JSON data into Protobuf messages based on a provided `.proto` file. This allows you to use Repeater or Intruder to quickly fuzz endpoints accepting Protobufs. 29 | 30 | To use this extension, please follow the steps below: 31 | 1. Create or obtain a `.proto` file you'd like to create protobuf messages 32 | 2. Use the `protoc` utility to compile your `.proto` file into Python format. (e.g. `protoc --python_out=./ MyMessage.proto`) 33 | 3. Click the 'Choose File' button to select your compiled protobuf file. 34 | 4. Check the 'Enable ProtoBurp' checkbox. 35 | 5. All requests sent with the header `ProtoBurp: True` will then be converted from JSON to a Protobuf! 36 | """ 37 | instructions = JTextArea(instructionText) 38 | instructions.setEditable(False) # Make the text area non-editable 39 | self._component.add(instructions, BorderLayout.PAGE_START) 40 | 41 | # Add file chooser button and checkbox 42 | topPanel = JTextArea() 43 | topPanel.setLayout(BoxLayout(topPanel, BoxLayout.Y_AXIS)) # Arrange components vertically 44 | 45 | fileLabel = JLabel("Compiled Protobuf File (Python Format): ") 46 | button = JButton("Choose File", actionPerformed=self.chooseFile) 47 | fileChooserPanel = JPanel() # A new panel to hold the file chooser components 48 | fileChooserPanel.add(fileLabel) 49 | fileChooserPanel.add(button) 50 | self._label = JLabel("No file chosen") 51 | fileChooserPanel.add(self._label) 52 | 53 | # Add option to enable/disable ProtoBurp 54 | enableProtoBurp = JCheckBox("Enable ProtoBurp", actionPerformed=self.toggleEnabled) 55 | 56 | topPanel.add(enableProtoBurp) 57 | topPanel.add(fileChooserPanel) 58 | 59 | 60 | self._component.add(topPanel, BorderLayout.CENTER) 61 | 62 | def toggleEnabled(self, event): 63 | self.protoburp_enabled = not self.protoburp_enabled 64 | print(self.protoburp_enabled) 65 | 66 | def chooseFile(self, event): 67 | chooser = JFileChooser() 68 | action = chooser.showOpenDialog(None) 69 | 70 | if action == JFileChooser.APPROVE_OPTION: 71 | file = chooser.getSelectedFile() 72 | self.selectedFilePath = file.getAbsolutePath() 73 | self._label.text = "Selected file: " + self.selectedFilePath 74 | print("Selected file: " + self.selectedFilePath) 75 | 76 | def getTabCaption(self): 77 | """Returns name on tab""" 78 | return "ProtoBurp" 79 | 80 | def getUiComponent(self): 81 | """Returns Java AWT component for tab""" 82 | return self._component 83 | -------------------------------------------------------------------------------- /test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dillonfranke/protoburp/249b4eaea52ea99b53003b04cd911a6d0125ced5/test_app/__init__.py -------------------------------------------------------------------------------- /test_app/addressbook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tutorial; 4 | 5 | message Person { 6 | string name = 1; 7 | int32 id = 2; 8 | string email = 3; 9 | 10 | enum PhoneType { 11 | PHONE_TYPE_UNSPECIFIED = 0; 12 | PHONE_TYPE_MOBILE = 1; 13 | PHONE_TYPE_HOME = 2; 14 | PHONE_TYPE_WORK = 3; 15 | } 16 | 17 | message PhoneNumber { 18 | string number = 1; 19 | PhoneType type = 2; 20 | } 21 | 22 | repeated PhoneNumber phones = 4; 23 | } 24 | 25 | message AddressBook { 26 | repeated Person people = 1; 27 | } 28 | -------------------------------------------------------------------------------- /test_app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | import sys 4 | # Add correct directory to sys.path 5 | _BASE_DIR = os.path.abspath( 6 | os.path.dirname(inspect.getfile(inspect.currentframe())) 7 | ) 8 | 9 | sys.path.insert(0, _BASE_DIR + "/../deps/protobuf/python/") 10 | 11 | import json 12 | from flask import Flask, request 13 | from flask_restful import Resource, Api 14 | from flask_sqlalchemy import SQLAlchemy 15 | from sqlalchemy import UniqueConstraint 16 | from sqlalchemy.engine import Engine 17 | from google.protobuf.json_format import MessageToJson, Parse 18 | import addressbook_pb2 19 | 20 | app = Flask(__name__) 21 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' 22 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 23 | api = Api(app) 24 | db = SQLAlchemy(app) 25 | 26 | @db.event.listens_for(Engine, "connect") 27 | def set_sqlite_pragma(dbapi_connection, connection_record): 28 | cursor = dbapi_connection.cursor() 29 | cursor.execute("PRAGMA foreign_keys=ON;") 30 | cursor.close() 31 | 32 | class PersonModel(db.Model): 33 | __tablename__ = 'person' 34 | id = db.Column(db.Integer, primary_key=True) 35 | name = db.Column(db.String(50), nullable=False) 36 | email = db.Column(db.String(50), unique=True) 37 | phones = db.relationship('PhoneModel', backref='person', lazy=True) 38 | 39 | class PhoneModel(db.Model): 40 | __tablename__ = 'phone' 41 | id = db.Column(db.Integer, primary_key=True) 42 | number = db.Column(db.String(50), nullable=False) 43 | type = db.Column(db.String(50)) 44 | person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False) 45 | 46 | 47 | with app.app_context(): 48 | db.create_all() 49 | 50 | 51 | PHONE_TYPE_MAP = { 52 | "0": "PHONE_TYPE_UNSPECIFIED", 53 | "1": "PHONE_TYPE_MOBILE", 54 | "2": "PHONE_TYPE_HOME", 55 | "3": "PHONE_TYPE_WORK" 56 | } 57 | 58 | class AddressBook(Resource): 59 | def get(self): 60 | people = PersonModel.query.all() 61 | address_book = addressbook_pb2.AddressBook() 62 | for person in people: 63 | p = addressbook_pb2.Person(name=person.name, id=person.id, email=person.email) 64 | for phone in person.phones: 65 | phone_type_label = PHONE_TYPE_MAP[phone.type] 66 | p.phones.add(number=phone.number, type=phone_type_label) 67 | address_book.people.append(p) 68 | json_dict = json.loads(MessageToJson(address_book)) 69 | return json_dict, 200 70 | 71 | def post(self): 72 | address_book = addressbook_pb2.AddressBook() 73 | address_book.ParseFromString(request.data) 74 | for person in address_book.people: 75 | p = PersonModel(name=person.name, id=person.id, email=person.email) 76 | for phone in person.phones: 77 | p.phones.append(PhoneModel(number=phone.number, type=phone.type)) 78 | db.session.add(p) 79 | db.session.commit() 80 | return 'Added', 201 81 | 82 | 83 | api.add_resource(AddressBook, '/addressbook') 84 | 85 | 86 | if __name__ == '__main__': 87 | app.run(debug=True) 88 | 89 | -------------------------------------------------------------------------------- /test_app/complexmessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package complex; 4 | 5 | message ComplexMessage { 6 | int32 id = 1; 7 | 8 | message SubMessage { 9 | string name = 1; 10 | int64 timestamp = 2; 11 | bytes data = 3; 12 | 13 | enum SubEnum { 14 | UNKNOWN = 0; 15 | FIRST = 1; 16 | SECOND = 2; 17 | } 18 | SubEnum state = 4; 19 | 20 | message NestedMessage { 21 | float percentage = 1; 22 | string status = 2; 23 | } 24 | NestedMessage details = 5; 25 | } 26 | SubMessage sub = 2; 27 | 28 | enum EnumType { 29 | UNDEFINED = 0; 30 | TYPE_A = 1; 31 | TYPE_B = 2; 32 | } 33 | EnumType type = 3; 34 | 35 | repeated int32 numbers = 4; 36 | map mappings = 5; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test_app/employee.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package example; 3 | 4 | message Employee { 5 | string name = 1; 6 | int32 id = 2; 7 | 8 | enum JobTitle { 9 | UNDEFINED = 0; 10 | ENGINEER = 1; 11 | MANAGER = 2; 12 | DIRECTOR = 3; 13 | } 14 | 15 | JobTitle title = 3; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test_app/userprofile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package example; 3 | 4 | message UserProfile { 5 | string username = 1; 6 | int32 id = 2; 7 | 8 | message Address { 9 | string street = 1; 10 | string city = 2; 11 | string state = 3; 12 | string zip = 4; 13 | } 14 | 15 | Address address = 3; 16 | 17 | message Contact { 18 | string phone = 1; 19 | string email = 2; 20 | } 21 | 22 | repeated Contact contacts = 4; 23 | } 24 | 25 | --------------------------------------------------------------------------------